# 触发器与自动化

触发器是消息、定时任务和服务器事件自动化的统一入口。默认使用模块化 Web UI；切换到 XFE Script 后，脚本仍会编译为同一份“事件 → 顶层条件 → 有序、可嵌套的条件/操作树”模型。XFE Script 是有界 DSL，不是 Java/JVM 脚本，也不能访问文件、网络或宿主类。

消息发送者名称可在 Web“设置”页全局配置，默认是 `XFEServerManager`，保存后立即生效。普通文本和未显式设置发送者的富文本会继承该值；富文本也可为单条消息覆盖发送者或隐藏发送者。

## 组、版本与迁移

- 一个触发器组可整体启停，并包含多个独立触发器。
- 单服最多 256 个触发器组、1,024 个触发器；单组最多 256 个触发器。上限在 SQLite 写事务内强制，不能通过并发创建绕过。
- 触发器和组都使用 revision compare-and-swap；旧页面保存时返回 `409 Conflict`，不会覆盖新修改。
- 首次升级会把每次入服欢迎、首次入服欢迎、按玩家每日公告、规则提醒和维护放行提醒，按原投递顺序迁移到“`Migrated messages / 已迁移消息`”组。旧投递路径在所有命中类别合并后最多发送 32 条，因此迁移只保留每个类别原本可达的前 32 条，并在每次入服匹配后继续按原类别顺序执行全局 32 条裁剪；迁移是幂等的。
- 迁移后触发器数据库是消息的唯一事实源。旧 `joinExperienceEnabled` 总开关及各消息配置字段仅保留为迁移输入，不能再通过 `/settings` 修改；请改为启停迁移组或其中的触发器。

administrator 可创建、编辑、停用和删除普通触发器；所有写入要求 CSRF、TOTP 和近期认证。`server_command` 与 `player_command` 等价于控制台能力，因此只允许具有 `CONSOLE_EXECUTE` 的 owner 保存。高风险操作在执行前必须先进入安全审计队列，审计不可用时故障关闭。

## 内置事件

| 类别 | 事件 ID |
|---|---|
| 定时 | `schedule.daily`, `schedule.interval` |
| 服务器 | `server.started`, `server.stopping` |
| 玩家会话 | `player.join`, `player.leave`, `player.respawn`, `player.dimension_change` |
| 玩家输入与交互 | `player.chat`, `player.command`, `player.command_trigger`, `player.interact`, `player.entity_interact`, `player.attack` |
| 玩家状态 | `player.death`, `player.hurt`, `player.heal`, `player.sleep`, `player.wake`, `player.advancement` |
| 物品 | `player.item_pickup`, `player.item_drop`, `player.item_use`, `player.item_use_finish`, `player.item_craft` |
| 方块 | `block.break`, `block.place`, `block.change`, `block.grow`, `block.tool_modify` |
| 实体 | `entity.spawn`, `entity.remove`, `entity.death` |
| 世界与区块 | `world.explosion`, `world.weather_change`, `world.load`, `world.unload`, `chunk.load`, `chunk.unload` |
| 崩服防护 | `protection.item_overflow`, `protection.mob_overflow`, `protection.entity_overflow`, `protection.mod_entity_overflow`, `protection.spawn_burst`, `protection.command_block_rate`, `protection.slow_tick`, `protection.loaded_chunk_overflow`, `protection.memory_pressure` |

这些是四个目标 Forge 版本上具备稳定事件 API 的内置集合。第三方模组或版本特有事件可从服务端代码发布为命名空间事件：

```java
ForgeManagementRuntime.fireCustomTrigger(
        "custom.example.boss_defeated",
        Map.of("boss.id", "example:ancient_guardian", "party.size", 4));
```

`custom` 只是 UI 中的选择占位符，真正保存的 ID 必须匹配 `custom.<namespace>.<event>`。这使任何模组事件都能接入，同时避免把没有实际 Forge 回调的事件伪装成内置支持。

`protection.*` 事件由[崩服防护](crash-protection.md)的有界扫描器和即时熔断器产生。事件阈值采用严格“超过”语义，并支持冷却；其上下文包含数量、阈值、范围、维度/区块、模组命名空间和实际采取的动作。

### 定时配置

- `schedule.daily`：`time=HH:mm`，并提供 IANA `timezone`，例如 `Asia/Shanghai`。
- `schedule.interval`：总间隔为 1 秒到 31,536,000 秒。模块化 UI 提供小时、分钟、秒输入，
  保存时规范化为总 `seconds`；API/XFE Script 既可继续使用旧格式 `seconds=<总秒数>`，也可同时
  使用 `hours`、`minutes`、`seconds` 分量（分和秒为 0..59）。执行点按服务器本地墙钟边界对齐；
  例如服务器本地时间为 12:58、间隔为 15 分钟时，下一次执行是 13:00，之后依次为
  13:15、13:30、13:45，而不是从编辑或启动时刻顺延。

### 玩家自定义指令

