ccgf-game-tool 0.0.0-stage → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,153 @@
1
- # Temporary Holding Version
1
+ # ccgf-game-tool
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ An editor extension for `cocos_gameframework`: it **automates** the framework's MVC conventions and **reports violations early** instead of letting them fail silently at runtime.
4
+
5
+ ## Background: the conventions
6
+
7
+ See [`docs/create-view-design.md`](./docs/create-view-design.md).
8
+
9
+ - **Module = Controller**: `@RegisterModule({ order, model })`, `model` is required; one module directory = one `XxxModule.ts` + `XxxModel.ts` pair
10
+ - **View = script + prefab**: `@RegisterView({ name, path, layer })`; the script lives under the module's `views/`, the prefab under `resources/views/<module>/`, with the script attached to the prefab root
11
+ - **`.meta` (with UUID) is generated by the editor**, never written by hand
12
+
13
+ The extension has two halves: a **generator** (from nothing to something) and a **doctor** (from something to something correct).
14
+
15
+ ## Panel
16
+
17
+ Menu **Developer → Workbench**. The panel has two tabs.
18
+
19
+ ### Modules tab
20
+
21
+ - **Left**: module list (order badge, warning dot when the Model script is missing) + create-module form
22
+ - **Right**: order editing, the module's scripts (click to open), view list (layer badge, missing-prefab warning)
23
+ - **Toolbar**: "Overview" (startup order + layer usage) and "Doctor" (one-click convention check)
24
+
25
+ Creating a view lets you pick the **layer** (framework `ELayer`) and a **prefab template**.
26
+
27
+ Creating a module writes **three** scripts:
28
+
29
+ | File | Purpose |
30
+ | --- | --- |
31
+ | `<Prefix>Module.ts` | the `Module` subclass, registered via `@RegisterModule` |
32
+ | `<Prefix>Model.ts` | the `Model` subclass |
33
+ | `<Prefix>Events.ts` | **event declaration** — a `declare global { interface IGameEvents { ... } }` augmentation |
34
+
35
+ The events file is pure declaration: it lets this module emit and others listen through the global `eventBus` while the two sides stay mutually unaware. Its explanatory comments are **deliberate** — in particular the trailing `export { };` must not be deleted (without `import`/`export` the file counts as a *global script* and `declare global` fails to compile). A test asserts exactly that, by compiling the template and checking that removing the line is rejected. Design and rules: `docs/event.md`.
36
+
37
+ ### Managers tab
38
+
39
+ Lists the global singletons under `assets/scripts/manager/`, and lets you create or delete them.
40
+
41
+ A manager is a **global service**, not part of a business module, so it lives in `assets/scripts/manager/` (a sibling of `framework/` and `gameplay/`) and extends the framework's `ASingleton`, obtained via `XxxManager.getInstance()`:
42
+
43
+ ```ts
44
+ import { ASingleton } from '../framework/common/Singleton';
45
+
46
+ export class ConfigManager extends ASingleton {
47
+
48
+ public init() {
49
+ }
50
+
51
+ public update(dt: number) {
52
+ }
53
+ }
54
+ ```
55
+
56
+ Managers that do not extend `ASingleton` get a warning dot in the list, and are reported by the `manager-not-singleton` rule.
57
+
58
+ ### Shared modules tab
59
+
60
+ Lists the framework's **shared modules** (optional, independently publishable packages) in two groups — **Installed** and **Available** — and installs/uninstalls them by running npm for you.
61
+
62
+ - **Install** runs `npm install <spec>…` **in the project root**; **Uninstall** runs `npm uninstall <name>`. Both use `--no-audit --no-fund` so the output stays focused on real problems.
63
+ - **A module's local dependencies are installed together with it.** Installing only the module would make npm look the dependency up on the registry — where an unpublished local package obviously isn't, so the install fails with `E404` (measured). The command shown in the panel and the command actually executed come from the *same* function, so copying it for a manual run behaves identically.
64
+ - **Failures are shown in full.** A failed run does not just say "install failed" — a modal shows the **exit code, the exact command (so you can rerun it yourself) and npm's raw stdout/stderr**, plus a one-line hint when the failure signature is recognized (npm not found, network, timeout, TLS/certificate, permissions, `ERESOLVE`, 404, `EBADENGINE`, …). See `source/shared.ts`.
65
+ - **The npm command is always on screen**, in the detail panel — the one matching the button (`install` when not installed, `uninstall` when installed) — together with a reminder that if automatic execution fails you can copy it and run it in the project root manually. The command is built by the **main process** and shown verbatim, so what you copy is exactly what the button would have run.
66
+ - Three states are distinguished: `installed` (declared **and** present in `node_modules`), `declared-not-installed` (in `package.json` but missing on disk — the last install probably failed), and `not-installed`.
67
+ - **Dependencies are shown in both directions**: what the module requires — each dependency with its live installed state, so a missing one is visible *before* you try to use the module — and **who requires it**. The reverse direction is also part of the uninstall confirmation, because checking only script imports would silently miss "another shared module depends on this".
68
+ - **Update state is reported in five cases, not two.** The latest version comes from the same registry search the module list already performs (measured: the search result's `version` equals the registry's `dist-tags.latest`, 6/6 sampled), so it costs no extra network round trip. Crucially, *"cannot tell"* is a distinct state from *"up to date"* — being offline is never rendered as up to date. Local (`file:`) installs report *not applicable*, because "updating" them would just reinstall the same local source. When an update exists, the install button reads "Update to x.y.z".
69
+ - Before uninstalling, scripts that still import the package are listed and confirmed, because their imports will break.
70
+ - Windows note: npm is launched through `cmd.exe /d /s /c` with hand-quoted arguments, because Node refuses to `spawn` a `.cmd` file directly since the CVE-2024-27980 fix — and the spawn **must** pass `windowsVerbatimArguments: true`, otherwise Node re-escapes the quotes, `cmd` treats the whole line as one command name, and **npm never actually runs** (the flag travels inside the spawn spec so it cannot be forgotten; `tests/npm-spawn.test.js` really executes a process to guard this). Child output is **not** decoded as plain UTF-8: it is decoded strictly as UTF-8 first and **falls back to the console code page (GBK)** otherwise — `cmd.exe`'s own messages are not UTF-8 on a Chinese Windows, and decoding them as UTF-8 is exactly what turns them into mojibake. Failure signatures are matched in **both English and Chinese** for the same reason. See `spawnSpecFor` / `decodeProcessOutput` in `source/shared.ts`.
71
+
72
+ Where the "Available" list comes from:
73
+
74
+ | Source | Status |
75
+ | --- | --- |
76
+ | The framework's **own workspace** (`packages/shared/*`) | implemented — installs via a `file:` specifier, so you get the workspace copy. **Development-only**: it is only recognized when the surrounding repo really is the framework's (checked by the root `package.json` name), so the concept never surfaces in a user project — not even if that project happens to contain a `packages/shared` directory |
77
+ | A remote npm registry | implemented — discovered via `npm search keywords:ccgf-shared --json`, which keeps discovery **independent of the package scope** (the search API's `scope:` qualifier does not work; `keywords:` does exact matching). Results are cached for 5 minutes, the Refresh button forces a refetch, and a failed fetch only shows a hint — it never blocks the rest of the panel. Going through npm rather than a hand-rolled HTTPS call means the project's `.npmrc` (registry, proxy, auth, `cafile`) is honoured, which is what private registries need |
78
+
79
+ Shared modules are recognized by a **marker in their `package.json`** (`cocosGameFramework.sharedModule`), written by `tools/build-shared.js` — not by matching package names, which would break as soon as the scope changes.
80
+
81
+ After installing or uninstalling, the script compiler may need a moment; if imports still report a missing module, let the editor recompile or restart it.
82
+
83
+ ## Prefab templates
84
+
85
+ | Template | Size | `_alignFlags` |
86
+ | --- | --- | --- |
87
+ | `empty` | 100×100 | no Widget |
88
+ | `fullscreen` | 1280×720 | 45 = TOP\|BOT\|LEFT\|RIGHT |
89
+ | `popup` | 600×400 | 192 = HORIZONTAL\|VERTICAL |
90
+ | `listitem` | 600×80 | 41 = TOP\|LEFT\|RIGHT |
91
+
92
+ ## Doctor rules
93
+
94
+ | Rule | Severity | What it catches |
95
+ | --- | --- | --- |
96
+ | `layers-drift` | error | framework `ELayer` and the extension's `LAYERS` are out of sync |
97
+ | `view-duplicate-name` | error | the same view class name in two modules — `ViewManager` looks views up globally by class name and will load the wrong prefab |
98
+ | `view-layer-invalid` | error | `layer` is not one of the framework layers |
99
+ | `view-path-not-found` | error | `@RegisterView.path` does not match any prefab |
100
+ | `view-script-not-mounted` | error | the prefab does not carry the view's script |
101
+ | `import-broken` | error | a relative import is wrong or points at a missing file |
102
+ | `module-order-collision` | warn | duplicate `order` values |
103
+ | `module-order-missing` | warn | no parseable `order` |
104
+ | `module-model-missing` | warn | module without its Model script |
105
+ | `view-name-mismatch` | warn | file name / `@RegisterView.name` / `@ccclass` / class name disagree |
106
+ | `view-without-module` | warn | a `views/` directory with no module |
107
+ | `manager-not-singleton` | warn | a manager under `assets/scripts/manager` that does not extend `ASingleton` |
108
+ | `dangling-script-ref` | warn | scenes/prefabs reference a deleted script (orphan component) |
109
+
110
+ Findings that carry a file path are **clickable** and open the asset in the editor.
111
+
112
+ ## Generator guarantees
113
+
114
+ - **Rollback on failure**: module creation is **all-or-nothing** across Module/Model/Events — any failed step deletes what was already created; a failed prefab creation rolls back the script
115
+ - **Explicit "already exists" errors** instead of a vague "may already exist"
116
+ - **Cross-module name collision is refused** before anything is written
117
+ - **Reference check before delete**: deleting a module lists the assets that reference its scripts
118
+ - **Empty directories are pruned** after deleting views/modules
119
+
120
+ ## Commands
121
+
122
+ ```bash
123
+ npm install
124
+ npm run build # npx tsc -> dist/
125
+ npm test # node --test (94 tests)
126
+
127
+ # Run the doctor against a project without opening the editor
128
+ node tests/manual-doctor.js [project root]
129
+ ```
130
+
131
+ After changing any `.ts` you must **rebuild and reload the extension**. Editing `panel.html` only needs the panel to be reopened (it is read at `ready()`), but `index.css` is read at module load, so it also needs an extension reload.
132
+
133
+ ## Source layout
134
+
135
+ ```
136
+ source/
137
+ main.ts extension main process (the only side-effecting layer)
138
+ panels/workbench/index.ts Vue3 panel
139
+ constants.ts single source of truth for layers/dirs/templates
140
+ types.ts snapshot and finding types
141
+ naming.ts name normalization (pure)
142
+ project.ts project snapshot reader (the only I/O layer)
143
+ doctor.ts the 12 convention checks (pure)
144
+ templates.ts script and prefab templates (pure)
145
+ tests/ node --test: naming / templates / doctor / contract / i18n
146
+ i18n/ zh.js + en.js, key parity enforced by tests
147
+ ```
148
+
149
+ ## Known limitations
150
+
151
+ - Errors thrown by the main process are Chinese-only (the panel UI is fully bilingual)
152
+ - Prefab templates only shape the root node; no child skeleton (title/close button) yet
153
+ - `queryModuleRefs` scans serialized assets as text rather than using `asset-db`'s `query-asset-users` (whose signature is not verified)
@@ -0,0 +1,159 @@
1
+ # ccgf-game-tool
2
+
3
+ `cocos_gameframework` 的编辑器扩展:把框架的 MVC 约定**自动化**,并在约定被破坏时**提前报出来**。
4
+
5
+ ## 背景:框架的约定
6
+
7
+ 约定写在 [`docs/create-view-design.md`](./docs/create-view-design.md):
8
+
9
+ - **Module = Controller**:`@RegisterModule({ order, model })`,`model` 必填;一个模块目录 = 一对 `XxxModule.ts` + `XxxModel.ts`
10
+ - **View = 脚本 + 预制体**:`@RegisterView({ name, path, layer })`;脚本放在模块的 `views/` 下,预制体放在 `resources/views/<模块>/` 下,脚本挂在预制体根节点
11
+ - **`.meta`(含 UUID)由编辑器自动生成**,不手写
12
+
13
+ 这套约定原本只在「创建那一刻」靠工具帮忙,之后就全靠自觉。而约定被破坏后通常是**运行期静默出错**(加载错预制体、启动顺序不对、脚本丢失),定位成本很高。所以本扩展分两边:**生成器**(从零到有)+ **体检**(从有到对)。
14
+
15
+ ## 面板
16
+
17
+ 菜单 **开发者 → 工作台**(也可通过 `面板 → ccgf-game-tool`)。面板分两个切页。
18
+
19
+ ### 模块切页
20
+
21
+ | 区域 | 内容 |
22
+ | --- | --- |
23
+ | 左栏 | 模块列表(order 徽标、缺 Model 时显示告警点)、新建模块表单 |
24
+ | 右栏 | 选中模块的 order 编辑、模块的三个脚本(点击打开)、View 列表(层级标签、预制体缺失告警) |
25
+ | 顶部 | 「概览」启动顺序 + 层级占用;「体检」一键检查 |
26
+
27
+ 创建 View 时可选择**层级**(框架 `ELayer`)与**预制体模板**。
28
+
29
+ 创建模块会生成**三个**脚本:
30
+
31
+ | 文件 | 用途 |
32
+ | --- | --- |
33
+ | `<前缀>Module.ts` | `Module` 子类,用 `@RegisterModule` 注册 |
34
+ | `<前缀>Model.ts` | `Model` 子类 |
35
+ | `<Prefix>Events.ts` | **事件声明** —— 一段 `declare global { interface IGameEvents { ... } }` 增强 |
36
+
37
+ 事件声明文件是纯声明:它让本模块能派发事件、别的模块能监听,而两边**不需要互相 import**。里面的说明注释是**刻意保留**的 —— 尤其末尾的 `export { };` 不能删(没有 `import`/`export` 的文件会被当成*全局脚本*,`declare global` 会直接编译报错)。有一条测试专门编译这个模板,并断言删掉那一行会被编译器拒绝。设计与规则见 `docs/event.md`。
38
+
39
+ ### Manager 切页
40
+
41
+ 列出 `assets/scripts/manager/` 下已有的全局单例,并可直接新建 / 删除。
42
+
43
+ 约定的理由:manager 是**全局服务**,不属于某个业务模块,所以放在与 `framework/`、`gameplay/` 平级的 `assets/scripts/manager/`,并且继承框架的 `ASingleton`,用 `XxxManager.getInstance()` 取实例。生成的模板:
44
+
45
+ ```ts
46
+ import { ASingleton } from '../framework/common/Singleton';
47
+
48
+ export class ConfigManager extends ASingleton {
49
+
50
+ public init() {
51
+ }
52
+
53
+ public update(dt: number) {
54
+ }
55
+ }
56
+ ```
57
+
58
+ 列表里没继承 `ASingleton` 的 manager 会带告警点,对应体检的 `manager-not-singleton`。
59
+
60
+ ### 公共模块切页
61
+
62
+ 列出框架的**公共模块**(可选、可独立发布的包),分成**已安装**与**未安装**两组,并**直接代跑 npm** 完成安装 / 卸载。
63
+
64
+ - **安装**在工程根目录执行 `npm install <说明符>…`;**卸载**执行 `npm uninstall <包名>`。两者都带 `--no-audit --no-fund`,让输出聚焦在真正的问题上。
65
+ - **模块的本地依赖会一起装**。只装它一个的话,npm 会去 registry 找依赖 —— 而未发布的本地包在 registry 上当然不存在,实测会直接 `E404` 失败。面板上显示的命令与实际执行的是**同一个函数**产出的,所以你复制去手动跑,行为完全一致。
66
+ - **失败会把原始信息整个给你。** 不是一句"安装失败",而是弹窗展示**退出码、完整命令(方便你自己重跑)、以及 npm 的原始 stdout/stderr**;认出故障特征时再补一句提示(找不到 npm / 网络不可达 / 超时 / 证书 / 权限 / `ERESOLVE` / 404 / `EBADENGINE` ……)。实现见 `source/shared.ts`。
67
+ - **npm 命令常驻在界面上**(详情面板里),显示的是与按钮即将执行的动作一致的那条(未安装显示 `install`,已安装显示 `uninstall`),并提示**自动执行失败时可以把命令复制到工程根目录手动跑**。命令由**主进程**拼好下发,界面上是原样展示 —— 你复制到的就是按钮本来会执行的那条。
68
+ - 区分**三种状态**:`installed`(依赖里声明了**且** `node_modules` 里存在)、`declared-not-installed`(声明了但磁盘上没有 —— 上次安装大概失败了)、`not-installed`。
69
+ - **依赖关系双向显示**:它依赖谁(每个依赖都带**实时安装状态**,所以缺哪个在真正用到之前就能看见),以及**谁依赖它**。后一个方向也进了卸载确认 —— 只检查脚本引用的话,"另一个公共模块依赖它"会被静默漏掉。
70
+ - **可更新状态分五种,而不是"能更新 / 不能更新"两种**。远程最新版本号直接取自列表用的那次 registry 搜索(实测搜索结果的 `version` 就等于 registry 的 `dist-tags.latest`,抽样 6/6 一致),所以**不增加任何网络开销**。关键是 **"无法确认"和"已是最新"是两回事** —— 离线绝不会被显示成"已是最新"。本地 `file:` 来源显示为**不适用**,因为对它"更新"只会把同一个本地来源再装一遍。有新版时安装按钮直接变成「更新到 x.y.z」。
71
+ - 卸载前会把**仍在 import 它的脚本**列出来让你确认,因为卸载后这些 import 会失败。
72
+ - Windows 注意:npm 是通过 `cmd.exe /d /s /c` 启动并且**自己加引号**的 —— Node 在修了 CVE-2024-27980 之后不允许直接 `spawn` 一个 `.cmd` 文件;而且 spawn **必须**带上 `windowsVerbatimArguments: true`,否则 Node 会把引号再转义一遍,cmd 把整行当成一个命令名,**npm 根本不会被执行**(这个标志由启动说明符一起带出,忘不掉;`tests/npm-spawn.test.js` 会真的起一个进程来守它)。子进程输出**不能**一律按 UTF-8 解:先**严解码** UTF-8,失败再回退到**控制台代码页(中文机器上是 GBK)** —— `cmd.exe` 自己的消息在中文 Windows 上不是 UTF-8,按 UTF-8 解正好就是乱码的来源。同理,故障特征**中英文签名都匹配**。实现见 `source/shared.ts` 的 `spawnSpecFor` / `decodeProcessOutput`。
73
+
74
+ 「未安装」列表的来源:
75
+
76
+ | 来源 | 状态 |
77
+ | --- | --- |
78
+ | 框架**自己的工作区**(`packages/shared/*`) | 已实现 —— 用 `file:` 说明符安装,装的是工作区里那份源码。**只在开发时成立**:只有外层仓库确实是本框架的(按根 `package.json` 的 name 判定)才认,所以这个概念**永远不会出现在用户工程里** —— 即使那个工程恰好有个 `packages/shared` 目录 |
79
+ | 远程 npm registry | 已实现 —— 用 `npm search keywords:ccgf-shared --json` 发现,因此**与包 scope 无关**(搜索 API 的 `scope:` 限定符实测无效,`keywords:` 才是精确匹配)。结果缓存 5 分钟,「刷新」按钮强制重拉;拉取失败只显示提示,**不影响面板其余功能**。走 npm 而不是自己发 HTTPS,是为了自动带上工程的 `.npmrc`(registry / 代理 / 认证 / `cafile`)—— 私有 registry 靠的正是这些 |
80
+
81
+ 公共模块是靠 `package.json` 里的**标记**(`cocosGameFramework.sharedModule`,由 `tools/build-shared.js` 写入)认出来的,**不是靠包名匹配** —— 那样 scope 一改就认不出来了。
82
+
83
+ 装 / 卸之后脚本编译器可能需要一点时间;如果 import 仍提示找不到模块,让编辑器重新编译一次或重启。
84
+
85
+ ## 预制体模板
86
+
87
+ 生成器的默认产物是一个 100×100 的空根节点。模板只改根节点的尺寸与 `cc.Widget` 对齐 —— 这正是每个 View 都要手设一遍的重复劳动:
88
+
89
+ | 模板 | 尺寸 | 对齐(`_alignFlags`) |
90
+ | --- | --- | --- |
91
+ | `empty` | 100×100 | 不加 Widget |
92
+ | `fullscreen` | 1280×720 | 45 = TOP\|BOT\|LEFT\|RIGHT(四边拉伸) |
93
+ | `popup` | 600×400 | 192 = HORIZONTAL\|VERTICAL(居中) |
94
+ | `listitem` | 600×80 | 41 = TOP\|LEFT\|RIGHT(顶部横向拉伸) |
95
+
96
+ ## 体检的检查项
97
+
98
+ | 规则 | 级别 | 挡住的问题 |
99
+ | --- | --- | --- |
100
+ | `layers-drift` | error | 框架 `ELayer` 与扩展 `LAYERS` 不同步(加层漏改一处) |
101
+ | `view-duplicate-name` | error | 跨模块同名 View —— `ViewManager` 按类名全局查找,会加载错预制体 |
102
+ | `view-layer-invalid` | error | `layer` 不在框架层级里 |
103
+ | `view-path-not-found` | error | `@RegisterView.path` 对不上真实预制体 |
104
+ | `view-script-not-mounted` | error | 预制体里没挂该 View 脚本 |
105
+ | `import-broken` | error | 相对导入上溯层数不对 / 目标文件不存在 |
106
+ | `module-order-collision` | warn | order 撞号(实际启动顺序取决于脚本加载顺序,不确定) |
107
+ | `module-order-missing` | warn | 解析不到 order |
108
+ | `module-model-missing` | warn | 有 Module 没 Model |
109
+ | `view-name-mismatch` | warn | 文件名 / `@RegisterView.name` / `@ccclass` / 类名 四者不一致 |
110
+ | `view-without-module` | warn | `views/` 目录存在但模块不存在 |
111
+ | `manager-not-singleton` | warn | `assets/scripts/manager` 下的 manager 没继承 `ASingleton` |
112
+ | `dangling-script-ref` | warn | 场景/预制体引用了已删除的脚本(悬空组件,控制台会持续报 missing) |
113
+
114
+ 体检结果里带文件路径的条目**可以点击**,直接在编辑器里打开对应资源。
115
+
116
+ ## 生成器的其它保障
117
+
118
+ - **失败回滚**:建模块是 **Module/Model/Events 三个文件的原子操作** —— 任一步失败都会删掉已建的部分;预制体建失败会回滚脚本,不留半成品
119
+ - **同名提示明确**:先查存在性再动手,报「模块已存在:BagModule.ts(如需重建,请先删除该模块)」,而不是含糊的"可能已存在"
120
+ - **跨模块重名拦截**:创建 View 前扫全工程,重名直接拒绝并指出被哪个模块占用
121
+ - **删除前查引用**:删除模块时列出引用了该模块脚本的资源,避免留下悬空引用
122
+ - **空目录清理**:删除 View/模块后收掉 `resources/views/<模块>` 这类空壳
123
+
124
+ ## 命令
125
+
126
+ ```bash
127
+ npm install
128
+ npm run build # npx tsc -> dist/
129
+ npm test # node --test(94 项)
130
+
131
+ # 不开编辑器,直接对工程跑一遍体检(开发/排查用)
132
+ node tests/manual-doctor.js [工程根目录]
133
+ ```
134
+
135
+ `.ts` 改动后必须**重新编译并重载扩展**才会生效;只改 `panel.html` 的话重开面板即可,但 `index.css` 是在模块加载时读取的,同样需要重载扩展。
136
+
137
+ ## 源码结构
138
+
139
+ ```
140
+ source/
141
+ main.ts 主进程:方法注册 + 编辑器 API 调用(唯一的副作用层)
142
+ panels/workbench/index.ts Vue3 面板
143
+ constants.ts 层级/目录/模板/正则的唯一定义
144
+ types.ts 快照与发现的类型
145
+ naming.ts 命名归一化(纯函数)
146
+ project.ts 工程快照读取(唯一的 I/O 层)
147
+ doctor.ts 体检的 12 项检查(纯函数)
148
+ templates.ts 脚本与预制体模板(纯函数)
149
+ tests/ node --test:naming / templates / doctor / 契约 / i18n
150
+ i18n/ zh.js + en.js,key 集合由测试保证一致
151
+ ```
152
+
153
+ `doctor` / `naming` / `templates` 都是不碰 `fs`、不碰 `Editor` 的纯函数,因此可以被单测完全覆盖 —— 这也是 `project.ts` 之所以要把 I/O 单独拆出来的原因。
154
+
155
+ ## 已知限制
156
+
157
+ - 主进程抛出的错误文案目前只有中文(面板 UI 已完整支持中英切换)
158
+ - 预制体模板只生成根节点,不生成子节点骨架(标题/关闭按钮等)
159
+ - `queryModuleRefs` 的引用检查靠扫描序列化资源文本,没有用 `asset-db` 的 `query-asset-users`(后者参数与返回结构未核实)
@@ -0,0 +1,19 @@
1
+ {
2
+ "$schema": "https://schemastore.azurewebsites.net/schemas/json/tsconfig.json",
3
+ "compilerOptions": {
4
+ "target": "ES2017",
5
+ "module": "CommonJS",
6
+ "moduleResolution": "node",
7
+ "inlineSourceMap": true,
8
+ "inlineSources": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "strict": true,
12
+ "experimentalDecorators": true,
13
+ "forceConsistentCasingInFileNames": true,
14
+ "resolveJsonModule": true,
15
+ "outDir": "./dist",
16
+ "rootDir": "./source",
17
+ "types": ["node", "@cocos/creator-types/editor"]
18
+ }
19
+ }
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ /**
3
+ * 扩展的常量与唯一事实来源。
4
+ *
5
+ * 注意:`LAYERS` 是框架 `ELayer` 在扩展侧的镜像。
6
+ * 面板的层级下拉由主进程下发(query-layers),因此全工程只有这两份,
7
+ * 且体检里的 `layers-drift` 检查会盯着它们是否同步。
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.REGISTRY_CACHE_MS = exports.DISCOVERY_KEYWORD = exports.SHARED_MODULE_MARKER = exports.WORKSPACE_PACKAGE_NAME = exports.WORKSPACE_SHARED_SUBDIR = exports.COMPRESSED_UUID_RE = exports.PREFAB_TEMPLATE_LABELS = exports.PREFAB_TEMPLATES = exports.MANAGER_SINGLETON_IMPORT = exports.SINGLETON_PATH = exports.MANAGER_DIR = exports.VIEWS_SUBDIR = exports.VIEW_DEFINITION_PATH = exports.FRAMEWORK_DEV_DIR = exports.FRAMEWORK_PACKAGE = exports.SCENES_DIR = exports.RESOURCES_DIR = exports.SCRIPTS_DIR = exports.GAMEPLAY_DIR = exports.LAYERS = void 0;
11
+ /** View 层级顺序,与 framework/core/mvc/ViewDefinition.ts 的 ELayer 一致 */
12
+ exports.LAYERS = ['Scene', 'Window', 'Popup', 'Guide', 'Tips'];
13
+ /** 工程内相对路径(统一用 / 分隔) */
14
+ exports.GAMEPLAY_DIR = 'assets/scripts/gameplay';
15
+ exports.SCRIPTS_DIR = 'assets/scripts';
16
+ exports.RESOURCES_DIR = 'assets/resources';
17
+ exports.SCENES_DIR = 'assets/scenes';
18
+ /** 框架发布成 npm 包之后的包名(新工程用这个引用框架) */
19
+ exports.FRAMEWORK_PACKAGE = '@ccgf/framework';
20
+ /** 开发工作区里框架源码所在目录:它在,说明是开发工作区,用相对路径引用 */
21
+ exports.FRAMEWORK_DEV_DIR = 'assets/scripts/framework';
22
+ /** 框架 ELayer 定义文件(相对工程根,无扩展名) */
23
+ exports.VIEW_DEFINITION_PATH = 'assets/scripts/framework/core/mvc/ViewDefinition';
24
+ /** 模块下存放 View 脚本的子目录名 */
25
+ exports.VIEWS_SUBDIR = 'views';
26
+ /**
27
+ * 全局单例 manager 所在目录(与 framework、gameplay 平级)。
28
+ * manager 是全局服务,不属于某个业务模块,所以单独一层。
29
+ */
30
+ exports.MANAGER_DIR = 'assets/scripts/manager';
31
+ /** 框架 ASingleton 基类(相对工程根,无扩展名) */
32
+ exports.SINGLETON_PATH = 'assets/scripts/framework/common/Singleton';
33
+ /**
34
+ * manager 脚本里 import ASingleton 的相对路径。
35
+ * 必须与 MANAGER_DIR 的层级保持一致(manager 在 scripts 下一层 -> 上溯一层)。
36
+ */
37
+ exports.MANAGER_SINGLETON_IMPORT = '../framework/common/Singleton';
38
+ /** View 预制体模板 */
39
+ exports.PREFAB_TEMPLATES = ['empty', 'fullscreen', 'popup', 'listitem'];
40
+ exports.PREFAB_TEMPLATE_LABELS = {
41
+ empty: '空节点 100×100',
42
+ fullscreen: '全屏页(铺满父节点)',
43
+ popup: '弹窗(居中 600×400)',
44
+ listitem: '列表项(600×80,横向铺满)',
45
+ };
46
+ /** Cocos 压缩 UUID(脚本组件 cid)的形状:5 位 hex + 18 位 base64,共 23 字符 */
47
+ exports.COMPRESSED_UUID_RE = /^[0-9a-f]{5}[0-9a-zA-Z+/]{18}$/;
48
+ /**
49
+ * 工作区里存放公共模块包的目录(相对工作区根)。
50
+ *
51
+ * **这是开发期才有的东西**:只有本框架自己的仓库里才有它。
52
+ * 扩展是面向使用者的,所以不能只凭"往上找到这个目录"就启用本地安装源 ——
53
+ * 还要先确认那个根目录确实是本框架的工作区(见 WORKSPACE_PACKAGE_NAME)。
54
+ */
55
+ exports.WORKSPACE_SHARED_SUBDIR = 'packages/shared';
56
+ /**
57
+ * 本框架工作区根 `package.json` 的 name。
58
+ * 用它来认定"这是框架自己的开发仓库" —— 这样用户工程里即使恰好有
59
+ * `packages/shared`,也不会冒出"本地工作区"这种只有开发时才成立的概念。
60
+ */
61
+ exports.WORKSPACE_PACKAGE_NAME = 'cocos-gameframework';
62
+ /**
63
+ * 公共模块包在自己 package.json 里打的标记 key。
64
+ * 由 `tools/build-shared.js` 写入 —— 扩展靠它识别公共模块,
65
+ * 而不是靠包名匹配(scope 还没定,见 ROADMAP 的 S1a)。
66
+ */
67
+ exports.SHARED_MODULE_MARKER = 'cocosGameFramework';
68
+ /**
69
+ * 公共模块的**发现关键词**:扩展靠 `npm search keywords:<它>` 从 registry 找出所有公共模块。
70
+ *
71
+ * 为什么用关键词而不是 npm scope:
72
+ * - scope 还没定(S1a),而且**搜索 API 的 `scope:` 限定符实测无效**,而 `keywords:` 是精确匹配;
73
+ * - 以后换 scope 不用改扩展;私有 registry 也是同一套。
74
+ *
75
+ * ⚠️ 必须与 `tools/build-shared.js` 里的 DISCOVERY_KEYWORD 一致 —— 不一致时的表现是
76
+ * "列表永远是空的"这种静默故障,所以有测试盯着(tools/tests/discovery-keyword.test.js)。
77
+ */
78
+ exports.DISCOVERY_KEYWORD = 'ccgf-shared';
79
+ /** registry 搜索结果缓存多久(一次搜索实测 1~4 秒,不能每次刷新面板都去搜) */
80
+ exports.REGISTRY_CACHE_MS = 5 * 60 * 1000;
81
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29uc3RhbnRzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc291cmNlL2NvbnN0YW50cy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiO0FBQUE7Ozs7OztHQU1HOzs7QUFFSCxtRUFBbUU7QUFDdEQsUUFBQSxNQUFNLEdBQUcsQ0FBQyxPQUFPLEVBQUUsUUFBUSxFQUFFLE9BQU8sRUFBRSxPQUFPLEVBQUUsTUFBTSxDQUFVLENBQUM7QUFHN0Usd0JBQXdCO0FBQ1gsUUFBQSxZQUFZLEdBQUcseUJBQXlCLENBQUM7QUFDekMsUUFBQSxXQUFXLEdBQUcsZ0JBQWdCLENBQUM7QUFDL0IsUUFBQSxhQUFhLEdBQUcsa0JBQWtCLENBQUM7QUFDbkMsUUFBQSxVQUFVLEdBQUcsZUFBZSxDQUFDO0FBRTFDLG1DQUFtQztBQUN0QixRQUFBLGlCQUFpQixHQUFHLGlCQUFpQixDQUFDO0FBRW5ELHlDQUF5QztBQUM1QixRQUFBLGlCQUFpQixHQUFHLDBCQUEwQixDQUFDO0FBRTVELGlDQUFpQztBQUNwQixRQUFBLG9CQUFvQixHQUFHLGtEQUFrRCxDQUFDO0FBRXZGLHlCQUF5QjtBQUNaLFFBQUEsWUFBWSxHQUFHLE9BQU8sQ0FBQztBQUVwQzs7O0dBR0c7QUFDVSxRQUFBLFdBQVcsR0FBRyx3QkFBd0IsQ0FBQztBQUVwRCxtQ0FBbUM7QUFDdEIsUUFBQSxjQUFjLEdBQUcsMkNBQTJDLENBQUM7QUFFMUU7OztHQUdHO0FBQ1UsUUFBQSx3QkFBd0IsR0FBRywrQkFBK0IsQ0FBQztBQUV4RSxpQkFBaUI7QUFDSixRQUFBLGdCQUFnQixHQUFHLENBQUMsT0FBTyxFQUFFLFlBQVksRUFBRSxPQUFPLEVBQUUsVUFBVSxDQUFVLENBQUM7QUFHekUsUUFBQSxzQkFBc0IsR0FBb0M7SUFDbkUsS0FBSyxFQUFFLGFBQWE7SUFDcEIsVUFBVSxFQUFFLFlBQVk7SUFDeEIsS0FBSyxFQUFFLGdCQUFnQjtJQUN2QixRQUFRLEVBQUUsa0JBQWtCO0NBQy9CLENBQUM7QUFFRiwrREFBK0Q7QUFDbEQsUUFBQSxrQkFBa0IsR0FBRyxnQ0FBZ0MsQ0FBQztBQUVuRTs7Ozs7O0dBTUc7QUFDVSxRQUFBLHVCQUF1QixHQUFHLGlCQUFpQixDQUFDO0FBRXpEOzs7O0dBSUc7QUFDVSxRQUFBLHNCQUFzQixHQUFHLHFCQUFxQixDQUFDO0FBRTVEOzs7O0dBSUc7QUFDVSxRQUFBLG9CQUFvQixHQUFHLG9CQUFvQixDQUFDO0FBRXpEOzs7Ozs7Ozs7R0FTRztBQUNVLFFBQUEsaUJBQWlCLEdBQUcsYUFBYSxDQUFDO0FBRS9DLGtEQUFrRDtBQUNyQyxRQUFBLGlCQUFpQixHQUFHLENBQUMsR0FBRyxFQUFFLEdBQUcsSUFBSSxDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiDmianlsZXnmoTluLjph4/kuI7llK/kuIDkuovlrp7mnaXmupDjgIJcbiAqXG4gKiDms6jmhI/vvJpgTEFZRVJTYCDmmK/moYbmnrYgYEVMYXllcmAg5Zyo5omp5bGV5L6n55qE6ZWc5YOP44CCXG4gKiDpnaLmnb/nmoTlsYLnuqfkuIvmi4nnlLHkuLvov5vnqIvkuIvlj5HvvIhxdWVyeS1sYXllcnPvvInvvIzlm6DmraTlhajlt6XnqIvlj6rmnInov5nkuKTku73vvIxcbiAqIOS4lOS9k+ajgOmHjOeahCBgbGF5ZXJzLWRyaWZ0YCDmo4Dmn6XkvJrnm6/nnYDlroPku6zmmK/lkKblkIzmraXjgIJcbiAqL1xuXG4vKiogVmlldyDlsYLnuqfpobrluo/vvIzkuI4gZnJhbWV3b3JrL2NvcmUvbXZjL1ZpZXdEZWZpbml0aW9uLnRzIOeahCBFTGF5ZXIg5LiA6Ie0ICovXG5leHBvcnQgY29uc3QgTEFZRVJTID0gWydTY2VuZScsICdXaW5kb3cnLCAnUG9wdXAnLCAnR3VpZGUnLCAnVGlwcyddIGFzIGNvbnN0O1xuZXhwb3J0IHR5cGUgVExheWVyID0gKHR5cGVvZiBMQVlFUlMpW251bWJlcl07XG5cbi8qKiDlt6XnqIvlhoXnm7jlr7not6/lvoTvvIjnu5/kuIDnlKggLyDliIbpmpTvvIkgKi9cbmV4cG9ydCBjb25zdCBHQU1FUExBWV9ESVIgPSAnYXNzZXRzL3NjcmlwdHMvZ2FtZXBsYXknO1xuZXhwb3J0IGNvbnN0IFNDUklQVFNfRElSID0gJ2Fzc2V0cy9zY3JpcHRzJztcbmV4cG9ydCBjb25zdCBSRVNPVVJDRVNfRElSID0gJ2Fzc2V0cy9yZXNvdXJjZXMnO1xuZXhwb3J0IGNvbnN0IFNDRU5FU19ESVIgPSAnYXNzZXRzL3NjZW5lcyc7XG5cbi8qKiDmoYbmnrblj5HluIPmiJAgbnBtIOWMheS5i+WQjueahOWMheWQje+8iOaWsOW3peeoi+eUqOi/meS4quW8leeUqOahhuaetu+8iSAqL1xuZXhwb3J0IGNvbnN0IEZSQU1FV09SS19QQUNLQUdFID0gJ0BjY2dmL2ZyYW1ld29yayc7XG5cbi8qKiDlvIDlj5Hlt6XkvZzljLrph4zmoYbmnrbmupDnoIHmiYDlnKjnm67lvZXvvJrlroPlnKjvvIzor7TmmI7mmK/lvIDlj5Hlt6XkvZzljLrvvIznlKjnm7jlr7not6/lvoTlvJXnlKggKi9cbmV4cG9ydCBjb25zdCBGUkFNRVdPUktfREVWX0RJUiA9ICdhc3NldHMvc2NyaXB0cy9mcmFtZXdvcmsnO1xuXG4vKiog5qGG5p62IEVMYXllciDlrprkuYnmlofku7bvvIjnm7jlr7nlt6XnqIvmoLnvvIzml6DmianlsZXlkI3vvIkgKi9cbmV4cG9ydCBjb25zdCBWSUVXX0RFRklOSVRJT05fUEFUSCA9ICdhc3NldHMvc2NyaXB0cy9mcmFtZXdvcmsvY29yZS9tdmMvVmlld0RlZmluaXRpb24nO1xuXG4vKiog5qih5Z2X5LiL5a2Y5pS+IFZpZXcg6ISa5pys55qE5a2Q55uu5b2V5ZCNICovXG5leHBvcnQgY29uc3QgVklFV1NfU1VCRElSID0gJ3ZpZXdzJztcblxuLyoqXG4gKiDlhajlsYDljZXkvosgbWFuYWdlciDmiYDlnKjnm67lvZXvvIjkuI4gZnJhbWV3b3Jr44CBZ2FtZXBsYXkg5bmz57qn77yJ44CCXG4gKiBtYW5hZ2VyIOaYr+WFqOWxgOacjeWKoe+8jOS4jeWxnuS6juafkOS4quS4muWKoeaooeWdl++8jOaJgOS7peWNleeLrOS4gOWxguOAglxuICovXG5leHBvcnQgY29uc3QgTUFOQUdFUl9ESVIgPSAnYXNzZXRzL3NjcmlwdHMvbWFuYWdlcic7XG5cbi8qKiDmoYbmnrYgQVNpbmdsZXRvbiDln7rnsbvvvIjnm7jlr7nlt6XnqIvmoLnvvIzml6DmianlsZXlkI3vvIkgKi9cbmV4cG9ydCBjb25zdCBTSU5HTEVUT05fUEFUSCA9ICdhc3NldHMvc2NyaXB0cy9mcmFtZXdvcmsvY29tbW9uL1NpbmdsZXRvbic7XG5cbi8qKlxuICogbWFuYWdlciDohJrmnKzph4wgaW1wb3J0IEFTaW5nbGV0b24g55qE55u45a+56Lev5b6E44CCXG4gKiDlv4XpobvkuI4gTUFOQUdFUl9ESVIg55qE5bGC57qn5L+d5oyB5LiA6Ie077yIbWFuYWdlciDlnKggc2NyaXB0cyDkuIvkuIDlsYIgLT4g5LiK5rqv5LiA5bGC77yJ44CCXG4gKi9cbmV4cG9ydCBjb25zdCBNQU5BR0VSX1NJTkdMRVRPTl9JTVBPUlQgPSAnLi4vZnJhbWV3b3JrL2NvbW1vbi9TaW5nbGV0b24nO1xuXG4vKiogVmlldyDpooTliLbkvZPmqKHmnb8gKi9cbmV4cG9ydCBjb25zdCBQUkVGQUJfVEVNUExBVEVTID0gWydlbXB0eScsICdmdWxsc2NyZWVuJywgJ3BvcHVwJywgJ2xpc3RpdGVtJ10gYXMgY29uc3Q7XG5leHBvcnQgdHlwZSBUUHJlZmFiVGVtcGxhdGUgPSAodHlwZW9mIFBSRUZBQl9URU1QTEFURVMpW251bWJlcl07XG5cbmV4cG9ydCBjb25zdCBQUkVGQUJfVEVNUExBVEVfTEFCRUxTOiBSZWNvcmQ8VFByZWZhYlRlbXBsYXRlLCBzdHJpbmc+ID0ge1xuICAgIGVtcHR5OiAn56m66IqC54K5IDEwMMOXMTAwJyxcbiAgICBmdWxsc2NyZWVuOiAn5YWo5bGP6aG177yI6ZO65ruh54i26IqC54K577yJJyxcbiAgICBwb3B1cDogJ+W8ueeql++8iOWxheS4rSA2MDDDlzQwMO+8iScsXG4gICAgbGlzdGl0ZW06ICfliJfooajpobnvvIg2MDDDlzgw77yM5qiq5ZCR6ZO65ruh77yJJyxcbn07XG5cbi8qKiBDb2NvcyDljovnvKkgVVVJRO+8iOiEmuacrOe7hOS7tiBjaWTvvInnmoTlvaLnirbvvJo1IOS9jSBoZXggKyAxOCDkvY0gYmFzZTY077yM5YWxIDIzIOWtl+espiAqL1xuZXhwb3J0IGNvbnN0IENPTVBSRVNTRURfVVVJRF9SRSA9IC9eWzAtOWEtZl17NX1bMC05YS16QS1aKy9dezE4fSQvO1xuXG4vKipcbiAqIOW3peS9nOWMuumHjOWtmOaUvuWFrOWFseaooeWdl+WMheeahOebruW9le+8iOebuOWvueW3peS9nOWMuuague+8ieOAglxuICpcbiAqICoq6L+Z5piv5byA5Y+R5pyf5omN5pyJ55qE5Lic6KW/KirvvJrlj6rmnInmnKzmoYbmnrboh6rlt7HnmoTku5PlupPph4zmiY3mnInlroPjgIJcbiAqIOaJqeWxleaYr+mdouWQkeS9v+eUqOiAheeahO+8jOaJgOS7peS4jeiDveWPquWHrVwi5b6A5LiK5om+5Yiw6L+Z5Liq55uu5b2VXCLlsLHlkK/nlKjmnKzlnLDlronoo4XmupAg4oCU4oCUXG4gKiDov5jopoHlhYjnoa7orqTpgqPkuKrmoLnnm67lvZXnoa7lrp7mmK/mnKzmoYbmnrbnmoTlt6XkvZzljLrvvIjop4EgV09SS1NQQUNFX1BBQ0tBR0VfTkFNRe+8ieOAglxuICovXG5leHBvcnQgY29uc3QgV09SS1NQQUNFX1NIQVJFRF9TVUJESVIgPSAncGFja2FnZXMvc2hhcmVkJztcblxuLyoqXG4gKiDmnKzmoYbmnrblt6XkvZzljLrmoLkgYHBhY2thZ2UuanNvbmAg55qEIG5hbWXjgIJcbiAqIOeUqOWug+adpeiupOWumlwi6L+Z5piv5qGG5p626Ieq5bex55qE5byA5Y+R5LuT5bqTXCIg4oCU4oCUIOi/meagt+eUqOaIt+W3peeoi+mHjOWNs+S9v+aBsOWlveaciVxuICogYHBhY2thZ2VzL3NoYXJlZGDvvIzkuZ/kuI3kvJrlhpLlh7pcIuacrOWcsOW3peS9nOWMulwi6L+Z56eN5Y+q5pyJ5byA5Y+R5pe25omN5oiQ56uL55qE5qaC5b+144CCXG4gKi9cbmV4cG9ydCBjb25zdCBXT1JLU1BBQ0VfUEFDS0FHRV9OQU1FID0gJ2NvY29zLWdhbWVmcmFtZXdvcmsnO1xuXG4vKipcbiAqIOWFrOWFseaooeWdl+WMheWcqOiHquW3sSBwYWNrYWdlLmpzb24g6YeM5omT55qE5qCH6K6wIGtleeOAglxuICog55SxIGB0b29scy9idWlsZC1zaGFyZWQuanNgIOWGmeWFpSDigJTigJQg5omp5bGV6Z2g5a6D6K+G5Yir5YWs5YWx5qih5Z2X77yMXG4gKiDogIzkuI3mmK/pnaDljIXlkI3ljLnphY3vvIhzY29wZSDov5jmsqHlrprvvIzop4EgUk9BRE1BUCDnmoQgUzFh77yJ44CCXG4gKi9cbmV4cG9ydCBjb25zdCBTSEFSRURfTU9EVUxFX01BUktFUiA9ICdjb2Nvc0dhbWVGcmFtZXdvcmsnO1xuXG4vKipcbiAqIOWFrOWFseaooeWdl+eahCoq5Y+R546w5YWz6ZSu6K+NKirvvJrmianlsZXpnaAgYG5wbSBzZWFyY2gga2V5d29yZHM6POWugz5gIOS7jiByZWdpc3RyeSDmib7lh7rmiYDmnInlhazlhbHmqKHlnZfjgIJcbiAqXG4gKiDkuLrku4DkuYjnlKjlhbPplK7or43ogIzkuI3mmK8gbnBtIHNjb3Bl77yaXG4gKiAtIHNjb3BlIOi/mOayoeWumu+8iFMxYe+8ie+8jOiAjOS4lCoq5pCc57SiIEFQSSDnmoQgYHNjb3BlOmAg6ZmQ5a6a56ym5a6e5rWL5peg5pWIKirvvIzogIwgYGtleXdvcmRzOmAg5piv57K+56Gu5Yy56YWN77ybXG4gKiAtIOS7peWQjuaNoiBzY29wZSDkuI3nlKjmlLnmianlsZXvvJvnp4HmnIkgcmVnaXN0cnkg5Lmf5piv5ZCM5LiA5aWX44CCXG4gKlxuICog4pqg77iPIOW/hemhu+S4jiBgdG9vbHMvYnVpbGQtc2hhcmVkLmpzYCDph4znmoQgRElTQ09WRVJZX0tFWVdPUkQg5LiA6Ie0IOKAlOKAlCDkuI3kuIDoh7Tml7bnmoTooajnjrDmmK9cbiAqIFwi5YiX6KGo5rC46L+c5piv56m655qEXCLov5nnp43pnZnpu5jmlYXpmpzvvIzmiYDku6XmnInmtYvor5Xnm6/nnYDvvIh0b29scy90ZXN0cy9kaXNjb3Zlcnkta2V5d29yZC50ZXN0Lmpz77yJ44CCXG4gKi9cbmV4cG9ydCBjb25zdCBESVNDT1ZFUllfS0VZV09SRCA9ICdjY2dmLXNoYXJlZCc7XG5cbi8qKiByZWdpc3RyeSDmkJzntKLnu5PmnpznvJPlrZjlpJrkuYXvvIjkuIDmrKHmkJzntKLlrp7mtYsgMX40IOenku+8jOS4jeiDveavj+asoeWIt+aWsOmdouadv+mDveWOu+aQnO+8iSAqL1xuZXhwb3J0IGNvbnN0IFJFR0lTVFJZX0NBQ0hFX01TID0gNSAqIDYwICogMTAwMDtcbiJdfQ==