feat: 增强 MCP Bridge 工具功能并完成全量中文化与文档更新

- 新增 manage_shader 工具,支持着色器资源的完整生命周期管理。
- 增强 manage_material 工具,深度适配 Cocos Creator 2.4.x 材质结构并支持增量更新。
- 优化 manage_components 工具,支持资源数组(如 materials)的智能异步加载与 UI 同步。
- 修复了材质 Inspector 显示异常、场景克隆崩溃及工具路由缺失等关键 Bug。
- 完成源码注释、错误提示及 MCP 工具描述的 100% 简体中文化。
- 更新 README.md 并新增 UPDATE_LOG.md 技术修复日志。
This commit is contained in:
火焰库拉
2026-02-10 09:14:50 +08:00
parent 23c6ea13f9
commit 256c91e9f5
4 changed files with 380 additions and 102 deletions

View File

@@ -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
- **描述**: 管理纹理
- **参数**:

52
UPDATE_LOG.md Normal file
View File

@@ -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 环境下的操作体验。

329
main.js
View File

@@ -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 <cc-global>
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 || {},
},
],
_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}`);
});
Editor.assetdb.create(path, materialContent, (err) => {
callback(err, err ? null : `Material created at ${path}`);
});
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,

View File

@@ -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; // 跳过后续的直接赋值
}
} 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;
}
},