`player.command_trigger` 会把一个启用的触发器实时注册成游戏指令。`command` 是不带 `/` 的
小写指令根；`arguments` 是按空格分隔、从左到右依次声明的参数名。例如
`command=reward`、`arguments=target amount` 会注册 `/reward <target> <amount>`。指令根和参数名
必须匹配 `[a-z][a-z0-9_-]{0,31}`，参数不能重名且最多 16 个；每个参数读取一个不含空格的
Brigadier word。只有玩家可以执行这种触发指令。

命令保存、启停、删除或所属组启停后，服务端会立即重建自己拥有的指令节点，并向在线玩家
重新发送命令树；数据包重载时也会恢复注册。指令根不能覆盖 Minecraft、其他模组或另一个
已启用触发器的指令。执行上下文提供 `command.name`、`command.raw` 和每个 `args.<参数名>`；
例如条件可读取 `args.amount`，消息或其他操作可插入 `{args.target}`。一次输入只会定向触发
注册该指令的那一个触发器，不会广播给所有 `player.command_trigger` 订阅者。

调度使用带 token 的短租约。过期租约会先重放它原有的 occurrence，再计算当天或下一间隔，因此跨日重启不会覆盖尚未结算的任务。若在第一个动作前失败，本次任务会延迟重试；一旦至少一个动作已经成功，本 occurrence 会在其余动作失败时记为“部分成功”并完成，以免重复先前副作用。进程恰好在动作生效后、完成状态落库前中断时仍可能重复，因此动作序列既不是数据库事务，也不承诺恰好一次。

## 条件

条件字段使用点分路径，并从事件快照读取，例如 `player.name`、`player.uuid`、`player.dimension`、`chat.message`、`command.name`、`command.raw`、`args.<参数名>`、`item.id`、`entity.type`、`block.id`、`world.dimension`、`server.online`、`server.maxPlayers`、`event.type` 与 `event.time`。UI 提供常用字段建议，但 `custom.*` 可以使用发布者定义的其他字段。

一个触发器可选择全部条件成立（`all`）或任意条件成立（`any`）：

| 类别 | 运算符 |
|---|---|
| 相等 | `eq`, `neq` |
| 文本 | `contains`, `not_contains`, `starts_with`, `not_starts_with`, `ends_with`, `not_ends_with` |
| 安全正则 | `matches`, `not_matches` |
| 数值 | `gt`, `gte`, `lt`, `lte`, `between`, `not_between` |
| 集合 | `in`, `not_in`（逗号分隔） |
| 空值 | `empty`, `not_empty` |
| 布尔 | `true`, `false` |
| 存在性 | `exists`, `not_exists` |

`between` 和 `not_between` 使用 `最小值,最大值`，边界包含在区间内。`empty` 和 `not_empty`
可判断空字符串、空集合、空映射与空数组；`true` 和 `false` 判断布尔值。`empty`、`not_empty`、
`true`、`false`、`exists` 和 `not_exists` 不需要填写比较值。`matches` 与 `not_matches` 使用保存时校验的保守正则
子集；回溯组、反向引用、环视和过量可变重复会被拒绝，以免玩家输入造成正则拒绝服务。
每个触发器最多 32 个条件。

## 操作

执行区域既可添加普通操作，也可添加条件容器；条件通过后才会按从上到下的顺序执行其子节点。
条件容器可以继续包含条件或操作。每个触发器的条件容器与普通操作合计最多 32 个节点，条件
最多嵌套 8 层，且每个条件容器至少包含一个可执行后代：

| 类别 | 操作 ID |
|---|---|
| 消息 | `send_player`, `broadcast`, `title`, `actionbar`, `sound`, `log` |
| 命令 | `server_command`, `player_command`（owner-only；`showFeedback=false` 默认静默） |
| 玩家管理 | `kick`, `teleport`, `give_item`, `clear_inventory`, `set_gamemode` |
| 玩家状态 | `add_effect`, `remove_effects`, `heal`, `feed` |
| 世界 | `set_time`, `set_weather` |
| 身份列表 | `whitelist_add`, `whitelist_remove`, `ban`, `pardon` |

### 命令反馈

`server_command` 和 `player_command` 的 `showFeedback` 默认为 `false`，触发器执行指令时不会把
`[Server: ...]` 成功反馈广播给 OP；确实需要查看原版反馈时可在该操作中选择“显示”。发放物品、
添加效果、传送等由触发器内部调用的辅助指令始终静默，但实际操作结果不受影响。

旧版本正在运行的服务器可用 `/gamerule sendCommandFeedback false` 立即关闭这类管理员广播；
`commandBlockOutput` 只控制命令方块，并不控制服务器命令源。若只想关闭控制台向 OP 的广播，
也可在 `server.properties` 设置 `broadcast-console-to-ops=false` 后重启服务器。

