From 429d21a72f1dde6953c8e895eca12423ff3c196e Mon Sep 17 00:00:00 2001 From: 1820982382 Date: Tue, 18 Aug 2026 22:23:25 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 + README.md | 268 ++++++++++--- README_EN.md | 277 ++++++++++---- example/infusion_example.js | 4 +- gradle.properties | 2 +- .../java/tckubejsbridge/TCKubeJSBridge.java | 8 +- .../tckubejsbridge/kubejs/TCKubeJSPlugin.java | 44 ++- .../tckubejsbridge/kubejs/TCRecipeBridge.java | 355 ++++++++++++++++-- 8 files changed, 798 insertions(+), 163 deletions(-) diff --git a/.gitignore b/.gitignore index 78bd68c..6860eca 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,6 @@ findings.md progress.md /modules/thaumicenergistics-neo/存储体系方案.md /modules/thaumicenergistics-neo/计划书-配方溯源.md +/.reasonix/ +/.tmp_*/ +__pycache__/ diff --git a/README.md b/README.md index 0604449..b23a4c5 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,21 @@ > **中文** | [English](README_EN.md) -将 **神秘时代4(Thaumcraft 4 移植版)** 的配方系统桥接到 **KubeJS**,让整合包作者/服务器管理员 -可以在 KubeJS 脚本中直接注册 TC 的注魔、炼金、奥术合成配方以及物品固有要素,并支持**替换原有配方**。 +将 **神秘时代4(Thaumcraft 4 移植版)** 的配方与研究系统桥接到 **KubeJS**。 + +整合包作者可以通过 `kubejs/server_scripts/` 下的 KubeJS 脚本直接注册: + +- 注魔配方 +- 坩埚炼金配方 +- 奥术有序 / 无序合成配方 +- 物品固有要素 +- 自定义研究分类 / 研究节点 +- 替换 TC 原有配方 作者:AlkSur | 许可证:GPL-3.0 +--- + ## 依赖 | Mod | 版本 | 关系 | @@ -16,43 +26,59 @@ | Thaumcraft (TC4 移植版) | 0.2.2.34-port.1 | 必需 | | KubeJS (NeoForge) | 2101.7.x | 必需(插件依赖其 API) | -> 本 mod 与 KubeJS 为**可选依赖关系**(mods.toml 中 `kubejs` 标记为 optional): -> 未安装 KubeJS 时本 mod 不加载插件部分,不影响游戏启动。 +> KubeJS 在 `neoforge.mods.toml` 中声明为 **optional**: +> 未安装 KubeJS 时本 mod 不会加载插件部分,不影响游戏启动。 -## 构建 +--- -工程位于 `modules/tc-kubejs-bridge`,属多 module Gradle 工程: +## 构建与安装 + +工程位于 `modules/tc-kubejs-bridge`,属于多 module Gradle 工程。 ```bash # 在仓库根目录执行 gradle :tckubejsbridge:build ``` -产物:`modules/tc-kubejs-bridge/build/libs/KubeJS Thaumcraft-+.jar` -(例:`KubeJS Thaumcraft-1.0.0+21.1.234.jar`) +构建产物: -编译依赖(本地 jar,路径在 `build.gradle` 中可改): -- KubeJS:`F:/downloads/kubejs-neoforge-2101.7.2-build.368.jar` -- Rhino:`F:/downloads/rhino-2101.2.7-build.81.jar`(KubeJS 的脚本引擎) -- Thaumcraft:`modules/thaumicenergistics-neo/library/thaumcraft-0.2.2.34-port.1.jar` +```text +modules/tc-kubejs-bridge/build/libs/KubeJS Thaumcraft-+.jar +``` -> 若本地路径不同,请修改 `build.gradle` 中 `dependencies` 的 `files(...)` 路径。 +例如: -## 脚本用法 +```text +KubeJS Thaumcraft-1.0.0+21.1.234.jar +``` -将脚本放入 `kubejs/server_scripts/`,然后**重启游戏或执行 `/reload` 生效**(见"注意事项"第 1 条)。 +把 jar 放入 `mods/` 文件夹即可。 + +--- + +## 快速开始 + +1. 在 `kubejs/server_scripts/` 下新建一个 `.js` 文件。 +2. 在脚本里使用 `ServerEvents.recipes(event => { ... })`。 +3. 保存后执行 `/reload`,或重启游戏。 + +所有 `thaumcraft.xxx` 都是**顶层绑定**,不要写成 `event.thaumcraft.xxx`。 + +--- + +## 配方注册 ### 1. 注魔配方 ```js ServerEvents.recipes(event => { event.recipes.thaumcraft.infusion( - 'MY_INFUSION', // research 键(全局唯一,用于魔导手册/去重) - 'minecraft:diamond', // 输出 - 3, // 不稳定性 - ['ignis 64', 'potentia 32'],// 源质:要素 tag + 数量 - 'minecraft:golden_apple', // 中心物品 - 'minecraft:blaze_rod', // 祭品(可多个) + 'MY_INFUSION', // research 键(全局唯一) + 'minecraft:diamond', // 输出 + 3, // 不稳定性 + ['ignis 64', 'potentia 32'], // 所需源质 + 'minecraft:golden_apple', // 中心物品 + 'minecraft:blaze_rod', // 祭品 'minecraft:ender_pearl' ); }); @@ -65,8 +91,8 @@ ServerEvents.recipes(event => { event.recipes.thaumcraft.crucible( 'MY_CRUCIBLE', 'minecraft:diamond', - 'minecraft:coal', // 催化剂 - { aer: 16, ignis: 8 } // 对象写法 + 'minecraft:coal', // 催化剂 + { aer: 16, ignis: 8 } // 对象写法 ); }); ``` @@ -79,7 +105,7 @@ ServerEvents.recipes(event => { 'MY_ARCANE', 'minecraft:diamond_sword', ['ignis 16', 'ordo 8'], - [' I ', ' I ', ' S '], // pattern:最多 3 行 + [' I ', ' I ', ' S '], // pattern:最多 3 行 { I: 'minecraft:iron_ingot', S: 'minecraft:stick' } ); }); @@ -87,37 +113,50 @@ ServerEvents.recipes(event => { ### 4. 奥术无序合成配方 +> 注意:`arcane_shapeless` 的材料必须放在**一个数组**里。 + ```js ServerEvents.recipes(event => { event.recipes.thaumcraft.arcane_shapeless( 'MY_SHAPELESS', 'minecraft:diamond_helmet', ['aer 8', 'aqua 8'], - 'minecraft:leather_helmet', - 'minecraft:diamond', - 'minecraft:diamond', - 'minecraft:diamond' + [ + 'minecraft:leather_helmet', + 'minecraft:diamond', + 'minecraft:diamond', + 'minecraft:diamond' + ] ); }); ``` -### 5. 物品固有要素 +--- + +## 物品固有要素 ```js ServerEvents.recipes(event => { - event.thaumcraft.addObjectTag('minecraft:stick', { aer: 1, ignis: 2 }); + // 添加 + thaumcraft.addObjectTag('minecraft:stick', { aer: 1, ignis: 2 }); + + // 移除 + thaumcraft.removeObjectTag('minecraft:stick'); }); ``` -### 6. 替换(先删后注册)已有配方 +--- + +## 替换已有配方 ```js ServerEvents.recipes(event => { - // 注意:thaumcraft 是顶层绑定,不是 event.thaumcraft - thaumcraft.removeInfusion('INFUSIONPROVIDER'); // 按 research 键删除 TC 注魔配方 - // thaumcraft.removeArcane('RESEARCH_KEY'); // 按 research 键删除奥术配方 + // 先删 + thaumcraft.removeInfusion('INFUSIONPROVIDER'); // 注魔 + // thaumcraft.removeArcane('RESEARCH_KEY'); // 奥术 + // thaumcraft.removeCrucible('RESEARCH_KEY'); // 坩埚 - // 然后注册新配方,实现真正替换 + // 后注册,实现真正替换 event.recipes.thaumcraft.infusion( 'INFUSIONPROVIDER', 'thaumicenergistics:infusion_provider', @@ -129,6 +168,96 @@ ServerEvents.recipes(event => { }); ``` +--- + +## 自定义研究分类 / 研究节点 + +### 添加研究分类 + +```js +ServerEvents.recipes(event => { + thaumcraft.addResearchCategory( + 'MY_CATEGORY', + 'textures/research/cat_icon.png', + 'textures/research/cat_bg.png' + ); +}); +``` + +- 两个参数也可以:`addResearchCategory('MY_CATEGORY', 'textures/research/cat_icon.png')` +- 不带命名空间的路径默认使用 `kubejs:` 命名空间。 + +### 添加研究节点 + +```js +ServerEvents.recipes(event => { + thaumcraft.addResearchNode({ + key: 'MY_RESEARCH', + category: 'MY_CATEGORY', + icon: 'textures/research/my_research.png', + parents: ['PARENT_KEY'], + autoUnlock: true, // 默认解锁 + virtual: false, // 显示在魔导手册中;true 会隐藏 + hidden: false, + col: 0, + row: 0, + complexity: 1, + aspects: { aer: 1, ignis: 1 } + }); +}); +``` + +支持字段: + +| 字段 | 说明 | +|---|---| +| `key` | 研究节点唯一 ID | +| `category` | 所属研究分类,必须已存在 | +| `icon` | 图标路径 | +| `parents` / `parentsHidden` / `siblings` | 研究依赖关系 | +| `autoUnlock` | 是否默认解锁 | +| `virtual` | 是否虚拟节点(`true` 不会显示在魔导手册) | +| `hidden` / `concealed` / `lost` | 可见性控制 | +| `special` / `stub` / `secondary` / `round` | 研究类型标记 | +| `col` / `row` / `complexity` | 研究树位置和复杂度 | +| `aspects` | 研究关联要素 | + +### 图标文件位置 + +脚本里写: + +```js +icon: 'textures/research/my_research.png' +``` + +对应文件: + +```text +kubejs/assets/kubejs/textures/research/my_research.png +``` + +也可以写完整路径: + +```js +icon: 'kubejs:textures/research/my_research.png' +``` + +### 研究名称 / 描述 + +TC 的研究名称通过语言文件翻译。 +在 `kubejs/assets/kubejs/lang/zh_cn.json` 中添加: + +```json +{ + "tc.research_category.MY_CATEGORY": "我的分类", + "tc.research_name.MY_RESEARCH": "我的研究", + "tc.research_text.MY_RESEARCH": "这是研究描述。" +} +``` + +英文则写在 `kubejs/assets/kubejs/lang/en_us.json`。 + +--- ## 要素 tag 参考(TC4 移植版实际值) **注意:移植版的要素 tag 是拉丁文原名,不是 TC4 汉化/英文名。** @@ -153,50 +282,65 @@ ServerEvents.recipes(event => { | 贪婪 GREED | `lucrum` | 工艺 CRAFT | `fabrico` | | 布料 CLOTH | `pannus` | 机制 MECHANISM | `machina` | -## 注意事项 +## 热重载说明 -1. **支持热重载**:改脚本后执行 `/reload` 即可生效,无需重启游戏。 - - KubeJS 每次 `/reload` 都会重新执行 `ServerEvents.recipes`,脚本会再次运行。 - - 若脚本使用"先删后注册"(`thaumcraft.removeInfusion(...)` 再 `infusion(...)`)写法, - 删除步骤会清掉本 mod 的去重键并移除 TC 内存列表中的旧配方,因此新配方能正确重新注册。 - - 纯新增(不先删)的脚本重复 `/reload` 会被去重拦截(`skipping`),请配合 - `thaumcraft.clearRegistered()` 使用或在脚本开头调用它。 -2. **research 键全局唯一**:重复键会被跳过(防止重复注册)。如需同名配方,请改键。 -3. **配方不进原版 RecipeManager**:TC 注魔/炼金/奥术配方是 TC 自己的内存列表。 - 本桥接在脚本执行时直接调用 TC API 注册,并对 KubeJS 输出无害占位配方, - 不影响其他 mod 的配方操作(schema 的 `typeOverride` 指向 `minecraft:crafting_shapeless` 以满足 KubeJS 的 serializer 查找)。 -4. **源质列表两种写法**:数组 `['ignis 64']` 或对象 `{ignis: 64}` 均可。 -5. **物品参数**:支持物品 id 字符串(`minecraft:xxx` / `ae2:xxx`)、`Item.of(...)`、物品 tag(`#minecraft:planks`)。 -6. **AE2 物品命名空间是 `ae2`**:如 AE2 接口是 `ae2:interface`,不是 `appliedenergistics2:interface`。 -7. **KubeJS 版本兼容**:桥接层基于 KubeJS 2101.7.x 的 RecipeSchema API(标注 - `@ApiStatus.Experimental`),升级 KubeJS 小版本后如遇编译错误,可能需要同步调整。 +| 内容 | `/reload` | 说明 | +|---|---|---| +| 注魔 / 坩埚 / 奥术配方 | ✅ | 可以热重载 | +| 物品固有要素 | ✅ | 可以热重载 | +| 研究分类 / 研究节点 | ❌ | TC 研究系统缓存较多,需要重启游戏 | + +### research 键说明 + +- 如果 `research` 不是 TC 已有研究,桥接会自动注册一个**默认解锁的虚拟研究节点**,并按“无研究要求”处理。 +- 如果 `research` 是 TC 已有研究,则仍然要求玩家完成对应研究。 +- `research` 键全局唯一,重复键会被跳过。 + + + + + + + + + + + + + + + + ## 排障 | 现象 | 原因 | |---|---| -| 日志 `Serializer for type thaumcraft:infusion is not found` | 缺 `typeOverride`(已内置修复,重新构建即可) | -| 日志 `Unknown aspect 'xxx'` | 要素 tag 写错,对照上面要素表 | -| 日志 `Item with ID xxx does not exist` | 物品 id 错误,注意 `ae2:` 命名空间 | -| 日志无任何 `TC-KubeJS-Bridge` 输出 | mod 未装入 mods、或脚本未放入 `server_scripts` | -| 脚本报 `event.thaumcraft is undefined` | 顶层绑定用 `thaumcraft.xxx`,不要写 `event.thaumcraft.xxx` | +| `Constructor for thaumcraft:arcane_shapeless with N arguments not found` | 材料没有放在数组里 | +| `Unknown aspect 'xxx'` | 要素 tag 写错 | +| `Item with ID xxx does not exist` | 物品 id 错误 | +| 研究分类名字显示为 `tc.research_category.XXX` | 缺少语言文件 | +| 研究图标 / 背景紫黑色 | 贴图不存在、路径不对、不是 PNG,或 KubeJS assets 未被加载 | +| `event.thaumcraft is undefined` | 顶层绑定是 `thaumcraft.xxx`,不是 `event.thaumcraft.xxx` | ## 项目结构 -``` +```text src/main/java/tckubejsbridge/ ├── TCKubeJSBridge.java // @Mod 主类 └── kubejs/ - ├── TCKubeJSPlugin.java // KubeJS 插件:注册 schema + bindings - ├── TCRecipeKJS.java // 自定义 KubeRecipe 基类(桥接 + 占位序列化) - ├── TCRecipeBridge.java // TC API 调用 + 去重 + 移除 + 占位 JSON - ├── AspectListComponent.java // 源质列表 RecipeComponent + ├── TCKubeJSPlugin.java // KubeJS 插件:schema + bindings + ├── TCRecipeKJS.java // 自定义 KubeRecipe 基类 + ├── TCRecipeBridge.java // TC API 调用 / 去重 / 移除 / 研究注册 + ├── AspectListComponent.java // 源质列表组件 ├── InfusionRecipeKJS.java // 注魔配方 - ├── CrucibleRecipeKJS.java // 坩埚炼金配方 + ├── CrucibleRecipeKJS.java // 坩埚配方 ├── ShapedArcaneRecipeKJS.java // 奥术有序配方 └── ShapelessArcaneRecipeKJS.java // 奥术无序配方 + src/main/resources/ ├── kubejs.plugins.txt // 插件发现文件 └── META-INF/neoforge.mods.toml + example/infusion_example.js // 示例脚本 ``` diff --git a/README_EN.md b/README_EN.md index 066a23e..bb32f75 100644 --- a/README_EN.md +++ b/README_EN.md @@ -2,12 +2,21 @@ > **English** | [中文](README.md) -Bridges the **Thaumcraft 4 (port)** recipe system to **KubeJS**, allowing modpack authors and -server admins to register TC infusion, crucible, arcane crafting recipes and item aspect tags -directly from KubeJS scripts — including **replacing existing recipes**. +Bridges the **Thaumcraft 4 (port)** recipe and research systems to **KubeJS**. + +Modpack authors can use scripts in `kubejs/server_scripts/` to register: + +- Infusion recipes +- Crucible recipes +- Shaped / shapeless arcane recipes +- Item aspect tags +- Custom research categories / research nodes +- Replacing existing TC recipes Author: AlkSur | License: GPL-3.0 +--- + ## Requirements | Mod | Version | Relation | @@ -20,40 +29,56 @@ Author: AlkSur | License: GPL-3.0 > KubeJS is declared **optional** in `neoforge.mods.toml`: without KubeJS the plugin part > simply does not load, and the game still starts. -## Building +--- -The project lives at `modules/tc-kubejs-bridge` inside a multi-module Gradle build: +## Building & Installation + +The project lives at `modules/tc-kubejs-bridge` inside a multi-module Gradle build. ```bash # Run from the repository root gradle :tckubejsbridge:build ``` -Artifact: `modules/tc-kubejs-bridge/build/libs/KubeJS Thaumcraft-+.jar` -(e.g. `KubeJS Thaumcraft-1.0.0+21.1.234.jar`) +Artifact: -Compile dependencies (local jars, paths editable in `build.gradle`): -- KubeJS: `F:/downloads/kubejs-neoforge-2101.7.2-build.368.jar` -- Rhino: `F:/downloads/rhino-2101.2.7-build.81.jar` (KubeJS script engine) -- Thaumcraft: `modules/thaumicenergistics-neo/library/thaumcraft-0.2.2.34-port.1.jar` +```text +modules/tc-kubejs-bridge/build/libs/KubeJS Thaumcraft-+.jar +``` -> Adjust the `files(...)` paths in the `dependencies` block if your local layout differs. +For example: -## Script Usage +```text +KubeJS Thaumcraft-1.0.0+21.1.234.jar +``` -Put scripts into `kubejs/server_scripts/`, then **restart the game or run `/reload`** (see Note #1). +Put the jar into your `mods/` folder. + +--- + +## Quick Start + +1. Create a `.js` file under `kubejs/server_scripts/`. +2. Use `ServerEvents.recipes(event => { ... })`. +3. Run `/reload` or restart the game. + +All `thaumcraft.xxx` calls are **top-level bindings**; do not use `event.thaumcraft.xxx`. + +--- + +## Recipe Registration ### 1. Infusion Recipe ```js ServerEvents.recipes(event => { event.recipes.thaumcraft.infusion( - 'MY_INFUSION', // research key (globally unique; used by Thaumonomicon/dedup) - 'minecraft:diamond', // result - 3, // instability - ['ignis 64', 'potentia 32'],// aspects: aspect tag + amount - 'minecraft:golden_apple', // central item - 'minecraft:blaze_rod', // components (one or more) + 'MY_INFUSION', // research key (globally unique) + 'minecraft:diamond', // result + 3, // instability + ['ignis 64', 'potentia 32'], // required aspects + 'minecraft:golden_apple', // central item + 'minecraft:blaze_rod', // component 'minecraft:ender_pearl' ); }); @@ -66,8 +91,8 @@ ServerEvents.recipes(event => { event.recipes.thaumcraft.crucible( 'MY_CRUCIBLE', 'minecraft:diamond', - 'minecraft:coal', // catalyst - { aer: 16, ignis: 8 } // object syntax also works + 'minecraft:coal', // catalyst + { aer: 16, ignis: 8 } // object syntax also works ); }); ``` @@ -80,7 +105,7 @@ ServerEvents.recipes(event => { 'MY_ARCANE', 'minecraft:diamond_sword', ['ignis 16', 'ordo 8'], - [' I ', ' I ', ' S '], // pattern: up to 3 rows + [' I ', ' I ', ' S '], // pattern: up to 3 rows { I: 'minecraft:iron_ingot', S: 'minecraft:stick' } ); }); @@ -88,37 +113,50 @@ ServerEvents.recipes(event => { ### 4. Shapeless Arcane Recipe +> Note: `arcane_shapeless` ingredients must be provided as **one array**. + ```js ServerEvents.recipes(event => { event.recipes.thaumcraft.arcane_shapeless( 'MY_SHAPELESS', 'minecraft:diamond_helmet', ['aer 8', 'aqua 8'], - 'minecraft:leather_helmet', - 'minecraft:diamond', - 'minecraft:diamond', - 'minecraft:diamond' + [ + 'minecraft:leather_helmet', + 'minecraft:diamond', + 'minecraft:diamond', + 'minecraft:diamond' + ] ); }); ``` -### 5. Item Aspect Tags +--- + +## Item Aspect Tags ```js ServerEvents.recipes(event => { - event.thaumcraft.addObjectTag('minecraft:stick', { aer: 1, ignis: 2 }); + // add + thaumcraft.addObjectTag('minecraft:stick', { aer: 1, ignis: 2 }); + + // remove + thaumcraft.removeObjectTag('minecraft:stick'); }); ``` -### 6. Replacing an Existing Recipe (remove first, then register) +--- + +## Replacing Existing Recipes ```js ServerEvents.recipes(event => { - // NOTE: thaumcraft is a TOP-LEVEL binding, not event.thaumcraft - thaumcraft.removeInfusion('INFUSIONPROVIDER'); // remove TC infusion recipes by research key - // thaumcraft.removeArcane('RESEARCH_KEY'); // remove arcane recipes by research key + // remove first + thaumcraft.removeInfusion('INFUSIONPROVIDER'); // infusion + // thaumcraft.removeArcane('RESEARCH_KEY'); // arcane + // thaumcraft.removeCrucible('RESEARCH_KEY'); // crucible - // then register the new recipe for a true replacement + // then register a new recipe event.recipes.thaumcraft.infusion( 'INFUSIONPROVIDER', 'thaumicenergistics:infusion_provider', @@ -130,6 +168,96 @@ ServerEvents.recipes(event => { }); ``` +--- + +## Custom Research Categories / Nodes + +### Add a Research Category + +```js +ServerEvents.recipes(event => { + thaumcraft.addResearchCategory( + 'MY_CATEGORY', + 'textures/research/cat_icon.png', + 'textures/research/cat_bg.png' + ); +}); +``` + +- A two-argument form is also available: `addResearchCategory('MY_CATEGORY', 'textures/research/cat_icon.png')` +- Paths without a namespace default to the `kubejs:` namespace. + +### Add a Research Node + +```js +ServerEvents.recipes(event => { + thaumcraft.addResearchNode({ + key: 'MY_RESEARCH', + category: 'MY_CATEGORY', + icon: 'textures/research/my_research.png', + parents: ['PARENT_KEY'], + autoUnlock: true, // unlocked by default + virtual: false, // visible in the Thaumonomicon; true hides it + hidden: false, + col: 0, + row: 0, + complexity: 1, + aspects: { aer: 1, ignis: 1 } + }); +}); +``` + +Supported fields: + +| Field | Description | +|---|---| +| `key` | Unique research node ID | +| `category` | Existing research category | +| `icon` | Icon path | +| `parents` / `parentsHidden` / `siblings` | Research dependencies | +| `autoUnlock` | Unlocked by default | +| `virtual` | Hidden from the Thaumonomicon when `true` | +| `hidden` / `concealed` / `lost` | Visibility flags | +| `special` / `stub` / `secondary` / `round` | Research type flags | +| `col` / `row` / `complexity` | Position and complexity on the research map | +| `aspects` | Associated aspects | + +### Icon File Location + +If you write: + +```js +icon: 'textures/research/my_research.png' +``` + +The file should be at: + +```text +kubejs/assets/kubejs/textures/research/my_research.png +``` + +You can also use a full path: + +```js +icon: 'kubejs:textures/research/my_research.png' +``` + +### Research Names / Descriptions + +TC research names use language files. +Add this to `kubejs/assets/kubejs/lang/en_us.json`: + +```json +{ + "tc.research_category.MY_CATEGORY": "My Category", + "tc.research_name.MY_RESEARCH": "My Research", + "tc.research_text.MY_RESEARCH": "This is a research description." +} +``` + +For Chinese, add the same keys to `kubejs/assets/kubejs/lang/zh_cn.json`. + +--- ## Aspect Tag Reference (actual values in the TC4 port) **Important: the port uses the original Latin tags, not the TC4 English/localized names.** @@ -154,57 +282,74 @@ ServerEvents.recipes(event => { | Greed | `lucrum` | Craft | `fabrico` | | Cloth | `pannus` | Mechanism | `machina` | -## Notes +## Hot Reload -1. **Hot-reload supported**: after editing scripts, run `/reload` — no game restart needed. - - KubeJS re-runs `ServerEvents.recipes` on every `/reload`, so scripts execute again. - - With the "remove-then-register" pattern (`thaumcraft.removeInfusion(...)` followed by - `infusion(...)`), the removal step clears this mod's dedup key and drops the old recipe - from TC's in-memory list, so the new recipe registers correctly. - - Scripts that only add recipes (without removing first) will be dedup-skipped on repeated - `/reload`; call `thaumcraft.clearRegistered()` at the top of the script to reset the set. -2. **Research keys are globally unique**: duplicate keys are skipped (prevents double - registration). Use a different key for same-name recipes. -3. **Recipes do NOT go into the vanilla RecipeManager**: TC infusion/crucible/arcane recipes - live in TC's own in-memory list. The bridge calls the TC API directly during script - execution and emits a harmless placeholder recipe to KubeJS, so it does not affect other - mods' recipe operations. (The schema uses `typeOverride = minecraft:crafting_shapeless` - so KubeJS can find a serializer.) -4. **Aspect lists accept two syntaxes**: array `['ignis 64']` or object `{ignis: 64}`. -5. **Item arguments** accept: item id strings (`minecraft:xxx` / `ae2:xxx`), `Item.of(...)`, - and item tags (`#minecraft:planks`). -6. **AE2's namespace is `ae2`**: e.g. the AE2 interface is `ae2:interface`, - NOT `appliedenergistics2:interface`. -7. **KubeJS version compatibility**: the bridge is built on KubeJS 2101.7.x RecipeSchema API - (marked `@ApiStatus.Experimental`). Upgrading KubeJS to another patch version may require - adjustments to the bridge layer. +| Content | `/reload` | Notes | +|---|---|---| +| Infusion / Crucible / Arcane recipes | ✅ | Can be hot-reloaded | +| Item aspect tags | ✅ | Can be hot-reloaded | +| Research categories / nodes | ❌ | Requires a game restart | + +### Research key notes + +- If `research` is **not** an existing TC research, the bridge registers a default-unlocked virtual research node and treats the recipe as having **no research requirement**. +- If `research` **is** an existing TC research, the recipe still requires that research to be completed. +- Research keys are globally unique; duplicate keys are skipped. + + + + + + + + + + + + + + + + + + + + + + + + + ## Troubleshooting | Symptom | Cause | |---|---| -| Log shows `Serializer for type thaumcraft:infusion is not found` | Missing `typeOverride` (already fixed in code; rebuild) | -| Log shows `Unknown aspect 'xxx'` | Wrong aspect tag — check the aspect table above | -| Log shows `Item with ID xxx does not exist` | Wrong item id — remember the `ae2:` namespace | -| No `TC-KubeJS-Bridge` lines in the log at all | Mod not installed, or script not in `server_scripts` | -| Script reports `event.thaumcraft is undefined` | Use the top-level `thaumcraft.xxx`, not `event.thaumcraft.xxx` | +| `Constructor for thaumcraft:arcane_shapeless with N arguments not found` | Ingredients were not placed in an array | +| `Unknown aspect 'xxx'` | Wrong aspect tag | +| `Item with ID xxx does not exist` | Wrong item id | +| Research category name shows `tc.research_category.XXX` | Missing language file | +| Research icon / background is purple-black | Texture missing, wrong path, not a PNG, or KubeJS assets not loaded | +| `event.thaumcraft is undefined` | Use top-level `thaumcraft.xxx`, not `event.thaumcraft.xxx` | ## Project Layout -``` +```text src/main/java/tckubejsbridge/ ├── TCKubeJSBridge.java // @Mod main class └── kubejs/ ├── TCKubeJSPlugin.java // KubeJS plugin: schemas + bindings - ├── TCRecipeKJS.java // Custom KubeRecipe base (bridge + placeholder serialization) - ├── TCRecipeBridge.java // TC API calls + dedup + removal + placeholder JSON - ├── AspectListComponent.java // Aspect list RecipeComponent + ├── TCRecipeKJS.java // Custom KubeRecipe base + ├── TCRecipeBridge.java // TC API calls / dedup / removal / research registration + ├── AspectListComponent.java // Aspect list component ├── InfusionRecipeKJS.java // Infusion recipe ├── CrucibleRecipeKJS.java // Crucible recipe ├── ShapedArcaneRecipeKJS.java // Shaped arcane recipe └── ShapelessArcaneRecipeKJS.java // Shapeless arcane recipe + src/main/resources/ ├── kubejs.plugins.txt // Plugin discovery file └── META-INF/neoforge.mods.toml + example/infusion_example.js // Example script ``` diff --git a/example/infusion_example.js b/example/infusion_example.js index 66d1c51..7bcf9a5 100644 --- a/example/infusion_example.js +++ b/example/infusion_example.js @@ -61,9 +61,9 @@ ServerEvents.recipes(event => { }); // ---------- 5. 物品固有要素 ---------- -// event.thaumcraft.addObjectTag(item, aspects) +// thaumcraft.addObjectTag(item, aspects) ServerEvents.recipes(event => { - event.thaumcraft.addObjectTag( + thaumcraft.addObjectTag( 'minecraft:stick', { aer: 1, ignis: 2 } ); diff --git a/gradle.properties b/gradle.properties index 58aac27..0c9bf3e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ # Project -mod_version=1.0.0 +mod_version=1.1.0 mod_group=tckubejsbridge # Mod diff --git a/src/main/java/tckubejsbridge/TCKubeJSBridge.java b/src/main/java/tckubejsbridge/TCKubeJSBridge.java index 9b42993..ea0d49f 100644 --- a/src/main/java/tckubejsbridge/TCKubeJSBridge.java +++ b/src/main/java/tckubejsbridge/TCKubeJSBridge.java @@ -10,7 +10,13 @@ import net.neoforged.fml.common.Mod; * - event.recipes.thaumcraft.crucible(...) 坩埚炼金配方 * - event.recipes.thaumcraft.arcane(...) 奥术有序合成配方 * - event.recipes.thaumcraft.arcane_shapeless(...) 奥术无序合成配方 - * - event.thaumcraft.addObjectTag(...) 物品固有要素 + * - thaumcraft.addObjectTag(...) 物品固有要素 + * - thaumcraft.removeObjectTag(...) 移除物品固有要素 + * - thaumcraft.removeInfusion(...) 移除注魔配方 + * - thaumcraft.removeArcane(...) 移除奥术配方 + * - thaumcraft.removeCrucible(...) 移除坩埚配方 + * - thaumcraft.addResearchCategory(...) 添加研究分类 + * - thaumcraft.addResearchNode(...) 添加研究节点 * * 实际注册逻辑在 {@link tckubejsbridge.kubejs.TCKubeJSPlugin}(KubeJS 插件)。 */ diff --git a/src/main/java/tckubejsbridge/kubejs/TCKubeJSPlugin.java b/src/main/java/tckubejsbridge/kubejs/TCKubeJSPlugin.java index f762422..78bfbe1 100644 --- a/src/main/java/tckubejsbridge/kubejs/TCKubeJSPlugin.java +++ b/src/main/java/tckubejsbridge/kubejs/TCKubeJSPlugin.java @@ -30,7 +30,13 @@ import java.util.Map; *
  • event.recipes.thaumcraft.crucible(...) — 坩埚炼金配方
  • *
  • event.recipes.thaumcraft.arcane(...) — 奥术有序合成配方
  • *
  • event.recipes.thaumcraft.arcane_shapeless(...) — 奥术无序合成配方
  • - *
  • event.thaumcraft.addObjectTag(...) — 物品固有要素
  • + *
  • thaumcraft.addObjectTag(...) — 物品固有要素
  • + *
  • thaumcraft.removeObjectTag(...) — 移除物品固有要素
  • + *
  • thaumcraft.removeInfusion(...) — 移除注魔配方
  • + *
  • thaumcraft.removeArcane(...) — 移除奥术配方
  • + *
  • thaumcraft.removeCrucible(...) — 移除坩埚配方
  • + *
  • thaumcraft.addResearchCategory(...) — 添加研究分类
  • + *
  • thaumcraft.addResearchNode(...) — 添加研究节点
  • * * * 发现方式:{@code kubejs.plugins.txt} 列出本类全名。 @@ -125,7 +131,7 @@ public class TCKubeJSPlugin implements KubeJSPlugin { bindings.add("thaumcraft", new ThaumcraftBindings()); } - /** 脚本侧绑定:event.thaumcraft.addObjectTag(...) / removeInfusion(...) 等 */ + /** 脚本侧顶层绑定:thaumcraft.addObjectTag(...) / removeInfusion(...) 等 */ public static final class ThaumcraftBindings { public void addObjectTag(ItemStack item, AspectList aspects) { if (item == null || item.isEmpty()) { @@ -136,6 +142,30 @@ public class TCKubeJSPlugin implements KubeJSPlugin { ConsoleJS.SERVER.info("TC-KubeJS-Bridge: registered object tag for " + item); } + /** 移除指定物品已注册的固有要素 */ + public boolean removeObjectTag(ItemStack item) { + if (item == null || item.isEmpty()) { + ConsoleJS.SERVER.error("TC-KubeJS-Bridge: removeObjectTag got empty item"); + return false; + } + return TCRecipeBridge.removeObjectTag(item); + } + + /** 添加新的研究分类;icon/background 为贴图路径,默认使用 kubejs 命名空间 */ + public boolean addResearchCategory(String key, String icon) { + return TCRecipeBridge.addResearchCategory(key, icon, icon); + } + + /** 添加新的研究分类;icon/background 为贴图路径,默认使用 kubejs 命名空间 */ + public boolean addResearchCategory(String key, String icon, String background) { + return TCRecipeBridge.addResearchCategory(key, icon, background); + } + + /** 添加新的研究节点;settings 支持 key/category/icon/parents/autoUnlock/virtual 等字段 */ + public boolean addResearchNode(Map settings) { + return TCRecipeBridge.addResearchNode(settings); + } + /** 按 research 键移除 TC 已有注魔配方(先删后注册 = 真正替换) */ public boolean removeInfusion(String research) { return TCRecipeBridge.removeInfusion(research); @@ -146,7 +176,15 @@ public class TCKubeJSPlugin implements KubeJSPlugin { return TCRecipeBridge.removeArcane(research); } - /** 清空去重集合(/reload 时若需重新注册可调用) */ + /** 按 research 键移除 TC 已有坩埚配方 */ + public boolean removeCrucible(String research) { + return TCRecipeBridge.removeCrucible(research); + } + + /** + * 兼容旧脚本保留的方法。 + * 现在 /reload 去重基于 TC 内存配方列表自动判断,通常不需要手动调用。 + */ public void clearRegistered() { TCRecipeBridge.clearRegistered(); } diff --git a/src/main/java/tckubejsbridge/kubejs/TCRecipeBridge.java b/src/main/java/tckubejsbridge/kubejs/TCRecipeBridge.java index 30331d3..89594b4 100644 --- a/src/main/java/tckubejsbridge/kubejs/TCRecipeBridge.java +++ b/src/main/java/tckubejsbridge/kubejs/TCRecipeBridge.java @@ -2,11 +2,25 @@ package tckubejsbridge.kubejs; import com.google.gson.JsonObject; import dev.latvian.mods.kubejs.script.ConsoleJS; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.Container; +import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; import thaumcraft.api.ThaumcraftApi; import thaumcraft.api.aspects.AspectList; +import thaumcraft.api.crafting.CrucibleRecipe; +import thaumcraft.api.crafting.InfusionRecipe; +import thaumcraft.api.crafting.ShapedArcaneRecipe; +import thaumcraft.api.crafting.ShapelessArcaneRecipe; +import thaumcraft.api.research.ResearchCategories; +import thaumcraft.api.research.ResearchItem; +import thaumcraft.common.research.TCResearchManager; +import java.util.ArrayList; import java.util.HashSet; +import java.util.List; +import java.util.Map; import java.util.Set; /** @@ -18,19 +32,40 @@ import java.util.Set; */ public final class TCRecipeBridge { - private static final Set REGISTERED_RESEARCH = new HashSet<>(); + /** 记录由本桥接自动创建的默认解锁研究键 */ + private static final Set AUTO_CREATED_RESEARCH = new HashSet<>(); private TCRecipeBridge() { } - /** 按研究键去重,防止 /reload 重复注册 */ - private static boolean alreadyRegistered(String research) { - return !REGISTERED_RESEARCH.add(research); + /** + * 保留给旧脚本调用的兼容方法。 + * 现在去重直接基于 TC 内存配方列表判断,/reload 时无需手动清空。 + */ + public static void clearRegistered() { + // no-op } - /** 清空去重集合(供 KubeJS 事件在 /reload 时调用,允许重新注册) */ - public static void clearRegistered() { - REGISTERED_RESEARCH.clear(); + private static synchronized boolean hasInfusion(String research) { + return ThaumcraftApi.getCraftingRecipes().stream() + .filter(InfusionRecipe.class::isInstance) + .map(InfusionRecipe.class::cast) + .anyMatch(r -> research.equals(r.research())); + } + + private static synchronized boolean hasArcane(String research) { + return ThaumcraftApi.getCraftingRecipes().stream().anyMatch(r -> { + if (r instanceof ShapedArcaneRecipe sr) return research.equals(sr.research); + if (r instanceof ShapelessArcaneRecipe slr) return research.equals(slr.research); + return false; + }); + } + + private static synchronized boolean hasCrucible(String research) { + return ThaumcraftApi.getCraftingRecipes().stream() + .filter(CrucibleRecipe.class::isInstance) + .map(CrucibleRecipe.class::cast) + .anyMatch(cr -> research.equals(cr.key)); } /** @@ -39,44 +74,298 @@ public final class TCRecipeBridge { * * @return 是否至少移除了一条 */ - public static boolean removeInfusion(String research) { + public static synchronized boolean removeInfusion(String research) { boolean removed = ThaumcraftApi.getCraftingRecipes().removeIf(r -> - r instanceof thaumcraft.api.crafting.InfusionRecipe ir && research.equals(ir.research())); + r instanceof InfusionRecipe ir && research.equals(ir.research())); if (removed) { ConsoleJS.SERVER.info("TC-KubeJS-Bridge: removed infusion recipe(s) for research '" + research + "'"); } - // 允许该键后续重新注册 - REGISTERED_RESEARCH.remove(research); return removed; } /** 按 research 键从 TC 配方列表中移除已有的奥术(有序/无序)配方 */ - public static boolean removeArcane(String research) { + public static synchronized boolean removeArcane(String research) { boolean removed = ThaumcraftApi.getCraftingRecipes().removeIf(r -> { - if (r instanceof thaumcraft.api.crafting.ShapedArcaneRecipe sr) return research.equals(sr.research); - if (r instanceof thaumcraft.api.crafting.ShapelessArcaneRecipe slr) return research.equals(slr.research); + if (r instanceof ShapedArcaneRecipe sr) return research.equals(sr.research); + if (r instanceof ShapelessArcaneRecipe slr) return research.equals(slr.research); return false; }); if (removed) { ConsoleJS.SERVER.info("TC-KubeJS-Bridge: removed arcane recipe(s) for research '" + research + "'"); } - // 允许该键后续重新注册 - REGISTERED_RESEARCH.remove(research); return removed; } - public static void registerInfusion(String research, ItemStack result, int instability, - AspectList aspects, ItemStack central, ItemStack... components) { - if (alreadyRegistered(research)) { + /** 按 research 键从 TC 配方列表中移除已有的坩埚配方 */ + public static synchronized boolean removeCrucible(String research) { + boolean removed = ThaumcraftApi.getCraftingRecipes().removeIf(r -> + r instanceof CrucibleRecipe cr && research.equals(cr.key)); + if (removed) { + ConsoleJS.SERVER.info("TC-KubeJS-Bridge: removed crucible recipe(s) for research '" + research + "'"); + } + return removed; + } + + /** 移除指定物品已注册的固有要素 */ + public static synchronized boolean removeObjectTag(ItemStack item) { + if (item == null || item.isEmpty()) { + return false; + } + boolean removed = ThaumcraftApi.objectTags.remove(ThaumcraftApi.ObjectTagKey.of(item)) != null; + if (removed) { + ConsoleJS.SERVER.info("TC-KubeJS-Bridge: removed object tag for " + item); + } + return removed; + } + + /** 添加新的研究分类 */ + public static synchronized boolean addResearchCategory(String key, String icon, String background) { + if (key == null || key.isBlank() || icon == null || icon.isBlank()) { + ConsoleJS.SERVER.error("TC-KubeJS-Bridge: addResearchCategory requires key and icon"); + return false; + } + ResourceLocation iconRL = parseAssetLocation(icon); + ResourceLocation backgroundRL = parseAssetLocation(background != null && !background.isBlank() ? background : icon); + if (iconRL == null || backgroundRL == null) { + ConsoleJS.SERVER.error("TC-KubeJS-Bridge: invalid icon/background path for research category '" + key + "'"); + return false; + } + ResearchCategories.registerCategory(key, iconRL, backgroundRL); + ConsoleJS.SERVER.info("TC-KubeJS-Bridge: registered research category '" + key + "'"); + return true; + } + + /** 添加新研究节点 */ + public static synchronized boolean addResearchNode(Map settings) { + if (settings == null) { + ConsoleJS.SERVER.error("TC-KubeJS-Bridge: addResearchNode requires a settings object"); + return false; + } + String key = str(settings, "key"); + String category = str(settings, "category"); + if (key == null || key.isBlank() || category == null || category.isBlank()) { + ConsoleJS.SERVER.error("TC-KubeJS-Bridge: addResearchNode requires key and category"); + return false; + } + if (ResearchCategories.getResearchList(category) == null) { + ConsoleJS.SERVER.error("TC-KubeJS-Bridge: research category '" + category + "' does not exist; call addResearchCategory first"); + return false; + } + + ResourceLocation icon = parseAssetLocation(str(settings, "icon")); + ResearchItem item; + if (icon != null) { + int col = integer(settings, "col", 0); + int row = integer(settings, "row", 0); + int complexity = integer(settings, "complexity", 1); + AspectList aspects; + try { + aspects = parseAspects(settings.get("aspects")); + } catch (Exception e) { + ConsoleJS.SERVER.error("TC-KubeJS-Bridge: invalid aspects for research '" + key + "': " + e.getMessage()); + return false; + } + item = new ResearchItem(key, category, aspects, col, row, complexity, icon); + } else { + item = new ResearchItem(key, category); + } + + if (bool(settings, "autoUnlock", false)) item.setAutoUnlock(); + if (bool(settings, "virtual", false)) item.setVirtual(); + if (bool(settings, "hidden", false)) item.setHidden(); + if (bool(settings, "concealed", false)) item.setConcealed(); + if (bool(settings, "lost", false)) item.setLost(); + if (bool(settings, "special", false)) item.setSpecial(); + if (bool(settings, "stub", false)) item.setStub(); + if (bool(settings, "secondary", false)) item.setSecondary(); + if (bool(settings, "round", false)) item.setRound(); + + List parents = stringList(settings, "parents"); + if (!parents.isEmpty()) item.setParents(parents.toArray(String[]::new)); + List parentsHidden = stringList(settings, "parentsHidden"); + if (!parentsHidden.isEmpty()) item.setParentsHidden(parentsHidden.toArray(String[]::new)); + List siblings = stringList(settings, "siblings"); + if (!siblings.isEmpty()) item.setSiblings(siblings.toArray(String[]::new)); + + item.registerResearchItem(); + ConsoleJS.SERVER.info("TC-KubeJS-Bridge: registered research node '" + key + "'"); + return true; + } + + /** 将脚本里写的路径解析为 ResourceLocation;不带命名空间时默认放到 kubejs 资源包命名空间 */ + private static ResourceLocation parseAssetLocation(String path) { + if (path == null || path.isBlank()) { + return null; + } + String s = path.trim(); + if (s.startsWith("/")) { + s = s.substring(1); + } + try { + if (!s.contains(":")) { + return ResourceLocation.fromNamespaceAndPath("kubejs", s); + } + return ResourceLocation.tryParse(s); + } catch (Exception e) { + return null; + } + } + + private static String str(Map map, String key) { + Object value = map.get(key); + return value == null ? null : String.valueOf(value); + } + + private static boolean bool(Map map, String key, boolean def) { + Object value = map.get(key); + return value instanceof Boolean b ? b : def; + } + + private static int integer(Map map, String key, int def) { + Object value = map.get(key); + return value instanceof Number n ? n.intValue() : def; + } + + private static List stringList(Map map, String key) { + Object value = map.get(key); + if (value == null) { + return List.of(); + } + if (value instanceof Iterable iterable) { + List out = new ArrayList<>(); + for (Object o : iterable) { + if (o != null) { + out.add(String.valueOf(o)); + } + } + return out; + } + if (value instanceof Object[] array) { + List out = new ArrayList<>(); + for (Object o : array) { + if (o != null) { + out.add(String.valueOf(o)); + } + } + return out; + } + if (value instanceof String s) { + return List.of(s); + } + return List.of(); + } + + private static AspectList parseAspects(Object from) { + if (from == null) { + return new AspectList(); + } + return AspectListComponent.INSTANCE.wrap(null, from); + } + + /** 判断该 research key 是否为 TC 已注册/已知的研究(未知的自定义键按“无研究要求”处理) */ + private static boolean isKnownResearch(String research) { + return research != null && !research.isEmpty() && ResearchCategories.getResearch(research) != null; + } + + /** + * 为未知的自定义 research key 注册一个虚拟、自动解锁的研究节点。 + * 这样 key 在 TC 研究系统中存在,且默认对所有玩家解锁。 + */ + private static synchronized void ensureDefaultResearch(String research) { + if (research == null || research.isEmpty() || isKnownResearch(research)) { + return; + } + new ResearchItem(research, "INFUSION") + .setAutoUnlock() + .setVirtual() + .registerResearchItem(); + AUTO_CREATED_RESEARCH.add(research); + } + + /** + * 注魔配方:对于未知的自定义 research key,不强制玩家完成对应研究。 + * 做法是临时清空 research 再调用父类匹配,匹配后恢复原 key, + * 因此去重、移除、JEI 展示仍使用脚本里写的原始 key。 + */ + private static final class NoResearchInfusionRecipe extends InfusionRecipe { + NoResearchInfusionRecipe(String research, ItemStack result, int instability, + AspectList aspects, ItemStack central, ItemStack... components) { + super(research, result, instability, aspects, central, components); + } + + @Override + public boolean matches(List suppliedComponents, ItemStack central, Player player) { + if (player != null) { + TCResearchManager.ensureAutoUnlocks(player); + } + String original = this.research; + this.research = ""; + try { + return super.matches(suppliedComponents, central, player); + } finally { + this.research = original; + } + } + } + + /** 奥术有序配方:未知 research key 同样不强制研究解锁 */ + private static ShapedArcaneRecipe noResearchArcane(String research, ItemStack result, + AspectList aspects, Object... recipe) { + return new ShapedArcaneRecipe(research, result, aspects, recipe) { + @Override + public boolean matches(Container inventory, Level level, Player player) { + if (player != null) { + TCResearchManager.ensureAutoUnlocks(player); + } + String original = this.research; + this.research = ""; + try { + return super.matches(inventory, level, player); + } finally { + this.research = original; + } + } + }; + } + + /** 奥术无序配方:未知 research key 同样不强制研究解锁 */ + private static ShapelessArcaneRecipe noResearchArcaneShapeless(String research, ItemStack result, + AspectList aspects, Object... recipe) { + return new ShapelessArcaneRecipe(research, result, aspects, recipe) { + @Override + public boolean matches(Container inventory, Level level, Player player) { + if (player != null) { + TCResearchManager.ensureAutoUnlocks(player); + } + String original = this.research; + this.research = ""; + try { + return super.matches(inventory, level, player); + } finally { + this.research = original; + } + } + }; + } + + public static synchronized void registerInfusion(String research, ItemStack result, int instability, + AspectList aspects, ItemStack central, ItemStack... components) { + if (hasInfusion(research)) { ConsoleJS.SERVER.info("TC-KubeJS-Bridge: infusion recipe '" + research + "' already registered, skipping"); return; } - ThaumcraftApi.addInfusionCraftingRecipe(research, result, instability, aspects, central, components); + if (isKnownResearch(research) && !AUTO_CREATED_RESEARCH.contains(research)) { + ThaumcraftApi.addInfusionCraftingRecipe(research, result, instability, aspects, central, components); + } else { + ensureDefaultResearch(research); + ThaumcraftApi.getCraftingRecipes().add( + new NoResearchInfusionRecipe(research, result, instability, aspects, central, components)); + } ConsoleJS.SERVER.info("TC-KubeJS-Bridge: registered infusion recipe '" + research + "'"); } - public static void registerCrucible(String research, ItemStack result, Object catalyst, AspectList aspects) { - if (alreadyRegistered(research)) { + public static synchronized void registerCrucible(String research, ItemStack result, Object catalyst, AspectList aspects) { + if (hasCrucible(research)) { ConsoleJS.SERVER.info("TC-KubeJS-Bridge: crucible recipe '" + research + "' already registered, skipping"); return; } @@ -84,21 +373,31 @@ public final class TCRecipeBridge { ConsoleJS.SERVER.info("TC-KubeJS-Bridge: registered crucible recipe '" + research + "'"); } - public static void registerArcane(String research, ItemStack result, AspectList aspects, Object... recipe) { - if (alreadyRegistered(research)) { + public static synchronized void registerArcane(String research, ItemStack result, AspectList aspects, Object... recipe) { + if (hasArcane(research)) { ConsoleJS.SERVER.info("TC-KubeJS-Bridge: arcane recipe '" + research + "' already registered, skipping"); return; } - ThaumcraftApi.addArcaneCraftingRecipe(research, result, aspects, recipe); + if (isKnownResearch(research) && !AUTO_CREATED_RESEARCH.contains(research)) { + ThaumcraftApi.addArcaneCraftingRecipe(research, result, aspects, recipe); + } else { + ensureDefaultResearch(research); + ThaumcraftApi.getCraftingRecipes().add(noResearchArcane(research, result, aspects, recipe)); + } ConsoleJS.SERVER.info("TC-KubeJS-Bridge: registered arcane recipe '" + research + "'"); } - public static void registerArcaneShapeless(String research, ItemStack result, AspectList aspects, Object... recipe) { - if (alreadyRegistered(research)) { + public static synchronized void registerArcaneShapeless(String research, ItemStack result, AspectList aspects, Object... recipe) { + if (hasArcane(research)) { ConsoleJS.SERVER.info("TC-KubeJS-Bridge: arcane shapeless recipe '" + research + "' already registered, skipping"); return; } - ThaumcraftApi.addShapelessArcaneCraftingRecipe(research, result, aspects, recipe); + if (isKnownResearch(research) && !AUTO_CREATED_RESEARCH.contains(research)) { + ThaumcraftApi.addShapelessArcaneCraftingRecipe(research, result, aspects, recipe); + } else { + ensureDefaultResearch(research); + ThaumcraftApi.getCraftingRecipes().add(noResearchArcaneShapeless(research, result, aspects, recipe)); + } ConsoleJS.SERVER.info("TC-KubeJS-Bridge: registered arcane shapeless recipe '" + research + "'"); }