Skip to content
Back to skills

Zinc Ui Async Separation

ASecurity

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 卡顿问题。

  • 11 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added May 26, 2026
toolsapi

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 3 files and shows the line behind each finding

Scanned September 22, 2026

npx -y skills add Crainax/War3Lib --skill zinc-ui-async-separation --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Zinc Ui Async Separation?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Zinc Ui Async Separation
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/crainax-zinc-ui-async-separation/badge)](https://www.skillsdirectory.com/skills/crainax-zinc-ui-async-separation)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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.md4.8 KB
  • agents/openai.yaml243 B
  • references/war3lib-ui-examples.md1.4 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.