本篇教程由作者设定使用 CC BY-NC-SA 协议。
前言
阅读这篇教程前,您需注意:
没有安装 ProbeJS 或未正确执行 dump 时,VSC 无法为您的 KubeJS 开发提供 JS 基础语法之外的任何帮助;
即使安装了 ProbeJS,像 Notepad 这样的普通文本编辑器也无法为您提供帮助。
所以,请您先检查以下内容,以确保您在阅读教程之后内容时,能正确地理解并使用。
您已经阅读过 JS 语法相关的教程,或您有 JS 或其他编程语言的基础;
您安装了 ProbeJS 模组;
您为 VSC 安装了 ProbeJS 扩展插件;
您正确地执行了 dump。以下是执行 dump 的方法与 dump 成功时的提示:
对于 ProbeJS 6.X 版本(常见于 1.20.1),输入 /probejs dump 执行。执行成功后,会在聊天框中弹出白色的文本:
ProbeJS typing generation finished. [<本次 dump 花费的时间>]
上文用<>包围的内容,为“必填内容”。其表示“<必填内容>”会被其所描述的文本替代。
对于 ProbeJS 7.X 版本(常见于 1.21.1),输入 /probejs dump,或在模组环境变动时 ProbeJS 主动触发执行。执行成功后,会在聊天框中弹出绿色的文本:
脚本类型 startup 的生成已完成。
脚本类型 client 的生成已完成。
脚本类型 server 的生成已完成。
脚本类型 package 的生成已完成。
您使用 VSC 打开了包含 kubejs 文件夹和 .probe 文件夹的工作区。
对于 KubeJS 6.X 版本,.probe 文件夹包含在 kubejs 文件夹内,但推荐打开整个版本文件夹作为工作区;
对于 KubeJS 7.X 版本,.probe 文件夹包含在版本文件夹内,需要打开整个版本文件夹作为工作区。
确认以上内容无误后,请您继续阅读教程的剩余部分。
类型查询与报错
注意:以下举例为 KubeJS 7.X,但 6.X 仅有部分类名不同。
在开发中,当我们遇到不认识的方法时,可能会因为不清楚方法传参而触发错误。一个典型的传参错误如下:
ServerEvents.recipes(event => {
...
// 使用 Ingredient.of() 方法,但错误地传入了三个参数
let test = Ingredient.of("minecraft:apple", 1, {})
...
})此处为报错的内容。为表示方便,我们在每个重点的后方换行。
main.js#2
Error in 'ServerEvents.recipes': ...
Exception: Can't find method ...
IngredientWrapper.of(string,number,object).
main.js#2:表示出现错误的文件与行数,此处出错的文件是 main.js。#2 表示错误出在第二行。此处行数不完全与编辑器内行数对应。
Error in Error in 'ServerEvents.recipes': 表示出现错误的位置。ServerEvents.recipes 是对配方进行操作的方法。
Exception: Can't find method: 表示出现错误的类型。这里出现的错误是无法找到传入对应参数的方法。
IngredientWrapper.of(string,number,object): 表示出现错误的方法。这里我们使用了 Ingredient.of()方法,但传入了错误的参数。
现在已知我们传入的参数错误。如何纠错?此时 ProbeJS 为我们提供的提示就发挥了作用。
鼠标指向 of,会出现一个悬浮提示框。最主要的文本如下,依然在每个重点的后方换行:
(method)
$IngredientWrapper["of"]
(ingredient: $Ingredient$$Type, count: integer)
: $SizedIngredient
(+1 overload)
(method): 表示当前是一个方法。
$IngredientWrapper["of"]:表示我们使用的方法是 $IngredientWrapper 类下的 of 方法。
(Ingredient: $Ingredient$$Type, count: integer):表示方法的传参。此处传递的参数为 $Ingredient$$Type 类型的量,和 integer 类型的量。
: $SizedIngredient:表示方法的返回类型。传入对应参数,调用此方法后,返回一个这样的量。
(+1 overload):表示这个方法还有另一个重载。不严谨地说,重载表示名称相同,但传参不同且执行不同的方法。
如此可得 Ingredient.of() 可以传入 2 个参数(另一个重载是 1 个参数)。我们知道 integer 是整数的意思,但 $Ingredient$$Type 是什么?
我们按住 Ctrl,同时对 of 点击鼠标左键。这个动作可以查询类、方法等的定义和引用。此时会跳转到 .probe 文件夹下的某一个以 .d.ts 结尾的文件。而我们的光标处就是 of 方法。
static "of"(ingredient: $Ingredient$$Type, count: integer): $SizedIngredient
和悬浮提示框相同,在这里出现了 $Ingredient$$Type 这个类型。同样对其按 Ctrl+左键 查询。
跳转到又一个文件后,我们就能看到 $Ingredient$$Type 的全貌,在每个重点的后方换行:
type $Ingredient$$Type =
($ItemStack$$Type) |
(($Ingredient$$Type)[]) |
(RegExp) | ("*") | ("-") |
(`#${Special.ItemTag}`) |
(`@${Special.Mod}`) |
(`%${Special.CreativeModeTab}`)
此时我们可以对其进行分析。两个内容中间的 | 表示或,也即需要 $Ingredient$$Type 的参数可以传入以下的任意一个。
我们可以传入一个 $ItemStack$$Type。您可以自行查询它的内容,但这里最常传入的是"<namespace>:<itemid>",如"minecraft:apple"。
一个 `#${Sepcial.ItemTag}`。可以传入 "#<namespace>:<tagid>",如"#minecraft:logs"。
一个 `@{Special.Mod}`。可以传入 "@<modid>",如"@create"。
以上三个最为常用。其它的还有:
一个 "*"。代表所有物品。
一个 RegExp。代表一个正则表达式。请自行搜索用法。
一个 `%${Special.CreativeModeTab}`。可以传入创造模式物品栏。
杂项
@ 的使用
ProbeJS 为我们提供补全,但通常只有在能接受对应类型的地方才会自动弹出补全。如果我们在其他地方也需要这样的补全,ProbeJS 也为我们提供了方法。
只要输入@,再输入要补全的内容,按下 Tab 或手动选择,就能触发补全。
@item_tag // 按 Tab 或选择补全
能够补全的内容有很多,包括但不限于:
@item, @block, @fluid, @item_tag, @block_tag, @fluid_tag, @creative_mode_tab 等。
部分其他模组的内容也能补全,如 @create:fan_processing_type,会补全出 "create_splashing" 等。
# 的使用
这一章的内容会为您介绍 VSC 和 ProbeJS 中 # 的实用方法。
Region
方法在传入的参数较多较长时,通常都会换行。长列表,方法体也会换行。此时我们可以将鼠标移动到左边行号的右侧,点击出现的箭头,就能将其收起。
∨ ServerEvents.recipes(event => {
// 一配方
// 另一堆配方
// 还有一坨配方
}> ServerEvents.recipes(event => { ...
}如果我们需要一个代码块也能像这样被收起,可以使用 #region。
//#region
// 一块代码
// 另一堆代码
// 还有一坨代码
//#endregion
// 远方的代码
此时,//#region 到 //#endregion 中间的代码块便可以被收起。
//#region ...
// 远方的代码
在实际使用时,我们可以输入 # 来唤起补全。
我们在使用 Region 时,可以为其添加名称。简单地将其写在 //#region 后面即可。
//#region 从别人那抄来的石山 ...
//#region 群里大佬施舍的代码 ...
//#region 大语言模型拉的一坨 ...
ProbeJS 提供的补全
在输入 # 时,我们注意到除了 #region 相关的补全之外,还有其他的补全。这些补全部分是由 ProbeJS 提供的,现在我们来了解它们的用法。
注意:以下内容仅在 KubeJS 7.X 和 ProbeJS 7.X 环境下测试。无法保证在 KubeJS 6.X 环境能够使用。
ignored
// ignored: true
这行注释将这份文件标记为忽略。KubeJS 将跳过对这份文件的加载。在文件对应的日志中,我们能发现如下一行:
[<时间>] [INFO] Skipped server_scripts:main.js: Ignored
即表示 main.js 被跳过加载了。
itemstack
#itemstack // 按 Tab 补全为 "1x minecraft:name_tag"
这串字符串能被大部分配方所识别为对应的物品及数量。为了理解简单,这里我们不引入 ItemStack。
此时,光标会自动选中前面的数字部分,方便我们修改。再次按下 Tab 后,光标会选中后面的 minecraft:name_tag 部分。此时我们能修改这个部分,并且能使用 @item 的补全。
priority
// priority: 1000
这行注释表示这份文件的加载优先级。优先级越高,在同类脚本里 KubeJS 就越先加载它。
如果我们遇到以下情况:明明在脚本中定义了一个量,跨文件访问却抛出 undefined 异常。例如:
main.js#2: Error in 'ServerEvents.recipes': dev.latvian.mods.rhino.EcmaError: ReferenceError: "id" is not defined.
这告诉我们有可能是加载顺序错误。我们可以查看文件对应的日志。
[<时间>] [INFO] Loaded script server_scripts:main.js in 0.001 s
[<时间>] [INFO] Loaded script server_scripts:dependencies.js in 0.001 s // 前置脚本在主脚本后加载
此时,我们就可以在前置文件中添加 // priority 注释,使其在主文件之前加载。
recipes
快捷生成一个配方注册的代码块。
// 以下是快捷补全后的代码块
ServerEvents.recipes(event => {
const { } = event.recipes
})
我们注意到,在常量声明后,ProbeJS 为我们添加了一个大括号。我们的光标也处于大括号中。这是 JavaScript 语法中的解构。
解构的具体用法请自行查询,示例:
// 以下两行等价
const son = father.son
const { son } = father
所以我们可以在其中填入 recipes 下对应的对象。如:
ServerEvents.recipes(event => {
// 对 event.recipes.minecraft 进行快捷访问
const { minecraft } = event.recipes
minecraft.smelting(
// 创建熔炉配方
)
})requires
这行注释表示这份文件加载需要加载的模组。
// requires: <模组ID>
条件不满足时,对应的日志,我们能发现如下一行:
[<时间>] [INFO] Skipped server_scripts:main.js: Mod <你填入的模组ID> is not loaded
uuid
生成一个随机的UUID。
"94918b5d-7a02-4e2a-a387-637e2680b16f" // 一串随机生成的UUID
packmode
待编写。
JSDoc
为函数编写
JS 是一种弱类型和动态类型的语言。这意味着已初始化变量的类型能随时改变。同时,JS 的方法不能指定,也不会检测传入参数的类型。
因此,当你制造一个轮子,也就是辅助方法时,传入的参数都是 any 类型,也就不能进行补全。更糟糕的是,在调用方法时,由于传入的参数是 any 类型,我们也无法直接享受 ProbeJS 带来的补全。
注意:以下举例为 KubeJS 7.X,但 6.X 仅有部分类名不同。
ServerEvents.recipes(event => {
const { minecraft } = event.recipes
// 调用方法
createSmeltingRecipe("minecraft:apple", "minecraft:golden_apple")
// 方法需要 input 和 output 两个参数,但使用时无法得知其具体的类型
function createSmeltingRecipe(input, output) {
return minecraft.smelting(output, input)
}
})鼠标指向 createSmeltingRecipe 时,我们可以看到:
(local function) createSmeltingRecipe(input: any, output: any): $Smelting
这证明我们的方法接受两个 any 参数。实际上,传入不正确的参数依然会报错。
此时,我们可以为方法编写 JSDoc 注释。
VSC 已经为我们提供了简易快捷的方法。要开始编写 JSDoc,我们在方法声明上方新增一行,然后输入 /**。接受弹出的补全,然后:
/**
*
* @param {*} input
* @param {*} output
* @returns
*/
从上往下,我们依次介绍每一行的作用:
空行:这里可以为方法添加说明。调用方法时,其会显示在方法传参返回值之下的第一行。
@param 行:这里可以为传入的参数指定类型与添加说明。大括号里的 * 表示此处接受 any 类型,行尾空格之后,我们可以为参数添加说明。
@returns 行:这里可以为方法的输出添加说明。由于输出为方法内代码决定,我们无法更改输出的类型。
我们试着输入以下内容:
/**
* 不知道为什么包装了 event.recipes.minecraft.smelting 的方法,它甚至还是小驼峰命名。
* @param {import("net.minecraft.world.item.crafting.Ingredient").$Ingredient$$Type} input 配方的输入。
* @param {import("net.minecraft.world.item.ItemStack").$ItemStack$$Type} output 配方的输出。
* @returns 配方本身。
*/
由于整个 import 语句过长,我们可以直接输入 $Ingredient$$Type 然后选择补全,自动生成 import 语句。
然后回到 createSmeltingRecipe 处,我们发现:
(local function) createSmeltingRecipe(input: $Ingredient$$Type, output: import("net.minecraft.world.item.ItemStack").$ItemStack$$Type): $Smelting
不知道为什么包装了 event.recipes.minecraft.smelting 的方法,它甚至还是小驼峰命名。
@param input 配方的输入。
@param output 配方的输出。
@returns 配方本身。我们为整个方法指定了输入类型,为其本身、输入、输出添加了说明。此时我们再调用方法就会更加方便。
关于更加具体的使用方法,请查阅 CryChic 文档上的内容。
为变量编写
除了在声明方法时会遇到上述情况,在处理部分变量时,我们同样会遇到类型不明,或类型被错误标记的情况。
此时,我们可以通过编写 JSDoc 注释来为变量指定类型。
EntityEvents.death("minecraft:player", e => {
/**
* @type {$ServerPlayer}
* */
let player = e.getEntity()
player.tell("杂鱼~") // 一些只有 ServerPlayer 能执行的逻辑
})

