diff --git a/docs/.vitepress/zh_cn.ts b/docs/.vitepress/zh_cn.ts index 233f916..b31404c 100644 --- a/docs/.vitepress/zh_cn.ts +++ b/docs/.vitepress/zh_cn.ts @@ -18,6 +18,7 @@ export const zh_cn = defineConfig({ { text: '事件', link: '/zh-cn/Events' }, { text: '命令', link: '/zh-cn/Commands' }, { text: '表达式', link: '/zh-cn/Expressions' }, + { text: '斜杠命令', link: '/zh-cn/BrigadierCommands' }, ] }, { @@ -25,6 +26,7 @@ export const zh_cn = defineConfig({ items: [ { text: '事件', link: '/zh-cn/develop/Events'}, { text: '命令', link: '/zh-cn/develop/Commands'} + { text: '表达式', link: '/zh-cn/develop/Expressions' } ] }, { diff --git a/docs/zh-cn/BrigadierCommands.md b/docs/zh-cn/BrigadierCommands.md new file mode 100644 index 0000000..8b23412 --- /dev/null +++ b/docs/zh-cn/BrigadierCommands.md @@ -0,0 +1,74 @@ +# 斜杠命令 + +命令官模组引入了一系列 `/` 样式的命令。这些命令都要求权限等级 2. + +## `cmd:arithmetica` + +这个命令可以让你在聊天栏中运行表达式。情境:`minecraft:level`(存档),`minecraft:origin`(维度)和可选的 `minecraft:this_entity`(实体)。这个命令需要一个字符串类型的表达式,以及可选的数据类型转换。 + +``` +cmd:arithmetica -> + \- expression + \- cast (默认:无) +``` + +示例:`cmd:arithmetica "sin(level.getDayTime)" bool` + +## `cmd:explode` + +这个简单的命令可以生成爆炸。它的结构树如下: + +``` +cmd:explode -> + \- entity (爆炸的造成者) + \- position + \- [power] (默认:4) + \- [fire] (默认:否) + \- position + \- [power] (默认:4) + \- [fire] (默认:否) +``` + +示例:`cmd:explode @s ~ ~ ~ 6.4 true` + +## `cmd:data` + +这一命令使你能够读写持久型数据,以便在后续的表达式中使用。提供的数据必须是数字或字符串。它的结构树如下: + +::: details 结构树 +``` +cmd:data + \- read (读取) + \- level (存档) + \- key (键值) + \- chunk (区块) + \- position (坐标位置) + \- key (键值) + \- entity (实体) + \- entity (实体) + \- key (键值) + \- block_entity (方块实体) + \- position (坐标位置) + \- key (键值) + \- write (写入) + \- level (存档) + \- key (键值) + \- data (数据) + \- chunk (区块) + \- position (坐标位置) + \- key (键值) + \- data (数据) + \- entity (实体) + \- entity (实体) + \- key (键值) + \- data (数据) + \- block_entity (方块实体) + \- position (坐标位置) + \- key (键值) + \- data (数据) +``` +::: + +读写示例:`cmd:data read entity @s "my_test_data"` 或 `cmd:data write entity @s "my_test_data" "Hello "` + +使用示例:`this_entity.storage.my_test_data + 'World!'` diff --git a/docs/zh-cn/Commands.md b/docs/zh-cn/Commands.md index a2e8b4b..11c6335 100644 --- a/docs/zh-cn/Commands.md +++ b/docs/zh-cn/Commands.md @@ -6,7 +6,7 @@ `condition` 字段可以应用于所有类型的命令,它能够在当下事件的情境中执行。这一字段使用了原版的条件系统,这个工具 [misode.github.io](https://misode.github.io/predicate/) 可以帮你快速创建条件。 -部分种类的命令有额外参数。 +部分种类的命令有额外形式参数。 尽管本模组尽量避免在事件外使用命令,其他的项目还是可能把命令整合到其他情境中。这就不在支持范围内了。 @@ -80,7 +80,9 @@ $(bool){{0}} 0 -> false ### `commander:all_of`, `commander:any_of`, `commander:defaulted` 这三种类型的功能相似。如果 condition(条件)返回 true,就执行在可选的 `then` 代码块中的命令。 -注意:就算条件在很前面就返回 true 了,还是会执行全部命令! +`all_of` 要求所有命令成功执行,`any_of` 要求其中任一命令成功执行,`defaulted` 要求其中任一命令执行失败。 + +注意,就算条件在很前面就返回 true 了,还是会执行全部命令!但是,在启用 `short_circuit` 后,条件不满足时,将立即退出执行。 ::: details 示例 ```json @@ -148,3 +150,43 @@ $(bool){{0}} 0 -> false } ``` ::: +::: + +### `commander:store_expression_data` 和 `commander:store_nbt_data` + +你可以通过它们将数据写入本模组的数据存储中。它们是 JSON 版本的 [`/cmd:data`](/zh-cn/BrigadierCommands#cmd-data) 命令。 + +这两个命令都需要以下实际参数: + +| 实际参数 | 描述 | +|---|---| +| `target` | 写入数据的目标。可以是 `level`(存档),`chunk`(区块),`entity`(实体),`block_entity`(方块实体)。 | +| `selector` | 用来选择指定种类的常规选择器。 | +| `key` | 被存储数据的标识用键值。 | + +`nbt` 命令需要一个静态数或字符串(`element` 字段)。 + +`expression` 命令需要一个能返回数字或字符串的表达式(`expression` 字段)。 + +::: details 示例 +```json +{ + "type": "commander:store_expression_data", + "target": "level", + "selector": "origin", + "key": "cmd_my_cool_data", + "expression": "random(0, 2)" +} +``` + +
+ +```json +{ + "type": "commander:store_nbt_data", + "target": "entity", + "selector": "this_entity", + "key": "cmd_my_cool_data", + "element": 45 +} +``` diff --git a/docs/zh-cn/Events.md b/docs/zh-cn/Events.md index 3b96ed8..33e4467 100644 --- a/docs/zh-cn/Events.md +++ b/docs/zh-cn/Events.md @@ -4,7 +4,7 @@ ## 订阅文件的引入 -当你订阅文件中声明一个事件时,实际上是在标识你想要订阅的事件。事件可以在 `parameters` 代码块中接受额外的参数,但(目前)内置事件并不要求具备它们。订阅文件将在 `commander/events` 下被读取。 +在订阅文件中声明一个事件,意味着标识你想要监听的事件。事件可以在 `parameters` 代码块中接受额外的形式参数,但(目前)内置事件并不要求具备它们。订阅文件将在 `commander/events` 下被读取。 ``` |- recipes @@ -41,7 +41,7 @@ ``` ::: -订阅文件可以订阅不同的(或者一样的)事件。 +订阅文件可以监听不同的(或者一样的)事件。 ::: details 示例 ```json diff --git a/docs/zh-cn/Expressions.md b/docs/zh-cn/Expressions.md index b87a950..0fe4aa5 100644 --- a/docs/zh-cn/Expressions.md +++ b/docs/zh-cn/Expressions.md @@ -2,19 +2,52 @@ 命令官模组凭借动态实体数据,实现了运算表达式的功能。 -算术使用 [EvalEx](https://ezylang.github.io/EvalEx/) 来实现运算功能。掌握它并非必要,但多有好处。 +算术使用 [EvalEx](https://ezylang.github.io/EvalEx/) 来实现运算功能。掌握它并非必要,但多有好处。这里的区别是,所有函数、变量、常量都对大小写有严格的准确度要求,并且所有函数名都是驼峰式大小写(除第一个词语,其它首字母大写,不加标点),而不是蛇式(全大写,且用_划分)。 ## 额外功能 本模组在 EvalEx 的基础上,添加了一些额外功能。 -| 函数 | 描述 | 参数 | 示例 | +这里的“匿名函数”指带有 `it` 形式参数的一般表达式。比如:`arrayFind(arrayOf(0, 1, 2), it == 1)`。匿名函数的形式参数都有 `λ` 作为标记。 + +可变实际参数(VarArgs)都有 `...` 标记(你可以无限指定实际参数) + +::: details 计算相关 + +| 函数 | 描述 | 实参 | 示例 | +|---|---|---|---| +| `random` | 在指定范围内生成随机数。 | `min`, `max` | `random(0, 23)` | +| `clamp` | 将其他两个实际参数约束在范围内。 | `value`, `min`, `max` | `clamp(12, 14, 16)` | +| `lerp` | 平滑地将初值过渡到末值。 | `delta`, `start`, `end` | `lerp(0.5, 10, 16)` | + +::: + +::: details 数组 + +所有的函数都会构建新的数组,不对原本的造成影响。 + +| 函数 | 描述 | 实参 | 示例 | +|---|---|---|---| +| `arrayOf` | 指定对象构建数组。 | `args...` | `arrayOf(0, 23)` | +| `arrayMap` | 对数组中的所有对象应用更改。 | `array`, `function(λ)` | `arrayMap(arrayOf(0,1,2), sqrt(it))` | +| `arrayFind` | 过滤掉所有不符合条件的对象。 | `array`, `predicate(λ)` | `arrayFind(arrayOf(0,1,2), it == 1)` | +| `arrayAnyMatch` | 检查是否数组中存在符合条件的对象。 | `array`, `predicate(λ)` | `arrayAnyMatch(arrayOf(0,1,2), it == 1)` | +| `arrayNoneMatch` | 检查是否数组中完全没有符合条件的对象。 | `array`, `predicate(λ)` | `arrayNoneMatch(arrayOf(0,1,2), it == 1)` | +| `arrayAllMatch` | 检查是否数组中的全部对象符合条件。 | `array`, `predicate(λ)` | `arrayAllMatch(arrayOf(0,1,2), it == 1)` | + +::: + +::: details 杂项 + +| 函数 | 描述 | 实参 | 示例 | |---|---|---|---| -| `random` | 在指定范围内生成随机数。 | `min`, `max` | `random(0, 23)` | -| `clamp` | 将其他两个值约束在范围内。 | `value`, `min`, `max` | `clamp(12, 14, 16)` | -| `lerp` | 平滑地将初值过渡到末值。 | `delta`, `start`, `end` | `lerp(0.5, 10, 16)` | -| `structContainsKey` | 检查结构是否包含指定键值。 | `struct`, `key` | `structContainsKey(this_entity, "getHealth")` | -| `hasContext` | 检查是否有指定语境。 | `key` | `hasContext("killer_entity")` | +| `structContainsKey` | 检查是否结构中包含指定键值。 | `struct`, `key...` | `structContainsKey(block_state.properties, 'candles')` | +| `hasContext` | 检查是否表达式的形式参数可用。 | `key...` | `hasContext('tool')` | +| `length` | 返回指定对象的长度或 0。 | `value` | `length('Hello World!')` | +| `strFormat` | 将字符串转化为指定格式。 | `pattern`, `args...` | `strFormat('Hello %s World!', 23)` | +| `ifMatches` | 类似于内置的 `if`,但介入匿名函数。 | `value`, `predicate(λ)`, `ifTrue(λ)`, `ifFalse(λ)` | `ifMatches(arrayFind(arrayOf(0,1,2), it == 1), length(it) > 0, it[0], 0)` | + +::: ## 读取情境数据 @@ -37,8 +70,62 @@ origin.reverse.y minecraft:level.getDayTime ``` +### 命令官拓展: + +为拓展用途,本模组为对象新增了一些特别的字段。 + +::: details `nbt` (物品,实体,方块实体) + +可用于物品,实体和方块实体。你能够用它读取对象的 NBT 数据。尽管便捷,这个检查的性能不尽人意,频繁调用(比如每刻)可能会导致游戏卡顿。 + +示例:`this_entity.nbt.Air` + +::: + +::: details `properties`(方块状态) + +可用于状态,比如方块状态。 + +示例:`block_state.properties.candles` + +::: + +::: details `attributes`(活体) + +你可以用它获取活体的数据。这个键值是标识符,所以可以写 `generic.luck`,也可以写 `minecraft:generic.luck`。 + +示例:`this_entity.attributes.'generic.luck'` + +::: + +::: details `storage`(存档,区块,实体,方块实体) + +你可以用它获取持久型数据。这一数据可以通过 [`/cmd:data`](/zh-cn/BrigadierCommands#cmd-data),[`commander:store_nbt_data`](/zh-cn/Commands#commander-store-expression-data-and-commander-store-nbt-data) 或 [`commander:store_expression_data`](/zh-cn/Commands#commander-store-expression-data-and-commander-store-nbt-data) 来读取或修改。 + +::: + +### P.S. + +如果你不得不在表达式中进行复杂的检查,你可以考虑延迟执行它,如果可以,你还可以通过 `level.getDayTime % 20 == 0` 为表达式添加 1 秒的“冷却”。同理,`% 40` 对应 2 秒,`% 10` 对应 0.5 秒。 + 表达式使用的是 Mojang 的映射(首次载入时下载),这意味着几乎所有公共的字段和 get 类型的方法都可以在表达式里使用。如果本模组无法设置映射,你可能需要依赖于平台的名称(比如 Fabric 的中间映射)。 +::: details 一些例子 + +这是原版矿车速度的计算公式: + +`if(this_entity.isInWater, 4, 8) / 20`, and furnace: `if(this_entity.isInWater, 3, 4) / 20` + +*** + +这个表达式能够将游戏时间转化为现实时间。https://bukkit.org/threads/how-can-i-convert-minecraft-long-time-to-real-hours-and-minutes.122912/ + +`strFormat('%02.0f:%02.0f', floor((level.getDayTime / 1000 + 8) % 24), floor(60 * (level.getDayTime % 1000) / 1000))` + +所以它的结果会是:`00:00`,或 `13:45` 这样的。 + +::: + ## 使用表达式 通过使用 `cmd:arithmetica` 命令,你可以快速上手。记得用 `"` 括住表达式来满足格式要求。 diff --git a/docs/zh-cn/develop/Commands.md b/docs/zh-cn/develop/Commands.md index 9fed5ee..6c5dd7e 100644 --- a/docs/zh-cn/develop/Commands.md +++ b/docs/zh-cn/develop/Commands.md @@ -4,7 +4,7 @@ ## 创建命令 -创建新的命令很简单,你要做的是:实现 `Command` 接口,通过创建一个[编码器](https://forge.gemwire.uk/wiki/Codecs)来序列化或反序列化命令,然后用 `CommandTypes.register()` 来注册编码器。 +创建新的命令很简单,你要做的是:实现 `Command` 接口,通过创建一个[映射编解码器](https://forge.gemwire.uk/wiki/Codecs)来序列化或反序列化命令,然后用 `CommandTypes.register()` 来注册编解码器。 让我们创建一个能够将字符串转化为标准输出的命令。 @@ -24,10 +24,10 @@ public record DummyCommand(String text) implements Command { } ``` -接下来,我们再为这个命令[编码器](https://forge.gemwire.uk/wiki/Codecs)创建一个编码器。 +接下来,我们再为这个命令[编解码器](https://forge.gemwire.uk/wiki/Codecs)创建一个编解码器。 ```java -public static final Codec CODEC = Codec.STRING.fieldOf("text").xmap(DummyCommand::new, DummyCommand::text).codec(); +public static final MapCodec CODEC = Codec.STRING.fieldOf("text").xmap(DummyCommand::new, DummyCommand::text); ``` 再然后,我们需要注册这一命令来获取 `CommandType`。 @@ -52,7 +52,7 @@ public CommandType type() { ```java context.lootContext().getWorld(); //返回服务端世界 -context.lootContext().get(LootContextParameters.TOOL); //如果不存在,返回参数或空值。 +context.lootContext().get(LootContextParameters.TOOL); //如果不存在,返回形式参数或空值。 -context.lootContext().requireParameter(LootContextParameters.TOOL); //如果不存在,返回参数或抛出异常。 +context.lootContext().requireParameter(LootContextParameters.TOOL); //如果不存在,返回形式参数或抛出异常。 ``` diff --git a/docs/zh-cn/develop/Events.md b/docs/zh-cn/develop/Events.md index 979b49c..3e05390 100644 --- a/docs/zh-cn/develop/Events.md +++ b/docs/zh-cn/develop/Events.md @@ -20,7 +20,7 @@ EventType.builder() .build(new Identifier("modid", "custom_event")); ``` -通过使用 `extension()`,事件可以接受额外的参数。在指定扩展后,你可以返回自定义类型。 +通过使用 `extension()`,事件可以接受额外的形式参数。在指定扩展后,你可以返回自定义类型。 ```java EventType.builder() @@ -53,7 +53,7 @@ private static LootContext makeContext(ServerWorld world, Entity entity, Vec3d o 如果你指定了扩展,或者使用了不受支持的返回类型,就需要编写自定义的解析逻辑。 -为了编写这一逻辑,你需要创建 `EventContext`。`EventContext` 用于把执行参数传递给命令。 +为了编写这一逻辑,你需要创建 `EventContext`。`EventContext` 用于把执行形式参数传递给命令。 ```java EventContext context = EventContext.builder(type) diff --git a/docs/zh-cn/develop/Expressions.md b/docs/zh-cn/develop/Expressions.md new file mode 100644 index 0000000..43c1d14 --- /dev/null +++ b/docs/zh-cn/develop/Expressions.md @@ -0,0 +1,170 @@ +# 表达式 + +正如在[表达式](/zh-cn/Expressions),章节所介绍的那样,命令官模组引入了一个具有高度可拓展性的表达式系统。这个系统可以通过接口从外部调用。在这个页面中,我们将一起创建示例,并探索使表达式成为可选集成的方式。 + +## 直接使用表达式 + +内置的 `Expression` 类提供了将字符串转化为表达式的编码器,以及 `parse` 方法。一个表达式相当于 `LootContext -> Expression.Result` 的函数。`Expression.Result` 代表可以被转化为 `BigDecimal`(高精度小数),`boolean`(布尔型),`String`(字符串),`Instant`(时间戳)和 `Duration`(持续时间)的返回值。 + +在应用表达表达式函数前,你必须提供 `LootContext` 的实例。 + +```java +public static final Expression EXP = Expression.parse("strFormat('%02.0f:%02.0f', floor((level.getDayTime / 1000 + 8) % 24), floor(60 * (level.getDayTime % 1000) / 1000))").result().orElseThrow(); + +public String worldTimeInHumanTime(LootContext context) { + return EXP.apply(context).getAsString(); +} +``` + +## 特殊函数 + +命令官模组附带了 `Arithmetica` 和 `BooleanExpression`。作为 `double` 和 `boolean` 函数,它们可以被编码为常量或表达式。值得注意的是,`"true"` 会被当成表达式解码,而 `true` 不会。 + +```json +2.2, "random(0, 3)" //Arithmetica(算术) + +true, "level.isDay" //BooleanExpression(布尔型表达式) +``` + +## 让表达式作为可选集成。 + +在实现对命令官模组的支持时,你可能希望让这种集成是非强制的。最简单的方法是,创建一个通用接口,然后委托给其中一个实现。实际上,这就是表达式在群星模组的新配置系统的配置方式。 + +让我们从定义一个简单的布尔型中介接口开始、我建议使用 supplier 作为形式参数,因为这样就不用为常量构建 `LootContext` 了。 + +```java +public interface BooleanIntermediary { + boolean asBoolean(Supplier supplier); +} +``` + +接下来,让我们实现常量委托。 +```java +public record ConstantBooleanIntermediary(boolean value) implements BooleanIntermediary { + + @Override + public boolean asBoolean(Supplier supplier) { + return this.value; + } +} +``` + +以及命令官委托。 + +```java +public final class CommanderBooleanIntermediary implements BooleanIntermediary { + + private final BooleanExpression expression; + private final boolean constant; + + public CommanderBooleanIntermediary(BooleanExpression expression) { + this.expression = expression; + this.constant = expression.toSource().left().isPresent(); //micro optimization + } + + @Override + public boolean asBoolean(Supplier supplier) { + return this.expression.applyAsBoolean(constant ? null : supplier.get()); + } + + public BooleanExpression getExpression() { + return this.expression; + } +} +``` + +委托做好后,接下来,我们需要动态地选择其中一个。让我们回到中介接口,为常量值创建一个 factory(工厂)。在这里,我用的是 Dark Matter 的 `Support`,你也可以直接复制这个方法: + +```java +public static T support(String mod, Supplier expected, Supplier fallback) { + return FabricLoader.getInstance().isModLoaded(mod) ? expected.get() : fallback.get(); +} +``` + +这个方法将检查是否命令官模组已被安装,并选择正确的工厂。 + +```java +public interface BooleanIntermediary { + Function FACTORY = Support.support("commander", + () -> b -> new CommanderBooleanIntermediary(BooleanExpression.constant(b)), + () -> ConstantBooleanIntermediary::new); + + static BooleanIntermediary of(boolean value) { + return FACTORY.apply(value); + } + //... +} +``` + +很棒,现在我们可以在配置文件中定义表达式了! + +```java +public class Config { + public BooleanIntermediary value = BooleanIntermediary.of(true); +} +``` + +等等,编码和解码该怎么办?这正是我们接下来要了解的。在这里,我们可以为我们的委托创建一个编解码器,并使用 `support` 来进行正确选择。 + +```java +public record ConstantBooleanIntermediary(boolean value) implements BooleanIntermediary { + public static final Codec CODEC = Codec.BOOL.xmap(ConstantBooleanIntermediary::new, ConstantBooleanIntermediary::value); + //... +} + +public final class CommanderBooleanIntermediary implements BooleanIntermediary { + public static final Codec CODEC = BooleanExpression.CODEC.xmap(CommanderBooleanIntermediary::new, CommanderBooleanIntermediary::getExpression); + //... +} +``` + +现在可以使用我们的编解码器了。 + +```java +Codec codec = (Codec) Support.fallback("commander", () -> CommanderBooleanIntermediary.CODEC, () -> ConstantBooleanIntermediary.CODEC); +``` + +### 在配置文件中使用中介。 + +::: info 提示 + +本节只在你使用 Gson 来读写配置文件时有用。 + +如果你使用了第三方库,且该库不支持传递自定义 Gson 示例,你可以通过 mixin 来修改它。 +::: +在 Gson 中,你可以通过提供自定义 JsonSerializers(Json 序列化器)或 JsonDeserializers(Json 反序列化器),使用编解码器来对中介进行编码。 +接下来将创建我们的 `CodecSerializer` 类。 + +::: details 编解码器 +```java +public record CodecSerializer(Codec codec) implements JsonSerializer, JsonDeserializer { + + public static CodecSerializer of(Codec codec) { + return new CodecSerializer<>(codec); + } + + @Override + public C deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + var r = this.codec.parse(JsonOps.INSTANCE, json); + if (r.error().isPresent()) throw new JsonParseException(r.error().orElseThrow().message()); + return r.result().orElseThrow(); + } + + @Override + public JsonElement serialize(C src, Type typeOfSrc, JsonSerializationContext context) { + var r = codec.encodeStart(JsonOps.INSTANCE, src); + if (r.error().isPresent()) throw new IllegalStateException(r.error().orElseThrow().message()); + return r.result().orElseThrow(); + } +} +``` + +::: + +现在可以通过我们的 GsonBuilder 来注册类型层次适配器了。 + +```java +builder.registerTypeHierarchyAdapter(BooleanIntermediary.class, CodecSerializer.of(codec)); +``` + +大功告成!