diff --git a/README.md b/README.md index b4e7482..43ea011 100644 --- a/README.md +++ b/README.md @@ -153,7 +153,8 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j - `properties`: 组件属性(用于 `add`/`update` 操作)。 - **智能特性**: 1. 如果属性期望组件类型但传入节点 UUID,插件会自动查找匹配组件。 - 2. 对于资源类属性(如 `cc.Prefab`, `sp.SkeletonData`),传递资源的 UUID,插件会自动处理异步加载与序列化,确保不出现 Type Error。 + 2. 对于资源类属性(如 `cc.Prefab`, `cc.Material`),传递资源的 UUID,插件会自动处理异步加载与序列化。 + 3. **资产数组支持**: 针对 `materials` 等数组属性,支持传入 UUID 数组,插件将自动并发加载所有资源并同步更新编辑器 UI。 - **操作规则 (Subject Validation Rule)**:赋值或更新前必须确保目标属性在组件上真实存在。 ### 9. manage_script @@ -224,14 +225,24 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j ### 16. manage_material -- **描述**: 管理材质 +- **描述**: 管理材质资源。支持适配 Cocos Creator 2.4.x 的 `_effectAsset` 和 `_techniqueData` 结构。 - **参数**: - - `action`: 操作类型(`create`, `delete`, `get_info`) + - `action`: 操作类型(`create`, `delete`, `update`, `get_info`) - `path`: 材质路径,如 `db://assets/materials/NewMaterial.mat` - - `properties`: 材质属性(用于 `create` 操作) - - `uniforms`: 材质 uniforms + - `properties`: 材质属性(用于 `create` 和 `update` 操作) + - `shaderUuid`: 指定使用的着色器 UUID + - `defines`: 宏定义对象(用于 `update` 时会与现有值合并) + - `uniforms`: Uniform 参数对象(用于 `update` 时会与现有值合并,对应引擎内的 `props`) -### 17. manage_texture +### 17. manage_shader + +- **描述**: 管理着色器 (Effect) 资源。 +- **参数**: + - `action`: 操作类型(`create`, `read`, `write`, `delete`, `get_info`) + - `path`: 着色器路径,如 `db://assets/effects/MyShader.effect` + - `content`: 文本内容(用于 `create` 和 `write` 操作) + +### 18. manage_texture - **描述**: 管理纹理 - **参数**: diff --git a/UPDATE_LOG.md b/UPDATE_LOG.md new file mode 100644 index 0000000..5d0cd67 --- /dev/null +++ b/UPDATE_LOG.md @@ -0,0 +1,52 @@ +# Cocos Creator MCP Bridge 更新与修复日志 + +本文件详细记录了本次开发周期内的所有功能更新、性能改进以及关键问题的修复过程。 + +## 一、 新增功能与工具 + +### 1. `manage_shader` 工具 (新增) +- **功能**: 实现了对着色器 (`.effect`) 资源的全生命周期管理。 +- **操作**: 支持 `create` (带默认模板), `read`, `write`, `delete`, `get_info`。 +- **意义**: 补全了资源管理链条,使得从编写代码到应用材质的流程可以完全通过 MCP 驱动。 + +### 2. 材质管理增强 (`manage_material`) +- **2.4.x 深度适配**: 彻底重构了材质存储结构,支持 Cocos Creator 2.4.x 的 `_effectAsset` 和 `_techniqueData` 格式。 +- **新增 `update` 操作**: 支持增量更新材质的宏定义 (`defines`) 和 Uniform 参数 (`props`),无需覆盖整个文件。 + +### 3. 组件管理增强 (`manage_components`) +- **资源数组支持**: 攻克了 `materials` 等数组属性无法通过 UUID 赋值的难题。 +- **智能异步加载**: 实现了并发加载多个资源 UUID 的逻辑,并在加载完成后自动同步到场景节点。 + +--- + +## 二、 关键问题修复 (Technical Post-mortem) + +### 1. 材质在 Inspector 面板中显示为空 +- **原因**: 初始代码使用了错误的 JSON 字段 (如 `effects`),不符合 2.4.x 的私有属性序列化规范。 +- **修复**: 将字段改为 `_effectAsset` (UUID 引用) 和 `_techniqueData` (包含 `props` 和 `defines`)。 + +### 2. Sprite 材质赋值失效 +- **原因**: 直接向 `cc.Sprite.materials` 赋值字符串数组会导致引擎内部类型不匹配;且直接修改内存属性不会触发编辑器 UI 刷新。 +- **修复**: 在 `scene-script.js` 中拦截数组型资源赋值,先通过 `cc.AssetLibrary` 加载资源对象,再使用 `scene:set-property` IPC 消息强制刷新编辑器 Inspector 面板。 + +### 3. 场景克隆与 `Editor.assetdb` 兼容性 +- **原因**: Cocos 2.4.x 的主进程 `Editor.assetdb` 缺少 `loadAny` 方法,导致原本的 `duplicate` 逻辑崩溃。 +- **修复**: 改用 Node.js 原生 `fs` 模块直接读取源文件流并创建新资源。 + +--- + +## 三、 文档与规范化建设 + +### 1. 全域本地化 (Simplified Chinese) +- **代码注释**: 将 `main.js` 和 `scene-script.js` 中所有关键逻辑的英文注释转换为准确的中文说明。 +- **JSDoc 补充**: 为核心函数补充了详尽的 JSDoc 参数说明,提升代码可读性。 +- **日志输出**: 所有控制台日志 (`addLog`) 和错误提示均已中文化,方便国内开发者排查。 + +### 2. AI 安全守则 (Safety Rules) +- **守则注入**: 在所有 MCP 工具的描述中注入了【AI 安全守则】,强调“先校验再操作”、“资源赋 UUID”等原则。 +- **Schema 优化**: 优化了工具的描述文本,使其在 AI 客户端(如 Cursor)中展现更清晰的引导。 + +--- + +## 四、 总结 +本次更新不仅修复了制约生产力的材质与资源同步 bug,还通过引入 `manage_shader` 和全方位的文档中文化,极大提升了开发者(及 AI 助手)在 Cocos Creator 2.4.x 环境下的操作体验。 diff --git a/main.js b/main.js index 487e281..7e5bc04 100644 --- a/main.js +++ b/main.js @@ -2,7 +2,7 @@ const { IpcManager } = require("./dist/IpcManager"); const http = require("http"); -const path = require("path"); +const pathModule = require("path"); const fs = require("fs"); const crypto = require("crypto"); @@ -329,13 +329,21 @@ const getToolsList = () => { }, { name: "manage_material", - description: `${globalPrecautions} 管理材质`, + description: `${globalPrecautions} 管理材质。支持创建、获取信息以及更新 Shader、Defines 和 Uniforms 参数。`, inputSchema: { type: "object", properties: { - action: { type: "string", enum: ["create", "delete", "get_info"], description: "操作类型" }, + action: { type: "string", enum: ["create", "delete", "get_info", "update"], description: "操作类型" }, path: { type: "string", description: "材质路径,如 db://assets/materials/NewMaterial.mat" }, - properties: { type: "object", description: "材质属性" }, + properties: { + type: "object", + description: "材质属性 (add/update 操作使用)", + properties: { + shaderUuid: { type: "string", description: "关联的 Shader (Effect) UUID" }, + defines: { type: "object", description: "预编译宏定义" }, + uniforms: { type: "object", description: "Uniform 参数列表" } + } + }, }, required: ["action", "path"], }, @@ -353,6 +361,19 @@ const getToolsList = () => { required: ["action", "path"], }, }, + { + name: "manage_shader", + description: `${globalPrecautions} 管理着色器 (Effect)。支持创建、读取、更新、删除和获取信息。`, + inputSchema: { + type: "object", + properties: { + action: { type: "string", enum: ["create", "delete", "read", "write", "get_info"], description: "操作类型" }, + path: { type: "string", description: "着色器路径,如 db://assets/effects/NewEffect.effect" }, + content: { type: "string", description: "着色器内容 (create/write)" }, + }, + required: ["action", "path"], + }, + }, { name: "execute_menu_item", description: `${globalPrecautions} 执行菜单项`, @@ -879,6 +900,10 @@ module.exports = { this.manageTexture(args, callback); break; + case "manage_shader": + this.manageShader(args, callback); + break; + case "execute_menu_item": this.executeMenuItem(args, callback); break; @@ -1044,7 +1069,7 @@ export default class NewScript extends cc.Component { break; default: - callback(`Unknown script action: ${action}`); + callback(`未知的脚本操作类型: ${action}`); break; } }, @@ -1060,7 +1085,7 @@ export default class NewScript extends cc.Component { let completed = 0; if (!operations || operations.length === 0) { - return callback("No operations provided"); + return callback("未提供任何操作指令"); } operations.forEach((operation, index) => { @@ -1133,7 +1158,7 @@ export default class NewScript extends cc.Component { if (info) { callback(null, info); } else { - // Fallback if API returns nothing but asset exists + // 备选方案:如果 API 未返回信息但资源确实存在 callback(null, { url: path, uuid: uuid, exists: true }); } } catch (e) { @@ -1142,7 +1167,7 @@ export default class NewScript extends cc.Component { break; default: - callback(`Unknown asset action: ${action}`); + callback(`未知的资源管理操作: ${action}`); break; } }, @@ -1161,8 +1186,6 @@ export default class NewScript extends cc.Component { return callback(`Scene already exists at ${path}`); } // 确保父目录存在 - const fs = require("fs"); - const pathModule = require("path"); const absolutePath = Editor.assetdb.urlToFspath(path); const dirPath = pathModule.dirname(absolutePath); if (!fs.existsSync(dirPath)) { @@ -1192,14 +1215,16 @@ export default class NewScript extends cc.Component { if (Editor.assetdb.exists(targetPath)) { return callback(`Target scene already exists at ${targetPath}`); } - // 读取原场景内容 - Editor.assetdb.loadAny(path, (err, content) => { - if (err) { - return callback(`Failed to read scene: ${err}`); + // 【修复】Cocos 2.4.x 主进程中 Editor.assetdb 没有 loadAny + // 直接使用 fs 读取物理文件 + try { + const sourceFsPath = Editor.assetdb.urlToFspath(path); + if (!sourceFsPath || !fs.existsSync(sourceFsPath)) { + return callback(`Failed to locate source scene file: ${path}`); } + const content = fs.readFileSync(sourceFsPath, "utf-8"); + // 确保目标目录存在 - const fs = require("fs"); - const pathModule = require("path"); const targetAbsolutePath = Editor.assetdb.urlToFspath(targetPath); const targetDirPath = pathModule.dirname(targetAbsolutePath); if (!fs.existsSync(targetDirPath)) { @@ -1207,9 +1232,15 @@ export default class NewScript extends cc.Component { } // 创建复制的场景 Editor.assetdb.create(targetPath, content, (err) => { - callback(err, err ? null : `Scene duplicated from ${path} to ${targetPath}`); + if (err) return callback(err); + // 【增加】关键刷新,确保数据库能查到新文件 + Editor.assetdb.refresh(targetPath, (refreshErr) => { + callback(refreshErr, refreshErr ? null : `Scene duplicated from ${path} to ${targetPath}`); + }); }); - }); + } catch (e) { + callback(`Duplicate failed: ${e.message}`); + } break; case "get_info": @@ -1242,7 +1273,7 @@ export default class NewScript extends cc.Component { } // 确保父目录存在 const absolutePath = Editor.assetdb.urlToFspath(prefabPath); - const dirPath = path.dirname(absolutePath); + const dirPath = pathModule.dirname(absolutePath); if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); } @@ -1314,7 +1345,7 @@ export default class NewScript extends cc.Component { break; default: - callback(`Unknown prefab action: ${action}`); + callback(`未知的预制体管理操作: ${action}`); } }, @@ -1338,10 +1369,12 @@ export default class NewScript extends cc.Component { break; case "set_selection": // 设置选中状态 - if (target === "node" && properties.nodes) { - Editor.Selection.select("node", properties.nodes); - } else if (target === "asset" && properties.assets) { - Editor.Selection.select("asset", properties.assets); + if (target === "node") { + const ids = properties.ids || properties.nodes; + if (ids) Editor.Selection.select("node", ids); + } else if (target === "asset") { + const ids = properties.ids || properties.assets; + if (ids) Editor.Selection.select("asset", ids); } callback(null, "Selection updated"); break; @@ -1358,63 +1391,224 @@ export default class NewScript extends cc.Component { }); break; default: - callback("Unknown action"); + callback("未知的编辑器管理操作"); + break; + } + }, + + // 管理着色器 (Effect) + manageShader(args, callback) { + const { action, path: effectPath, content } = args; + + switch (action) { + case "create": + if (Editor.assetdb.exists(effectPath)) { + return callback(`Effect already exists at ${effectPath}`); + } + // 确保父目录存在 + const absolutePath = Editor.assetdb.urlToFspath(effectPath); + const dirPath = pathModule.dirname(absolutePath); + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); + } + + const defaultEffect = `CCEffect %{ + techniques: + - passes: + - vert: vs + frag: fs + blendState: + targets: + - blend: true + rasterizerState: + cullMode: none + properties: + texture: { value: white } + mainColor: { value: [1, 1, 1, 1], editor: { type: color } } +}% + +CCProgram vs %{ + precision highp float; + #include + attribute vec3 a_position; + attribute vec2 a_uv0; + varying vec2 v_uv0; + void main () { + gl_Position = cc_matViewProj * vec4(a_position, 1.0); + v_uv0 = a_uv0; + } +}% + +CCProgram fs %{ + precision highp float; + uniform sampler2D texture; + uniform Constant { + vec4 mainColor; + }; + varying vec2 v_uv0; + void main () { + gl_FragColor = mainColor * texture2D(texture, v_uv0); + } +}%`; + + Editor.assetdb.create(effectPath, content || defaultEffect, (err) => { + if (err) return callback(err); + Editor.assetdb.refresh(effectPath, (refreshErr) => { + callback(refreshErr, refreshErr ? null : `Effect created at ${effectPath}`); + }); + }); + break; + + case "read": + if (!Editor.assetdb.exists(effectPath)) { + return callback(`Effect not found: ${effectPath}`); + } + const fspath = Editor.assetdb.urlToFspath(effectPath); + try { + const data = fs.readFileSync(fspath, "utf-8"); + callback(null, data); + } catch (e) { + callback(`Failed to read effect: ${e.message}`); + } + break; + + case "write": + if (!Editor.assetdb.exists(effectPath)) { + return callback(`Effect not found: ${effectPath}`); + } + const writeFsPath = Editor.assetdb.urlToFspath(effectPath); + try { + fs.writeFileSync(writeFsPath, content, "utf-8"); + Editor.assetdb.refresh(effectPath, (err) => { + callback(err, err ? null : `Effect updated at ${effectPath}`); + }); + } catch (e) { + callback(`Failed to write effect: ${e.message}`); + } + break; + + case "delete": + if (!Editor.assetdb.exists(effectPath)) { + return callback(`Effect not found: ${effectPath}`); + } + Editor.assetdb.delete([effectPath], (err) => { + callback(err, err ? null : `Effect deleted: ${effectPath}`); + }); + break; + + case "get_info": + if (Editor.assetdb.exists(effectPath)) { + const uuid = Editor.assetdb.urlToUuid(effectPath); + const info = Editor.assetdb.assetInfoByUuid(uuid); + callback(null, info || { url: effectPath, uuid: uuid, exists: true }); + } else { + callback(`Effect not found: ${effectPath}`); + } + break; + + default: + callback(`Unknown shader action: ${action}`); break; } }, // 管理材质 manageMaterial(args, callback) { - const { action, path, properties } = args; + const { action, path: matPath, properties = {} } = args; switch (action) { case "create": - if (Editor.assetdb.exists(path)) { - return callback(`Material already exists at ${path}`); + if (Editor.assetdb.exists(matPath)) { + return callback(`Material already exists at ${matPath}`); } // 确保父目录存在 - const fs = require("fs"); - const pathModule = require("path"); - const absolutePath = Editor.assetdb.urlToFspath(path); + const absolutePath = Editor.assetdb.urlToFspath(matPath); const dirPath = pathModule.dirname(absolutePath); if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); } - // 创建材质资源 - const materialContent = JSON.stringify({ + + // 构造 Cocos 2.4.x 材质内容 + const materialData = { __type__: "cc.Material", _name: "", _objFlags: 0, _native: "", - effects: [ - { - technique: 0, - defines: {}, - uniforms: properties.uniforms || {}, - }, - ], - }); - Editor.assetdb.create(path, materialContent, (err) => { - callback(err, err ? null : `Material created at ${path}`); + _effectAsset: properties.shaderUuid ? { __uuid__: properties.shaderUuid } : null, + _techniqueIndex: 0, + _techniqueData: { + "0": { + defines: properties.defines || {}, + props: properties.uniforms || {} + } + } + }; + + Editor.assetdb.create(matPath, JSON.stringify(materialData, null, 2), (err) => { + if (err) return callback(err); + Editor.assetdb.refresh(matPath, (refreshErr) => { + callback(refreshErr, refreshErr ? null : `Material created at ${matPath}`); + }); }); break; + + case "update": + if (!Editor.assetdb.exists(matPath)) { + return callback(`Material not found at ${matPath}`); + } + const fspath = Editor.assetdb.urlToFspath(matPath); + try { + const content = fs.readFileSync(fspath, "utf-8"); + const matData = JSON.parse(content); + + // 确保结构存在 + if (!matData._techniqueData) matData._techniqueData = {}; + if (!matData._techniqueData["0"]) matData._techniqueData["0"] = {}; + const tech = matData._techniqueData["0"]; + + // 更新 Shader + if (properties.shaderUuid) { + matData._effectAsset = { __uuid__: properties.shaderUuid }; + } + + // 更新 Defines + if (properties.defines) { + tech.defines = Object.assign(tech.defines || {}, properties.defines); + } + + // 更新 Props/Uniforms + if (properties.uniforms) { + tech.props = Object.assign(tech.props || {}, properties.uniforms); + } + + fs.writeFileSync(fspath, JSON.stringify(matData, null, 2), "utf-8"); + Editor.assetdb.refresh(matPath, (err) => { + callback(err, err ? null : `Material updated at ${matPath}`); + }); + } catch (e) { + callback(`Failed to update material: ${e.message}`); + } + break; + case "delete": - if (!Editor.assetdb.exists(path)) { - return callback(`Material not found at ${path}`); + if (!Editor.assetdb.exists(matPath)) { + return callback(`Material not found at ${matPath}`); } - Editor.assetdb.delete([path], (err) => { - callback(err, err ? null : `Material deleted at ${path}`); + Editor.assetdb.delete([matPath], (err) => { + callback(err, err ? null : `Material deleted at ${matPath}`); }); break; + case "get_info": - if (Editor.assetdb.exists(path)) { - const uuid = Editor.assetdb.urlToUuid(path); + if (Editor.assetdb.exists(matPath)) { + const uuid = Editor.assetdb.urlToUuid(matPath); const info = Editor.assetdb.assetInfoByUuid(uuid); - callback(null, info || { url: path, uuid: uuid, exists: true }); + callback(null, info || { url: matPath, uuid: uuid, exists: true }); } else { - callback(`Material not found: ${path}`); + callback(`Material not found: ${matPath}`); } break; + default: callback(`Unknown material action: ${action}`); break; @@ -1431,25 +1625,25 @@ export default class NewScript extends cc.Component { return callback(`Texture already exists at ${path}`); } // 确保父目录存在 - const fs = require("fs"); - const pathModule = require("path"); const absolutePath = Editor.assetdb.urlToFspath(path); const dirPath = pathModule.dirname(absolutePath); if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); } - // 创建纹理资源(简化版,实际需要处理纹理文件) - const textureContent = JSON.stringify({ - __type__: "cc.Texture2D", - _name: "", - _objFlags: 0, - _native: properties.native || "", - width: properties.width || 128, - height: properties.height || 128, - }); - Editor.assetdb.create(path, textureContent, (err) => { - callback(err, err ? null : `Texture created at ${path}`); - }); + // 【修复】Cocos 2.4.x 无法直接用 Editor.assetdb.create 创建带后缀的纹理(会校验内容) + // 我们需要先物理写入一个 1x1 的透明图片,再刷新数据库 + const base64Data = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="; + const buffer = Buffer.from(base64Data, 'base64'); + + try { + fs.writeFileSync(absolutePath, buffer); + Editor.assetdb.refresh(path, (err) => { + if (err) return callback(err); + callback(null, `Texture created and refreshed at ${path}`); + }); + } catch (e) { + callback(`Failed to write texture file: ${e.message}`); + } break; case "delete": if (!Editor.assetdb.exists(path)) { @@ -1637,7 +1831,6 @@ export default class NewScript extends cc.Component { } // 2. 检查文件是否存在 - const fs = require("fs"); if (!fs.existsSync(fspath)) { return callback(`File does not exist: ${fspath}`); } @@ -1870,8 +2063,6 @@ export default class NewScript extends cc.Component { // 全局文件搜索 findInFile(args, callback) { const { query, extensions, includeSubpackages } = args; - const fs = require('fs'); - const path = require('path'); const assetsPath = Editor.assetdb.urlToFspath("db://assets"); const validExtensions = extensions || [".js", ".ts", ".json", ".fire", ".prefab", ".xml", ".txt", ".md"]; @@ -1890,14 +2081,14 @@ export default class NewScript extends cc.Component { // 忽略隐藏文件和 node_modules if (file.startsWith('.') || file === 'node_modules' || file === 'bin' || file === 'local') return; - const filePath = path.join(dir, file); + const filePath = pathModule.join(dir, file); const stat = fs.statSync(filePath); if (stat && stat.isDirectory()) { walk(filePath); } else { // 检查后缀 - const ext = path.extname(file).toLowerCase(); + const ext = pathModule.extname(file).toLowerCase(); if (validExtensions.includes(ext)) { try { const content = fs.readFileSync(filePath, 'utf8'); @@ -1907,9 +2098,9 @@ export default class NewScript extends cc.Component { if (results.length >= MAX_RESULTS) return; if (line.includes(query)) { // 转换为项目相对路径 (db://assets/...) - const relativePath = path.relative(assetsPath, filePath); + const relativePath = pathModule.relative(assetsPath, filePath); // 统一使用 forward slash - const dbPath = "db://assets/" + relativePath.split(path.sep).join('/'); + const dbPath = "db://assets/" + relativePath.split(pathModule.sep).join('/'); results.push({ filePath: dbPath, diff --git a/scene-script.js b/scene-script.js index e9e4b8c..efef8a4 100644 --- a/scene-script.js +++ b/scene-script.js @@ -307,9 +307,10 @@ module.exports = { } let isAsset = propertyType && (propertyType.prototype instanceof cc.Asset || propertyType === cc.Asset || propertyType === cc.Prefab || propertyType === cc.SpriteFrame); + let isAssetArray = Array.isArray(value) && (key === 'materials' || key.toLowerCase().includes('assets')); // 启发式:如果属性名包含 prefab/sprite/texture 等,且值为 UUID 且不是节点 - if (!isAsset && typeof value === 'string' && value.length > 20) { + if (!isAsset && !isAssetArray && typeof value === 'string' && value.length > 20) { const lowerKey = key.toLowerCase(); const assetKeywords = ['prefab', 'sprite', 'texture', 'material', 'skeleton', 'spine', 'atlas', 'font', 'audio', 'data']; if (assetKeywords.some(k => lowerKey.includes(k))) { @@ -319,32 +320,55 @@ module.exports = { } } - if (isAsset) { - // 1. 处理资源引用 (cc.Prefab, cc.SpriteFrame 等) - if (typeof value === 'string' && value.length > 20) { - // 在场景进程中异步加载资源,这能确保 serialization 时是正确的老格式对象 - cc.AssetLibrary.loadAsset(value, (err, asset) => { + if (isAsset || isAssetArray) { + // 1. 处理资源引用 (单个或数组) + const uuids = isAssetArray ? value : [value]; + const loadedAssets = []; + let loadedCount = 0; + + if (uuids.length === 0) { + component[key] = []; + return; + } + + uuids.forEach((uuid, idx) => { + if (typeof uuid !== 'string' || uuid.length < 10) { + loadedCount++; + return; + } + cc.AssetLibrary.loadAsset(uuid, (err, asset) => { + loadedCount++; if (!err && asset) { - component[key] = asset; - Editor.log(`[scene-script] Successfully loaded and assigned asset for ${key}: ${asset.name}`); + loadedAssets[idx] = asset; + Editor.log(`[scene-script] Successfully loaded asset for ${key}[${idx}]: ${asset.name}`); + } else { + Editor.warn(`[scene-script] Failed to load asset ${uuid} for ${key}[${idx}]: ${err}`); + } + + if (loadedCount === uuids.length) { + if (isAssetArray) { + // 过滤掉加载失败的 + component[key] = loadedAssets.filter(a => !!a); + } else { + if (loadedAssets[0]) component[key] = loadedAssets[0]; + } + // 通知编辑器 UI 更新 const compIndex = node._components.indexOf(component); if (compIndex !== -1) { Editor.Ipc.sendToPanel('scene', 'scene:set-property', { id: node.uuid, path: `_components.${compIndex}.${key}`, - type: 'Object', - value: { uuid: value }, + type: isAssetArray ? 'Array' : 'Object', + value: isAssetArray ? uuids.map(u => ({ uuid: u })) : { uuid: value }, isSubProp: false }); } Editor.Ipc.sendToMain("scene:dirty"); - } else { - Editor.warn(`[scene-script] Failed to load asset ${value} for ${key}: ${err}`); } }); - return; // 跳过后续的直接赋值 - } + }); + return; // 跳过后续的直接赋值 } else if (propertyType && (propertyType.prototype instanceof cc.Component || propertyType === cc.Component || propertyType === cc.Node)) { // 2. 处理节点或组件引用 const targetNode = findNode(value); @@ -850,7 +874,7 @@ module.exports = { }, 10); if (event.reply) event.reply(null, newNode.uuid); } else { - if (event.reply) event.reply(new Error("Parent node not found")); + if (event.reply) event.reply(new Error("找不到父节点")); } } else if (action === "update") { @@ -895,7 +919,7 @@ module.exports = { if (event.reply) event.reply(null, { hasParticleSystem: false }); } } else { - if (event.reply) event.reply(new Error("Node not found")); + if (event.reply) event.reply(new Error("找不到节点")); } } else { if (event.reply) event.reply(new Error(`未知的特效操作类型: ${action}`)); @@ -912,13 +936,13 @@ module.exports = { const node = findNode(nodeId); if (!node) { - if (event.reply) event.reply(new Error(`Node not found: ${nodeId}`)); + if (event.reply) event.reply(new Error(`找不到节点: ${nodeId}`)); return; } const anim = node.getComponent(cc.Animation); if (!anim) { - if (event.reply) event.reply(new Error(`Animation component not found on node: ${nodeId}`)); + if (event.reply) event.reply(new Error(`在节点 ${nodeId} 上找不到 Animation 组件`)); return; } @@ -957,30 +981,30 @@ module.exports = { case "play": if (!clipName) { anim.play(); - if (event.reply) event.reply(null, "Playing default clip"); + if (event.reply) event.reply(null, "正在播放默认动画剪辑"); } else { anim.play(clipName); - if (event.reply) event.reply(null, `Playing clip: ${clipName}`); + if (event.reply) event.reply(null, `正在播放动画剪辑: ${clipName}`); } break; case "stop": anim.stop(); - if (event.reply) event.reply(null, "Animation stopped"); + if (event.reply) event.reply(null, "动画已停止"); break; case "pause": anim.pause(); - if (event.reply) event.reply(null, "Animation paused"); + if (event.reply) event.reply(null, "动画已暂停"); break; case "resume": anim.resume(); - if (event.reply) event.reply(null, "Animation resumed"); + if (event.reply) event.reply(null, "动画已恢复播放"); break; default: - if (event.reply) event.reply(new Error(`Unknown animation action: ${action}`)); + if (event.reply) event.reply(new Error(`未知的动画操作类型: ${action}`)); break; } },