更新
This commit is contained in:
@@ -15,3 +15,6 @@ findings.md
|
||||
progress.md
|
||||
/modules/thaumicenergistics-neo/存储体系方案.md
|
||||
/modules/thaumicenergistics-neo/计划书-配方溯源.md
|
||||
/.reasonix/
|
||||
/.tmp_*/
|
||||
__pycache__/
|
||||
|
||||
@@ -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-<mod_version>+<neoforge_version>.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-<mod_version>+<neoforge_version>.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` |
|
||||
|
||||
## 注意事项
|
||||
## 热重载说明
|
||||
|
||||
| 内容 | `/reload` | 说明 |
|
||||
|---|---|---|
|
||||
| 注魔 / 坩埚 / 奥术配方 | ✅ | 可以热重载 |
|
||||
| 物品固有要素 | ✅ | 可以热重载 |
|
||||
| 研究分类 / 研究节点 | ❌ | TC 研究系统缓存较多,需要重启游戏 |
|
||||
|
||||
### research 键说明
|
||||
|
||||
- 如果 `research` 不是 TC 已有研究,桥接会自动注册一个**默认解锁的虚拟研究节点**,并按“无研究要求”处理。
|
||||
- 如果 `research` 是 TC 已有研究,则仍然要求玩家完成对应研究。
|
||||
- `research` 键全局唯一,重复键会被跳过。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
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 小版本后如遇编译错误,可能需要同步调整。
|
||||
|
||||
## 排障
|
||||
|
||||
| 现象 | 原因 |
|
||||
|---|---|
|
||||
| 日志 `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 // 示例脚本
|
||||
```
|
||||
|
||||
+211
-66
@@ -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-<mod_version>+<neoforge_version>.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-<mod_version>+<neoforge_version>.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
|
||||
|
||||
| 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.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
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.
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
@@ -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 }
|
||||
);
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
# Project
|
||||
mod_version=1.0.0
|
||||
mod_version=1.1.0
|
||||
mod_group=tckubejsbridge
|
||||
|
||||
# Mod
|
||||
|
||||
@@ -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 插件)。
|
||||
*/
|
||||
|
||||
@@ -30,7 +30,13 @@ import java.util.Map;
|
||||
* <li>event.recipes.thaumcraft.crucible(...) — 坩埚炼金配方</li>
|
||||
* <li>event.recipes.thaumcraft.arcane(...) — 奥术有序合成配方</li>
|
||||
* <li>event.recipes.thaumcraft.arcane_shapeless(...) — 奥术无序合成配方</li>
|
||||
* <li>event.thaumcraft.addObjectTag(...) — 物品固有要素</li>
|
||||
* <li>thaumcraft.addObjectTag(...) — 物品固有要素</li>
|
||||
* <li>thaumcraft.removeObjectTag(...) — 移除物品固有要素</li>
|
||||
* <li>thaumcraft.removeInfusion(...) — 移除注魔配方</li>
|
||||
* <li>thaumcraft.removeArcane(...) — 移除奥术配方</li>
|
||||
* <li>thaumcraft.removeCrucible(...) — 移除坩埚配方</li>
|
||||
* <li>thaumcraft.addResearchCategory(...) — 添加研究分类</li>
|
||||
* <li>thaumcraft.addResearchNode(...) — 添加研究节点</li>
|
||||
* </ul>
|
||||
*
|
||||
* 发现方式:{@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();
|
||||
}
|
||||
|
||||
@@ -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<String> REGISTERED_RESEARCH = new HashSet<>();
|
||||
/** 记录由本桥接自动创建的默认解锁研究键 */
|
||||
private static final Set<String> 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<String> parents = stringList(settings, "parents");
|
||||
if (!parents.isEmpty()) item.setParents(parents.toArray(String[]::new));
|
||||
List<String> parentsHidden = stringList(settings, "parentsHidden");
|
||||
if (!parentsHidden.isEmpty()) item.setParentsHidden(parentsHidden.toArray(String[]::new));
|
||||
List<String> 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<String> stringList(Map<?, ?> map, String key) {
|
||||
Object value = map.get(key);
|
||||
if (value == null) {
|
||||
return List.of();
|
||||
}
|
||||
if (value instanceof Iterable<?> iterable) {
|
||||
List<String> out = new ArrayList<>();
|
||||
for (Object o : iterable) {
|
||||
if (o != null) {
|
||||
out.add(String.valueOf(o));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
if (value instanceof Object[] array) {
|
||||
List<String> 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<ItemStack> 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 + "'");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user