自定义发包与安全边界
当内置动作无法表达服务器自己的业务时,可以使用 packet 把自定义事件交给服务端插件。ChaUI 负责验证页面会话并触发 API 事件,具体业务由监听 ChaUIPacketEvent 的插件决定。
编辑器的隔离预览不会发送 packet。只有正常打开的运行时页面会把自定义事件交给服务端,避免测试页面时误触发真实业务。
packet() 是普通脚本动作,不要求写在点击事件里。只要真实运行时确实执行到了这一句,就会发送,包括:
- 页面
open、close、key.*、mouse.*事件; - 组件
create、三键按下、松开、点击、悬浮进入和悬浮离开事件; - 页面自定义方法;
timer回调;- 其他插件通过 API 让当前页面执行的运行时脚本。
如果 packet() 写在 if 的未进入分支里,它不会发送。编辑器编辑状态和隔离预览也不会发送。
基本写法
只发送事件标识:
packet("shop:refresh")
发包("shop:refresh")同时携带多项字符串数据:
packet("shop:select", "item_a", "2")
发包("dialog:reply", "accept", "chapter_1")第一个参数是 packetId,其余参数会按顺序进入字符串列表 data。packetId 必须直接写成固定字符串,不会展开变量;数据参数会在发送前展开 {vars.xxx}、{vals.xxx} 和花括号中的表达式。
packet("shop:select", "{vars.itemId}", "{vars.amount}")
发包("dialog:reply", "玩家生命:{vals.player.health}")数据参数使用与 log() 相同的响应式规则。需要发送普通左花括号时,连续写两个左花括号;变量缺失或表达式无效时按安全空值处理,不会把未解析的模板原样冒充有效数据。
参数限制
| 项目 | 限制 | 说明 |
|---|---|---|
packetId 字符 | 英文字母、数字、_、-、.、: | 建议使用 系统:动作 形式,方便分类 |
| 数据类型 | 字符串列表 | 数字和复杂对象也需要转换成字符串 |
| 数据项数量 | 最多 64 项 | 超出限制的请求会被拒绝 |
| 单项长度 | 最多 8192 个字符 | 不适合传输大型文件或图片 |
需要发送复杂结构时,可以把 JSON 作为某一项字符串:
packet("shop:order", "{\"item\":\"item_a\",\"amount\":2}")服务端插件应自行验证 JSON 字段、取值范围和玩家权限,不能因为数据来自 ChaUI 页面就直接信任。
服务端收到的上下文
通过 ChaUIPacketEvent,业务插件可以获得:
| 信息 | 用途 |
|---|---|
| 玩家 | 判断是谁触发了事件 |
| 页面 ID | 区分事件来自哪个页面 |
| 组件 ID | 组件事件中是组件 ID;页面级事件中为空字符串 |
| 触发类型 | 原始组件事件或页面事件;方法和计时器继承创建它们的上下文,没有明确事件来源时为 script |
packetId | 识别具体业务动作 |
data | 读取页面作者发送的字符串参数 |
ChaUI 不会自动解释 packetId,也不会自动修改经济、背包、任务或其他业务数据。没有插件监听时,自定义包不会产生默认业务效果。
例如,把发包写在页面打开事件中:
events:
open: |-
packet("example:page_opened", "welcome")这时服务端事件的 elementId 是空字符串,trigger 是 open。如果 open 事件调用方法或启动计时器,其中的发包仍继承这个页面上下文;由没有明确事件名的运行时脚本执行时,trigger 为 script。
页面示例
下面的确认按钮会把当前页面中的选择结果交给服务端业务插件:
elements:
- id: submit_button
type: button
layout:
x: 20
y: 80
width: 90
height: 20
button:
label: "提交选择"
events:
leftClick: |
packet("example:submit", "item_a", "1")
component("status").text.value = "请求已发送"packet 的参数按字符串传递。服务端仍需重新校验物品是否存在、数量是否合法、玩家是否满足条件,以及事件是否可以重复提交。动态值必须写在带引号的数据模板中,例如 "{vars.amount}";不能把未加引号的变量或整个组件对象直接作为参数。
component("id") 返回的是页面内的组件对象,不能把整个对象直接作为 packet 参数发送。需要检查组件状态时,先读取到页面变量用于本地判断或显示,再按业务协议发送明确的字符串数据:
events:
leftClick: |-
vars.buttonEnabled = component("submit_button").enabled
log("提交按钮启用状态:{vars.buttonEnabled}")
packet("example:submit", "{vars.itemId}", "{vars.amount}")安全边界
- 客户端提交的页面会话必须属于当前玩家,页面 ID 必须与会话一致。
packetId和data都是玩家客户端提供的输入,业务插件必须验证。- 不要直接把
data拼接成高权限指令、数据库语句或文件路径。 - 涉及物品、货币、奖励和权限的结果必须由服务端计算并写入。
- 对可能重复触发的按钮,应在业务层设计防重复提交或冷却机制。
- 单个非法脚本或非法自定义包不应导致页面、客户端或服务端崩溃。
与内置服务端动作的区别
open、close 和指令类内置动作由服务端从已保存页面脚本中读取,客户端不能替换其中的动作内容。packet 则用于传递页面交互数据,服务端业务插件应把每个字段都视为待校验输入。
如果你刚开始使用脚本,可以返回ChaUI Script 简介,从第一个点击事件开始制作。
猹件开发组