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));
+```
+
+大功告成!