Back to skills
SKILL.md
Zinc Ui Async Separation
ASecurityWar3Lib/Xlimon UI 组件开发行为规范(Zinc in .j):约束 UI 本地异步行为与同步数据逻辑分离;规范 onClick/onEnter/onLeave 与 spClick/spEnter/spLeave;规范本地事件转同步事件时的数据打包与触发器接收;防止难度确认、开局、休闲模式、HeroSelector 前阶段过早创建 DzFrameBindWidget/世界单位绑定 UI;约束 Museum、分页卡片、配置存档 UI 的字符串编解码与刷新性能。用于创建、重构或排查 Jass/ui/composite 下的 UI(如 HeroSelector/Selector/Museum)、UIDifficulty、Arena、RelaxMode 及相关回调、频繁 refresh 卡顿问题。
- 11 stars
- 0 votes
- 0 copies
- 2 views
- Added May 26, 2026
Works with
Security analysis
100/100Pro scans all 3 files and shows the line behind each finding
npx -y skills add Crainax/War3Lib --skill zinc-ui-async-separation --agent claude-codeAre you the author of Zinc Ui Async Separation?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/crainax-zinc-ui-async-separation)---
name: zinc-ui-async-separation
description: War3Lib/Xlimon UI 组件开发行为规范(Zinc in .j):约束 UI 本地异步行为与同步数据逻辑分离;规范 onClick/onEnter/onLeave 与 spClick/spEnter/spLeave;规范本地事件转同步事件时的数据打包与触发器接收;防止难度确认、开局、休闲模式、HeroSelector 前阶段过早创建 DzFrameBindWidget/世界单位绑定 UI;约束 Museum、分页卡片、配置存档 UI 的字符串编解码与刷新性能。用于创建、重构或排查 Jass/ui/composite 下的 UI(如 HeroSelector/Selector/Museum)、UIDifficulty、Arena、RelaxMode 及相关回调、频繁 refresh 卡顿问题。
---
# War3 UI 异步分层规范
## 强制规则
- 保持 `UI 展示层` 与 `数据/游戏状态层` 分离。
- 把 `GetLocalPlayer()` 分支内的逻辑视为异步本地逻辑;禁止在其中修改同步状态(单位、计时器、全局对局数据等)。
- `show/hide/refresh` 等 UI 生命周期方法禁止做业务数据写入;尤其禁止在 UI 操作链路内执行 `DzAPI_Map_Store*`和其他会导致数据变动的操作。
- 在 UI 回调里只做三类事情:
1. 本地 UI 更新(show/hide/text/texture/位置)
2. 事件参数写入(`currentPosAsync` / `args*` / `eventdata.bind*`)
3. 触发异步到同步桥接(如 `syncBus.DzSyncDataEx`)
## Simple UI 显隐防雷(重要)
- 对 `uiImage.createSimple` / `uiText.createSimple` / `uiBtn.createSimple` 创建的控件,禁止直接调用 `.show(true/false)` 作为显隐手段。
- 原因:`show()` 底层走 `DzFrameShow`,在部分 simple 原生链路会触发本地函数报错。
- 推荐做法:显隐改为 `clearPoint + setPoint`,显示时放回目标锚点,隐藏时移到屏幕外(例如 `DzGetGameUI()` 左下角外)。
- 对 `uiBtn.createSimple` / `SIMPLEBUTTON` 要更严格:不要用两个相对锚点依赖 image/text 去撑开点击区域,显示时优先 `setSize(固定宽高) + setAbsPoint(绝对坐标)`。
- 隐藏 `uiBtn.createSimple` 时不要只 `clearPoint + setAbsPoint`,旧 hit rect 可能残留并截断原生单位面板 hover/click;先 `setSize(0.001, 0.001)` 再移出屏幕。
- 若出现“贴图/文字已经消失,但鼠标仍有点击声或挡住 UnitPanel/Attr 图标”的现象,优先检查 simpleButton 点击矩形是否没有被固定尺寸/缩小尺寸刷新。
- 初始化后必须做句柄保护:若 `obj == 0` 或 `obj.ui == 0`,立刻 `return`,禁止继续 `setTexture/onClick/show` 链式调用。
- 若报错栈含 `DzFrameShow -> s__uimage_show / s__uitext_show / s__uibtn_show`,优先按“simple 调用了 show”定位。
## 开局/难度阶段 DzFrame 绑定防雷
- 不要在 `onInit()`、地图开局短 timer、`AfterChooseDiff()` 的同步主链路中批量创建 `DzFrameBindWidget` 到世界单位的 UI,尤其不要在 HeroSelector 创建前创建绑定到商店/NPC/马甲单位的框。
- 休闲模式会在选难度后隐藏全图单位;如果此前已把 frame 绑定到这些单位,再叠加每帧 `DzGetUnitUnderMouse()` / `DzFrameShow()` 刷新,容易导致客户端卡死或未响应。
- 把“数据初始化”和“本地 UI 绑定创建”拆开:同步数据可用短 timer 分帧初始化;`DzFrameBindWidget`、商店头顶文字、世界单位跟随 UI 应在首次真正需要刷新/显示时懒创建。
- 每帧 UI 刷新函数必须先做轻量 guard:没有任何已创建 frame root 时直接 `return`,不要提前调用 `DzGetUnitUnderMouse()` 或遍历显隐。
- 若必须在难度确认后预热数据,每 tick 只处理少量在线玩家/对象;不要把多玩家随机表、frame 创建、单位显隐挤在同一帧。
- 排查“只在休闲模式卡死,普通难度正常”时,优先检查休闲模式隐藏单位与 `DzFrameBindWidget`、世界单位绑定 UI、每帧鼠标检测之间的交互,而不是只看难度数值逻辑。
## 事件绑定选择
- 无数据绑定的事件用 `onClick/onEnter/onLeave`。
- 需要携带绑定数据(frame -> eventdata -> 业务参数)时用 `spClick/spEnter/spLeave`。
- 使用 `sp*` 时,先绑定 `uiHashTable(frame).eventdata`,再在回调中解包;禁止直接依赖外层局部变量闭包。
- 不需要 `frame` / `eventdata` 的回调必须优先复用具名 `code`:例如 `.onLeave(function Foo.clearTooltip)`,避免每个控件生成一层只转发的 lambda。
- 需要 `frame` 的 `sp*` 回调也优先传具名 `uiEvent`:例如 `.spEnter(uiEvent.FooOnEnter)`;不要写 `.spEnter(function(integer frame) { Foo.onEnter(frame); })` 这类每个控件一份的纯转发包装。
- 若目标逻辑在 struct method 中,为兼容 vjassc/jasshelper,抽一个库级 `public function FooOnEnter(integer frame)` 再以 `uiEvent.FooOnEnter` 传入;多个控件共享这个适配函数。
- 只有在确实需要适配参数、临时组合多步逻辑、或写入一次性参数槽时,才使用匿名 lambda;重复出现时抽成具名静态方法。
## 异步转同步(本地事件出网)
- 本地点击/交互不直接执行业务数据修改;先编码 payload 并发送(示例:`syncBus.DzSyncDataEx(channel, payload)`)。
- 在 `onDataSync(channel, ...)` 中解码并做权限校验(owner/player/对象存在性),通过后才触发 `trClick/trClose/trBtn1` 等同步逻辑。
- payload 至少包含三段信息:
1. 事件类型(如 `D/C/F/L/R`)
2. 对象标识(如 `sd`、英雄索引)
3. 位置信息(如 `pos`)
## 回调参数传递
- 为匿名回调准备静态参数槽(如 `currentSDAsync/currentPosAsync/argsHeroIndex`)。
- 触发器前写入参数,触发后按需清理或覆盖,避免跨回调污染。
- 提供统一读取函数(`Get*Async` / `Get*Pos`)给外部逻辑层。
## 生命周期与清理
- 销毁 UI 时,补发必要的离开事件(若存在“进入未离开”状态)。
- 销毁顺序保持“子组件 -> 主组件”,并把句柄置零。
- 本地 UI 关闭仅处理本地资源;同步状态变更交给同步事件处理器。
## Museum/配置型 UI 的存档字符串性能
- 禁止在点击、hover、翻页、`refresh()`、`refreshCard()` 等交互热路径中反复调用会执行完整字符串标准化/解析的 getter。
- 存档字符串只允许在两个边界转换:打开或收到同步结果时解码一次;点击保存时编码一次。收包端仍保留完整格式、点数与业务合法性校验。
- 交互阶段使用按字段拆分的本地草稿缓存,例如 `localLevel[]`、`localDisabled[]`、`serverLevel[]`、`serverDisabled[]`。不同语义优先用独立类型数组,不为形式统一强塞二维整数数组。
- 初始化草稿时一次性计算 `localSpent`、`localPoolCount` 等汇总值;加减等级和开关状态时按差值增量维护,禁止每张卡重新扫描整份配置。
- `refresh()` 开头只计算一次公共状态(如总点数、是否可编辑、dirty 状态),再作为参数传给当前页卡片刷新;禁止每张卡重复获取或计算。
- 分页 UI 只刷新当前页卡片。换页时隐藏上一页、显示并刷新下一页,禁止每次操作遍历所有卡片后再由卡片自行判断是否属于当前页。
- 洗点直接清空草稿数组并重置汇总缓存,保存时再构造默认配置;不要为了 UI 预览反复拼接完整字符串。
- 发现 getter 内部调用 `NormalizeConfig()`,且外层循环又逐卡调用该 getter 时,按嵌套全量解析处理:先解码为数组缓存,再优化 UI 原生调用数量。
推荐流程:
```text
load/sync -> normalize once -> decode arrays -> calculate aggregates once
click -> mutate one array slot -> update aggregate deltas -> refresh current page
save -> encode arrays once -> local precheck -> sync payload -> receiver full validation
```
## 创建期 OOS 防线(换肤/外观类)
- 在“英雄创建”这类同步流程里,禁止根据 `BLoaded` / 本地缓存是否已读档来走不同同步分支。
- 创建期若需要应用外观:
1. 先在全端应用同一份确定参数(通常是默认皮肤/默认外观)。
2. 再由本地玩家发送同步 payload。
3. 最终只在 `onDataSync` 中按 payload 统一应用真实外观。
- `onDataSync` 若依赖对象存在(如 `H[idx]`),必须容忍“消息先到、对象后到”的时序;必要时补一次下一拍重试或在创建后再次触发同步。
- 避免在创建单位的同一帧立即发同步;可延迟一拍(如 `0.03s`)降低竞态概率。
- `DzAPI_Map_GetStored*` 的服务器开局快照不是本地异步输入:按 `.codex/skills/dzapi-server-save-load/SKILL.md` 的规则,由所有客户端在一致时机、按固定玩家顺序直接读取到同步缓存;禁止包进 `GetLocalPlayer`,也禁止重新发包广播读档结果。
- 只有真正的本地 UI 点击、键鼠输入或本地设置缓存,在需要驱动全端同步状态时才先转成同步消息。
## Dz 同步与随机数
- `DzSyncData` / `DzSyncDataImmediately` 的接收回调里不要直接消耗 `GetRandom*`,也不要立刻串起会消耗随机数的业务链路;需要时先落同步状态,再用短计时器延后到稳定帧处理。
- 普通伤害事件、周期战斗事件等原生同步事件里的 `GetRandom*` 不属于 Dz 收包风险;不要为了 OOS 排查把这些热路径全局队列化。
- `SyncBus` 的 OOS 探针如果会定时调用 `GetRandom*`,必须有条件编译模式:关闭模式不注册探针且不消耗随机数,低频模式用于常规验证,高频模式只用于短时间定位。
- 排查 OOS 时优先比较双端 Trace / 日志样本;确认不是探针自身放大随机序列差异后,再扩大探测频率。
## 确定性伪随机
- 需要替代 `GetRandom*` 时,优先使用纯整数确定性种子 + 16807 LCG(Schrage 法)+ Fisher-Yates 洗牌;这样概率分布接近原洗牌,同时不消耗魔兽原生随机序列。
- 不要在热路径构造动态长字符串后 `StringHash("A|" + I2S(x) + "|" + name)`;War3 字符串拼接有额外开销,也更难审查输入稳定性。
- 可接受的输入方式:对已有稳定字符串单独 `StringHash`,例如 `GetVersion()`、`GetPlayerName(p)`、难度文本;再把开局时间、玩家序号、刷新次数等整数通过整数混合函数并入种子。
- 种子混合要保留原本参与随机的稳定信息,例如开局时间、版本、玩家名 hash、玩家序号、难度;只改变混合方式,不随意删掉影响分布/区分度的输入。
- 对候选池选择多个结果时,用确定性 Fisher-Yates 后取前 N 个,避免连续 `ModuloInteger(seed, count)` 造成明显线性相关。
## 硬性检查清单(CR Gate)
在提交 UI 代码前,逐条检查。任意 `FAIL` 均禁止合并。
1. `GetLocalPlayer()` 分支内不修改同步状态
PASS: 仅做 UI 显示、本地参数写入、发送同步消息。
FAIL: 创建/销毁单位、改全局业务数据、改计时器/组等对局同步状态。
2. 本地 UI 回调不直改业务数据
PASS: `onClick/spClick` 只编码 payload 或写 `current*Async/args*`。
FAIL: 在 UI 点击回调里直接执行奖励发放、状态切换、背包改动等。
3. 正确选择 `on*` vs `sp*`
PASS: 无绑定数据用 `onClick/onEnter/onLeave`;有 frame 数据解包需求用 `spClick/spEnter/spLeave`。
FAIL: 需要 `eventdata` 却使用 `on*`,或无数据需求滥用 `sp*`。
PASS: `sp*` 直接传具名 `uiEvent`,`on*` 直接传具名 `code`。
FAIL: 回调主体只是 `Foo.bar(frame);` 或 `Foo.clearTooltip();`,仍为每个控件创建匿名 lambda 包装。
4. `sp*` 回调前完成 eventdata 绑定
PASS: 对应按钮存在 `uiHashTable(btn.ui).eventdata.bind*`。
FAIL: 回调中读取 `eventdata` 但未绑定或绑定不完整(缺少 bind2/bind3)。
5. 本地事件统一走“发送 -> 接收 -> 处理”链路
PASS: `DzSyncDataEx(channel, payload)` 发出后,在 `onDataSync(channel, ...)` 解码并处理。
FAIL: 本地事件绕过总线直接触发同步业务逻辑。
6. `onDataSync` 完成安全校验
PASS: 校验对象存在性、`owner/player` 一致性、索引范围合法性后再触发业务触发器。
FAIL: 未校验即执行 `trClick/trClose/...`。
7. payload 结构可逆且稳定
PASS: payload 至少含 `事件类型 + 对象标识 + 位置信息`,解码规则与编码规则一一对应。
FAIL: 拼接格式歧义、长度位错误、解码位置错位。
8. 异步参数槽完整可读
PASS: 提供 `Get*Async/Get*Pos` 读取接口,并在触发前写入对应参数。
FAIL: 回调依赖外层局部变量或隐式状态,导致异步读取不稳定。
9. UI 销毁流程完整
PASS: 销毁前补发必要 leave,按子到父销毁并置零,避免悬空句柄。
FAIL: 直接销毁主框导致子组件泄漏,或 leave 状态丢失。
10. 外部回调职责清晰
PASS: UI 层只发事件与展示;数据层在外部触发器/总线接收端处理。
FAIL: UI 文件内混入业务数据写入分支。
11. `createSimple` 控件显隐方式正确
PASS: `uiImage/uiText` 使用“改锚点/移出屏幕”的 simple-safe 显隐方案;`uiBtn.createSimple` 显示时固定尺寸定位,隐藏时缩到 `0.001` 后移出屏幕;都有 `ui!=0` 保护。
FAIL: 对 `createSimple` 组件直接 `.show()`;或 `uiBtn.createSimple` 只改锚点、依赖相对锚点撑点击区、无句柄保护就调用 UI 原生函数。
12. 配置型 UI 不在交互热路径重复解析存档字符串
PASS: 加载时解码一次、交互时使用数组和增量汇总、保存时编码一次;刷新只处理当前页并复用公共状态。
FAIL: `refreshCard()` 逐卡调用会完整 `NormalizeConfig()` 的 getter,或每次点击反复拆解、重建同一存档字符串。
## 快速判定
- 全部 12 项 `PASS`:允许合并。
- 任意 1 项 `FAIL`:先修复再提审。
- 若无法判定:默认按 `FAIL` 处理并补充注释说明边界。
## 参考实现(按需阅读)
- `references/war3lib-ui-examples.md`
Files in this skill
- SKILL.md
- agents/openai.yaml
- references/war3lib-ui-examples.md
Attribution
Comments
Loading comments…
Newsletter
Get the weekly list of safe new skills, and the ones we pulled.