消息参数使用一个所见即所得富文本输入框；框选任意文字即可设置颜色、粗体、斜体、下划线、
删除线、随机字符、点击动作、悬停说明和 Shift+点击插入内容。点击“插入变量”会打开可搜索的
变量目录；目录由服务端 `/triggers/catalog` 下发，包含 100 项以上实际可解析的变量，并显示
中英文名称、说明、数据类型、适用事件、插入示例和时间格式提示。目录按当前事件过滤条件字段；
玩家、物品、方块、伤害、指令参数等上下文不存在时，相应模板会保留原占位符，而不会被悄悄
替换为空字符串。旧消息仍兼容 `{player}`、`{online}`、`{maxPlayers}` 和 `{date}`。

常用变量包括 `{player.name}`、`{server.online}`、`{server.maxPlayers}`、
`{server.defaultMessageSender}`、`{server.tickRate}`（目标游戏速度）、`{server.tps}`（实际 TPS）、
`{server.mspt.average}`、`{world.gameTime}`（世界总刻）、`{world.timeOfDay}`（当日 0..23999 刻）、
`{world.day}`（游戏内天数）以及命令触发器动态生成的 `{args.<参数名>}`。完整清单以运行中服务端
返回的目录为准；出于安全和性能边界，目录不提供玩家 IP、世界种子、服务端文件路径、认证信息、
完整 NBT/Data Components，也不展开无限长度的玩家、实体或区块集合。

服务器时间可直接使用 `{server.time}`，也可在冒号后使用受限的 Java DateTimeFormatter 模式：

```text
{server.time:uuuu-MM-dd HH:mm:ss}
{server.time:uuuu年MM月dd日 HH:mm:ss}
{event.time:HH:mm:ss}
```

推荐使用 `uuuu` 表示公历年、`MM` 表示月、`dd` 表示月内日期、`HH` 表示 24 小时、`mm` 表示分、
`ss` 表示秒。`yyyy` 也可用于常见公元年份；不要使用 `YYYY`（周历年）或 `DD`（年内日序），
它们在跨年附近或表达月内日期时容易得到意外结果。模式最多 64 个字符，只允许日期时间格式字段
与普通分隔文字；非法模式会在校验/保存时拒绝。`event.time` 的原值是事件捕获时刻的 UTC
ISO-8601 文本，格式化后按服务器时区显示；`server.timezone` 可查看所用 IANA 时区。

模板在富文本 JSON 解析后逐字段替换，玩家输入不能注入颜色、点击事件或新的富文本文档；
粘贴也只接受纯文本。富文本最多 256 个样式段、
每段 2,048 字符且可见文本合计不超过 8,192 字符；URL、点击命令和各辅助字段在保存时校验。
定时、服务器、世界和区块生命周期事件没有玩家上下文，不能搭配玩家专用操作；
`player.leave` 发生后玩家已离线，只允许广播、日志或基于身份列表的操作。上述检查会递归覆盖
条件容器内的所有操作；保存与脚本校验阶段会直接拒绝不兼容组合、未知参数、非法资源 ID、
越界数值和多行命令。

## XFE Script

```text
# 每天上海时间 08:30 广播一次
on schedule.daily
set time="08:30"
set timezone="Asia/Shanghai"
match all
when server.online gt 0
do broadcast message="早上好，当前在线 {server.online} 人"
do log message="daily greeting sent" level=info
```

嵌套执行条件使用 `if` / `end`：

```text
on player.command_trigger
set command="reward"
set arguments="target amount"
match all
if args.amount between "1,64"
  do server_command command="give {args.target} minecraft:diamond {args.amount}"
end
```

语句包括：

- `on <event-id>`：唯一的事件声明；
- `set <name>=<value>`：定时事件配置；
- `match all|any`：条件组合方式；
- `when <field> <operator> [value]`：条件；
- `if <field> <operator> [value]` / `end`：包围一组仅在条件成立时执行的子条件或操作；
- `do <action> key="value" ...`：有序操作。

脚本最多 65,536 个字符。可视化模式切到代码模式会生成等价脚本；代码模式切回可视化模式时，服务端先编译并校验，再更新模块，编译失败不会覆盖当前草稿。

## 执行边界

事件总线先检查不可变订阅索引；没有订阅者时不会排入后台队列。Minecraft 状态在服务器线程复制为有界不可变快照，脚本条件与调度数据库随后在独立的有界触发器执行器工作；玩家、世界与 Brigadier 操作只在服务器线程执行。一次事件最多匹配 128 个已启用触发器，每 tick 最多执行 64 个动作。普通高频事件在队列饱和时会被丢弃并限速告警，不会占满 HTTP/策略控制执行器；定时任务的结算使用独立有界通道并保留待重试状态。

旧首次入服/每日消息的投递状态会在加入事件进入内存执行队列前提交；若进程恰好在两者之间崩溃，该次消息可能被标记为已消费而未实际发送。彻底消除此窗口需要后续引入持久化的 join claim → acknowledgement 流程；当前实现不宣称入服消息恰好一次投递。
