commit 14c5b00f1496b02870d0a34a5186a804070e4363 Author: furao Date: Sat Jun 6 11:33:19 2026 +0800 Initial public release: cc-3-8-x-mcp Cocos Creator 3.8.x MCP bridge extension with a built-in offline CLI. Components: - Editor extension: in-process MCP server exposing scene / asset-db / preview / local / editor-process-control tools - stdio router: aggregates multiple editor instances on one machine, with shortName dedup - offline CLI (cocos-mcp-cli): headless prefab read/write + a wrapper around the Cocos CLI build Pure Node.js, zero third-party dependencies. Licensed under Apache-2.0. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f803280 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# 依赖与构建产物 +node_modules/ + +# 临时与本机产物 +.dev/ +.DS_Store +*.log +*.bak +*.bak.* + +# 开发过程文档 / 实验(不对外开源) +REVIEW_*.md +CLI_ARRAY_REF_REPORT.md +poc/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..cc384d4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative + Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2026 付饶 + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..0023bf2 --- /dev/null +++ b/NOTICE @@ -0,0 +1,6 @@ +cc-3-8-x-mcp +Copyright 2026 付饶 + +This product is licensed under the Apache License, Version 2.0. +See the LICENSE file for the full license text, or visit +http://www.apache.org/licenses/LICENSE-2.0 diff --git a/QUICK-REF.md b/QUICK-REF.md new file mode 100644 index 0000000..c30a16c --- /dev/null +++ b/QUICK-REF.md @@ -0,0 +1,163 @@ +# Quick Ref — Agent 速查表 + +> 一页搞定。复杂场景看 [`doc/cli.md`](./doc/cli.md)。 + +## 入口(在 `forest/` 项目根执行) + +```bash +node extensions/cc-3-8-x-mcp/cli/bin/cocos-mcp-cli.js +``` + +## 节点定位三种形式(`node` / `parent` / `target` / `source` / `refNode` 通用) + +| 写法 | 用途 | +|---|---| +| `"itemList"` | 名字,首个匹配 | +| `{"id": 65}` | __id__;**stub 节点 `_name` 为 null,必须用这个** | +| `{"path": "Canvas/Main/itemList"}` | DOM-like 路径;同名节点多时用 | + +## 我要做什么 → 用什么 + +### 看 prefab + +| 场景 | 命令 | +|---|---| +| 看节点树 | `query --selector tree` | +| 看节点树 + 所有组件字段 | `query --selector tree --with-comps` | +| 看单节点详情 | `query --selector node --name X --with-comps` | +| 拿单组件单字段值(脚本管道) | `query --selector field --name X --comp cc.UITransform --field _anchorPoint` | +| 列所有 cc.Label 的 id | `query --selector find --type cc.Label` | +| 看 stub 节点已写入的所有 propertyOverrides | `query --selector overrides --id 58` | +| 比较两个 prefab 字段差异 | `diff ` | + +### 改 prefab — 节点字段 + +| 场景 | op | +|---|---| +| 改 _active | `set-active` | +| 改 _name | `rename-node` | +| 改 _lpos(绝对值 x/y/z) | `set-position` | +| 改 _lpos(相对偏移 dx/dy/dz) | `adjust-position` | +| 改 _color(r/g/b/a,0-255) | `set-node-color` | +| 调子节点顺序 | `reorder-children`(order 必须含全部子节点) | +| 把节点搬到另一个父节点下 | `reparent`(node + parent + index?;不复制;普通 inline 节点;自带循环检测)| + +### 改 prefab — 组件字段 + +| 场景 | op | +|---|---| +| 改普通节点任意组件任意字段(含嵌套路径 `["_color","r"]`) | `set-component-field` | +| 改 stub 内部组件字段 | `set-nested-component-field` | +| 清 stub 字段 override(回滚嵌套默认) | `reset-overrides`(单条 + property / `all:true` 清空) | +| 启用/禁用某组件 | `set-component-enabled` | +| cc.UITransform 锚点(含自动补偿 lpos,stub 也支持) | `set-anchor`(带 `compensatePosition: true`) | +| cc.UITransform 尺寸(stub 也支持) | `set-size` | +| cc.Label 文字 | `set-label-text` | +| cc.Label 多字段(fontSize / overflow / bold 等) | `set-label` | +| cc.Sprite 换图 | `set-sprite-frame` | +| cc.Sprite 模式字段(sizeMode / type / grayscale) | `set-sprite` | +| cc.Button 多字段(interactable / transition 等) | `set-button` | +| cc.EditBox 多字段(inputMode / maxLength / placeholder) | `set-editbox` | +| cc.Layout 多字段(type / spacing / padding) | `set-layout` | +| cc.RichText 多字段(text / maxWidth) | `set-richtext` | +| 一次改一批(按组件类型 / 名前缀 / 正则筛选) | `bulk-set` | + +### 改 prefab — 节点结构 / 引用 + +| 场景 | op | +|---|---| +| 加节点 | `add-node` | +| 加嵌套 prefab 实例(stub) | `add-nested-prefab`(parent + prefabUuid + name? + lpos?) | +| 替换嵌套 prefab 的 asset uuid(保留 stub 结构) | `replace-nested-prefab`(target + prefabUuid + clearOverrides?) | +| 删节点 | `remove-node` | +| 清悬空嵌套实例根(删了一半的 prefab 残留:父引用没了但根 PrefabInfo 登记还在,残留 asset 仍被加载 → 404)| `sync-nested-roots`(无参,重建根 nestedPrefabInstanceRoots) | +| 复制节点 | `clone-node` | +| 加组件 | `add-component` | +| 删组件(普通节点) | `remove-component`(stub 不支持,用 `set-component-enabled` 禁用) | +| **新建 .ts / .json 后让 cli 当场可识别** | `ensure-meta`(path 相对项目根或绝对路径,建 v4 uuid meta)。**新建脚本必须用**——不然 add-component 会因 cli 查不到 .meta 抛错。同 batch 内放在 `add-component` 前 | +| 给脚本 @property 挂节点引用 | `set-component-ref`(refType=`cc.Node`) | +| 给脚本 @property 挂组件引用 | `set-component-ref`(refType=`cc.Button` 等) | +| 给脚本 @property 挂 stub 内组件 | `set-component-ref`(refNode 是 stub,自动走 TargetOverrideInfo) | +| 给脚本 @property 挂多层嵌套 stub 内组件 | `set-component-ref`(refSubNode 用字符串数组 `["A","B"]`) | +| 给脚本 @property **数组字段** 按索引挂载(`_items[0]`/`_items[1]`…) | `set-component-ref`(property 写 `"_items.0"` 或 `"_items[0]"`,多次调用各索引共存) | +| 合并同节点重复组件(cli 字符串版 + 编辑器压缩版) | `dedupe-component` | + +### 跨文件 / 其他 + +| 场景 | 命令 | +|---|---| +| 跑 ops.json | `batch ` | +| 干跑预览(不写盘) | `batch --dry-run` | +| 跨多个 prefab 跑同一组 ops | `batch --glob ""`(先 `--dry-run` 确认匹配) | +| 操作 .anim 文件 | `anim query` / `anim batch ` | +| 单字段快捷写入(active / label.text / position.x\|y\|z) | `set ` | +| 创建新 prefab(最小 root + UITransform) | `create-prefab [--name X] [--width W] [--height H]` | +| 创建 spine prefab(root + UITransform + sp.Skeleton) | `create-prefab --add-spine `,批量靠 shell `for` 循环喂 .skel.meta 的 uuid | +| **从 src 提取某子节点为独立 prefab**(含组件 + PrefabInfo + stub 嵌套等所有引用闭包) | `extract-prefab --node [--name X]`。selector 同 batch 三种(名/`{id:N}`/`{path:"A/B"}`)。新根 `_parent=null`,PrefabInfo.root 指自己、asset 指 idx 0;适合"把 HomeBottom.btnTask 拆成 task BottomEntry.prefab" 这类场景 | +| **清 prefab data 数组 null 槽位 + 重映射 __id__**(早期手工生成的历史包袱) | `compact-prefab [--dry-run]`。Cocos editor 反序列化容错跳过 null,但 build worker 严格 scan 撞 null 崩 `Cannot read properties of undefined (reading '__type__')`。算法同 extract-cmd line 105-132 紧凑 push + remap,但不剔除任何东西。dry-run 输出 dangling 引用警告(指向已删 null 的 `__id__`)。只清顶层 data 数组 null;子节点字段里的 null 引用要靠 GUI 重存或 query+set 单点修复 | + +## 写 ops.json 速记 + +```json +[ + {"op": "set-active", "node": "X", "active": false}, + {"op": "rename-node", "node": "X", "name": "Y"}, + {"op": "reparent", "node": "child", "parent": "newParent"}, + {"op": "reparent", "node": {"id": 33}, "parent": "container", "index": 0}, + {"op": "set-anchor", "node": "X", "y": 1, "compensatePosition": true}, + {"op": "set-size", "node": "X", "width": 600, "height": 800}, + {"op": "set-component-field", "node": "X", "componentType": "cc.UITransform", "property": "_anchorPoint", "value": {"__type__": "cc.Vec2", "x": 0.5, "y": 1}}, + {"op": "set-component-field", "node": "X", "componentType": "cc.Label", "property": ["_color", "r"], "value": 255}, + {"op": "set-nested-component-field", "node": {"id": 33}, "componentType": "cc.Label", "property": "_string", "value": "新文字"}, + {"op": "set-component-ref", "node": "X", "componentType": "MyUI", "property": "_role", "refNode": {"id": 33}, "refType": "cc.Node"}, + {"op": "set-component-ref", "node": "X", "componentType": "MyUI", "property": "_items.0", "refNode": {"id": 27}, "refType": "ItemComp"}, + {"op": "set-component-ref", "node": "X", "componentType": "MyUI", "property": "_items[1]", "refNode": {"id": 40}, "refType": "ItemComp"}, + {"op": "bulk-set", "selector": {"byComponent": "cc.Label"}, "target": "component:cc.Label", "property": "_isItalic", "value": true}, + {"op": "reorder-children", "node": "list", "order": ["item3", "item1", "item2"]} +] +``` + +## 高频踩坑(详见 doc/cli.md §8) + +| 症状 | 原因 / 解法 | +|---|---| +| `找不到节点 "xxx"` 但 query 看见了 | xxx 是 stub,`_name` 为 null。改用 `{id:N}` | +| 修改后 Cocos 编辑器打开发现字段没生效 | stub 节点要走 propertyOverrides。`set-component-field` 用错了,stub 改字段必须用 `set-nested-component-field` | +| 改 anchor 后视觉位置偏了 | 加 `compensatePosition: true` 自动补偿 lpos | +| 字段拼错 / 缺必填 / 未知 op / 类型错 | schema 校验跑前一次报齐(`comp` → `componentType` 拼写提示;`width:"100"` 类型错明示期望类型) | +| 改 cc.Vec2/Vec3/Size 字段后运行时取值不对 | `value` 必须带 `__type__: "cc.Vec2"`,`set-anchor` / `set-size` 已自动带 | +| `reorder-children` 抛错 "order 长度 ≠ _children 长度" | order 必须列全部子节点(不允许只列要前置的几个) | +| `{path:...}` 抛错"同名子节点 N 个" | path 不能消歧。用 `{id:N}` 精确定位 | +| `_name` 为 null(query tree 输出里看到) | stub 节点正常现象,名字在 PrefabInstance.propertyOverrides,运行时填 | +| `set-component-ref` 报"未挂 XXX 组件",但 add-component 刚刚加上 | `componentType` 格式不一致:add-component 传了原始 UUID,set-component-ref 传了 @ccclass 名,两者被规范化成不同字符串。**修复已合入**:三种形式(@ccclass 名 / 原始 UUID / 压缩 classId)现在可混用,同 batch add+ref 也不再需要拆分。 | +| `set-component-ref` 多次挂同一数组字段(`_items`),后一条覆盖前一条 | 旧版幂等检查按 `propertyPath[0]` 去重,同字段名只保留一条。**修复已合入**:`property` 支持 `"_items.0"` / `"_items[0]"` 写法,幂等 key 改为完整路径数组,各索引独立共存。 | +| nested prefab 数组字段 override 写入后 Cocos inspector 显示空 / 运行时 `_items[N]` 全是 null | **数组索引 propertyPath 必须 int 不是 string**:propertyPath 末段(数组索引)类型必须是 number,不是 string。Cocos 编辑器按 string key 匹配不到数组槽,override 静默失效。CLI `addRootTargetOverride` 已在写入前自动 normalize(2026-05-18 修),调用 `set-component-ref` 传 `"_items.0"` 或 `"_items[0]"` 均安全,直接传 `["_items", "0"]` string 数组也会被自动转 int。 | +| stub 节点 `set-position` / `rename-node` 写入后,Cocos 加载该 prefab 时 _name / _lpos override 没生效(嵌套实例显示默认值) | **stub-node-field 类型 override.targetInfo.localID 必须是「嵌套 prefab 内根节点 PrefabInfo.fileId」**,不是「外层 stub 自己的 PrefabInfo.fileId」。早期 fgui→cc3 转出的 prefab 设计上两个 fileId 一致,所以巧合工作;手编/重设计的 prefab 一般不一致就暴露。CLI `setOverrideProperty` 已在写入前加载嵌套 prefab 拿真实根 fileId(2026-05-20 修,**解析失败抛错**),并在命中旧版 stubFileId 写入的条目时**自动矫正** localID(一次性迁移历史脏数据)。`listOverrides` / `reset-overrides` 只识别真值 localID。 | +| `set-component-ref` 新加单字段引用(如 `_btnClose`)后,Cocos 加载时 `ui._xxx` 仍为 null,对应字段没赋值 | **Cocos 加载 rootTargetOverrides 数组时,单字段 override 必须排在数组字段 override(`_items[N]`)之前**,否则被静默跳过。CLI `addRootTargetOverride` 已自动按 propertyPath 长度分插(2026-05-20 修,`cli/src/editor/nested.js`):`propertyPath.length === 1` 插到第一个数组字段 override 之前,`length > 1` 追加到末尾。原 `rootPrefabInfo.targetOverrides.push(...)` 改为 `splice(firstArrayIdx, 0, ...)`。 | +| `remove-node` 删嵌套 stub 后,Cocos 加载父 prefab 报错 / 外层脚本对该 stub 内部的引用(如 `_passScoreView`)悬空 | 删 stub 时只移除了 `_children`/`mountedChildren`/`nestedPrefabInstanceRoots`,**根 PrefabInfo.targetOverrides 里 source/target 指向被删子树的条目没清**——外层脚本 @property 引用嵌套实例内部组件/节点走 targetOverride,残留为**可达**悬空引用(区别于软删保留的不可达孤儿,后者无害)。**修复已合入**(2026-06-01,`cli/src/editor/ops/remove-node.js` 加 `collectSubtreeIds` + `cleanupRootTargetOverrides`):删子树前收集全部 __id__,从根 targetOverrides 过滤掉 source/target 落入子树的条目(被删 override 对象本身保留为孤儿,符合软删策略);`target=null`(走 targetInfo.localID)的无关条目保留。 | + +## 标准工作流 + +```bash +# 1. 看节点树拿 id +node extensions/cc-3-8-x-mcp/cli/bin/cocos-mcp-cli.js query --selector tree + +# 2. 写 /tmp/ops.json + +# 3. 干跑预览 +node extensions/cc-3-8-x-mcp/cli/bin/cocos-mcp-cli.js batch /tmp/ops.json --dry-run + +# 4. 落盘 +node extensions/cc-3-8-x-mcp/cli/bin/cocos-mcp-cli.js batch /tmp/ops.json + +# 5. 类型检查 +npx tsc --noEmit +``` + +## 完整文档 + +- [`README.md`](./README.md) — MCP 扩展定位 + 架构 + tools 清单 +- [`doc/cli.md`](./doc/cli.md) — **CLI 完整手册**(命令、配方、已知坑、源码导航) +- [`doc/prefab-schema.md`](./doc/prefab-schema.md) — prefab JSON 结构 +- [`doc/nested-prefab-protocol.md`](./doc/nested-prefab-protocol.md) — `cc.TargetOverrideInfo` 协议 +- [`doc/anim-schema.md`](./doc/anim-schema.md) — `.anim` 文件结构 diff --git a/README.md b/README.md new file mode 100644 index 0000000..afa872e --- /dev/null +++ b/README.md @@ -0,0 +1,222 @@ +# cc-3-8-x-mcp + +Cocos Creator 3.8.x 的 MCP 桥接扩展 + 离线 prefab 读写 CLI。 + +把编辑器的 scene/asset/preview/local 能力以 MCP 协议暴露给 Claude Code;同时提供无需编辑器运行的 offline prefab 编辑能力,支持节点增删克隆与 stub 节点属性覆写。 + +--- + +## 文档导航 + +| 文档 | 内容 | 阅读时机 | +|---|---|---| +| [`QUICK-REF.md`](./QUICK-REF.md) | **一页速查表**:节点定位三式、场景 → op 对照、ops.json 速记、踩坑表 | agent 起手第一份,挑不到再翻 cli.md | +| [`doc/cli.md`](./doc/cli.md) | **CLI 完整手册**:命令、26 个 op 全表、配方、已知坑、源码导航 | 改 `.prefab` / `.anim` 文件前必读 | +| [`doc/prefab-schema.md`](./doc/prefab-schema.md) | CC3 prefab JSON 结构速查(节点 / 组件 / 引用字段格式) | 看不懂 prefab 字段时查 | +| [`doc/nested-prefab-protocol.md`](./doc/nested-prefab-protocol.md) | `cc.TargetOverrideInfo` 协议(跨 nested @property 挂载) | 调试 set-component-ref 跨 stub 失败时 | +| [`doc/prefab-direct-edit.md`](./doc/prefab-direct-edit.md) | offline CLI vs 编辑器路径决策表 | 不确定该用 CLI 还是 scene_set_property 时 | +| [`doc/anim-schema.md`](./doc/anim-schema.md) | `.anim` 文件结构 + Track 字段规范 | 改动画文件结构时 | + +--- + +## 架构 + +``` +Claude Code (MCP client) + │ stdio (JSON-RPC) + ▼ +┌─────────────────────────────┐ +│ router/bin.js │ ← 统一入口,聚合多个编辑器 +│ - 扫 ~/.cocos-mcp/editors/ │ +│ - offline prefab tools │ +└───────────┬─────────────────┘ + │ HTTP MCP (JSON-RPC) + ┌───────┴────────┐ + │ │ + ▼ ▼ +编辑器实例 A 编辑器实例 B ← 每个项目一个编辑器进程 +server/mcp-server.js ← 在 Cocos 编辑器进程内跑 HTTP MCP server +``` + +offline prefab tools(`prefab_query` / `prefab_edit` / `prefab_batch`)在 router 进程内直接执行,调用 `cli/src/index.js`,不需要编辑器运行。 + +--- + +## 三个组件 + +### 1. 编辑器扩展(`main.js` + panel + server) + +在 Cocos Creator 编辑器进程内运行。启动时拉起 `server/mcp-server.js`(HTTP MCP server),并把自身信息写入 `~/.cocos-mcp/editors/.json`(心跳注册)。 + +**暴露的 tool 域**: + +| 域 | 职责 | +|---|---| +| scene | 查询/修改节点树,打开/保存/重载场景,调用组件方法 | +| asset-db | 资源查询、导入、创建、保存、删除、移动 | +| preview | 预览地址查询、浏览器控制、截图、JS 注入 | +| local | 本地状态、worktree 列表、.dev 目录管理 | + +### 2. stdio router(`router/`) + +入口:`router/bin.js` + +**职责**: + +- 扫描 `~/.cocos-mcp/editors/` 发现活跃编辑器(心跳超 120s 视为已死) +- 每隔 15s 自动发现新实例 +- 给每个编辑器的 tool 加 `__` 前缀,合并后暴露给 Claude Code +- 内置 offline prefab tools(`prefab_query` / `prefab_edit` / `prefab_batch`),不带前缀,全局可用 + +**tool 路由示例**: + +``` +forest__scene_query_node_tree → forest 编辑器的 HTTP MCP server +another__asset_query_assets → another 编辑器的 HTTP MCP server +prefab_query → router 本地执行(cli),无需编辑器 +``` + +### 3. cocos-mcp-cli(`cli/`) + +零依赖 Node CLI,直接读写 `.prefab` 文件,无需 Cocos 编辑器运行。详见 [`doc/cli.md`](./doc/cli.md)。 + +--- + +## 功能面板 + +通过菜单「扩展 → Cocos MCP → 功能面板」打开,或在编辑器扩展面板停靠。 + +| 区块 | 内容 | +|---|---| +| MCP Server | 运行状态指示灯 / 端点地址 / tool 数量 / 请求计数;复制端点、复制 CLI 命令、重启 | +| 编辑器状态 | 当前分支 / HEAD / 预览地址和端口 / 编辑器 PID / Watcher 状态 / 最后更新时间 | +| 快捷动作 | 一键刷新(资源+场景+预览)/ 软重载场景 / 打开预览浏览器 / 截图 / 打开 .dev 目录 / 清理临时文件 / 手动输入路径重新导入 | +| Debug 注入 | 在预览页面执行任意 JS(`eval_js`),结果直接展示;自定义快捷按钮(配置见下方) | +| 同机 Worktree | 列出同机其他 worktree 及其预览端口,方便多开切换 | +| 命令日志 | 最近 30 条操作记录(时间 / 来源 / 命令) | + +**自定义 Debug 按钮**:在项目根目录新建 `.dev/cc-mcp-panel.json`: + +```json +{ "buttons": [{ "label": "解锁签到", "code": "app.userMod.setUserValue(0,1)" }] } +``` + +--- + +## Tools 清单 + +### scene 域(需编辑器运行) + +| Tool | 说明 | +|---|---| +| `scene_query_node_tree` | 查询当前场景节点树;传 uuid 查子树 | +| `scene_query_node` | 查询单节点完整 dump(含所有组件属性) | +| `scene_set_property` | 设置节点/组件属性(path 为 dump path,如 `position`、`__comps__.0.string`) | +| `scene_open_scene` | 打开场景(传场景资源 uuid) | +| `scene_save_scene` | 保存当前场景 | +| `scene_soft_reload` | 软重载场景,不清编辑器状态 | +| `scene_execute_component_method` | 调用指定节点的组件方法 | + +> 修改 prefab 资源文件属性建议用 `prefab_edit`(offline),`scene_set_property` 只适用于运行时节点或需要编辑器上下文的情况。 + +### asset-db 域(需编辑器运行) + +| Tool | 说明 | +|---|---| +| `asset_query_assets` | 按 glob pattern 列资源(如 `db://assets/**/*.prefab`) | +| `asset_query_info` | 查资源详情(传 uuid 或 url) | +| `asset_query_url` | 由 uuid 查资源 url | +| `asset_query_uuid` | 由 url 查资源 uuid | +| `asset_refresh` | 刷新资源(全量或指定路径) | +| `asset_reimport` | 重新导入指定资源 | +| `asset_create` | 创建新资源 | +| `asset_save` | 保存资源内容 | +| `asset_delete` | 删除资源 | +| `asset_move` | 移动/重命名资源 | + +### preview 域(需编辑器运行) + +| Tool | 说明 | +|---|---| +| `preview_query_url` | 查询当前预览 URL | +| `preview_open_browser` | 在系统浏览器打开预览 | +| `preview_refresh_browser` | 刷新预览页面 | +| `preview_screenshot` | 截图,返回图片路径 | +| `preview_eval_js` | 在预览页面执行 JS,返回执行结果 | +| `preview_refresh_and_reload` | 重新导入资源后刷新预览(offline 改完必须调此工具才能看到效果) | + +### local 域(需编辑器运行) + +| Tool | 说明 | +|---|---| +| `local_get_status` | 获取编辑器本地状态(git branch、预览端口、PID 等) | +| `local_list_worktrees` | 列出同机所有 worktree | +| `local_open_dev_dir` | 在 Finder 中打开 .dev 目录 | +| `local_clean_dev_dir` | 清理 .dev 临时文件 | + +### offline 域(router 级,无需编辑器运行) + +| Tool | 说明 | +|---|---| +| `prefab_query` | 查询 prefab 节点树或单节点详情 | +| `prefab_edit` | 声明式批量编辑 prefab,所有 op 成功后一次性落盘 | +| `prefab_batch` | 从 JSON 文件读取 ops 后批量编辑 prefab | + +完整 op 列表与配方见 [`doc/cli.md`](./doc/cli.md)。 + +> offline tools 的 `filePath` 和 `opsJsonPath` 必须为绝对路径;router 以 stdio 模式运行,cwd 不确定,相对路径有歧义。 + +--- + +## 多开支持 + +每个 Cocos 编辑器实例启动时向 `~/.cocos-mcp/editors/.json` 写入注册信息: + +```json +{ + "pid": 12345, + "url": "http://127.0.0.1:7788/mcp", + "shortName": "forest", + "projectPath": "/path/to/project" +} +``` + +router 定期扫此目录,心跳超过 120s 的记录视为死亡自动剔除。tool 名以 `__` 为前缀隔离,同机多开不冲突。 + +--- + +## 接入 Claude Code + +```bash +claude mcp add cocos -- node /path/to/forest/extensions/cc-3-8-x-mcp/router/bin.js +``` + +接入后 Claude Code 即可调用所有活跃编辑器的 tool,以及全局 offline prefab tools。 + +--- + +## `.dev/refresh` 信号协议 + +外部往 `/.dev/refresh` 文件写一行命令,编辑器扩展的 watcher 读到后执行并清空文件。fire-and-forget,无返回值。 + +协议精简:**只支持 `restart-package`**——禁用→启用本扩展,让 `main.js` / `tools.js` / `server/*` 的代码改动生效。资源刷新 / 场景重载 / 预览刷新 / 截图等都走 MCP tool(`preview_refresh_and_reload` / `asset_reimport` / `preview_screenshot` 等)或面板按钮。 + +```bash +echo "restart-package" > .dev/refresh +``` + +面板上的「重启 MCP Server」按钮只重启 HTTP server 实例,Node require 缓存不动,**改不到 main.js 的代码改动**。`restart-package` 走 `Editor.Package.disable + enable`,整个扩展沙箱重建,所有 JS 重新 require。注意命令是 fire-and-forget,不返回结果;扩展重启过程中 MCP 连接会短暂中断(≈1-2s),重连由 router 自动发现完成。 + +--- + +## 已知限制 + +1. **stub 节点的组件字段**:`set-label-text` / `set-sprite-frame` / `set-nested-component-field` 已通过 `uuid-resolver.js` 解析 `CompPrefabInfo.fileId` 写入 stub 节点的 override +2. **多层嵌套 stub**:CLI 已通过 `refSubNode` 字符串数组路径支持。极端深嵌套场景仍建议走 tools pipeline(`tools/step-3-script/bind-prefab-components.ts`) +3. **offline 改完后需手动触发刷新**:直接写磁盘 Cocos 编辑器不会自动感知。改完后调用 `asset_reimport`(编辑器在线时)或 `preview_refresh_and_reload`,否则编辑器和预览页面看到的仍是旧内容 + +--- + +## 反馈 + +使用本扩展(MCP tool / offline CLI)时遇到 bug、行为异常或文档与实际不符,agent 必须把问题反馈给用户,由用户决定是否修复扩展本身,不要在调用方加 workaround 绕过。 diff --git a/cli/bin/cocos-mcp-cli.js b/cli/bin/cocos-mcp-cli.js new file mode 100755 index 0000000..0126244 --- /dev/null +++ b/cli/bin/cocos-mcp-cli.js @@ -0,0 +1,7 @@ +#!/usr/bin/env node +'use strict'; + +require('../src/index.js'); // 确保模块可正常加载 +const { main } = require('../src/cli/main.js'); + +main(process.argv.slice(2)); diff --git a/cli/package.json b/cli/package.json new file mode 100644 index 0000000..1954e7d --- /dev/null +++ b/cli/package.json @@ -0,0 +1,9 @@ +{ + "name": "cocos-mcp-cli", + "version": "0.1.0", + "private": true, + "bin": { "cocos-mcp-cli": "./bin/cocos-mcp-cli.js" }, + "main": "./src/index.js", + "license": "Apache-2.0", + "author": "付饶" +} diff --git a/cli/src/anim-primitives.js b/cli/src/anim-primitives.js new file mode 100644 index 0000000..8ec5e63 --- /dev/null +++ b/cli/src/anim-primitives.js @@ -0,0 +1,378 @@ +// ============================================================ +// CC3 AnimationClip (.anim) 对象构建原语(纯 CJS,零三方依赖) +// +// .anim 文件和 .prefab 一样是 JSON 数组 + `__id__` 交叉引用, +// 复用 parsePrefab / writePrefab 解析写入。 +// +// 但 .anim 内部对象类型(AnimationClip / Track / Curve / Channel) +// 有自己的 schema 规范,最容易踩的坑: +// +// ✅ cc.animation.RealTrack → 单通道 → `_channel: {__id__: X}` (单数) +// ✅ cc.animation.ObjectTrack → 单通道 → `_channel: {__id__: X}` (单数) +// ✅ cc.animation.VectorTrack → 多通道 → `_channels: [x, y, z]` (复数数组) +// ✅ cc.animation.ColorTrack → 多通道 → `_channels: [r, g, b, a]` (复数数组) +// +// 写错字段名(比如给 RealTrack 写 `_channels: [...]`)会导致 CC3 编辑器 +// 按 schema 验证时直接忽略整条轨道,表现为「anim 文件里有数据但编辑器 +// 不显示关键帧、运行时也不播」——历史踩过的坑,见 anim-schema.md。 +// +// 用本文件里的 make* 工厂函数,保证字段名一定写对。 +// ============================================================ + +'use strict'; + +const { ref } = require('./primitives.js'); + +// ───────────────────────────────────────────── +// 插值模式常量 +// 对应 cc.RealCurve.interpolationMode 枚举 +// ───────────────────────────────────────────── +const InterpolationMode = Object.freeze({ + LINEAR: 0, // 线性插值,两关键帧之间平滑过渡 + CONSTANT: 1, // 常量插值,保持当前值直到下一帧瞬变 + CUBIC: 2, // 三次插值,带缓动曲线 +}); + +// ───────────────────────────────────────────── +// Extrapolation 模式常量 +// 对应 cc.RealCurve.preExtrapolation / postExtrapolation +// ───────────────────────────────────────────── +const Extrapolation = Object.freeze({ + LINEAR: 0, + CLAMP: 1, // 最常用:首帧之前保持首帧值,末帧之后保持末帧值 + REPEAT: 2, + PINGPONG: 3, +}); + +// ───────────────────────────────────────────── +// RealKeyframeValue:单个浮点关键帧 +// ───────────────────────────────────────────── +/** + * @param {object} opts + * @param {number} opts.value + * @param {number} [opts.interpolationMode=CONSTANT] 0=LINEAR 1=CONSTANT 2=CUBIC + * @param {number} [opts.easingMethod=0] 0=Linear,其余见 cc.EasingMethod + */ +function makeRealKeyframe(opts) { + const { value, interpolationMode = InterpolationMode.CONSTANT, easingMethod = 0 } = opts; + return { + __type__: 'cc.RealKeyframeValue', + interpolationMode, + tangentWeightMode: 0, + value, + rightTangent: 0, + rightTangentWeight: 1, + leftTangent: 0, + leftTangentWeight: 1, + easingMethod, + __editorExtras__: null, + }; +} + +// ───────────────────────────────────────────── +// cc.RealCurve:浮点曲线 +// times/values 一一对应,长度必须相同 +// ───────────────────────────────────────────── +/** + * @param {object} opts + * @param {number[]} opts.times - 递增时间点(秒) + * @param {object[]} opts.values - RealKeyframeValue 对象数组,与 times 同长 + * @param {number} [opts.preExtrapolation=1] CLAMP + * @param {number} [opts.postExtrapolation=1] CLAMP + */ +function makeRealCurve(opts) { + const { times, values, preExtrapolation = Extrapolation.CLAMP, postExtrapolation = Extrapolation.CLAMP } = opts; + if (times.length !== values.length) { + throw new Error(`makeRealCurve: times.length (${times.length}) !== values.length (${values.length})`); + } + return { + __type__: 'cc.RealCurve', + _times: times, + _values: values, + preExtrapolation, + postExtrapolation, + }; +} + +// ───────────────────────────────────────────── +// cc.ObjectCurve:对象引用曲线(用于 spriteFrame 等资产序列) +// values 是资产引用对象数组(`{ __uuid__, __expectedType__ }`) +// ───────────────────────────────────────────── +/** + * @param {object} opts + * @param {number[]} opts.times + * @param {object[]} opts.values - 资产引用对象 + */ +function makeObjectCurve(opts) { + const { times, values, preExtrapolation = Extrapolation.CLAMP, postExtrapolation = Extrapolation.CLAMP } = opts; + if (times.length !== values.length) { + throw new Error(`makeObjectCurve: times.length (${times.length}) !== values.length (${values.length})`); + } + return { + __type__: 'cc.ObjectCurve', + _times: times, + _values: values, + preExtrapolation, + postExtrapolation, + }; +} + +// ───────────────────────────────────────────── +// cc.animation.Channel:Track → Curve 的中间层 +// ───────────────────────────────────────────── +/** + * @param {number} curveIdx - RealCurve/ObjectCurve 在 objects 数组里的下标 + */ +function makeChannel(curveIdx) { + return { + __type__: 'cc.animation.Channel', + _curve: ref(curveIdx), + }; +} + +// ───────────────────────────────────────────── +// cc.animation.HierarchyPath / ComponentPath / TrackPath +// 用于定位轨道的目标节点与属性 +// ───────────────────────────────────────────── +/** + * @param {string} path - 节点层级路径,如 "n4" 或 "content/titleBar";根节点自身写 "" + */ +function makeHierarchyPath(path) { + return { __type__: 'cc.animation.HierarchyPath', path }; +} + +/** + * @param {string} component - 组件类型,如 "cc.UIOpacity" / "cc.Sprite" + */ +function makeComponentPath(component) { + return { __type__: 'cc.animation.ComponentPath', component }; +} + +/** + * @param {unknown[]} parts - 混合 HierarchyPath/ComponentPath 引用与属性字符串 + * 例:[ref(hierIdx), ref(compIdx), "opacity"] + * 或根节点属性:[ref(compIdx), "position"](无 hierarchy path) + */ +function makeTrackPath(parts) { + return { __type__: 'cc.animation.TrackPath', _paths: parts }; +} + +// ───────────────────────────────────────────── +// cc.animation.RealTrack(单通道浮点轨道) +// ⚠️ 字段必须是 `_channel`(单数),不是 `_channels` +// ⚠️ 写错会被 CC3 编辑器 schema 验证忽略,轨道形同虚设 +// ───────────────────────────────────────────── +/** + * @param {number} trackPathIdx - TrackPath 在 objects 数组里的下标 + * @param {number} channelIdx - Channel 在 objects 数组里的下标 + */ +function makeRealTrack(trackPathIdx, channelIdx) { + return { + __type__: 'cc.animation.RealTrack', + _binding: { + __type__: 'cc.animation.TrackBinding', + path: ref(trackPathIdx), + proxy: null, + }, + _channel: ref(channelIdx), + }; +} + +// ───────────────────────────────────────────── +// cc.animation.ObjectTrack(单通道对象轨道,常用于 spriteFrame 序列帧) +// ⚠️ 字段必须是 `_channel`(单数) +// ───────────────────────────────────────────── +/** + * @param {number} trackPathIdx + * @param {number} channelIdx + */ +function makeObjectTrack(trackPathIdx, channelIdx) { + return { + __type__: 'cc.animation.ObjectTrack', + _binding: { + __type__: 'cc.animation.TrackBinding', + path: ref(trackPathIdx), + proxy: null, + }, + _channel: ref(channelIdx), + }; +} + +// ───────────────────────────────────────────── +// cc.animation.VectorTrack(多通道,x/y/z 或 x/y/z/w) +// ⚠️ 字段必须是 `_channels`(复数,数组),且必须提供 `_nComponents` +// ───────────────────────────────────────────── +/** + * @param {number} trackPathIdx + * @param {number[]} channelIndices - 各轴 Channel 下标数组(长度 = nComponents) + * @param {number} nComponents - 2(Vec2) / 3(Vec3) / 4(Vec4/Quat) + */ +function makeVectorTrack(trackPathIdx, channelIndices, nComponents) { + if (channelIndices.length !== nComponents) { + throw new Error(`makeVectorTrack: channelIndices.length (${channelIndices.length}) !== nComponents (${nComponents})`); + } + return { + __type__: 'cc.animation.VectorTrack', + _binding: { + __type__: 'cc.animation.TrackBinding', + path: ref(trackPathIdx), + proxy: null, + }, + _channels: channelIndices.map(ref), + _nComponents: nComponents, + }; +} + +// ───────────────────────────────────────────── +// cc.animation.ColorTrack(4 通道 r/g/b/a) +// ⚠️ 字段必须是 `_channels`(复数,数组),无 `_nComponents` +// ───────────────────────────────────────────── +/** + * @param {number} trackPathIdx + * @param {number[]} channelIndices - [rIdx, gIdx, bIdx, aIdx] + */ +function makeColorTrack(trackPathIdx, channelIndices) { + if (channelIndices.length !== 4) { + throw new Error(`makeColorTrack: expected 4 channel indices, got ${channelIndices.length}`); + } + return { + __type__: 'cc.animation.ColorTrack', + _binding: { + __type__: 'cc.animation.TrackBinding', + path: ref(trackPathIdx), + proxy: null, + }, + _channels: channelIndices.map(ref), + }; +} + +// ───────────────────────────────────────────── +// cc.AnimationClipAdditiveSettings(每个 clip 尾巴一份) +// ───────────────────────────────────────────── +function makeAdditiveSettings() { + return { + __type__: 'cc.AnimationClipAdditiveSettings', + enabled: false, + refClip: null, + }; +} + +// ───────────────────────────────────────────── +// cc.AnimationClip 文件头(objects[0]) +// ───────────────────────────────────────────── +/** + * @param {object} opts + * @param {string} opts.name + * @param {number} opts.sample - 采样帧率 + * @param {number} opts.duration - 秒 + * @param {number} [opts.speed=1] + * @param {number} [opts.wrapMode=1] 1=Normal, 2=Loop, 22=PingPong, 36=PingPongLoop + * @param {number} opts.hash - 哈希值(通常 hashString(name)) + * @param {number[]} opts.trackIndices - 所有 Track 在 objects 数组里的下标 + * @param {number} opts.additiveIdx - AdditiveSettings 下标 + * @param {number[]} [opts.embeddedPlayerIndices=[]] - EmbeddedPlayer 下标 + */ +function makeAnimationClip(opts) { + const { + name, + sample, + duration, + speed = 1, + wrapMode = 1, + hash, + trackIndices, + additiveIdx, + embeddedPlayerIndices = [], + } = opts; + return { + __type__: 'cc.AnimationClip', + _name: name, + _objFlags: 0, + __editorExtras__: { embeddedPlayerGroups: [] }, + _native: '', + sample, + speed, + wrapMode, + enableTrsBlending: false, + _duration: duration, + _hash: hash, + _tracks: trackIndices.map(ref), + _exoticAnimation: null, + _events: [], + _embeddedPlayers: embeddedPlayerIndices.map(ref), + _additiveSettings: ref(additiveIdx), + _auxiliaryCurveEntries: [], + }; +} + +// ───────────────────────────────────────────── +// cc.animation.EmbeddedPlayer / EmbeddedAnimationClipPlayable +// 用于 movieclip 这类「主 clip 触发节点自己 Animation 播子 clip」的结构 +// ───────────────────────────────────────────── +/** + * @param {object} opts + * @param {number} opts.begin - 主 clip 时间线上子 clip 开始秒 + * @param {number} opts.end - 主 clip 时间线上子 clip 结束秒 + * @param {number} opts.playableIdx - EmbeddedAnimationClipPlayable 下标 + */ +function makeEmbeddedPlayer(opts) { + const { begin, end, playableIdx } = opts; + return { + __type__: 'cc.animation.EmbeddedPlayer', + begin, + end, + reconciledSpeed: false, + playable: ref(playableIdx), + }; +} + +/** + * @param {object} opts + * @param {string} opts.path - 目标节点路径(相对 clip 所在节点) + * @param {string} opts.clipUuid - 子 clip 资产 UUID + */ +function makeEmbeddedAnimationClipPlayable(opts) { + const { path, clipUuid } = opts; + return { + __type__: 'cc.animation.EmbeddedAnimationClipPlayable', + path, + clip: { + __uuid__: clipUuid, + __expectedType__: 'cc.AnimationClip', + }, + }; +} + +// ───────────────────────────────────────────── +// 哈希函数:生成 AnimationClip._hash +// 用简单字符串哈希,只需保证同名 clip 得到同一 hash 即可 +// ───────────────────────────────────────────── +function hashString(s) { + let hash = 0; + for (let i = 0; i < s.length; i++) { + hash = ((hash << 5) - hash) + s.charCodeAt(i); + hash |= 0; + } + return Math.abs(hash); +} + +module.exports = { + InterpolationMode, + Extrapolation, + makeRealKeyframe, + makeRealCurve, + makeObjectCurve, + makeChannel, + makeHierarchyPath, + makeComponentPath, + makeTrackPath, + makeRealTrack, + makeObjectTrack, + makeVectorTrack, + makeColorTrack, + makeAdditiveSettings, + makeAnimationClip, + makeEmbeddedPlayer, + makeEmbeddedAnimationClipPlayable, + hashString, +}; diff --git a/cli/src/classid-resolver.js b/cli/src/classid-resolver.js new file mode 100644 index 0000000..eb6e323 --- /dev/null +++ b/cli/src/classid-resolver.js @@ -0,0 +1,138 @@ +// ============================================================ +// ClassIdResolver:扫描 assets/scripts/ 下所有 .ts,建立 +// className → { uuid, classId, scriptPath } 映射。 +// +// 来源: +// - @ccclass('SomeName') 装饰器提供 className +// - 同路径