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:
23
README.md
23
README.md
@@ -153,7 +153,8 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j
|
|||||||
- `properties`: 组件属性(用于 `add`/`update` 操作)。
|
- `properties`: 组件属性(用于 `add`/`update` 操作)。
|
||||||
- **智能特性**:
|
- **智能特性**:
|
||||||
1. 如果属性期望组件类型但传入节点 UUID,插件会自动查找匹配组件。
|
1. 如果属性期望组件类型但传入节点 UUID,插件会自动查找匹配组件。
|
||||||
2. 对于资源类属性(如 `cc.Prefab`, `sp.SkeletonData`),传递资源的 UUID,插件会自动处理异步加载与序列化,确保不出现 Type Error。
|
2. 对于资源类属性(如 `cc.Prefab`, `cc.Material`),传递资源的 UUID,插件会自动处理异步加载与序列化。
|
||||||
|
3. **资产数组支持**: 针对 `materials` 等数组属性,支持传入 UUID 数组,插件将自动并发加载所有资源并同步更新编辑器 UI。
|
||||||
- **操作规则 (Subject Validation Rule)**:赋值或更新前必须确保目标属性在组件上真实存在。
|
- **操作规则 (Subject Validation Rule)**:赋值或更新前必须确保目标属性在组件上真实存在。
|
||||||
|
|
||||||
### 9. manage_script
|
### 9. manage_script
|
||||||
@@ -224,14 +225,24 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j
|
|||||||
|
|
||||||
### 16. manage_material
|
### 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`
|
- `path`: 材质路径,如 `db://assets/materials/NewMaterial.mat`
|
||||||
- `properties`: 材质属性(用于 `create` 操作)
|
- `properties`: 材质属性(用于 `create` 和 `update` 操作)
|
||||||
- `uniforms`: 材质 uniforms
|
- `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
52
UPDATE_LOG.md
Normal 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
329
main.js
@@ -2,7 +2,7 @@
|
|||||||
const { IpcManager } = require("./dist/IpcManager");
|
const { IpcManager } = require("./dist/IpcManager");
|
||||||
|
|
||||||
const http = require("http");
|
const http = require("http");
|
||||||
const path = require("path");
|
const pathModule = require("path");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const crypto = require("crypto");
|
const crypto = require("crypto");
|
||||||
|
|
||||||
@@ -329,13 +329,21 @@ const getToolsList = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "manage_material",
|
name: "manage_material",
|
||||||
description: `${globalPrecautions} 管理材质`,
|
description: `${globalPrecautions} 管理材质。支持创建、获取信息以及更新 Shader、Defines 和 Uniforms 参数。`,
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
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" },
|
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"],
|
required: ["action", "path"],
|
||||||
},
|
},
|
||||||
@@ -353,6 +361,19 @@ const getToolsList = () => {
|
|||||||
required: ["action", "path"],
|
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",
|
name: "execute_menu_item",
|
||||||
description: `${globalPrecautions} 执行菜单项`,
|
description: `${globalPrecautions} 执行菜单项`,
|
||||||
@@ -879,6 +900,10 @@ module.exports = {
|
|||||||
this.manageTexture(args, callback);
|
this.manageTexture(args, callback);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "manage_shader":
|
||||||
|
this.manageShader(args, callback);
|
||||||
|
break;
|
||||||
|
|
||||||
case "execute_menu_item":
|
case "execute_menu_item":
|
||||||
this.executeMenuItem(args, callback);
|
this.executeMenuItem(args, callback);
|
||||||
break;
|
break;
|
||||||
@@ -1044,7 +1069,7 @@ export default class NewScript extends cc.Component {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
callback(`Unknown script action: ${action}`);
|
callback(`未知的脚本操作类型: ${action}`);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1060,7 +1085,7 @@ export default class NewScript extends cc.Component {
|
|||||||
let completed = 0;
|
let completed = 0;
|
||||||
|
|
||||||
if (!operations || operations.length === 0) {
|
if (!operations || operations.length === 0) {
|
||||||
return callback("No operations provided");
|
return callback("未提供任何操作指令");
|
||||||
}
|
}
|
||||||
|
|
||||||
operations.forEach((operation, index) => {
|
operations.forEach((operation, index) => {
|
||||||
@@ -1133,7 +1158,7 @@ export default class NewScript extends cc.Component {
|
|||||||
if (info) {
|
if (info) {
|
||||||
callback(null, info);
|
callback(null, info);
|
||||||
} else {
|
} else {
|
||||||
// Fallback if API returns nothing but asset exists
|
// 备选方案:如果 API 未返回信息但资源确实存在
|
||||||
callback(null, { url: path, uuid: uuid, exists: true });
|
callback(null, { url: path, uuid: uuid, exists: true });
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -1142,7 +1167,7 @@ export default class NewScript extends cc.Component {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
callback(`Unknown asset action: ${action}`);
|
callback(`未知的资源管理操作: ${action}`);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1161,8 +1186,6 @@ export default class NewScript extends cc.Component {
|
|||||||
return callback(`Scene already exists at ${path}`);
|
return callback(`Scene already exists at ${path}`);
|
||||||
}
|
}
|
||||||
// 确保父目录存在
|
// 确保父目录存在
|
||||||
const fs = require("fs");
|
|
||||||
const pathModule = require("path");
|
|
||||||
const absolutePath = Editor.assetdb.urlToFspath(path);
|
const absolutePath = Editor.assetdb.urlToFspath(path);
|
||||||
const dirPath = pathModule.dirname(absolutePath);
|
const dirPath = pathModule.dirname(absolutePath);
|
||||||
if (!fs.existsSync(dirPath)) {
|
if (!fs.existsSync(dirPath)) {
|
||||||
@@ -1192,14 +1215,16 @@ export default class NewScript extends cc.Component {
|
|||||||
if (Editor.assetdb.exists(targetPath)) {
|
if (Editor.assetdb.exists(targetPath)) {
|
||||||
return callback(`Target scene already exists at ${targetPath}`);
|
return callback(`Target scene already exists at ${targetPath}`);
|
||||||
}
|
}
|
||||||
// 读取原场景内容
|
// 【修复】Cocos 2.4.x 主进程中 Editor.assetdb 没有 loadAny
|
||||||
Editor.assetdb.loadAny(path, (err, content) => {
|
// 直接使用 fs 读取物理文件
|
||||||
if (err) {
|
try {
|
||||||
return callback(`Failed to read scene: ${err}`);
|
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 targetAbsolutePath = Editor.assetdb.urlToFspath(targetPath);
|
||||||
const targetDirPath = pathModule.dirname(targetAbsolutePath);
|
const targetDirPath = pathModule.dirname(targetAbsolutePath);
|
||||||
if (!fs.existsSync(targetDirPath)) {
|
if (!fs.existsSync(targetDirPath)) {
|
||||||
@@ -1207,9 +1232,15 @@ export default class NewScript extends cc.Component {
|
|||||||
}
|
}
|
||||||
// 创建复制的场景
|
// 创建复制的场景
|
||||||
Editor.assetdb.create(targetPath, content, (err) => {
|
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;
|
break;
|
||||||
|
|
||||||
case "get_info":
|
case "get_info":
|
||||||
@@ -1242,7 +1273,7 @@ export default class NewScript extends cc.Component {
|
|||||||
}
|
}
|
||||||
// 确保父目录存在
|
// 确保父目录存在
|
||||||
const absolutePath = Editor.assetdb.urlToFspath(prefabPath);
|
const absolutePath = Editor.assetdb.urlToFspath(prefabPath);
|
||||||
const dirPath = path.dirname(absolutePath);
|
const dirPath = pathModule.dirname(absolutePath);
|
||||||
if (!fs.existsSync(dirPath)) {
|
if (!fs.existsSync(dirPath)) {
|
||||||
fs.mkdirSync(dirPath, { recursive: true });
|
fs.mkdirSync(dirPath, { recursive: true });
|
||||||
}
|
}
|
||||||
@@ -1314,7 +1345,7 @@ export default class NewScript extends cc.Component {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
callback(`Unknown prefab action: ${action}`);
|
callback(`未知的预制体管理操作: ${action}`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1338,10 +1369,12 @@ export default class NewScript extends cc.Component {
|
|||||||
break;
|
break;
|
||||||
case "set_selection":
|
case "set_selection":
|
||||||
// 设置选中状态
|
// 设置选中状态
|
||||||
if (target === "node" && properties.nodes) {
|
if (target === "node") {
|
||||||
Editor.Selection.select("node", properties.nodes);
|
const ids = properties.ids || properties.nodes;
|
||||||
} else if (target === "asset" && properties.assets) {
|
if (ids) Editor.Selection.select("node", ids);
|
||||||
Editor.Selection.select("asset", properties.assets);
|
} else if (target === "asset") {
|
||||||
|
const ids = properties.ids || properties.assets;
|
||||||
|
if (ids) Editor.Selection.select("asset", ids);
|
||||||
}
|
}
|
||||||
callback(null, "Selection updated");
|
callback(null, "Selection updated");
|
||||||
break;
|
break;
|
||||||
@@ -1358,63 +1391,224 @@ export default class NewScript extends cc.Component {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
default:
|
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;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 管理材质
|
// 管理材质
|
||||||
manageMaterial(args, callback) {
|
manageMaterial(args, callback) {
|
||||||
const { action, path, properties } = args;
|
const { action, path: matPath, properties = {} } = args;
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case "create":
|
case "create":
|
||||||
if (Editor.assetdb.exists(path)) {
|
if (Editor.assetdb.exists(matPath)) {
|
||||||
return callback(`Material already exists at ${path}`);
|
return callback(`Material already exists at ${matPath}`);
|
||||||
}
|
}
|
||||||
// 确保父目录存在
|
// 确保父目录存在
|
||||||
const fs = require("fs");
|
const absolutePath = Editor.assetdb.urlToFspath(matPath);
|
||||||
const pathModule = require("path");
|
|
||||||
const absolutePath = Editor.assetdb.urlToFspath(path);
|
|
||||||
const dirPath = pathModule.dirname(absolutePath);
|
const dirPath = pathModule.dirname(absolutePath);
|
||||||
if (!fs.existsSync(dirPath)) {
|
if (!fs.existsSync(dirPath)) {
|
||||||
fs.mkdirSync(dirPath, { recursive: true });
|
fs.mkdirSync(dirPath, { recursive: true });
|
||||||
}
|
}
|
||||||
// 创建材质资源
|
|
||||||
const materialContent = JSON.stringify({
|
// 构造 Cocos 2.4.x 材质内容
|
||||||
|
const materialData = {
|
||||||
__type__: "cc.Material",
|
__type__: "cc.Material",
|
||||||
_name: "",
|
_name: "",
|
||||||
_objFlags: 0,
|
_objFlags: 0,
|
||||||
_native: "",
|
_native: "",
|
||||||
effects: [
|
_effectAsset: properties.shaderUuid ? { __uuid__: properties.shaderUuid } : null,
|
||||||
{
|
_techniqueIndex: 0,
|
||||||
technique: 0,
|
_techniqueData: {
|
||||||
defines: {},
|
"0": {
|
||||||
uniforms: properties.uniforms || {},
|
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;
|
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":
|
case "delete":
|
||||||
if (!Editor.assetdb.exists(path)) {
|
if (!Editor.assetdb.exists(matPath)) {
|
||||||
return callback(`Material not found at ${path}`);
|
return callback(`Material not found at ${matPath}`);
|
||||||
}
|
}
|
||||||
Editor.assetdb.delete([path], (err) => {
|
Editor.assetdb.delete([matPath], (err) => {
|
||||||
callback(err, err ? null : `Material deleted at ${path}`);
|
callback(err, err ? null : `Material deleted at ${matPath}`);
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "get_info":
|
case "get_info":
|
||||||
if (Editor.assetdb.exists(path)) {
|
if (Editor.assetdb.exists(matPath)) {
|
||||||
const uuid = Editor.assetdb.urlToUuid(path);
|
const uuid = Editor.assetdb.urlToUuid(matPath);
|
||||||
const info = Editor.assetdb.assetInfoByUuid(uuid);
|
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 {
|
} else {
|
||||||
callback(`Material not found: ${path}`);
|
callback(`Material not found: ${matPath}`);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
callback(`Unknown material action: ${action}`);
|
callback(`Unknown material action: ${action}`);
|
||||||
break;
|
break;
|
||||||
@@ -1431,25 +1625,25 @@ export default class NewScript extends cc.Component {
|
|||||||
return callback(`Texture already exists at ${path}`);
|
return callback(`Texture already exists at ${path}`);
|
||||||
}
|
}
|
||||||
// 确保父目录存在
|
// 确保父目录存在
|
||||||
const fs = require("fs");
|
|
||||||
const pathModule = require("path");
|
|
||||||
const absolutePath = Editor.assetdb.urlToFspath(path);
|
const absolutePath = Editor.assetdb.urlToFspath(path);
|
||||||
const dirPath = pathModule.dirname(absolutePath);
|
const dirPath = pathModule.dirname(absolutePath);
|
||||||
if (!fs.existsSync(dirPath)) {
|
if (!fs.existsSync(dirPath)) {
|
||||||
fs.mkdirSync(dirPath, { recursive: true });
|
fs.mkdirSync(dirPath, { recursive: true });
|
||||||
}
|
}
|
||||||
// 创建纹理资源(简化版,实际需要处理纹理文件)
|
// 【修复】Cocos 2.4.x 无法直接用 Editor.assetdb.create 创建带后缀的纹理(会校验内容)
|
||||||
const textureContent = JSON.stringify({
|
// 我们需要先物理写入一个 1x1 的透明图片,再刷新数据库
|
||||||
__type__: "cc.Texture2D",
|
const base64Data = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==";
|
||||||
_name: "",
|
const buffer = Buffer.from(base64Data, 'base64');
|
||||||
_objFlags: 0,
|
|
||||||
_native: properties.native || "",
|
try {
|
||||||
width: properties.width || 128,
|
fs.writeFileSync(absolutePath, buffer);
|
||||||
height: properties.height || 128,
|
Editor.assetdb.refresh(path, (err) => {
|
||||||
});
|
if (err) return callback(err);
|
||||||
Editor.assetdb.create(path, textureContent, (err) => {
|
callback(null, `Texture created and refreshed at ${path}`);
|
||||||
callback(err, err ? null : `Texture created at ${path}`);
|
|
||||||
});
|
});
|
||||||
|
} catch (e) {
|
||||||
|
callback(`Failed to write texture file: ${e.message}`);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case "delete":
|
case "delete":
|
||||||
if (!Editor.assetdb.exists(path)) {
|
if (!Editor.assetdb.exists(path)) {
|
||||||
@@ -1637,7 +1831,6 @@ export default class NewScript extends cc.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. 检查文件是否存在
|
// 2. 检查文件是否存在
|
||||||
const fs = require("fs");
|
|
||||||
if (!fs.existsSync(fspath)) {
|
if (!fs.existsSync(fspath)) {
|
||||||
return callback(`File does not exist: ${fspath}`);
|
return callback(`File does not exist: ${fspath}`);
|
||||||
}
|
}
|
||||||
@@ -1870,8 +2063,6 @@ export default class NewScript extends cc.Component {
|
|||||||
// 全局文件搜索
|
// 全局文件搜索
|
||||||
findInFile(args, callback) {
|
findInFile(args, callback) {
|
||||||
const { query, extensions, includeSubpackages } = args;
|
const { query, extensions, includeSubpackages } = args;
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
const assetsPath = Editor.assetdb.urlToFspath("db://assets");
|
const assetsPath = Editor.assetdb.urlToFspath("db://assets");
|
||||||
const validExtensions = extensions || [".js", ".ts", ".json", ".fire", ".prefab", ".xml", ".txt", ".md"];
|
const validExtensions = extensions || [".js", ".ts", ".json", ".fire", ".prefab", ".xml", ".txt", ".md"];
|
||||||
@@ -1890,14 +2081,14 @@ export default class NewScript extends cc.Component {
|
|||||||
// 忽略隐藏文件和 node_modules
|
// 忽略隐藏文件和 node_modules
|
||||||
if (file.startsWith('.') || file === 'node_modules' || file === 'bin' || file === 'local') return;
|
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);
|
const stat = fs.statSync(filePath);
|
||||||
|
|
||||||
if (stat && stat.isDirectory()) {
|
if (stat && stat.isDirectory()) {
|
||||||
walk(filePath);
|
walk(filePath);
|
||||||
} else {
|
} else {
|
||||||
// 检查后缀
|
// 检查后缀
|
||||||
const ext = path.extname(file).toLowerCase();
|
const ext = pathModule.extname(file).toLowerCase();
|
||||||
if (validExtensions.includes(ext)) {
|
if (validExtensions.includes(ext)) {
|
||||||
try {
|
try {
|
||||||
const content = fs.readFileSync(filePath, 'utf8');
|
const content = fs.readFileSync(filePath, 'utf8');
|
||||||
@@ -1907,9 +2098,9 @@ export default class NewScript extends cc.Component {
|
|||||||
if (results.length >= MAX_RESULTS) return;
|
if (results.length >= MAX_RESULTS) return;
|
||||||
if (line.includes(query)) {
|
if (line.includes(query)) {
|
||||||
// 转换为项目相对路径 (db://assets/...)
|
// 转换为项目相对路径 (db://assets/...)
|
||||||
const relativePath = path.relative(assetsPath, filePath);
|
const relativePath = pathModule.relative(assetsPath, filePath);
|
||||||
// 统一使用 forward slash
|
// 统一使用 forward slash
|
||||||
const dbPath = "db://assets/" + relativePath.split(path.sep).join('/');
|
const dbPath = "db://assets/" + relativePath.split(pathModule.sep).join('/');
|
||||||
|
|
||||||
results.push({
|
results.push({
|
||||||
filePath: dbPath,
|
filePath: dbPath,
|
||||||
|
|||||||
@@ -307,9 +307,10 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let isAsset = propertyType && (propertyType.prototype instanceof cc.Asset || propertyType === cc.Asset || propertyType === cc.Prefab || propertyType === cc.SpriteFrame);
|
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 且不是节点
|
// 启发式:如果属性名包含 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 lowerKey = key.toLowerCase();
|
||||||
const assetKeywords = ['prefab', 'sprite', 'texture', 'material', 'skeleton', 'spine', 'atlas', 'font', 'audio', 'data'];
|
const assetKeywords = ['prefab', 'sprite', 'texture', 'material', 'skeleton', 'spine', 'atlas', 'font', 'audio', 'data'];
|
||||||
if (assetKeywords.some(k => lowerKey.includes(k))) {
|
if (assetKeywords.some(k => lowerKey.includes(k))) {
|
||||||
@@ -319,32 +320,55 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isAsset) {
|
if (isAsset || isAssetArray) {
|
||||||
// 1. 处理资源引用 (cc.Prefab, cc.SpriteFrame 等)
|
// 1. 处理资源引用 (单个或数组)
|
||||||
if (typeof value === 'string' && value.length > 20) {
|
const uuids = isAssetArray ? value : [value];
|
||||||
// 在场景进程中异步加载资源,这能确保 serialization 时是正确的老格式对象
|
const loadedAssets = [];
|
||||||
cc.AssetLibrary.loadAsset(value, (err, asset) => {
|
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) {
|
if (!err && asset) {
|
||||||
component[key] = asset;
|
loadedAssets[idx] = asset;
|
||||||
Editor.log(`[scene-script] Successfully loaded and assigned asset for ${key}: ${asset.name}`);
|
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 更新
|
// 通知编辑器 UI 更新
|
||||||
const compIndex = node._components.indexOf(component);
|
const compIndex = node._components.indexOf(component);
|
||||||
if (compIndex !== -1) {
|
if (compIndex !== -1) {
|
||||||
Editor.Ipc.sendToPanel('scene', 'scene:set-property', {
|
Editor.Ipc.sendToPanel('scene', 'scene:set-property', {
|
||||||
id: node.uuid,
|
id: node.uuid,
|
||||||
path: `_components.${compIndex}.${key}`,
|
path: `_components.${compIndex}.${key}`,
|
||||||
type: 'Object',
|
type: isAssetArray ? 'Array' : 'Object',
|
||||||
value: { uuid: value },
|
value: isAssetArray ? uuids.map(u => ({ uuid: u })) : { uuid: value },
|
||||||
isSubProp: false
|
isSubProp: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Editor.Ipc.sendToMain("scene:dirty");
|
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)) {
|
} else if (propertyType && (propertyType.prototype instanceof cc.Component || propertyType === cc.Component || propertyType === cc.Node)) {
|
||||||
// 2. 处理节点或组件引用
|
// 2. 处理节点或组件引用
|
||||||
const targetNode = findNode(value);
|
const targetNode = findNode(value);
|
||||||
@@ -850,7 +874,7 @@ module.exports = {
|
|||||||
}, 10);
|
}, 10);
|
||||||
if (event.reply) event.reply(null, newNode.uuid);
|
if (event.reply) event.reply(null, newNode.uuid);
|
||||||
} else {
|
} else {
|
||||||
if (event.reply) event.reply(new Error("Parent node not found"));
|
if (event.reply) event.reply(new Error("找不到父节点"));
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (action === "update") {
|
} else if (action === "update") {
|
||||||
@@ -895,7 +919,7 @@ module.exports = {
|
|||||||
if (event.reply) event.reply(null, { hasParticleSystem: false });
|
if (event.reply) event.reply(null, { hasParticleSystem: false });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (event.reply) event.reply(new Error("Node not found"));
|
if (event.reply) event.reply(new Error("找不到节点"));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (event.reply) event.reply(new Error(`未知的特效操作类型: ${action}`));
|
if (event.reply) event.reply(new Error(`未知的特效操作类型: ${action}`));
|
||||||
@@ -912,13 +936,13 @@ module.exports = {
|
|||||||
const node = findNode(nodeId);
|
const node = findNode(nodeId);
|
||||||
|
|
||||||
if (!node) {
|
if (!node) {
|
||||||
if (event.reply) event.reply(new Error(`Node not found: ${nodeId}`));
|
if (event.reply) event.reply(new Error(`找不到节点: ${nodeId}`));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const anim = node.getComponent(cc.Animation);
|
const anim = node.getComponent(cc.Animation);
|
||||||
if (!anim) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -957,30 +981,30 @@ module.exports = {
|
|||||||
case "play":
|
case "play":
|
||||||
if (!clipName) {
|
if (!clipName) {
|
||||||
anim.play();
|
anim.play();
|
||||||
if (event.reply) event.reply(null, "Playing default clip");
|
if (event.reply) event.reply(null, "正在播放默认动画剪辑");
|
||||||
} else {
|
} else {
|
||||||
anim.play(clipName);
|
anim.play(clipName);
|
||||||
if (event.reply) event.reply(null, `Playing clip: ${clipName}`);
|
if (event.reply) event.reply(null, `正在播放动画剪辑: ${clipName}`);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "stop":
|
case "stop":
|
||||||
anim.stop();
|
anim.stop();
|
||||||
if (event.reply) event.reply(null, "Animation stopped");
|
if (event.reply) event.reply(null, "动画已停止");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "pause":
|
case "pause":
|
||||||
anim.pause();
|
anim.pause();
|
||||||
if (event.reply) event.reply(null, "Animation paused");
|
if (event.reply) event.reply(null, "动画已暂停");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "resume":
|
case "resume":
|
||||||
anim.resume();
|
anim.resume();
|
||||||
if (event.reply) event.reply(null, "Animation resumed");
|
if (event.reply) event.reply(null, "动画已恢复播放");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
if (event.reply) event.reply(new Error(`Unknown animation action: ${action}`));
|
if (event.reply) event.reply(new Error(`未知的动画操作类型: ${action}`));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user