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.
This commit is contained in:
furao
2026-06-06 11:33:19 +08:00
commit 14c5b00f14
96 changed files with 15855 additions and 0 deletions
+378
View File
@@ -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.ChannelTrack → 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.ColorTrack4 通道 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,
};