本篇教程由作者设定使用 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).
  1. main.js#2:表示出现错误的文件与行数,此处出错的文件是 main.js。#2 表示错误出在第二行。此处行数不完全与编辑器内行数对应。

  2. Error in Error in 'ServerEvents.recipes': 表示出现错误的位置。ServerEvents.recipes 是对配方进行操作的方法。

  3. Exception: Can't find method: 表示出现错误的类型。这里出现的错误是无法找到传入对应参数的方法

  4. IngredientWrapper.of(string,number,object): 表示出现错误的方法。这里我们使用了 Ingredient.of()方法,但传入了错误的参数

现在已知我们传入的参数错误。如何纠错?此时 ProbeJS 为我们提供的提示就发挥了作用。

鼠标指向 of,会出现一个悬浮提示框。最主要的文本如下,依然在每个重点的后方换行:

(method)
$IngredientWrapper["of"]
(ingredient: $Ingredient$$Type, count: integer)
: $SizedIngredient
(+1 overload)
  1. (method): 表示当前是一个方法。

  2. $IngredientWrapper["of"]:表示我们使用的方法是 $IngredientWrapper 类下的 of 方法。

  3. (Ingredient: $Ingredient$$Type, count: integer):表示方法的传参。此处传递的参数为 $Ingredient$$Type 类型的量,和 integer 类型的量。

  4. : $SizedIngredient:表示方法的返回类型。传入对应参数,调用此方法后,返回一个这样的量。

  5. (+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 
*/

从上往下,我们依次介绍每一行的作用:

  1. 空行:这里可以为方法添加说明。调用方法时,其会显示在方法传参返回值之下的第一行。

  2. @param 行:这里可以为传入的参数指定类型添加说明。大括号里的 * 表示此处接受 any 类型,行尾空格之后,我们可以为参数添加说明。

  3. @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 能执行的逻辑
})