"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // packages/cocos-ext/src/scene.ts var scene_exports = {}; __export(scene_exports, { methods: () => methods }); module.exports = __toCommonJS(scene_exports); var path = __toESM(require("path")); var COCOS_MCP_WIRE_PROTOCOL_VERSION = 1; var uuidToNid = /* @__PURE__ */ new Map(); var nidToUuid = /* @__PURE__ */ new Map(); var detachedNodesForUndo = /* @__PURE__ */ new Map(); var DETACHED_NODE_STASH_MAX = 2048; var nextNid = 1; var currentSceneUuid = ""; var _modulePaths = module.paths; try { const EditorApp = globalThis.Editor?.App; if (EditorApp?.path) { const nm = path.join(EditorApp.path, "node_modules"); if (!_modulePaths.includes(nm)) { _modulePaths.push(nm); } } } catch { } function getCC() { return require("cc"); } function shortComponentName(comp, cc) { try { const full = cc.js?.getClassName?.(comp); if (full && typeof full === "string") { const parts2 = full.split("."); return parts2[parts2.length - 1] || full; } } catch { } const raw = comp.name || comp.constructor?.name || "Component"; const parts = raw.split("."); return parts[parts.length - 1] || raw; } function getOrCreateNid(uuid) { let nid = uuidToNid.get(uuid); if (nid === void 0) { nid = nextNid++; uuidToNid.set(uuid, nid); nidToUuid.set(nid, uuid); } return nid; } function componentList(node, cc) { let list = []; try { list = node.getComponents(cc.Component); } catch { return ""; } const names = list.map((c) => shortComponentName(c, cc)).filter(Boolean); return names.join(","); } function getCcValueCtors() { return require("cc"); } function getCcAssetManager() { const cc = require("cc"); if (!cc.assetManager) { throw new Error("cc.assetManager is unavailable in this runtime."); } return cc.assetManager; } function runtimeDirectSnapshot(value) { if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") { return value; } if (!value || typeof value !== "object" || Array.isArray(value)) { return void 0; } const record = value; const copyNumbers = (keys) => { const output = {}; for (const key of keys) { const item = record[key]; if (typeof item !== "number" || !Number.isFinite(item)) { return void 0; } output[key] = item; } return output; }; return copyNumbers(["r", "g", "b", "a"]) ?? copyNumbers(["width", "height"]) ?? copyNumbers(["x", "y"]); } function materializeRuntimeDirectValue(value) { if (value === null || typeof value !== "object") { return value; } const cc = getCcValueCtors(); if ("r" in value && "g" in value && "b" in value && "a" in value) { return new cc.Color(value.r, value.g, value.b, value.a); } if ("width" in value && "height" in value) { return new cc.Size(value.width, value.height); } if ("x" in value && "y" in value) { return new cc.Vec2(value.x, value.y); } return value; } function assetClassName(asset, cc) { if (!asset || typeof asset !== "object") { return ""; } try { const full = cc.js?.getClassName?.(asset); if (typeof full === "string" && full.length > 0) { return full; } } catch { } const constructor = asset.constructor; return typeof constructor?.name === "string" ? constructor.name : ""; } function validateAssetKind(asset, componentType, property, cc) { const className = assetClassName(asset, cc).toLowerCase(); const normalizedProperty = property.trim().replace(/^_/, "").toLowerCase(); if (normalizedProperty === "spriteframe" && !className.includes("spriteframe")) { return `Loaded asset is ${className || "unknown"}, expected SpriteFrame for ${componentType}.${property}.`; } if (normalizedProperty === "font" && !className.includes("font")) { return `Loaded asset is ${className || "unknown"}, expected Font for ${componentType}.${property}.`; } return null; } function readAssetReferenceUuid(value, label) { if (value === null || value === void 0) { return { ok: true, uuid: null }; } if (typeof value === "object" && !Array.isArray(value)) { const uuid = value.uuid; if (typeof uuid === "string" && uuid.trim().length > 0) { return { ok: true, uuid: uuid.trim() }; } } return { ok: false, message: `${label} contains a non-null asset reference whose UUID cannot be observed safely.` }; } async function loadCcAssetByUuid(assetManager, uuid) { const cached = assetManager.assets?.get?.(uuid); if (cached) { return cached; } if (typeof assetManager.loadAny !== "function") { throw new Error("assetManager.loadAny is unavailable in this runtime."); } return await new Promise((resolve, reject) => { let settled = false; const timer = setTimeout(() => { if (!settled) { settled = true; reject(new Error(`Timed out loading asset UUID ${uuid}.`)); } }, 5e3); const finish = (error, loaded) => { if (settled) { return; } settled = true; clearTimeout(timer); if (error) { reject(error); return; } const candidate = Array.isArray(loaded) ? loaded.find((item) => { if (!item || typeof item !== "object") return false; return item.uuid === uuid; }) ?? loaded[0] : loaded; if (!candidate) { reject(new Error(`Asset manager returned no asset for UUID ${uuid}.`)); return; } resolve(candidate); }; try { assetManager.loadAny?.call(assetManager, uuid, finish); } catch (error) { finish(error instanceof Error ? error : new Error(String(error))); } }); } function coerceNonNegativeNumber(val, prop) { const n = typeof val === "number" ? val : Number(val); if (!Number.isFinite(n)) { throw new Error(`${prop} \u5FC5\u987B\u662F\u6709\u9650\u6570\u5B57\u3002`); } if (n < 0) { throw new Error(`${prop} \u5FC5\u987B\u662F\u975E\u8D1F\u6570\u3002`); } return n; } function coerceFiniteNumber(val, prop) { const n = typeof val === "number" ? val : Number(val); if (!Number.isFinite(n)) { throw new Error(`${prop} \u5FC5\u987B\u662F\u6709\u9650\u6570\u5B57\u3002`); } return n; } function coerceBoolean(val, prop) { if (typeof val !== "boolean") { throw new Error(`${prop} \u5FC5\u987B\u662F\u5E03\u5C14\u503C\u3002`); } return val; } function coerceString(val, prop, maxLen = 8192) { if (typeof val !== "string") { throw new Error(`${prop} \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u3002`); } if (val.length > maxLen) { throw new Error(`${prop} \u957F\u5EA6\u4E0D\u80FD\u8D85\u8FC7 ${maxLen}\u3002`); } return val; } function coerceEnumString(val, prop, mapping) { if (typeof val !== "string") { throw new Error(`${prop} \u5FC5\u987B\u662F\u679A\u4E3E\u5B57\u7B26\u4E32\u3002`); } const key = val.trim().toUpperCase(); const hit = mapping[key]; if (hit === void 0) { throw new Error(`${prop} \u975E\u6CD5\u679A\u4E3E\u503C\u3002\u53EF\u9009\uFF1A${Object.keys(mapping).join(", ")}`); } return hit; } function parseHexColorRGBA(input) { const s = input.trim(); if (!/^#([0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(s)) { throw new Error("color \u5FC5\u987B\u662F #RRGGBB \u6216 #RRGGBBAA \u7684 Hex \u5B57\u7B26\u4E32\u3002"); } const hex = s.slice(1); const r = Number.parseInt(hex.slice(0, 2), 16); const g = Number.parseInt(hex.slice(2, 4), 16); const b = Number.parseInt(hex.slice(4, 6), 16); const a = hex.length === 8 ? Number.parseInt(hex.slice(6, 8), 16) : 255; return { r, g, b, a }; } function coerceHexColor(val) { if (typeof val !== "string") { throw new Error("color \u5165\u53C2\u5FC5\u987B\u662F Hex \u5B57\u7B26\u4E32\u3002"); } const rgba = parseHexColorRGBA(val); const cc = getCcValueCtors(); const c = new cc.Color(rgba.r, rgba.g, rgba.b, rgba.a); return { r: c.r, g: c.g, b: c.b, a: c.a }; } function coerceToSize(val) { if (!val || typeof val !== "object" || Array.isArray(val)) { throw new Error("size \u5165\u53C2\u5FC5\u987B\u662F { width, height } \u5BF9\u8C61\u3002"); } const o = val; const cc = getCcValueCtors(); const w = coerceNonNegativeNumber(o.width, "width"); const h = coerceNonNegativeNumber(o.height, "height"); const sz = new cc.Size(w, h); return { width: sz.width, height: sz.height }; } function coerceToVec2(val) { if (!val || typeof val !== "object" || Array.isArray(val)) { throw new Error("vec2 \u5165\u53C2\u5FC5\u987B\u662F { x, y } \u5BF9\u8C61\u3002"); } const o = val; const cc = getCcValueCtors(); const x = typeof o.x === "number" ? o.x : Number(o.x); const y = typeof o.y === "number" ? o.y : Number(o.y); if (!Number.isFinite(x) || !Number.isFinite(y)) { throw new Error("vec2 \u5750\u6807\u5FC5\u987B\u662F\u6709\u9650\u6570\u5B57\u3002"); } const v = new cc.Vec2(x, y); return { x: v.x, y: v.y }; } function coerceAssetUuid(val, prop) { if (typeof val !== "string") { throw new Error(`${prop} \u8D44\u6E90\u5165\u53C2\u5FC5\u987B\u662F UUID \u5B57\u7B26\u4E32\u3002`); } const uuid = val.trim(); if (!uuid) { throw new Error(`${prop} \u8D44\u6E90 UUID \u4E0D\u80FD\u4E3A\u7A7A\u3002`); } return { __uuid__: uuid }; } var COMPONENT_SCHEMAS = { UITransform: { width: { component: "UITransform", prop: "width", type: "number", coercer: (val) => coerceNonNegativeNumber(val, "width") }, height: { component: "UITransform", prop: "height", type: "number", coercer: (val) => coerceNonNegativeNumber(val, "height") }, anchorPoint: { component: "UITransform", prop: "anchorPoint", type: "vec2", coercer: coerceToVec2 } }, Sprite: { color: { component: "Sprite", prop: "color", type: "hex-color", coercer: coerceHexColor }, spriteFrame: { component: "Sprite", prop: "spriteFrame", dumpProp: "_spriteFrame", type: "asset-uuid", coercer: (val) => coerceAssetUuid(val, "spriteFrame") }, type: { component: "Sprite", prop: "type", type: "enum-string", coercer: (val) => coerceEnumString(val, "type", { SIMPLE: 0, SLICED: 1, TILED: 2, FILLED: 3, MESH: 4 }) }, sizeMode: { component: "Sprite", prop: "sizeMode", type: "enum-string", coercer: (val) => coerceEnumString(val, "sizeMode", { CUSTOM: 0, TRIMMED: 1, RAW: 2 }) } }, Label: { color: { component: "Label", prop: "color", type: "hex-color", coercer: coerceHexColor }, string: { component: "Label", prop: "string", type: "string", coercer: (val) => coerceString(val, "string") }, fontSize: { component: "Label", prop: "fontSize", type: "number", coercer: (val) => coerceNonNegativeNumber(val, "fontSize") }, lineHeight: { component: "Label", prop: "lineHeight", type: "number", coercer: (val) => coerceNonNegativeNumber(val, "lineHeight") }, overflow: { component: "Label", prop: "overflow", type: "enum-string", coercer: (val) => coerceEnumString(val, "overflow", { NONE: 0, CLAMP: 1, SHRINK: 2, RESIZE_HEIGHT: 3 }) }, horizontalAlign: { component: "Label", prop: "horizontalAlign", type: "enum-string", coercer: (val) => coerceEnumString(val, "horizontalAlign", { LEFT: 0, CENTER: 1, RIGHT: 2 }) }, verticalAlign: { component: "Label", prop: "verticalAlign", type: "enum-string", coercer: (val) => coerceEnumString(val, "verticalAlign", { TOP: 0, CENTER: 1, BOTTOM: 2 }) }, font: { component: "Label", prop: "font", dumpProp: "_font", type: "asset-uuid", coercer: (val) => coerceAssetUuid(val, "font") } }, Button: { interactable: { component: "Button", prop: "interactable", type: "boolean", coercer: (val) => coerceBoolean(val, "interactable") }, transition: { component: "Button", prop: "transition", type: "enum-string", coercer: (val) => coerceEnumString(val, "transition", { NONE: 0, COLOR: 1, SPRITE: 2, SCALE: 3 }) }, zoomScale: { component: "Button", prop: "zoomScale", type: "number", coercer: (val) => coerceNonNegativeNumber(val, "zoomScale") } }, Layout: { type: { component: "Layout", prop: "type", type: "enum-string", coercer: (val) => coerceEnumString(val, "type", { NONE: 0, HORIZONTAL: 1, VERTICAL: 2, GRID: 3 }) }, resizeMode: { component: "Layout", prop: "resizeMode", type: "enum-string", coercer: (val) => coerceEnumString(val, "resizeMode", { NONE: 0, CONTAINER: 1, CHILDREN: 2 }) }, constraint: { component: "Layout", prop: "constraint", type: "enum-string", coercer: (val) => coerceEnumString(val, "constraint", { NONE: 0, FIXED_ROW: 1, FIXED_COL: 2 }) }, spacingX: { component: "Layout", prop: "spacingX", type: "number", coercer: (val) => coerceNonNegativeNumber(val, "spacingX") }, spacingY: { component: "Layout", prop: "spacingY", type: "number", coercer: (val) => coerceNonNegativeNumber(val, "spacingY") }, paddingLeft: { component: "Layout", prop: "paddingLeft", type: "number", coercer: (val) => coerceNonNegativeNumber(val, "paddingLeft") }, paddingRight: { component: "Layout", prop: "paddingRight", type: "number", coercer: (val) => coerceNonNegativeNumber(val, "paddingRight") }, paddingTop: { component: "Layout", prop: "paddingTop", type: "number", coercer: (val) => coerceNonNegativeNumber(val, "paddingTop") }, paddingBottom: { component: "Layout", prop: "paddingBottom", type: "number", coercer: (val) => coerceNonNegativeNumber(val, "paddingBottom") }, cellSize: { component: "Layout", prop: "cellSize", type: "size", coercer: coerceToSize }, constraintNum: { component: "Layout", prop: "constraintNum", type: "number", coercer: (val) => coerceNonNegativeNumber(val, "constraintNum") } }, Widget: { isAlignTop: { component: "Widget", prop: "isAlignTop", type: "boolean", coercer: (val) => coerceBoolean(val, "isAlignTop") }, isAlignBottom: { component: "Widget", prop: "isAlignBottom", type: "boolean", coercer: (val) => coerceBoolean(val, "isAlignBottom") }, isAlignLeft: { component: "Widget", prop: "isAlignLeft", type: "boolean", coercer: (val) => coerceBoolean(val, "isAlignLeft") }, isAlignRight: { component: "Widget", prop: "isAlignRight", type: "boolean", coercer: (val) => coerceBoolean(val, "isAlignRight") }, isAlignHorizontalCenter: { component: "Widget", prop: "isAlignHorizontalCenter", type: "boolean", coercer: (val) => coerceBoolean(val, "isAlignHorizontalCenter") }, isAlignVerticalCenter: { component: "Widget", prop: "isAlignVerticalCenter", type: "boolean", coercer: (val) => coerceBoolean(val, "isAlignVerticalCenter") }, top: { component: "Widget", prop: "top", type: "number", coercer: (val) => coerceFiniteNumber(val, "top") }, bottom: { component: "Widget", prop: "bottom", type: "number", coercer: (val) => coerceFiniteNumber(val, "bottom") }, left: { component: "Widget", prop: "left", type: "number", coercer: (val) => coerceFiniteNumber(val, "left") }, right: { component: "Widget", prop: "right", type: "number", coercer: (val) => coerceFiniteNumber(val, "right") }, horizontalCenter: { component: "Widget", prop: "horizontalCenter", type: "number", coercer: (val) => coerceFiniteNumber(val, "horizontalCenter") }, verticalCenter: { component: "Widget", prop: "verticalCenter", type: "number", coercer: (val) => coerceFiniteNumber(val, "verticalCenter") } } }; function shouldSkipNode(name, includeInternal) { if (includeInternal) { return false; } return name.startsWith("_"); } var EDITOR_DECORATION_ROOT_NAMES = new Set( [ "Editor Scene Foreground", "Editor Scene Background", "gizmoRoot", "GAME_VIEW", "Editor Debugger", "Scene Gizmo Camera", "Scene Gizmo Root", "Internal Gizmo", "Scene Editor Camera" ].map((n) => n.toLowerCase()) ); function shouldSkipEditorDecorationRoot(name, includeEditorDecorations) { if (includeEditorDecorations) { return false; } return EDITOR_DECORATION_ROOT_NAMES.has(name.trim().toLowerCase()); } function parentInspectMeta(node, scene) { const par = node.parent ?? null; if (par == null || par.uuid === scene.uuid) { return { parentNid: null, parentRelation: "scene" }; } const pnid = uuidToNid.get(par.uuid); if (pnid !== void 0) { return { parentNid: pnid, parentRelation: "indexed" }; } return { parentNid: null, parentRelation: "unknown" }; } function findNodeByUuid(root, targetUuid) { if (!root) { return null; } if (root.uuid === targetUuid) { return root; } for (const child of root.children) { const found = findNodeByUuid(child, targetUuid); if (found) { return found; } } return null; } function releaseDetachedNodeStash(uuid) { const stashed = detachedNodesForUndo.get(uuid); if (!stashed) { return { released: false, destroyed: false, liveNodePreserved: false }; } let reachable = null; try { reachable = findNodeByUuid(stashed.scene, uuid); } catch { reachable = null; } const liveNodePreserved = reachable === stashed.node || stashed.node.parent !== null; detachedNodesForUndo.delete(uuid); if (liveNodePreserved) { return { released: true, destroyed: false, liveNodePreserved: true }; } try { stashed.node.destroy?.(); return { released: true, destroyed: true, liveNodePreserved: false }; } catch { return { released: true, destroyed: false, liveNodePreserved: false }; } } function nearlyEqual(actual, expected, epsilon = 0.01) { return typeof actual === "number" && Number.isFinite(actual) && Math.abs(actual - expected) <= epsilon; } function requestedUiSize(expected) { const defaults = expected.kind === "label" ? { width: 220, height: 48 } : expected.kind === "button" ? { width: 180, height: 56 } : expected.kind === "container" ? { width: 320, height: 180 } : { width: 100, height: 100 }; return expected.size ?? defaults; } function settledUiSize(expected) { const seed = requestedUiSize(expected); const layout = expected.layout; if (!layout || layout.resizeMode !== "container") { return seed; } const implicitChildren = expected.kind === "button" ? [ { kind: "label", size: expected.size ?? { width: 180, height: 56 }, fontSize: expected.fontSize ?? 24 } ] : []; const children = [...implicitChildren, ...expected.children ?? []].filter((child) => child.active !== false).map((child) => settledUiSize(child)); const count = children.length; const paddingH = (layout.paddingLeft ?? 0) + (layout.paddingRight ?? 0); const paddingV = (layout.paddingTop ?? 0) + (layout.paddingBottom ?? 0); if (layout.type === "horizontal") { return { width: children.reduce((sum, child) => sum + child.width, 0) + (count - 1) * (layout.spacingX ?? 0) + paddingH, height: seed.height }; } if (layout.type === "vertical") { return { width: seed.width, height: children.reduce((sum, child) => sum + child.height, 0) + (count - 1) * (layout.spacingY ?? 0) + paddingV }; } let columns = null; const constraintNum = layout.constraintNum ?? 2; const constraint = layout.constraint ?? ((layout.constraintNum ?? 0) > 0 ? "fixed-column" : "none"); if (constraint === "fixed-column" && constraintNum > 0) { columns = constraintNum; } else if (constraint === "fixed-row" && constraintNum > 0) { columns = count > 0 ? Math.ceil(count / constraintNum) : 1; } const rowHeights = []; if (columns !== null) { for (let index = 0; index < count; index += columns) { rowHeights.push(Math.max(...children.slice(index, index + columns).map((child) => child.height), 0)); } } else { const availableWidth = Math.max(0, seed.width - paddingH); let rowWidth = 0; let rowHeight = 0; for (const child of children) { const nextWidth = rowWidth === 0 ? child.width : rowWidth + (layout.spacingX ?? 0) + child.width; if (rowWidth > 0 && nextWidth > availableWidth) { rowHeights.push(rowHeight); rowWidth = child.width; rowHeight = child.height; } else { rowWidth = nextWidth; rowHeight = Math.max(rowHeight, child.height); } } if (rowWidth > 0) { rowHeights.push(rowHeight); } } return { width: seed.width, height: rowHeights.reduce((sum, height) => sum + height, 0) + Math.max(0, rowHeights.length - 1) * (layout.spacingY ?? 0) + paddingV }; } function uiComponent(node, cc, name) { try { return node.getComponents(cc.Component).find((component) => shortComponentName(component, cc) === name) ?? null; } catch { return null; } } function rgbaMatches(actual, expectedHex) { if (!actual || typeof actual !== "object" || Array.isArray(actual)) { return false; } const record = actual; const expected = parseHexColorRGBA(expectedHex); return record.r === expected.r && record.g === expected.g && record.b === expected.b && record.a === expected.a; } function verifyUiNodeRuntime(node, expected, cc) { const mismatches = []; const parentLayout = node.parent ? uiComponent(node.parent, cc, "Layout") : null; const widgetOwnsX = expected.widget?.left !== void 0 || expected.widget?.right !== void 0 || expected.widget?.horizontalCenter !== void 0; const widgetOwnsY = expected.widget?.top !== void 0 || expected.widget?.bottom !== void 0 || expected.widget?.verticalCenter !== void 0; const parentLayoutOwnsX = parentLayout?.type === 1 || parentLayout?.type === 3; const parentLayoutOwnsY = parentLayout?.type === 2 || parentLayout?.type === 3; if (expected.name && node.name !== expected.name) { mismatches.push(`name expected ${expected.name}, observed ${node.name}`); } if (expected.active !== void 0 && node.active !== expected.active) { mismatches.push(`active expected ${String(expected.active)}, observed ${String(node.active)}`); } if (expected.position) { for (const axis of ["x", "y", "z"]) { const engineOwned = axis === "x" && (widgetOwnsX || parentLayoutOwnsX) || axis === "y" && (widgetOwnsY || parentLayoutOwnsY); if (engineOwned) { if (!Number.isFinite(node.position[axis])) { mismatches.push(`engine-owned position.${axis} is invalid: ${String(node.position[axis])}`); } } else if (!nearlyEqual(node.position[axis], expected.position[axis])) { mismatches.push(`position.${axis} expected ${expected.position[axis]}, observed ${node.position[axis]}`); } } } const settledExpectedSize = node.activeInHierarchy === false ? requestedUiSize(expected) : settledUiSize(expected); let expectedWidth = settledExpectedSize.width; let expectedHeight = settledExpectedSize.height; const numeric = (value, fallback = 0) => typeof value === "number" && Number.isFinite(value) ? value : fallback; const scaleX = Math.abs(node.scale.x) || 1; const scaleY = Math.abs(node.scale.y) || 1; const parentTransform = node.parent ? uiComponent(node.parent, cc, "UITransform") : null; const parentContentSize = parentTransform?.contentSize; const parentWidth = numeric(parentTransform?.width, numeric(parentContentSize?.width)); const parentHeight = numeric(parentTransform?.height, numeric(parentContentSize?.height)); if (expected.widget?.left !== void 0 && expected.widget?.right !== void 0 && node.parent) { expectedWidth = (parentWidth - expected.widget.left - expected.widget.right) / scaleX; } if (expected.widget?.top !== void 0 && expected.widget?.bottom !== void 0 && node.parent) { expectedHeight = (parentHeight - expected.widget.top - expected.widget.bottom) / scaleY; } if (parentLayout?.resizeMode === 2 && node.parent) { const usefulChildren = node.parent.children.filter( (child) => child.activeInHierarchy !== false && uiComponent(child, cc, "UITransform") !== null ); const childCount = usefulChildren.length; const affectedByScale = parentLayout.affectedByScale === true; const usedScaleX = affectedByScale ? scaleX : 1; const usedScaleY = affectedByScale ? scaleY : 1; if (parentLayout.type === 1 && childCount > 0) { expectedWidth = (parentWidth - numeric(parentLayout.paddingLeft) - numeric(parentLayout.paddingRight) - (childCount - 1) * numeric(parentLayout.spacingX)) / childCount / usedScaleX; } else if (parentLayout.type === 2 && childCount > 0) { expectedHeight = (parentHeight - numeric(parentLayout.paddingTop) - numeric(parentLayout.paddingBottom) - (childCount - 1) * numeric(parentLayout.spacingY)) / childCount / usedScaleY; } else if (parentLayout.type === 3) { const cellSize = parentLayout.cellSize; expectedWidth = numeric(cellSize?.width) / usedScaleX; expectedHeight = numeric(cellSize?.height) / usedScaleY; } } const transform = uiComponent(node, cc, "UITransform"); if (!transform) { mismatches.push("UITransform component missing"); } else { const contentSize = transform.contentSize; const width = typeof transform.width === "number" ? transform.width : contentSize?.width; const height = typeof transform.height === "number" ? transform.height : contentSize?.height; if (!nearlyEqual(width, expectedWidth)) { mismatches.push(`width expected ${expectedWidth}, observed ${String(width)}`); } if (!nearlyEqual(height, expectedHeight)) { mismatches.push(`height expected ${expectedHeight}, observed ${String(height)}`); } } if (expected.kind === "shape" || expected.kind === "sprite" || expected.kind === "button") { const sprite = uiComponent(node, cc, "Sprite"); if (!sprite) { mismatches.push("Sprite component missing"); } else { if (!sprite.spriteFrame) { mismatches.push("Sprite.spriteFrame is empty"); } const expectedColor = expected.color ?? (expected.kind === "shape" ? "#FFFFFFFF" : expected.kind === "button" ? "#3478F6FF" : void 0); if (expectedColor && !rgbaMatches(sprite.color, expectedColor)) { mismatches.push(`Sprite.color does not match ${expectedColor}`); } if (expected.nineSlice === true && sprite.type !== 1) { mismatches.push(`Sprite.type expected SLICED(1), observed ${String(sprite.type)}`); } } } if (expected.kind === "label") { const label = uiComponent(node, cc, "Label"); if (!label) { mismatches.push("Label component missing"); } else { const expectedText = expected.text ?? expected.buttonLabel ?? "Label"; if (label.string !== expectedText) { mismatches.push(`Label.string expected ${JSON.stringify(expectedText)}, observed ${JSON.stringify(label.string)}`); } if (!nearlyEqual(label.fontSize, expected.fontSize ?? 28)) { mismatches.push(`Label.fontSize expected ${expected.fontSize ?? 28}, observed ${String(label.fontSize)}`); } if (!nearlyEqual(label.lineHeight, expected.lineHeight ?? expected.fontSize ?? 32)) { mismatches.push( `Label.lineHeight expected ${expected.lineHeight ?? expected.fontSize ?? 32}, observed ${String(label.lineHeight)}` ); } if (label.overflow !== 1) { mismatches.push(`Label.overflow expected CLAMP(1), observed ${String(label.overflow)}`); } const horizontal = { left: 0, center: 1, right: 2 }; const vertical = { top: 0, center: 1, bottom: 2 }; if (label.horizontalAlign !== horizontal[expected.horizontalAlign ?? "center"]) { mismatches.push("Label.horizontalAlign does not match"); } if (label.verticalAlign !== vertical[expected.verticalAlign ?? "center"]) { mismatches.push("Label.verticalAlign does not match"); } if (expected.color && !rgbaMatches(label.color, expected.color)) { mismatches.push(`Label.color does not match ${expected.color}`); } } } if (expected.kind === "button") { const button = uiComponent(node, cc, "Button"); if (!button) { mismatches.push("Button component missing"); } else if (button.interactable !== (expected.interactable !== false)) { mismatches.push(`Button.interactable expected ${String(expected.interactable !== false)}`); } } if (expected.layout) { const layout = uiComponent(node, cc, "Layout"); if (!layout) { mismatches.push("Layout component missing"); } else { const typeMap = { horizontal: 1, vertical: 2, grid: 3 }; const resizeMap = { none: 0, container: 1, children: 2 }; const constraintMap = { none: 0, "fixed-row": 1, "fixed-column": 2 }; if (layout.type !== typeMap[expected.layout.type]) { mismatches.push(`Layout.type does not match ${expected.layout.type}`); } if (layout.resizeMode !== resizeMap[expected.layout.resizeMode ?? "none"]) { mismatches.push(`Layout.resizeMode does not match ${expected.layout.resizeMode ?? "none"}`); } if (expected.layout.constraint && layout.constraint !== constraintMap[expected.layout.constraint]) { mismatches.push(`Layout.constraint does not match ${expected.layout.constraint}`); } for (const property of [ "spacingX", "spacingY", "paddingLeft", "paddingRight", "paddingTop", "paddingBottom", "constraintNum" ]) { const expectedValue = expected.layout[property]; if (typeof expectedValue === "number" && !nearlyEqual(layout[property], expectedValue)) { mismatches.push(`Layout.${property} expected ${expectedValue}, observed ${String(layout[property])}`); } } if (expected.layout.cellSize) { const cellSize = layout.cellSize; if (!cellSize || !nearlyEqual(cellSize.width, expected.layout.cellSize.width) || !nearlyEqual(cellSize.height, expected.layout.cellSize.height)) { mismatches.push("Layout.cellSize does not match"); } } } } if (expected.widget) { const widget = uiComponent(node, cc, "Widget"); if (!widget) { mismatches.push("Widget component missing"); } else { const alignments = [ ["top", "isAlignTop"], ["bottom", "isAlignBottom"], ["left", "isAlignLeft"], ["right", "isAlignRight"], ["horizontalCenter", "isAlignHorizontalCenter"], ["verticalCenter", "isAlignVerticalCenter"] ]; for (const [valueProperty, enabledProperty] of alignments) { const expectedValue = expected.widget[valueProperty]; if (typeof expectedValue === "number") { if (widget[enabledProperty] !== true) { mismatches.push(`Widget.${enabledProperty} is not enabled`); } if (!nearlyEqual(widget[valueProperty], expectedValue)) { mismatches.push(`Widget.${valueProperty} expected ${expectedValue}, observed ${String(widget[valueProperty])}`); } } } } } return mismatches; } function resolveComponentTypeInput(input) { const raw = input.trim(); if (!raw) { throw new Error("componentType must be non-empty."); } if (raw.includes(".")) { return raw; } if (raw.startsWith("cc")) { return raw; } return `cc.${raw}`; } function resolveCtorByPath(cc, fullName) { const raw = fullName.trim(); if (!raw) { return null; } const pathParts = raw.split(".").filter(Boolean); if (pathParts.length === 0) { return null; } const parts = pathParts.length === 1 ? ["cc", pathParts[0]] : pathParts; let cur = cc; for (const p of parts) { if (!cur || typeof cur !== "object") { return null; } cur = cur[p]; } if (typeof cur === "function") { return cur; } try { const ccJs = cc.js ?? void 0; const byName = ccJs?.getClassByName?.(raw); if (typeof byName === "function") { return byName; } if (raw.startsWith("cc.")) { const short = raw.slice(3); const byShort = ccJs?.getClassByName?.(short); if (typeof byShort === "function") { return byShort; } } } catch { } return null; } function resolveComponentCtor(rawType) { const trimmed = rawType.trim(); if (!trimmed) { return null; } const fullName = trimmed.includes(".") ? trimmed : `cc.${trimmed}`; const ccAny = getCC(); const byPath = resolveCtorByPath(ccAny, fullName); if (byPath) { return byPath; } const js = ccAny.js; if (typeof js?.getClassByName === "function") { try { const c = js.getClassByName(fullName); if (typeof c === "function") { return c; } } catch { } } return null; } function isNodeUnderAncestorUuid(node, ancestorUuid) { let cur = node; while (cur) { if (cur.uuid === ancestorUuid) { return true; } cur = cur.parent ?? null; } return false; } function buildInspectPlain(node, cc, scene) { const p = node.position; const e = node.eulerAngles; const s = node.scale; const wp = node.worldPosition ?? p; const activeInHierarchy = typeof node.activeInHierarchy === "boolean" ? node.activeInHierarchy : node.active; const layer = typeof node.layer === "number" && Number.isFinite(node.layer) ? node.layer : 0; const childCount = Array.isArray(node.children) ? node.children.length : 0; const { parentNid, parentRelation } = parentInspectMeta(node, scene); let comps = []; try { comps = node.getComponents(cc.Component); } catch { comps = []; } const components = comps.map((c) => shortComponentName(c, cc)); return { name: node.name, active: node.active, activeInHierarchy, layer, childCount, position: { x: p.x, y: p.y, z: p.z }, worldPosition: { x: wp.x, y: wp.y, z: wp.z }, rotationEuler: { x: e.x, y: e.y, z: e.z }, scale: { x: s.x, y: s.y, z: s.z }, components, parentNid, parentRelation }; } var methods = { ping() { return { status: "ok", layer: "scene" }; }, /** * Runtime scene name + asset uuid for Main (`asset-db` → MCP `assetPath`). UUID is never sent to MCP clients. */ getCurrentSceneMeta() { const cc = getCC(); const scene = cc.director.getScene(); if (!scene) { return { ok: true, hasScene: false }; } return { ok: true, hasScene: true, sceneName: scene.name, sceneAssetUuid: scene.uuid }; }, getHierarchy(...args) { const opts = args[0] || {}; const maxDepth = opts.maxDepth !== void 0 ? Math.max(0, Math.min(100, opts.maxDepth)) : 5; const maxNodes = opts.maxNodes !== void 0 ? Math.max(1, Math.min(5e4, opts.maxNodes)) : 800; const includeInternal = opts.includeInternal === true; const includeEditorDecorations = opts.includeEditorDecorations === true; const cc = getCC(); const scene = cc.director.getScene(); if (!scene) { return "[Error] No active scene in director."; } if (scene.uuid !== currentSceneUuid) { uuidToNid.clear(); nidToUuid.clear(); nextNid = 1; currentSceneUuid = scene.uuid; } const lines = []; lines.push( `[Scene: ${scene.name} | proto:${COCOS_MCP_WIRE_PROTOCOL_VERSION} | Session Time: ${Date.now()} | Note: NIDs are session-only | editorDecor:${includeEditorDecorations ? "on" : "off"}]` ); let nodeCount = 0; let truncatedByDepth = false; let truncatedByNodes = false; const visit = (node, depth) => { if (nodeCount >= maxNodes) { truncatedByNodes = true; return; } if (shouldSkipNode(node.name, includeInternal)) { return; } if (shouldSkipEditorDecorationRoot(node.name, includeEditorDecorations)) { return; } if (depth > maxDepth) { truncatedByDepth = true; return; } const nid = getOrCreateNid(node.uuid); nodeCount += 1; const indent = " ".repeat(depth); const comps = componentList(node, cc); const inactive = node.active ? "" : " [INACTIVE]"; lines.push(`${indent}[${nid}] ${node.name} (${comps})${inactive}`); if (depth === maxDepth) { if (node.children.length > 0) { truncatedByDepth = true; } return; } for (const child of node.children) { if (nodeCount >= maxNodes) { truncatedByNodes = true; return; } visit(child, depth + 1); } }; visit(scene, 0); if (truncatedByDepth) { lines.push("... (truncated by maxDepth limitation)"); } if (truncatedByNodes) { lines.push("... (truncated by maxNodes limitation)"); } return lines.join("\n"); }, searchNodes(...args) { const opts = args[0] || {}; const nameSub = (opts.nameSubstring ?? "").trim().toLowerCase(); const compF = (opts.componentFilter ?? "").trim().toLowerCase(); if (!nameSub && !compF) { return "[Error] Provide non-empty nameSubstring and/or componentFilter to limit matches."; } const maxResults = opts.maxResults !== void 0 ? Math.max(1, Math.min(500, opts.maxResults)) : 80; const includeInternal = opts.includeInternal === true; const includeEditorDecorations = opts.includeEditorDecorations === true; const cc = getCC(); const scene = cc.director.getScene(); if (!scene) { return "[Error] No active scene in director."; } if (scene.uuid !== currentSceneUuid) { uuidToNid.clear(); nidToUuid.clear(); nextNid = 1; currentSceneUuid = scene.uuid; } const lines = []; lines.push( `[Search | Scene: ${scene.name} | proto:${COCOS_MCP_WIRE_PROTOCOL_VERSION} | maxResults: ${maxResults} | NIDs session-scoped]` ); let count = 0; let capped = false; const visit = (node, pathSegs) => { if (count >= maxResults) { capped = true; return; } if (shouldSkipNode(node.name, includeInternal)) { return; } if (shouldSkipEditorDecorationRoot(node.name, includeEditorDecorations)) { return; } const comps = componentList(node, cc); const nm = node.name.toLowerCase(); const cl = comps.toLowerCase(); const nameOk = !nameSub || nm.includes(nameSub); const compOk = !compF || cl.includes(compF); if (nameOk && compOk) { count += 1; const nid = getOrCreateNid(node.uuid); const pathStr = pathSegs.length > 0 ? `${pathSegs.join("/")}/${node.name}` : node.name; lines.push(`[${nid}] ${pathStr} (${comps})`); } for (const child of node.children) { if (count >= maxResults) { capped = true; return; } visit(child, [...pathSegs, node.name]); } }; for (const child of scene.children) { if (count >= maxResults) { capped = true; break; } visit(child, [scene.name]); } if (count === 0) { lines.push("(no matches)"); } if (capped) { lines.push("... (truncated by maxResults)"); } return lines.join("\n"); }, inspectNode(...args) { const raw = args[0]; const nid = raw?.nid; if (typeof nid !== "number" || !Number.isFinite(nid) || !Number.isInteger(nid)) { return { ok: false, error: "invalid_args", message: "nid must be an integer (use getHierarchy NIDs)." }; } const uuid = nidToUuid.get(nid); if (!uuid) { return { ok: false, error: "nid_not_found", message: "Unknown nid \u2014 scene may have changed; call getHierarchy again." }; } const cc = getCC(); const scene = cc.director.getScene(); if (!scene) { return { ok: false, error: "no_scene", message: "No active scene." }; } if (scene.uuid !== currentSceneUuid) { return { ok: false, error: "stale_session", message: "Scene changed since last getHierarchy; refresh NID map first." }; } const node = findNodeByUuid(scene, uuid); if (!node) { return { ok: false, error: "node_missing", message: "Node uuid no longer in scene; call getHierarchy again." }; } return { ok: true, data: buildInspectPlain(node, cc, scene) }; }, /** * Force deferred Label/Layout/Widget work to a stable runtime state before * task-level verification. Creator otherwise flushes these components on a * later render frame, which can make an immediate read a false positive. */ settleUiNodes(...args) { const rawNids = args[0]; if (!Array.isArray(rawNids) || rawNids.length < 1 || rawNids.length > 64) { return { ok: false, error: "ui_settle_failed", message: "settleUiNodes requires an array of 1 to 64 nids." }; } const nids = [...new Set(rawNids)]; if (nids.some((nid) => typeof nid !== "number" || !Number.isInteger(nid))) { return { ok: false, error: "ui_settle_failed", message: "settleUiNodes requires integer nids." }; } const cc = getCC(); const scene = cc.director.getScene(); if (!scene) { return { ok: false, error: "ui_settle_failed", message: "No active scene." }; } const nodes = []; for (const nid of nids) { const uuid = nidToUuid.get(nid); const node = uuid ? findNodeByUuid(scene, uuid) : null; if (!node) { return { ok: false, error: "ui_settle_failed", message: `UI node ${nid} is missing from the active runtime scene.` }; } nodes.push(node); } const depthOf = (node) => { let depth = 0; let current = node.parent; while (current && depth < 64) { depth += 1; current = current.parent; } return depth; }; const shallowToDeep = nodes.map((node, index) => ({ node, index, depth: depthOf(node) })).sort((left, right) => left.depth - right.depth || left.index - right.index).map((entry) => entry.node); const deepToShallow = [...shallowToDeep].reverse(); const stableNumber = (value) => typeof value === "number" && Number.isFinite(value) ? Math.round(value * 1e4) / 1e4 : null; const geometrySnapshot = () => JSON.stringify( shallowToDeep.map((node) => { const transform = uiComponent(node, cc, "UITransform"); const contentSize = transform?.contentSize; return [ node.uuid, stableNumber(node.position.x), stableNumber(node.position.y), stableNumber(node.position.z), stableNumber(typeof transform?.width === "number" ? transform.width : contentSize?.width), stableNumber(typeof transform?.height === "number" ? transform.height : contentSize?.height) ]; }) ); let labelUpdates = 0; let layoutUpdates = 0; let widgetUpdates = 0; let previousSnapshot = null; let stablePasses = 0; try { const maxPasses = Math.min(16, Math.max(4, shallowToDeep.length + 2)); for (let pass = 1; pass <= maxPasses; pass += 1) { for (const node of shallowToDeep) { if (node.activeInHierarchy === false) continue; const label = uiComponent(node, cc, "Label"); const updateRenderData = label?.updateRenderData; if (typeof updateRenderData === "function") { updateRenderData.call(label, true); labelUpdates += 1; } } for (const node of deepToShallow) { if (node.activeInHierarchy === false) continue; const layout = uiComponent(node, cc, "Layout"); const updateLayout = layout?.updateLayout; if (typeof updateLayout === "function") { updateLayout.call(layout, true); layoutUpdates += 1; } } for (const node of shallowToDeep) { if (node.activeInHierarchy === false) continue; const widget = uiComponent(node, cc, "Widget"); const updateAlignment = widget?.updateAlignment; if (typeof updateAlignment === "function") { updateAlignment.call(widget); widgetUpdates += 1; } } for (const node of deepToShallow) { if (node.activeInHierarchy === false) continue; const layout = uiComponent(node, cc, "Layout"); const updateLayout = layout?.updateLayout; if (typeof updateLayout === "function") { updateLayout.call(layout, true); layoutUpdates += 1; } } for (const node of shallowToDeep) { if (node.activeInHierarchy === false) continue; const label = uiComponent(node, cc, "Label"); const updateRenderData = label?.updateRenderData; if (typeof updateRenderData === "function") { updateRenderData.call(label, true); labelUpdates += 1; } } const snapshot = geometrySnapshot(); if (previousSnapshot === snapshot) { stablePasses = pass; break; } previousSnapshot = snapshot; } } catch (error) { const message = error instanceof Error ? error.message : String(error); return { ok: false, error: "ui_settle_failed", message: `UI settle failed: ${message}` }; } if (stablePasses === 0) { return { ok: false, error: "ui_settle_failed", message: "UI geometry did not stabilize within the bounded settle passes." }; } return { ok: true, passes: stablePasses, labelUpdates, layoutUpdates, widgetUpdates }; }, verifyUiNode(...args) { const raw = args[0]; if (!raw || typeof raw.nid !== "number" || !Number.isInteger(raw.nid)) { return { ok: false, error: "ui_verification_failed", message: "verifyUiNode requires an integer nid.", mismatches: ["invalid nid"] }; } if (!raw.expected || typeof raw.expected !== "object" || Array.isArray(raw.expected)) { return { ok: false, error: "ui_verification_failed", message: "verifyUiNode requires an expected UI node specification.", mismatches: ["invalid expected UI node specification"] }; } const uuid = nidToUuid.get(raw.nid); const cc = getCC(); const scene = cc.director.getScene(); const node = uuid && scene ? findNodeByUuid(scene, uuid) : null; if (!node) { return { ok: false, error: "ui_verification_failed", message: `UI node ${raw.nid} is missing from the active runtime scene.`, mismatches: ["node missing"] }; } const mismatches = verifyUiNodeRuntime(node, raw.expected, cc); if (mismatches.length > 0) { return { ok: false, error: "ui_verification_failed", message: `Runtime UI verification failed for ${node.name}: ${mismatches.join("; ")}`, mismatches }; } return { ok: true }; }, /** * Returns node uuid for Main to call `scene` `query-node` + `set-property` with `record: true` (undo). * Does not mutate the scene. */ prepareSetNodeActive(...args) { const raw = args[0]; const nid = raw?.nid; const nextActive = raw?.active; if (typeof nid !== "number" || !Number.isFinite(nid) || !Number.isInteger(nid)) { return { ok: false, error: "invalid_args", message: "nid must be an integer (use getHierarchy NIDs)." }; } if (typeof nextActive !== "boolean") { return { ok: false, error: "invalid_args", message: "active must be a boolean." }; } const uuid = nidToUuid.get(nid); if (!uuid) { return { ok: false, error: "nid_not_found", message: "Unknown nid \u2014 scene may have changed; call getHierarchy again." }; } const cc = getCC(); const scene = cc.director.getScene(); if (!scene) { return { ok: false, error: "no_scene", message: "No active scene." }; } if (scene.uuid !== currentSceneUuid) { return { ok: false, error: "stale_session", message: "Scene changed since last getHierarchy; refresh NID map first." }; } const node = findNodeByUuid(scene, uuid); if (!node) { return { ok: false, error: "node_missing", message: "Node uuid no longer in scene; call getHierarchy again." }; } return { ok: true, uuid: node.uuid }; }, /** Same nid/stale checks as `prepareSetNodeActive`; Main applies transform via query-node + set-property. */ prepareSetNodeTransform(...args) { const raw = args[0]; const nid = raw?.nid; if (typeof nid !== "number" || !Number.isFinite(nid) || !Number.isInteger(nid)) { return { ok: false, error: "invalid_args", message: "nid must be an integer (use getHierarchy NIDs)." }; } const uuid = nidToUuid.get(nid); if (!uuid) { return { ok: false, error: "nid_not_found", message: "Unknown nid \u2014 scene may have changed; call getHierarchy again." }; } const cc = getCC(); const scene = cc.director.getScene(); if (!scene) { return { ok: false, error: "no_scene", message: "No active scene." }; } if (scene.uuid !== currentSceneUuid) { return { ok: false, error: "stale_session", message: "Scene changed since last getHierarchy; refresh NID map first." }; } const node = findNodeByUuid(scene, uuid); if (!node) { return { ok: false, error: "node_missing", message: "Node uuid no longer in scene; call getHierarchy again." }; } return { ok: true, uuid: node.uuid }; }, /** Validates trimmed node `name` (1–256 chars, no ASCII control); Main sets path `name` via `set-property`. */ prepareSetNodeName(...args) { const raw = args[0]; const nid = raw?.nid; const rawName = raw?.name; if (typeof nid !== "number" || !Number.isFinite(nid) || !Number.isInteger(nid)) { return { ok: false, error: "invalid_args", message: "nid must be an integer (use getHierarchy NIDs)." }; } if (typeof rawName !== "string") { return { ok: false, error: "invalid_args", message: "name must be a string." }; } const name = rawName.trim(); if (name.length === 0) { return { ok: false, error: "invalid_args", message: "name must be non-empty after trim." }; } if (name.length > 256) { return { ok: false, error: "invalid_args", message: "name exceeds 256 characters." }; } if (/[\u0000-\u001F\u007F]/.test(name)) { return { ok: false, error: "invalid_args", message: "name must not contain control characters." }; } const uuid = nidToUuid.get(nid); if (!uuid) { return { ok: false, error: "nid_not_found", message: "Unknown nid \u2014 scene may have changed; call getHierarchy again." }; } const cc = getCC(); const scene = cc.director.getScene(); if (!scene) { return { ok: false, error: "no_scene", message: "No active scene." }; } if (scene.uuid !== currentSceneUuid) { return { ok: false, error: "stale_session", message: "Scene changed since last getHierarchy; refresh NID map first." }; } const node = findNodeByUuid(scene, uuid); if (!node) { return { ok: false, error: "node_missing", message: "Node uuid no longer in scene; call getHierarchy again." }; } return { ok: true, uuid: node.uuid, name }; }, /** Validates parent `nid` + trimmed `name`; Main calls `scene` `create-node`. */ prepareCreateNode(...args) { const raw = args[0]; const parentNid = raw?.parentNid; const rawName = raw?.name; if (typeof parentNid !== "number" || !Number.isFinite(parentNid) || !Number.isInteger(parentNid)) { return { ok: false, error: "invalid_args", message: "parentNid must be an integer (use getHierarchy NIDs)." }; } if (typeof rawName !== "string") { return { ok: false, error: "invalid_args", message: "name must be a string." }; } const name = rawName.trim(); if (name.length === 0) { return { ok: false, error: "invalid_args", message: "name must be non-empty after trim." }; } if (name.length > 256) { return { ok: false, error: "invalid_args", message: "name exceeds 256 characters." }; } if (/[\u0000-\u001F\u007F]/.test(name)) { return { ok: false, error: "invalid_args", message: "name must not contain control characters." }; } const parentUuid = nidToUuid.get(parentNid); if (!parentUuid) { return { ok: false, error: "nid_not_found", message: "Unknown parentNid \u2014 call getHierarchy again." }; } const cc = getCC(); const scene = cc.director.getScene(); if (!scene) { return { ok: false, error: "no_scene", message: "No active scene." }; } if (scene.uuid !== currentSceneUuid) { return { ok: false, error: "stale_session", message: "Scene changed since last getHierarchy; refresh NID map first." }; } const parentNode = findNodeByUuid(scene, parentUuid); if (!parentNode) { return { ok: false, error: "node_missing", message: "Parent node no longer in scene; call getHierarchy again." }; } return { ok: true, parentUuid: parentNode.uuid, name }; }, /** Parent `nid` for prefab instantiation (`create-node` + `assetUuid` in Main). */ prepareInstantiatePrefab(...args) { const raw = args[0]; const parentNid = raw?.parentNid; if (typeof parentNid !== "number" || !Number.isFinite(parentNid) || !Number.isInteger(parentNid)) { return { ok: false, error: "invalid_args", message: "parentNid must be an integer (use getHierarchy NIDs)." }; } const parentUuid = nidToUuid.get(parentNid); if (!parentUuid) { return { ok: false, error: "nid_not_found", message: "Unknown parentNid \u2014 call getHierarchy again." }; } const cc = getCC(); const scene = cc.director.getScene(); if (!scene) { return { ok: false, error: "no_scene", message: "No active scene." }; } if (scene.uuid !== currentSceneUuid) { return { ok: false, error: "stale_session", message: "Scene changed since last getHierarchy; refresh NID map first." }; } const parentNode = findNodeByUuid(scene, parentUuid); if (!parentNode) { return { ok: false, error: "node_missing", message: "Parent node no longer in scene; call getHierarchy again." }; } return { ok: true, parentUuid: parentNode.uuid }; }, /** Validates delete (scene root forbidden; leaf-only unless `cascade`). */ prepareDeleteNode(...args) { const raw = args[0]; if (raw?.confirm !== true) { return { ok: false, error: "invalid_args", message: "confirm must be true (explicit opt-in)." }; } const nid = raw.nid; const cascade = raw.cascade === true; if (typeof nid !== "number" || !Number.isFinite(nid) || !Number.isInteger(nid)) { return { ok: false, error: "invalid_args", message: "nid must be an integer (use getHierarchy NIDs)." }; } const uuid = nidToUuid.get(nid); if (!uuid) { return { ok: false, error: "nid_not_found", message: "Unknown nid \u2014 call getHierarchy again." }; } const cc = getCC(); const scene = cc.director.getScene(); if (!scene) { return { ok: false, error: "no_scene", message: "No active scene." }; } if (scene.uuid !== currentSceneUuid) { return { ok: false, error: "stale_session", message: "Scene changed since last getHierarchy; refresh NID map first." }; } if (uuid === scene.uuid) { return { ok: false, error: "invalid_target", message: "Cannot delete the scene root node." }; } const node = findNodeByUuid(scene, uuid); if (!node) { return { ok: false, error: "node_missing", message: "Node no longer in scene; call getHierarchy again." }; } if (node.children.length > 0 && !cascade) { return { ok: false, error: "non_leaf", message: "Node has children: pass cascade: true to delete the whole subtree, or delete children first." }; } return { ok: true, uuid: node.uuid }; }, /** Validates reparent (`set-parent`): nids in map, no scene-root move, no cycles. */ prepareReparentNode(...args) { const raw = args[0]; const childNid = raw?.childNid; const parentNid = raw?.parentNid; const keepWorldTransform = raw?.keepWorldTransform !== false; if (typeof childNid !== "number" || !Number.isFinite(childNid) || !Number.isInteger(childNid)) { return { ok: false, error: "invalid_args", message: "childNid must be an integer (use getHierarchy NIDs)." }; } if (typeof parentNid !== "number" || !Number.isFinite(parentNid) || !Number.isInteger(parentNid)) { return { ok: false, error: "invalid_args", message: "parentNid must be an integer (use getHierarchy NIDs)." }; } if (childNid === parentNid) { return { ok: false, error: "invalid_args", message: "childNid and parentNid must differ." }; } const childUuid = nidToUuid.get(childNid); const newParentUuid = nidToUuid.get(parentNid); if (!childUuid) { return { ok: false, error: "nid_not_found", message: "Unknown childNid \u2014 call getHierarchy again." }; } if (!newParentUuid) { return { ok: false, error: "nid_not_found", message: "Unknown parentNid \u2014 call getHierarchy again." }; } const cc = getCC(); const scene = cc.director.getScene(); if (!scene) { return { ok: false, error: "no_scene", message: "No active scene." }; } if (scene.uuid !== currentSceneUuid) { return { ok: false, error: "stale_session", message: "Scene changed since last getHierarchy; refresh NID map first." }; } if (childUuid === scene.uuid) { return { ok: false, error: "invalid_target", message: "Cannot reparent the scene root node." }; } const childNode = findNodeByUuid(scene, childUuid); const newParentNode = findNodeByUuid(scene, newParentUuid); if (!childNode || !newParentNode) { return { ok: false, error: "node_missing", message: "Node no longer in scene; call getHierarchy again." }; } if (isNodeUnderAncestorUuid(newParentNode, childUuid)) { return { ok: false, error: "invalid_target", message: "Invalid reparent: new parent is the child itself or inside the child subtree (cycle)." }; } const par = childNode.parent ?? null; const previousParentUuid = par == null || par.uuid === scene.uuid ? scene.uuid : par.uuid; const noOp = previousParentUuid === newParentUuid; return { ok: true, childUuid: childNode.uuid, newParentUuid: newParentNode.uuid, previousParentUuid, keepWorldTransform, noOp }; }, /** After Main `create-node`, assign the next session `nid` for MCP tools. */ registerNidForNewNode(...args) { const uuid = args[0]; if (typeof uuid !== "string" || uuid.trim().length === 0) { return { ok: false, message: "registerNidForNewNode: invalid uuid" }; } const existing = uuidToNid.get(uuid); if (existing !== void 0) { return { ok: true, nid: existing }; } const nid = nextNid++; uuidToNid.set(uuid, nid); nidToUuid.set(nid, uuid); return { ok: true, nid }; }, /** Drop session mapping (e.g. after MCP undo removed the node). */ forgetNodeNid(...args) { const uuid = args[0]; if (typeof uuid !== "string") { return { ok: true }; } const nid = uuidToNid.get(uuid); if (nid !== void 0) { uuidToNid.delete(uuid); nidToUuid.delete(nid); } return { ok: true }; }, /** Remove session nids for a node and all descendants (call before `remove-node` when cascade). */ forgetNidsInSubtree(...args) { const rootUuid = args[0]; if (typeof rootUuid !== "string") { return { ok: true }; } const cc = getCC(); const scene = cc.director.getScene(); if (!scene) { return { ok: true }; } const root = findNodeByUuid(scene, rootUuid); if (!root) { return { ok: true }; } const forget = (n) => { const mapped = uuidToNid.get(n.uuid); if (mapped !== void 0) { uuidToNid.delete(n.uuid); nidToUuid.delete(mapped); } for (const c of n.children) { forget(c); } }; forget(root); return { ok: true }; }, /** Snapshot currently mapped uuid↔nid pairs in a subtree (used for transactional delete). */ snapshotNidMappingsInSubtree(...args) { const rootUuid = args[0]; if (typeof rootUuid !== "string") { return { ok: true, entries: [] }; } const cc = getCC(); const scene = cc.director.getScene(); if (!scene) { return { ok: true, entries: [] }; } const root = findNodeByUuid(scene, rootUuid); if (!root) { return { ok: true, entries: [] }; } const entries = []; const walk = (n) => { const nid = uuidToNid.get(n.uuid); if (nid !== void 0) { entries.push({ uuid: n.uuid, nid }); } for (const c of n.children) { walk(c); } }; walk(root); return { ok: true, entries }; }, /** * Detach a subtree without destroying it so MCP local undo can restore the * exact objects and UUIDs. This avoids Creator's scene-wide global undo, * which can decode unrelated render assets while a frame is being drawn. */ detachNodeForMcpUndo(...args) { const uuid = args[0]; if (typeof uuid !== "string" || uuid.length === 0) { return { ok: false, message: "detachNodeForMcpUndo: uuid must be a non-empty string." }; } if (detachedNodesForUndo.has(uuid)) { return { ok: false, message: `detachNodeForMcpUndo: ${uuid} is already detached.` }; } const cc = getCC(); const scene = cc.director.getScene(); if (!scene) { return { ok: false, message: "No active scene." }; } const node = findNodeByUuid(scene, uuid); if (!node || node === scene) { return { ok: false, message: "Node is missing or is the scene root." }; } const parent = node.parent; if (!parent) { return { ok: false, message: "Node has no parent." }; } const siblingIndex = parent.children.findIndex((child) => child.uuid === uuid); try { if (typeof node.removeFromParent === "function") { node.removeFromParent(); } else { node.parent = null; } } catch (error) { const message = error instanceof Error ? error.message : String(error); return { ok: false, message: `detachNodeForMcpUndo failed: ${message}` }; } if (node.parent || findNodeByUuid(scene, uuid)) { return { ok: false, message: "detachNodeForMcpUndo did not remove the node from the scene tree." }; } while (detachedNodesForUndo.size >= DETACHED_NODE_STASH_MAX) { const oldest = detachedNodesForUndo.keys().next().value; if (!oldest) { break; } releaseDetachedNodeStash(oldest); } detachedNodesForUndo.set(uuid, { node, scene, parentUuid: parent.uuid, siblingIndex: Math.max(0, siblingIndex) }); return { ok: true, uuid, parentUuid: parent.uuid, siblingIndex: Math.max(0, siblingIndex) }; }, /** Restore a subtree previously detached by `detachNodeForMcpUndo`. */ restoreDetachedNodeForMcpUndo(...args) { const uuid = args[0]; if (typeof uuid !== "string" || uuid.length === 0) { return { ok: false, message: "restoreDetachedNodeForMcpUndo: uuid must be a non-empty string." }; } const stashed = detachedNodesForUndo.get(uuid); if (!stashed) { const current = getCC().director.getScene(); if (current && findNodeByUuid(current, uuid)) { return { ok: true, uuid, alreadyRestored: true }; } return { ok: false, terminal: true, message: `No detached node stash exists for ${uuid}; the local undo entry is no longer recoverable.` }; } const cc = getCC(); const scene = cc.director.getScene(); if (!scene) { return { ok: false, message: "No active scene." }; } if (scene !== stashed.scene) { releaseDetachedNodeStash(uuid); return { ok: false, terminal: true, message: "The active scene changed after the node was detached; the stale undo entry was discarded." }; } const existing = findNodeByUuid(scene, uuid); if (existing) { if (existing !== stashed.node) { releaseDetachedNodeStash(uuid); } else { detachedNodesForUndo.delete(uuid); } return { ok: true, uuid, alreadyRestored: true }; } const parent = findNodeByUuid(scene, stashed.parentUuid); if (!parent) { return { ok: false, message: "Original parent is no longer in the active scene." }; } try { if (typeof parent.addChild === "function") { parent.addChild(stashed.node); } else { stashed.node.parent = parent; } stashed.node.setSiblingIndex?.(stashed.siblingIndex); } catch (error) { const message = error instanceof Error ? error.message : String(error); return { ok: false, message: `restoreDetachedNodeForMcpUndo failed: ${message}` }; } if (findNodeByUuid(scene, uuid) !== stashed.node) { return { ok: false, message: "Restored node is not reachable from the active scene root." }; } detachedNodesForUndo.delete(uuid); return { ok: true, uuid }; }, /** * Dispose detached subtrees when the MCP undo stack is cleared on unload. * With no UUID list, release every stash (including entries evicted from the * bounded Main undo stack). A node that another editor undo already restored * into a live tree is never destroyed. */ discardDetachedNodesForMcpUndo(...args) { const requested = Array.isArray(args[0]) ? args[0] : Array.from(detachedNodesForUndo.keys()); let discarded = 0; let liveNodesPreserved = 0; for (const value of requested) { if (typeof value !== "string") { continue; } const released = releaseDetachedNodeStash(value); if (released.released) { discarded += 1; } if (released.liveNodePreserved) { liveNodesPreserved += 1; } } return { ok: true, discarded, liveNodesPreserved }; }, /** * Remove nodes created by one UI task in creation order. Removing an outer * root first also removes its descendants; later UUIDs are therefore skipped * without issuing noisy component-level undo operations. */ removeCreatedNodesForMcpUndo(...args) { const requested = args[0]; const expectedSceneUuid = args[1]; if (!Array.isArray(requested) || requested.some((value) => typeof value !== "string")) { return { ok: false, message: "removeCreatedNodesForMcpUndo requires an array of UUID strings.", removed: [], remaining: [] }; } const cc = getCC(); const scene = cc.director.getScene(); if (!scene) { return { ok: false, message: "No active scene.", removed: [], remaining: [...requested] }; } if (typeof expectedSceneUuid === "string" && expectedSceneUuid.length > 0 && scene.uuid !== expectedSceneUuid) { return { ok: false, terminal: true, message: "The active scene changed after the UI transaction was created.", removed: [], remaining: [...requested] }; } const removed = []; const forget = (node) => { const nid = uuidToNid.get(node.uuid); if (nid !== void 0) { uuidToNid.delete(node.uuid); nidToUuid.delete(nid); } for (const child of node.children) { forget(child); } }; let failure = null; for (const uuid of requested) { try { const node = findNodeByUuid(scene, uuid); if (!node || node === scene) { continue; } forget(node); if (typeof node.removeFromParent === "function") { node.removeFromParent(); } else { node.parent = null; } node.destroy?.(); removed.push(uuid); } catch (error) { failure = error instanceof Error ? error.message : String(error); break; } } const remaining = requested.filter((uuid) => findNodeByUuid(scene, uuid) !== null); if (failure || remaining.length > 0) { return { ok: false, message: `removeCreatedNodesForMcpUndo was incomplete${failure ? `: ${failure}` : ""} (remaining=${remaining.length}).`, removed, remaining }; } return { ok: true, removed, remaining, sceneUuid: scene.uuid }; }, /** Drop mappings by a pre-snapshotted list of uuids (safe after subtree was removed). */ forgetNidMappingsByUuids(...args) { const list = args[0]; if (!Array.isArray(list)) { return { ok: true }; } for (const v of list) { if (typeof v !== "string") { continue; } const nid = uuidToNid.get(v); if (nid !== void 0) { uuidToNid.delete(v); nidToUuid.delete(nid); } } return { ok: true }; }, /** Restore `uuid` ↔ `nid` after editor undo brought a deleted node back (MCP `delete_node` undo). */ restoreNidMapping(...args) { const uuid = args[0]; const nid = args[1]; if (typeof uuid !== "string" || typeof nid !== "number" || !Number.isInteger(nid)) { return { ok: false, message: "restoreNidMapping: invalid uuid or nid" }; } const prevUuid = nidToUuid.get(nid); if (prevUuid !== void 0 && prevUuid !== uuid) { uuidToNid.delete(prevUuid); } const prevNid = uuidToNid.get(uuid); if (prevNid !== void 0 && prevNid !== nid) { nidToUuid.delete(prevNid); } uuidToNid.set(uuid, nid); nidToUuid.set(nid, uuid); return { ok: true }; }, prepareAddComponent(...args) { const raw = args[0]; const nid = raw?.nid; const componentTypeIn = raw?.componentType; if (typeof nid !== "number" || !Number.isInteger(nid)) { return { ok: false, error: "invalid_args", message: "nid must be an integer." }; } if (typeof componentTypeIn !== "string" || !componentTypeIn.trim()) { return { ok: false, error: "invalid_args", message: "componentType must be a non-empty string." }; } const uuid = nidToUuid.get(nid); if (!uuid) { return { ok: false, error: "nid_not_found", message: "Unknown nid \u2014 call getHierarchy again." }; } const componentType = resolveComponentTypeInput(componentTypeIn); return { ok: true, uuid, componentType }; }, prepareRemoveComponent(...args) { const raw = args[0]; const nid = raw?.nid; const componentIndex = raw?.componentIndex; if (typeof nid !== "number" || !Number.isInteger(nid)) { return { ok: false, error: "invalid_args", message: "nid must be an integer." }; } if (typeof componentIndex !== "number" || !Number.isInteger(componentIndex) || componentIndex < 0) { return { ok: false, error: "invalid_args", message: "componentIndex must be a non-negative integer." }; } const uuid = nidToUuid.get(nid); if (!uuid) { return { ok: false, error: "nid_not_found", message: "Unknown nid \u2014 call getHierarchy again." }; } const cc = getCC(); const scene = cc.director.getScene(); if (!scene) { return { ok: false, error: "no_scene", message: "No active scene." }; } const node = findNodeByUuid(scene, uuid); if (!node) { return { ok: false, error: "node_missing", message: "Node uuid no longer in scene; call getHierarchy again." }; } let comps = []; try { comps = node.getComponents(cc.Component); } catch { comps = []; } if (componentIndex >= comps.length) { return { ok: false, error: "invalid_args", message: `componentIndex ${componentIndex} out of range (node has ${comps.length} component(s)).` }; } const component = comps[componentIndex]; const componentType = shortComponentName(component, cc); const componentUuid = typeof component.uuid === "string" && component.uuid.trim().length > 0 ? component.uuid.trim() : void 0; return { ok: true, uuid, componentUuid, componentIndex, componentType }; }, prepareDuplicateNode(...args) { const raw = args[0]; const nid = raw?.nid; if (typeof nid !== "number" || !Number.isInteger(nid)) { return { ok: false, error: "invalid_args", message: "nid must be an integer." }; } const uuid = nidToUuid.get(nid); if (!uuid) { return { ok: false, error: "nid_not_found", message: "Unknown nid \u2014 call getHierarchy again." }; } const cc = getCC(); const scene = cc.director.getScene(); if (!scene) { return { ok: false, error: "no_scene", message: "No active scene." }; } const node = findNodeByUuid(scene, uuid); if (!node) { return { ok: false, error: "node_missing", message: "Node uuid no longer in scene; call getHierarchy again." }; } const parent = node.parent; if (!parent) { return { ok: false, error: "invalid_args", message: "Cannot duplicate scene root." }; } return { ok: true, uuid, parentUuid: parent.uuid }; }, /** * Live scene-graph child UUIDs (order matches editor hierarchy). Used by Main after `duplicate-node` * when the IPC return value is empty — `query-node` on the parent often omits children in the inode dump. */ listChildUuidsUnderParent(...args) { const raw0 = args[0]; if (typeof raw0 !== "string" || raw0.length === 0) { return { ok: false, message: "parentUuid must be a non-empty string." }; } const parentUuid = raw0; const cc = getCC(); const scene = cc.director.getScene(); if (!scene) { return { ok: false, message: "No active scene." }; } const parent = findNodeByUuid(scene, parentUuid); if (!parent) { return { ok: false, message: "Parent node not found in scene graph." }; } const uuids = []; for (const child of parent.children) { try { if (child && typeof child.uuid === "string" && child.uuid.length > 0) { uuids.push(child.uuid); } } catch { } } return { ok: true, uuids }; }, /** * Fallback duplication when editor IPC `scene.duplicate-node` is a no-op in some Creator builds. * Clones the node in the live scene graph via `cc.instantiate` and appends it under the same parent. */ duplicateNodeFallback(...args) { const raw0 = args[0]; if (typeof raw0 !== "string" || raw0.length === 0) { return { ok: false, message: "uuid must be a non-empty string." }; } const uuid = raw0; const cc = getCC(); const scene = cc.director.getScene(); if (!scene) { return { ok: false, message: "No active scene." }; } const node = findNodeByUuid(scene, uuid); if (!node) { return { ok: false, message: "Node uuid no longer in scene; call getHierarchy again." }; } const parent = node.parent; if (!parent) { return { ok: false, message: "Cannot duplicate scene root." }; } const inst = cc.instantiate; if (typeof inst !== "function") { return { ok: false, message: "cc.instantiate is not available in this runtime." }; } let clone = null; try { clone = inst(node); } catch (e) { const msg = e instanceof Error ? e.message : String(e); return { ok: false, message: `cc.instantiate failed: ${msg}` }; } if (!clone || typeof clone.uuid !== "string" || clone.uuid.length === 0) { return { ok: false, message: "instantiate returned unexpected value." }; } try { const oldIdx = parent.children.findIndex((n) => n.uuid === node.uuid); const addChild = parent.addChild; if (typeof addChild === "function") { addChild.call(parent, clone); } else { clone.parent = parent; } const setSiblingIndex = clone.setSiblingIndex; if (typeof setSiblingIndex === "function" && oldIdx >= 0) { setSiblingIndex.call(clone, oldIdx + 1); } } catch { } return { ok: true, uuid: clone.uuid }; }, /** * Fallback remove path for Main `undo add_component` when scene IPC `remove-component` by index * does not mutate node components in some Creator builds. */ removeLastComponentByType(...args) { const rawUuid = args[0]; const rawType = args[1]; if (typeof rawUuid !== "string" || rawUuid.length === 0) { return { ok: false, message: "uuid must be a non-empty string." }; } if (typeof rawType !== "string" || rawType.length === 0) { return { ok: false, message: "componentType must be a non-empty string." }; } const normalized = rawType.trim().replace(/^cc\./, "").toLowerCase(); const cc = getCC(); const scene = cc.director.getScene(); if (!scene) { return { ok: false, message: "No active scene." }; } const node = findNodeByUuid(scene, rawUuid); if (!node) { return { ok: false, message: "Node uuid no longer in scene; call getHierarchy again." }; } let comps = []; try { comps = node.getComponents(cc.Component); } catch { comps = []; } const beforeCount = comps.length; const candidates = comps.filter((comp) => shortComponentName(comp, cc).toLowerCase() === normalized); if (candidates.length === 0) { return { ok: true, removed: false, beforeCount, afterCount: beforeCount }; } const target = candidates[candidates.length - 1]; try { if (typeof node.removeComponent === "function") { node.removeComponent(target); } else if (typeof target.destroy === "function") { target.destroy(); } else { return { ok: false, message: "Neither node.removeComponent nor component.destroy is available." }; } } catch (e) { const message = e instanceof Error ? e.message : String(e); return { ok: false, message: `removeLastComponentByType failed: ${message}` }; } let after = beforeCount; try { after = node.getComponents(cc.Component).length; } catch { } return { ok: true, removed: after < beforeCount, beforeCount, afterCount: after }; }, /** * Fallback add path for Main `undo remove_component` when scene IPC `create-component` * does not mutate node components in some Creator builds. */ addLastComponentByType(...args) { const rawUuid = args[0]; const rawType = args[1]; if (typeof rawUuid !== "string" || rawUuid.length === 0) { return { ok: false, message: "uuid must be a non-empty string." }; } if (typeof rawType !== "string" || rawType.length === 0) { return { ok: false, message: "componentType must be a non-empty string." }; } const cc = getCC(); const scene = cc.director.getScene(); if (!scene) { return { ok: false, message: "No active scene." }; } const node = findNodeByUuid(scene, rawUuid); if (!node) { return { ok: false, message: "Node uuid no longer in scene; call getHierarchy again." }; } const ctor = resolveComponentCtor(rawType); if (!ctor) { return { ok: false, message: `Unable to resolve component constructor: ${rawType}` }; } let before = []; try { before = node.getComponents(cc.Component); } catch { before = []; } const beforeCount = before.length; try { const addComponent = node.addComponent; if (typeof addComponent !== "function") { return { ok: false, message: "node.addComponent is not available in this runtime." }; } addComponent.call(node, ctor); } catch (e) { const msg = e instanceof Error ? e.message : String(e); return { ok: false, message: `addLastComponentByType failed: ${msg}` }; } let after = []; try { after = node.getComponents(cc.Component); } catch { after = before; } const afterCount = after.length; return { ok: true, added: afterCount > beforeCount, beforeCount, afterCount }; }, /** * Returns live component totals for a node and a specific component short-name type. * Used by Main to verify undo effects when `query-node` dumps are stale/noisy. */ getComponentStatsByType(...args) { const rawUuid = args[0]; const rawType = args[1]; if (typeof rawUuid !== "string" || rawUuid.length === 0) { return { ok: false, message: "uuid must be a non-empty string." }; } if (typeof rawType !== "string" || rawType.length === 0) { return { ok: false, message: "componentType must be a non-empty string." }; } const normalized = rawType.trim().replace(/^cc\./, "").toLowerCase(); const cc = getCC(); const scene = cc.director.getScene(); if (!scene) { return { ok: false, message: "No active scene." }; } const node = findNodeByUuid(scene, rawUuid); if (!node) { return { ok: false, message: "Node uuid no longer in scene; call getHierarchy again." }; } let comps = []; try { comps = node.getComponents(cc.Component); } catch { comps = []; } const totalCount = comps.length; const typeCount = comps.filter((comp) => shortComponentName(comp, cc).toLowerCase() === normalized).length; return { ok: true, totalCount, typeCount }; }, /** Exact live truth for component UUID removal/undo verification. */ getComponentUuidState(...args) { const rawNodeUuid = args[0]; const rawComponentUuid = args[1]; if (typeof rawNodeUuid !== "string" || rawNodeUuid.length === 0) { return { ok: false, message: "nodeUuid must be a non-empty string." }; } if (typeof rawComponentUuid !== "string" || rawComponentUuid.length === 0) { return { ok: false, message: "componentUuid must be a non-empty string." }; } const cc = getCC(); const scene = cc.director.getScene(); if (!scene) { return { ok: false, message: "No active scene." }; } const node = findNodeByUuid(scene, rawNodeUuid); if (!node) { return { ok: false, message: "Node uuid no longer in scene; call getHierarchy again." }; } let components = []; try { components = node.getComponents(cc.Component); } catch (error) { return { ok: false, message: error instanceof Error ? error.message : String(error) }; } const componentIndex = components.findIndex((component) => component.uuid === rawComponentUuid); if (componentIndex < 0) { return { ok: true, present: false }; } return { ok: true, present: true, componentIndex, componentType: shortComponentName(components[componentIndex], cc) }; }, /** * Fallback reorder path when editor IPC `scene.move-node` is unavailable. */ setSiblingIndexFallback(...args) { const rawUuid = args[0]; const rawIndex = args[1]; if (typeof rawUuid !== "string" || rawUuid.length === 0) { return { ok: false, message: "uuid must be a non-empty string." }; } if (typeof rawIndex !== "number" || !Number.isInteger(rawIndex) || rawIndex < 0) { return { ok: false, message: "index must be a non-negative integer." }; } const cc = getCC(); const scene = cc.director.getScene(); if (!scene) { return { ok: false, message: "No active scene." }; } const node = findNodeByUuid(scene, rawUuid); if (!node || !node.parent) { return { ok: false, message: "Node or parent not found in scene graph." }; } const siblings = node.parent.children; const previousIndex = siblings.findIndex((n) => n.uuid === node.uuid); if (previousIndex < 0) { return { ok: false, message: "Node is not attached to parent children list." }; } const maxIndex = Math.max(0, siblings.length - 1); const index = Math.max(0, Math.min(maxIndex, rawIndex)); if (typeof node.setSiblingIndex !== "function") { return { ok: false, message: "node.setSiblingIndex is not available in this runtime." }; } try { node.setSiblingIndex(index); } catch (e) { const message = e instanceof Error ? e.message : String(e); return { ok: false, message: `setSiblingIndexFallback failed: ${message}` }; } return { ok: true, appliedIndex: index, previousIndex }; }, setComponentScalarPropertyDirect(...args) { const rawUuid = args[0]; const rawIndex = args[1]; const rawProperty = args[2]; const rawValue = args[3]; if (typeof rawUuid !== "string" || rawUuid.length === 0) { return { ok: false, message: "uuid must be a non-empty string." }; } if (typeof rawIndex !== "number" || !Number.isInteger(rawIndex) || rawIndex < 0) { return { ok: false, message: "componentIndex must be a non-negative integer." }; } if (typeof rawProperty !== "string" || rawProperty.trim().length === 0) { return { ok: false, message: "property must be a non-empty string." }; } const requestedValue = runtimeDirectSnapshot(rawValue); if (requestedValue === void 0) { return { ok: false, message: "runtime-direct fallback supports scalars, Color, Vec2, and Size values only." }; } const cc = getCC(); const scene = cc.director.getScene(); if (!scene) { return { ok: false, message: "No active scene." }; } const node = findNodeByUuid(scene, rawUuid); if (!node) { return { ok: false, message: "Node uuid no longer in scene; call getHierarchy again." }; } let comps = []; try { comps = node.getComponents(cc.Component); } catch { comps = []; } if (rawIndex >= comps.length) { return { ok: false, message: `componentIndex ${rawIndex} is out of range.` }; } const component = comps[rawIndex]; const prop = rawProperty.trim(); const field = [prop, `_${prop}`].find((candidate) => candidate in component); if (!field) { return { ok: false, message: `component property is not available at runtime: ${prop}` }; } const beforeValue = runtimeDirectSnapshot(component[field]); if (beforeValue === void 0) { return { ok: false, message: `component property is not supported by runtime-direct fallback: ${prop}` }; } try { component[field] = materializeRuntimeDirectValue(requestedValue); } catch (e) { const message = e instanceof Error ? e.message : String(e); return { ok: false, message: `runtime-direct property assignment failed: ${message}` }; } const afterValue = runtimeDirectSnapshot(component[field]); if (afterValue === void 0) { return { ok: false, message: `component property became unsupported after assignment: ${prop}` }; } return { ok: true, beforeValue, afterValue, changed: JSON.stringify(beforeValue) !== JSON.stringify(afterValue) }; }, /** * Read asset-ref UUID from live component instance for known asset properties. * Used by Main to verify typed direct asset writes and their local undo. */ getComponentAssetRefUuid(...args) { const rawUuid = args[0]; const rawComponentType = args[1]; const rawProperty = args[2]; if (typeof rawUuid !== "string" || rawUuid.length === 0) { return { ok: false, message: "uuid must be a non-empty string." }; } if (typeof rawComponentType !== "string" || rawComponentType.length === 0) { return { ok: false, message: "componentType must be a non-empty string." }; } if (typeof rawProperty !== "string" || rawProperty.length === 0) { return { ok: false, message: "property must be a non-empty string." }; } const cc = getCC(); const scene = cc.director.getScene(); if (!scene) { return { ok: false, message: "No active scene." }; } const node = findNodeByUuid(scene, rawUuid); if (!node) { return { ok: false, message: "Node uuid no longer in scene; call getHierarchy again." }; } let comps = []; try { comps = node.getComponents(cc.Component); } catch { comps = []; } const normType = rawComponentType.trim().replace(/^cc\./, "").toLowerCase(); const prop = rawProperty.trim(); const typed = comps.filter((c) => shortComponentName(c, cc).toLowerCase() === normType); if (typed.length === 0) { return { ok: false, message: `No component of type ${rawComponentType} on target node.` }; } const target = typed[typed.length - 1]; const candidates = [prop, `_${prop}`]; for (const k of candidates) { if (k in target) { return readAssetReferenceUuid(target[k], `${rawComponentType}.${k}`); } } return { ok: false, message: `Asset property ${rawComponentType}.${prop} is not available at runtime.` }; }, async setComponentAssetRefDirect(...args) { const rawUuid = args[0]; const rawComponentType = args[1]; const rawProperty = args[2]; const rawAssetUuid = args[3]; if (typeof rawUuid !== "string" || rawUuid.length === 0) { return { ok: false, message: "uuid must be a non-empty string." }; } if (typeof rawComponentType !== "string" || rawComponentType.length === 0) { return { ok: false, message: "componentType must be a non-empty string." }; } if (typeof rawProperty !== "string" || rawProperty.length === 0) { return { ok: false, message: "property must be a non-empty string." }; } if (typeof rawAssetUuid !== "string" || rawAssetUuid.length === 0) { return { ok: false, message: "assetUuid must be a non-empty string." }; } const cc = getCC(); const scene = cc.director.getScene(); if (!scene) { return { ok: false, message: "No active scene." }; } const node = findNodeByUuid(scene, rawUuid); if (!node) { return { ok: false, message: "Node uuid no longer in scene; call getHierarchy again." }; } let comps = []; try { comps = node.getComponents(cc.Component); } catch { comps = []; } const normType = rawComponentType.trim().replace(/^cc\./, "").toLowerCase(); const typed = comps.filter((c) => shortComponentName(c, cc).toLowerCase() === normType); if (typed.length === 0) { return { ok: false, message: `No component of type ${rawComponentType} on target node.` }; } const comp = typed[typed.length - 1]; const prop = rawProperty.trim(); const propCandidates = [prop, `_${prop}`]; const field = propCandidates.find((candidate) => candidate in comp); if (!field) { return { ok: false, message: `Asset property ${rawComponentType}.${prop} is not available at runtime.` }; } const beforeValue = comp[field]; const before = readAssetReferenceUuid(beforeValue, `${rawComponentType}.${field}`); if (!before.ok) { return before; } const beforeUuid = before.uuid; const am = getCcAssetManager(); let asset; try { asset = await loadCcAssetByUuid(am, rawAssetUuid); } catch (error) { const message = error instanceof Error ? error.message : String(error); return { ok: false, message: `Unable to load asset UUID ${rawAssetUuid}: ${message}` }; } const kindError = validateAssetKind(asset, rawComponentType, prop, cc); if (kindError) { return { ok: false, message: kindError }; } try { comp[field] = asset; } catch (e) { const message = e instanceof Error ? e.message : String(e); return { ok: false, message: `direct asset assignment failed: ${message}` }; } const after = readAssetReferenceUuid(comp[field], `${rawComponentType}.${field}`); if (!after.ok) { try { comp[field] = beforeValue; } catch { return { ok: false, message: `${after.message} Rollback to the prior asset reference also failed.` }; } return { ok: false, message: `${after.message} The prior asset reference was restored.` }; } const afterUuid = after.uuid; if (afterUuid !== rawAssetUuid) { try { comp[field] = beforeValue; } catch { return { ok: false, message: `Runtime asset assignment did not stick (expected ${rawAssetUuid}, observed ${afterUuid ?? "null"}) and rollback to the prior asset reference failed.` }; } return { ok: false, message: `Runtime asset assignment did not stick: expected ${rawAssetUuid}, observed ${afterUuid ?? "null"}. The prior asset reference was restored.` }; } return { ok: true, beforeUuid, afterUuid, changed: beforeUuid !== afterUuid }; }, setComponentAssetRefFromScene(...args) { const rawUuid = args[0]; const rawComponentType = args[1]; const rawProperty = args[2]; const rawAssetUuid = args[3]; if (typeof rawUuid !== "string" || rawUuid.length === 0) { return { ok: false, message: "uuid must be a non-empty string." }; } if (typeof rawComponentType !== "string" || rawComponentType.length === 0) { return { ok: false, message: "componentType must be a non-empty string." }; } if (typeof rawProperty !== "string" || rawProperty.length === 0) { return { ok: false, message: "property must be a non-empty string." }; } if (typeof rawAssetUuid !== "string" || rawAssetUuid.length === 0) { return { ok: false, message: "assetUuid must be a non-empty string." }; } const cc = getCC(); const scene = cc.director.getScene(); if (!scene) { return { ok: false, message: "No active scene." }; } const normType = rawComponentType.trim().replace(/^cc\./, "").toLowerCase(); const prop = rawProperty.trim(); const fieldCandidates = [prop, `_${prop}`]; const targetNode = findNodeByUuid(scene, rawUuid); if (!targetNode) { return { ok: false, message: "Node uuid no longer in scene; call getHierarchy again." }; } let targetComps = []; try { targetComps = targetNode.getComponents(cc.Component); } catch { targetComps = []; } const targetTyped = targetComps.filter((c) => shortComponentName(c, cc).toLowerCase() === normType); if (targetTyped.length === 0) { return { ok: false, message: `No component of type ${rawComponentType} on target node.` }; } const targetComp = targetTyped[targetTyped.length - 1]; let sourceAssetRef = null; const walk = (node) => { let comps = []; try { comps = node.getComponents(cc.Component); } catch { comps = []; } for (const comp of comps) { if (shortComponentName(comp, cc).toLowerCase() !== normType) { continue; } const c = comp; for (const k of fieldCandidates) { const v = c[k]; if (v && typeof v === "object" && !Array.isArray(v)) { const u = v.uuid; if (typeof u === "string" && u === rawAssetUuid) { sourceAssetRef = v; return true; } } } } for (const child of node.children) { if (walk(child)) { return true; } } return false; }; walk(scene); if (!sourceAssetRef) { return { ok: false, message: `Asset UUID ${rawAssetUuid} is not currently loaded on any ${rawComponentType}.${prop} in scene.` }; } const targetField = fieldCandidates.find((candidate) => candidate in targetComp); if (!targetField) { return { ok: false, message: `Asset property ${rawComponentType}.${prop} is not available at runtime.` }; } const beforeValue = targetComp[targetField]; const before = readAssetReferenceUuid( beforeValue, `${rawComponentType}.${targetField}` ); if (!before.ok) { return before; } const beforeUuid = before.uuid; try { targetComp[targetField] = sourceAssetRef; } catch (e) { const message = e instanceof Error ? e.message : String(e); return { ok: false, message: `setComponentAssetRefFromScene failed: ${message}` }; } const after = readAssetReferenceUuid( targetComp[targetField], `${rawComponentType}.${targetField}` ); if (!after.ok) { try { targetComp[targetField] = beforeValue; } catch { return { ok: false, message: `${after.message} Rollback to the prior asset reference also failed.` }; } return { ok: false, message: `${after.message} The prior asset reference was restored.` }; } const afterUuid = after.uuid; if (afterUuid !== rawAssetUuid) { try { targetComp[targetField] = beforeValue; } catch { return { ok: false, message: `Scene asset assignment did not stick (expected ${rawAssetUuid}, observed ${afterUuid ?? "null"}) and rollback to the prior asset reference failed.` }; } return { ok: false, message: `Scene asset assignment did not stick: expected ${rawAssetUuid}, observed ${afterUuid ?? "null"}. The prior asset reference was restored.` }; } return { ok: true, beforeUuid, afterUuid, changed: beforeUuid !== afterUuid }; }, /** * Clear (set to null) an asset-ref field on the last matching component instance. * Used by Main to implement undo when the previous asset UUID is unknown/unobservable. */ clearComponentAssetRef(...args) { const rawUuid = args[0]; const rawComponentType = args[1]; const rawProperty = args[2]; if (typeof rawUuid !== "string" || rawUuid.length === 0) { return { ok: false, message: "uuid must be a non-empty string." }; } if (typeof rawComponentType !== "string" || rawComponentType.length === 0) { return { ok: false, message: "componentType must be a non-empty string." }; } if (typeof rawProperty !== "string" || rawProperty.length === 0) { return { ok: false, message: "property must be a non-empty string." }; } const cc = getCC(); const scene = cc.director.getScene(); if (!scene) { return { ok: false, message: "No active scene." }; } const node = findNodeByUuid(scene, rawUuid); if (!node) { return { ok: false, message: "Node uuid no longer in scene; call getHierarchy again." }; } let comps = []; try { comps = node.getComponents(cc.Component); } catch { comps = []; } const normType = rawComponentType.trim().replace(/^cc\./, "").toLowerCase(); const typed = comps.filter((c) => shortComponentName(c, cc).toLowerCase() === normType); if (typed.length === 0) { return { ok: false, message: `No component of type ${rawComponentType} on target node.` }; } const comp = typed[typed.length - 1]; const prop = rawProperty.trim(); const candidates = [prop, `_${prop}`]; const field = candidates.find((candidate) => candidate in comp); if (!field) { return { ok: false, message: `Asset property ${rawComponentType}.${prop} is not available at runtime.` }; } const beforeValue = comp[field]; const before = readAssetReferenceUuid(beforeValue, `${rawComponentType}.${field}`); if (!before.ok) { return before; } const beforeUuid = before.uuid; try { comp[field] = null; } catch (e) { const message = e instanceof Error ? e.message : String(e); return { ok: false, message: `clearComponentAssetRef failed: ${message}` }; } const after = readAssetReferenceUuid(comp[field], `${rawComponentType}.${field}`); if (!after.ok) { try { comp[field] = beforeValue; } catch { return { ok: false, message: `${after.message} Rollback to the prior asset reference also failed.` }; } return { ok: false, message: `${after.message} The prior asset reference was restored.` }; } const afterUuid = after.uuid; if (afterUuid !== null) { try { comp[field] = beforeValue; } catch { return { ok: false, message: `Asset clear did not stick (observed ${afterUuid}) and rollback to the prior reference failed.` }; } return { ok: false, message: `Asset clear did not stick (observed ${afterUuid}); the prior reference was restored.` }; } return { ok: true, beforeUuid, afterUuid, changed: beforeUuid !== afterUuid }; }, getNodeActiveState(...args) { const rawUuid = args[0]; if (typeof rawUuid !== "string" || rawUuid.length === 0) { return { ok: false, message: "uuid must be a non-empty string." }; } const cc = getCC(); const scene = cc.director.getScene(); if (!scene) { return { ok: false, message: "No active scene." }; } const node = findNodeByUuid(scene, rawUuid); if (!node) { return { ok: false, message: "Node uuid no longer in scene; call getHierarchy again." }; } return { ok: true, active: !!node.active }; }, prepareSetNodeSiblingIndex(...args) { const raw = args[0]; const nid = raw?.nid; const indexIn = raw?.index; if (typeof nid !== "number" || !Number.isInteger(nid)) { return { ok: false, error: "invalid_args", message: "nid must be an integer." }; } if (typeof indexIn !== "number" || !Number.isInteger(indexIn) || indexIn < 0) { return { ok: false, error: "invalid_args", message: "index must be a non-negative integer." }; } const uuid = nidToUuid.get(nid); if (!uuid) { return { ok: false, error: "nid_not_found", message: "Unknown nid \u2014 call getHierarchy again." }; } const cc = getCC(); const scene = cc.director.getScene(); if (!scene) { return { ok: false, error: "no_scene", message: "No active scene." }; } const node = findNodeByUuid(scene, uuid); if (!node) { return { ok: false, error: "node_missing", message: "Node uuid no longer in scene; call getHierarchy again." }; } const parent = node.parent; if (!parent) { return { ok: false, error: "invalid_args", message: "Cannot reorder scene root." }; } const siblings = parent.children; const previousIndex = siblings.findIndex((n) => n.uuid === node.uuid); if (previousIndex < 0) { return { ok: false, error: "node_missing", message: "Node is not attached to parent children list." }; } const maxIndex = Math.max(0, siblings.length - 1); const index = Math.max(0, Math.min(maxIndex, indexIn)); return { ok: true, uuid, parentUuid: parent.uuid, index, previousIndex, noOp: previousIndex === index }; }, /** * Validates whitelist + coerces `value`. Main reads property dump via `query-node` and calls `set-property`. * `componentIndex` matches `inspectNode` / `getComponents` order. */ prepareSetComponentProperty(...args) { const raw = args[0]; const nid = raw?.nid; const componentIndex = raw?.componentIndex; const property = raw?.property; const valueIn = raw?.value; if (typeof nid !== "number" || !Number.isFinite(nid) || !Number.isInteger(nid)) { return { ok: false, error: "invalid_args", message: "nid must be an integer (use getHierarchy NIDs)." }; } if (typeof componentIndex !== "number" || !Number.isInteger(componentIndex) || componentIndex < 0) { return { ok: false, error: "invalid_args", message: "componentIndex must be a non-negative integer." }; } if (typeof property !== "string" || !property.trim()) { return { ok: false, error: "invalid_args", message: "property must be a non-empty string." }; } const prop = property.trim(); const uuid = nidToUuid.get(nid); if (!uuid) { return { ok: false, error: "nid_not_found", message: "Unknown nid \u2014 scene may have changed; call getHierarchy again." }; } const cc = getCC(); const scene = cc.director.getScene(); if (!scene) { return { ok: false, error: "no_scene", message: "No active scene." }; } if (scene.uuid !== currentSceneUuid) { return { ok: false, error: "stale_session", message: "Scene changed since last getHierarchy; refresh NID map first." }; } const node = findNodeByUuid(scene, uuid); if (!node) { return { ok: false, error: "node_missing", message: "Node uuid no longer in scene; call getHierarchy again." }; } let comps = []; try { comps = node.getComponents(cc.Component); } catch { comps = []; } if (componentIndex >= comps.length) { return { ok: false, error: "invalid_args", message: `componentIndex ${componentIndex} out of range (node has ${comps.length} component(s)).` }; } const shortName = shortComponentName(comps[componentIndex], cc); const schemasForType = COMPONENT_SCHEMAS[shortName] ?? {}; const schema = schemasForType[prop]; if (!schema) { const allowedType = Object.keys(schemasForType); return { ok: false, error: "not_whitelisted", message: `\u5C5E\u6027\u672A\u5728\u5B89\u5168\u767D\u540D\u5355\u4E2D\u914D\u7F6E\u3002\u7EC4\u4EF6 ${shortName} \u5F53\u524D\u652F\u6301\u7684\u5C5E\u6027\u6709\uFF1A${allowedType.length > 0 ? allowedType.join(", ") : "(none)"}` }; } let coerced; try { coerced = schema.coercer(valueIn); } catch (e) { const message = e instanceof Error ? e.message : String(e); return { ok: false, error: "invalid_args", message }; } const dumpProp = schema.dumpProp ?? prop; const path2 = `__comps__.${componentIndex}.${dumpProp}`; return { ok: true, uuid: node.uuid, path: path2, componentType: shortName, value: coerced }; } }; var cocosCjsExports = typeof exports === "object" && exports ? exports : null; if (cocosCjsExports) { cocosCjsExports.methods = methods; } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { methods });