fix(scripts): final review Critical+Important fixes (Widget alignment, full readLayerXml, Label _string, atlas selection, hard-coded paths, exit code, dead code removal)

This commit is contained in:
2026-09-02 09:11:51 +08:00
parent 7e0a8b79a4
commit cb3bd54175
19 changed files with 5076 additions and 186 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1 -36
View File
@@ -4,42 +4,7 @@
"requires": true,
"packages": {
"": {
"name": "YouleNexus",
"dependencies": {
"@xmldom/xmldom": "^0.9.12",
"iconv-lite": "^0.7.3"
}
},
"node_modules/@xmldom/xmldom": {
"version": "0.9.12",
"resolved": "https://registry.npmmirror.com/@xmldom/xmldom/-/xmldom-0.9.12.tgz",
"integrity": "sha512-5AXjrcMClTryPe9LgZrygpB1lj7s0S9E0+W+AHaVKAVyHanafK86iPSvG5xHVSp/jC+VH1UXu0TAEmY279xH7A==",
"license": "MIT",
"engines": {
"node": ">=14.6"
}
},
"node_modules/iconv-lite": {
"version": "0.7.3",
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.3.tgz",
"integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
"name": "YouleNexus"
}
}
}
@@ -3,9 +3,5 @@
"uuid": "15252ac4-d24e-4b2c-86b0-51caaf24f779",
"creator": {
"version": "3.8.8"
},
"dependencies": {
"@xmldom/xmldom": "^0.9.12",
"iconv-lite": "^0.7.3"
}
}
@@ -1,11 +0,0 @@
import { readFileSync } from 'node:fs';
import iconv from 'iconv-lite';
const buf = readFileSync('framework-tests/legacy-layer-migration/fixtures/Layer00002.xml');
// NOTE: 实际 Layer00002.xml 是 UTF-8 编码 (XML 头声明 + 字节验证),
// spec/brief 要求的 GB18030 在该文件上会乱码 (微信登录背景 不可读).
// 转换工具应支持两种编码, 优先按 XML 声明, 否则按 GB18030 fallback.
const text = iconv.decode(buf, 'utf8');
console.log('decoded length:', text.length);
console.log('contains 微信登录背景:', text.includes('微信登录背景'));
console.log('contains Login_Layer:', text.includes('Login_Layer'));
@@ -1,6 +1,6 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { computeLpos, computeContentSize } from '../../scripts/legacy-layer-to-cocos-prefab.mjs';
import { computeLpos, computeContentSize, computeWidgetAlignment } from '../../scripts/legacy-layer-to-cocos-prefab.mjs';
test('computeLpos 锚点转换(实测验证:渠道logo)', () => {
const spirit = { x: '734', y: '143', w: '59', h: '148' };
@@ -47,4 +47,49 @@ test('computeContentSize 默认 SizeCalcMode=0(字段缺失视作 0)', () =>
test('computeContentSize SizeCalcMode=1 → throw(不静默)', () => {
const spirit = { w: '100', h: '50', sizeCalcMode: '1' };
assert.throws(() => computeContentSize(spirit), /SizeCalcMode=1.*reverse gameabc\.min\.js/);
});
test('computeContentSize SizeCalcMode=2 uses ImgResID natural size and relative ratios', () => {
const spirit = {
id: '2', imgResID: '20', w: '1280', h: '720', sizeCalcMode: '2',
sizeRelaWidth: '10000', sizeRelaHeight: '10000',
};
const size = computeContentSize(spirit);
assert.equal(size.width, 1280);
assert.equal(size.height, 720);
});
test('computeWidgetAlignment derives full-screen, normal, and top-left alignment cases', () => {
const fullScreen = computeWidgetAlignment({
name: '微信登录背景', x: '0', y: '0', w: '1280', h: '720', lpos: { x: 0, y: 0 },
});
assert.equal(fullScreen._alignFlags, 45);
assert.equal(fullScreen._left, 0);
assert.equal(fullScreen._right, 0);
assert.equal(fullScreen._top, 0);
assert.equal(fullScreen._bottom, 0);
assert.equal(fullScreen._horizontalCenter, undefined);
assert.equal(fullScreen._verticalCenter, undefined);
assert.equal(fullScreen._originalWidth, 100);
assert.equal(fullScreen._originalHeight, 100);
const normal = computeWidgetAlignment({
name: '渠道logo', x: '734', y: '143', w: '59', h: '148', lpos: { x: 123.5, y: 0 },
});
assert.equal(normal._alignFlags, 16);
assert.equal(normal._horizontalCenter, 123.5);
assert.equal(normal._verticalCenter, 0);
assert.equal(normal._left, undefined);
assert.equal(normal._right, undefined);
assert.equal(normal._top, undefined);
assert.equal(normal._bottom, undefined);
const topLeft = computeWidgetAlignment({
name: '版本显示', x: '16', y: '9', w: '96', h: '31',
});
assert.equal(topLeft._alignFlags, 9);
assert.equal(topLeft._left, -576);
assert.equal(topLeft._top, 335.5);
assert.equal(topLeft._horizontalCenter, undefined);
assert.equal(topLeft._verticalCenter, undefined);
});
@@ -34,7 +34,7 @@
"rationale": "直接读精灵自身 Width/Height 字段作为 _contentSize; gameabc.min.js 中 SizeCalcMode=0 是最简路径 (背景图 Layer00001/00002 全屏 1280×720 即此路径)"
},
"2": {
"formula": "parent_size = lookup_Spirit(Spirit.SizeParentID)._contentSize; _contentSize = { width: parent_size.width * Spirit.SizeRelaWidth / 10000, height: parent_size.height * Spirit.SizeRelaHeight / 10000 }; 当 WidthCalcMode=0 时回退到 Spirit.Width (parseFloat), 当 WidthCalcMode=1 时用 SizeRelaWidth 公式; HeightCalcMode 同理",
"formula": "image_natural_size = lookup gameabc_Image.json ImageFileList[imgResID === Spirit.ImgResID].w_all/h_all; parent_size = { width: image_natural_size.width, height: image_natural_size.height }; _contentSize = { width: parent_size.width * Spirit.SizeRelaWidth / 10000, height: parent_size.height * Spirit.SizeRelaHeight / 10000 }; 当 WidthCalcMode=0 时回退到 Spirit.Width (parseFloat), 当 WidthCalcMode=1 时用 SizeRelaWidth 公式; HeightCalcMode 同理",
"rationale": "Layer00001 '友乐背景' (SizeCalcMode=2, SizeParentID=21, SizeRelaWidth=10000) / Layer00002 '微信登录背景' (SizeCalcMode=2, SizeParentID=20, SizeRelaWidth=10000) 全量采样: 全部 WidthCalcMode=1, HeightCalcMode=1, SizeRelaWidth=10000, SizeRelaHeight=10000; 即按 100% 比例继承父 spirit 尺寸; 这种 pattern 用于 '充满父容器' 的背景 (1280×720 全屏背景), SizeParentID 指向另一个 1280×720 的 spirit"
}
},
@@ -1,7 +1,9 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { readLayerXml, emitPrefabJson } from '../../scripts/legacy-layer-to-cocos-prefab.mjs';
import { ROOT, HOST_PROJECT } from '../../scripts/lib/paths.mjs';
// PoC 阶段 3 端到端: Layer00002.xml Login_Layer (18 spirits, group 2, 包含 Spirit405 渠道logo +
// Spirit177 游客登录 anchor-test cases). 验证生成 prefab 与 fixtures/Login_Layer.prefab 在
@@ -16,8 +18,8 @@ import { readLayerXml, emitPrefabJson } from '../../scripts/legacy-layer-to-coco
// layer.spirits 只保留 id=405 (渠道logo) + id=177 (游客登录) 两个 anchor-test case, 完全避开
// ImgResID 427 的依赖. 这是 Task 11 fix round 1 的 spec intent: 验证 anchor 公式精度,
// 而不是验证 CLI 完整管线 (后者已由 poc-iconbutton.test.mjs / poc-protocol.test.mjs 覆盖).
const FIXTURE_XML = 'G:/Works/YouleGamesCocosCreator/cocoscreator_projects/framework-tests/legacy-layer-migration/fixtures/Layer00002.xml';
const REFERENCE_PREFAB = 'G:/Works/YouleGamesCocosCreator/cocoscreator_projects/YouleNexus/assets/framework/ui/prefabs/Login_Layer.prefab';
const FIXTURE_XML = join(ROOT, 'framework-tests', 'legacy-layer-migration', 'fixtures', 'Layer00002.xml');
const REFERENCE_PREFAB = join(HOST_PROJECT, 'assets', 'framework', 'ui', 'prefabs', 'Login_Layer.prefab');
function buildFilteredLayer() {
const layer = readLayerXml(FIXTURE_XML);
@@ -27,6 +29,43 @@ function buildFilteredLayer() {
return layer;
}
function nodeComponent(json, node, componentType) {
return json[node._components.map(ref => ref.__id__).find(index => json[index]?.__type__ === componentType)];
}
test('Login_Layer full-screen and normal node geometry matches reference strictly', () => {
const layer = readLayerXml(FIXTURE_XML);
layer.spirits = layer.spirits.filter(s => s.id === '2' || s.id === '405');
const generated = emitPrefabJson(layer);
const expected = JSON.parse(readFileSync(REFERENCE_PREFAB, 'utf8'));
const background = generated.find(o => o.__type__ === 'cc.Node' && o._name === '微信登录背景');
const backgroundExpected = expected.find(o => o.__type__ === 'cc.Node' && o._name === '微信登录背景');
assert.ok(background);
assert.ok(backgroundExpected);
assert.equal(background._lpos.x, backgroundExpected._lpos.x);
assert.equal(background._lpos.y, backgroundExpected._lpos.y);
assert.equal(nodeComponent(generated, background, 'cc.Widget')._alignFlags, 45);
assert.equal(nodeComponent(generated, background, 'cc.Widget')._originalWidth, 100);
assert.equal(nodeComponent(generated, background, 'cc.Widget')._originalHeight, 100);
assert.equal(nodeComponent(generated, background, 'cc.UITransform')._contentSize.width, 1600);
assert.equal(nodeComponent(generated, background, 'cc.UITransform')._contentSize.height, 720);
const normal = generated.find(o => o.__type__ === 'cc.Node' && o._name === 'Spirit405');
const normalExpected = expected.find(o => o.__type__ === 'cc.Node' && o._name === '渠道logo');
assert.ok(normal);
assert.ok(normalExpected);
assert.equal(normal._lpos.x, normalExpected._lpos.x);
assert.equal(normal._lpos.y, normalExpected._lpos.y);
assert.equal(nodeComponent(generated, normal, 'cc.Widget')._alignFlags, 16);
assert.equal(nodeComponent(generated, normal, 'cc.Widget')._horizontalCenter, 123.5);
assert.equal(nodeComponent(generated, normal, 'cc.Widget')._verticalCenter, 0);
assert.equal(nodeComponent(generated, normal, 'cc.Widget')._left, undefined);
assert.equal(nodeComponent(generated, normal, 'cc.Widget')._top, undefined);
assert.equal(nodeComponent(generated, normal, 'cc.UITransform')._contentSize.width, 59);
assert.equal(nodeComponent(generated, normal, 'cc.UITransform')._contentSize.height, 148);
});
test('PoC 阶段 3 - 渠道logo _lpos 误差 < 0.001 (bypass CLI to avoid sprite atlas dep)', () => {
const layer = buildFilteredLayer();
const json = emitPrefabJson(layer);
@@ -10,7 +10,7 @@ import { existsSync, readFileSync } from 'node:fs';
// Layer00010.xml 4 个 spirit 全属 group 37 (无 Layer 根直挂), 生成的 prefab 应有:
// - 1 root Widget (default _alignFlags=45, 全屏铺满)
// - 1 group-37 Widget (default _alignFlags=45)
// - 4 Spirit Widget (apply computeWidgetAlignment: _alignFlags=24=16|8 水平+垂直居中)
// - 4 Spirit Widget (apply computeWidgetAlignment: normal spirits use _alignFlags=16)
const CLI = 'node scripts/legacy-layer-to-cocos-prefab-cli.mjs';
const INPUT = '../projects/Game_Surface_3/save/Layer00010.xml';
const TARGET = 'YouleNexus/assets/framework/ui/prefabs/widgets/Layer10_Protocol.prefab';
@@ -32,7 +32,7 @@ test('Layer10_Protocol 生成 + 含 cc.Widget 节点 (computeWidgetAlignment 锚
`应有 ≥6 个 cc.Widget 节点 (1 root + 1 group + 4 spirit), 实得 ${widgetNodes.length}`
);
// 2) Spirit Widget 应 _alignFlags=24 (16|8 水平+垂直居中) — 由 computeWidgetAlignment 算出.
// 2) Spirit Widget 应使用 normal spirit 的 _alignFlags=16 — 由 computeWidgetAlignment 算出.
// 这是 PoC 阶段 2 的关键验证: 证明 computeWidgetAlignment 真实生效 (而非全部用默认 45).
// Spirit Widget 与 root/group Widget 的区别: Widget.node.__id__ 指向的 cc.Node
// _name 不为 'Protol_Layer' (root) 也不以 'group-' 开头 (group 父节点).
@@ -48,7 +48,7 @@ test('Layer10_Protocol 生成 + 含 cc.Widget 节点 (computeWidgetAlignment 锚
`应有 4 个 Spirit Widget, 实得 ${spiritWidgetCount}`
);
// 3) Spirit Widget _alignFlags 必须等于 24 (= 16 | 8, 水平居中 + 垂直居中).
// 3) Spirit Widget _alignFlags 必须等于 16 (normal spirit = horizontal center).
// 这条断言专门验证 computeWidgetAlignment 已被实际调用 — 未实现时所有 Widget
// 都用默认 _alignFlags=45, 此断言必 FAIL.
const spiritWidgets = widgetNodes.filter(w => {
@@ -60,9 +60,11 @@ test('Layer10_Protocol 生成 + 含 cc.Widget 节点 (computeWidgetAlignment 锚
&& !targetNode._name.startsWith('group-');
});
for (const w of spiritWidgets) {
const targetNode = json[w.node.__id__];
const expectedFlags = targetNode._name === '协议遮罩' ? 45 : targetNode._name === '协议背景' ? 9 : 16;
assert.equal(
w._alignFlags, 24,
`Spirit Widget 应 _alignFlags=24 (16|8), 实得 ${w._alignFlags}`
w._alignFlags, expectedFlags,
`Spirit Widget ${targetNode._name} 应 _alignFlags=${expectedFlags}, 实得 ${w._alignFlags}`
);
}
});
@@ -27,6 +27,30 @@ test('emitPrefabJson 含 PrefabInfo 块', () => {
assert.equal(prefabInfo.asset.__id__, 0);
});
test('emitPrefabJson uses XML Text for labels and targetName overrides the output name', () => {
const layer = {
layerId: '10', layerName: 'Protol_Layer', spirits: [{
id: '1', name: 'Spirit1', text: '登录', type: '1', fontSize: '18', fontColor: '16777215',
alignHorz: '0', alignVert: '1', fontBold: '0', wordWrap: '0', lineSpace: '0', rowSpace: '0',
x: '100', y: '100', w: '100', h: '30', group: '37', indexOfLayer: '1',
}],
};
const json = emitPrefabJson(layer, { targetName: 'Layer10_Protocol' });
assert.equal(json[0]._name, 'Layer10_Protocol');
const textNode = json.find(o => o.__type__ === 'cc.Node' && o._name === 'Spirit1');
assert.ok(textNode);
const label = json.filter(o => o.__type__ === 'cc.Label')[0];
assert.equal(label._string, '登录');
assert.equal(label._fontSize, 18);
assert.equal(label._color.r, 255);
assert.equal(label._color.g, 255);
assert.equal(label._color.b, 255);
assert.equal(label._horizontalAlign, 0);
assert.equal(label._verticalAlign, 1);
assert.equal(label._isBold, false);
assert.equal(label._enableWrapText, false);
});
test('emitPrefabJson 渠道logo _lpos 误差 < 0.001', () => {
// 注: brief 原版用 spirit name='Spirit405' 但断言搜 /渠道/; 实际 fixture 里 id=405 的 spirit
// 坐标 (734, 143, 59, 148) 严格对应 Login_Layer '渠道logo' 节点; 这里 name 用 '渠道logo'
@@ -1,12 +1,12 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { mapSpiritTypeToComponent } from '../../scripts/legacy-layer-to-cocos-prefab.mjs';
import { ROOT } from '../../scripts/lib/paths.mjs';
// 绝对路径 -- tests 从 cocoscreator_projects/ 运行, 相对路径会解析到仓库根,
// 漏掉 cocoscreator_projects/ 段导致 ENOENT (Task 2 fix round 1 Finding 3).
const SEMANTICS = JSON.parse(readFileSync(
'G:/Works/YouleGamesCocosCreator/cocoscreator_projects/framework-tests/legacy-layer-migration/fixtures/spirit-semantics.json',
join(ROOT, 'framework-tests', 'legacy-layer-migration', 'fixtures', 'spirit-semantics.json'),
'utf8'
));
@@ -1,9 +1,11 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { ROOT } from '../../scripts/lib/paths.mjs';
const SEMANTICS = JSON.parse(readFileSync(
'G:/Works/YouleGamesCocosCreator/cocoscreator_projects/framework-tests/legacy-layer-migration/fixtures/spirit-semantics.json',
join(ROOT, 'framework-tests', 'legacy-layer-migration', 'fixtures', 'spirit-semantics.json'),
'utf8'
));
@@ -1,6 +1,6 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { resolveSpriteFrameUuid } from '../../scripts/legacy-layer-to-cocos-prefab.mjs';
import { resolveSpriteFrameUuid, selectAtlasForLayerName } from '../../scripts/legacy-layer-to-cocos-prefab.mjs';
// 数据源权威: gameabc_Image.json id=20 -> bmp="00020.png" 位于 atlas-login/,
// 该 .meta 的 subMetas.f9941.uuid 为 e36acab8-e623-4bfb-af05-3c6fd2fddfb4@f9941
@@ -12,6 +12,12 @@ test('resolveSpriteFrameUuid("20") 返回 atlas-login/00020.png 的 SpriteFrame
assert.match(uuid, /@f9941$/, '必须含 SpriteFrame sub-asset 后缀 @f9941');
});
test('layer name prefix selects the expected atlas directory', () => {
assert.equal(selectAtlasForLayerName('Login_Layer'), 'atlas-login');
assert.equal(selectAtlasForLayerName('MainScene_PlayerInfo'), 'atlas-hall');
assert.equal(selectAtlasForLayerName('Other_Layer'), 'atlas-login');
});
test('未知 imgResId → throw(不静默兜底)', () => {
// 数据源权威唯一、下游不兜底: ImgResID 不在 gameabc_Image.json 时必须显式报错.
assert.throws(
@@ -1,10 +1,10 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { join } from 'node:path';
import { readLayerXml } from '../../scripts/legacy-layer-to-cocos-prefab.mjs';
import { ROOT } from '../../scripts/lib/paths.mjs';
// 绝对路径 -- tests 从 cocoscreator_projects/ 运行, 相对路径会解析到仓库根,
// 漏掉 cocoscreator_projects/ 段导致 ENOENT (Task 2 fix round 1 Finding 3).
const FIXTURE = 'G:/Works/YouleGamesCocosCreator/cocoscreator_projects/framework-tests/legacy-layer-migration/fixtures/Layer00002.xml';
const FIXTURE = join(ROOT, 'framework-tests', 'legacy-layer-migration', 'fixtures', 'Layer00002.xml');
test('readLayerXml 解码 UTF-8 并提取 Layer00002 字段', () => {
const layer = readLayerXml(FIXTURE);
@@ -41,3 +41,34 @@ test('BelongGroupID 提取(group=0 节点也存在)', () => {
const g0 = layer.spirits.filter(s => s.group === '0');
assert.ok(g0.length >= 0); // Login_Layer 没有 group=0,但提取逻辑应支持
});
test('readLayerXml 保留 SizeCalcMode、坐标、归属与 type-1 字体契约字段', () => {
const layer = readLayerXml(FIXTURE);
const background = layer.spirits.find(s => s.id === '2');
const text = layer.spirits.find(s => s.id === '285');
assert.ok(background);
assert.ok(text);
assert.equal(background.text, undefined);
assert.equal(background.sizeCalcMode, '2');
assert.equal(background.sizeParentID, '20');
assert.equal(background.widthCalcMode, '1');
assert.equal(background.heightCalcMode, '1');
assert.equal(background.sizeRelaWidth, '10000');
assert.equal(background.sizeRelaHeight, '10000');
assert.equal(background.coordSelfPos, '0');
assert.equal(background.coordRelaX, '0');
assert.equal(background.coordRelaY, '0');
assert.equal(background.coordParentID, '0');
assert.equal(background.belongLayerID, '2');
assert.equal(text.text, '版本显示');
assert.equal(text.fontSize, '18');
assert.equal(text.fontColor, '16777215');
assert.equal(text.alignHorz, '0');
assert.equal(text.alignVert, '0');
assert.equal(text.fontBold, '0');
assert.equal(text.wordWrap, '0');
assert.equal(text.lineSpace, '0');
assert.equal(text.rowSpace, undefined);
});
+1 -24
View File
@@ -8,8 +8,7 @@
"name": "youle-monorepo-tools",
"version": "0.1.0",
"dependencies": {
"@xmldom/xmldom": "^0.9.12",
"iconv-lite": "^0.7.3"
"@xmldom/xmldom": "^0.9.12"
},
"devDependencies": {
"@types/node": "^26.0.1",
@@ -518,22 +517,6 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/iconv-lite": {
"version": "0.7.3",
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.3.tgz",
"integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/pngjs": {
"version": "7.0.0",
"resolved": "https://registry.npmmirror.com/pngjs/-/pngjs-7.0.0.tgz",
@@ -543,12 +526,6 @@
"node": ">=14.19.0"
}
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/tsx": {
"version": "4.22.4",
"resolved": "https://registry.npmmirror.com/tsx/-/tsx-4.22.4.tgz",
+1 -2
View File
@@ -25,7 +25,6 @@
"ws": "^8.21.0"
},
"dependencies": {
"@xmldom/xmldom": "^0.9.12",
"iconv-lite": "^0.7.3"
"@xmldom/xmldom": "^0.9.12"
}
}
@@ -1,28 +1,47 @@
#!/usr/bin/env node
// CLI 入口: Layer XML → Cocos prefab JSON (+ 对比 diff, 如提供 YouleNexus 现存 prefab).
//
// 用法: node legacy-layer-to-cocos-prefab-cli.mjs <input.xml> [<youle-prefab>] <output-dir>
// 用法: node legacy-layer-to-cocos-prefab-cli.mjs <input.xml> [<youle-prefab>] <output-dir> [--target-name <name>]
//
// 设计要点 (Task 9 brief):
// - 只输出到隔离目录 (out/legacy-migration/), 不直接覆盖 YouleNexus 现有 prefab.
// - 必填参数: inputXml + outputDir; youlePrefab 可选, 提供且文件存在时生成 .diff.
// - diff 命令来自 Git Bash / POSIX 环境 (Windows: Git for Windows 自带 diff).
import { readLayerXml, emitPrefabJson } from './legacy-layer-to-cocos-prefab.mjs';
// 设计要点:
// - 只输出到隔离目录 (out/legacy-migration/), 不直接覆盖 YouleNexus 原 prefab.
// - targetName 只影响输出的 prefab/root 名称与 atlas 选择; 默认使用 XML layerName.
import { readLayerXml, emitPrefabJson, selectAtlasForLayerName } from './legacy-layer-to-cocos-prefab.mjs';
import { writeFileSync, mkdirSync, existsSync } from 'node:fs';
import { execSync } from 'node:child_process';
import { join } from 'node:path';
const [, , inputXml, youlePrefab, outputDir] = process.argv;
const args = process.argv.slice(2);
let inputXml = null;
let youlePrefab = null;
let outputDir = null;
let targetName = null;
for (let i = 0; i < args.length; i += 1) {
const arg = args[i];
if (arg === '--target-name') {
targetName = args[++i];
if (!targetName) throw new Error('--target-name requires a value');
continue;
}
if (!inputXml) inputXml = arg;
else if (!youlePrefab) youlePrefab = arg;
else if (!outputDir) outputDir = arg;
else throw new Error(`Unexpected argument: ${arg}`);
}
if (!inputXml || !outputDir) {
console.error('Usage: node legacy-layer-to-cocos-prefab-cli.mjs <input.xml> <youle-prefab-path?> <output-dir>');
console.error('Usage: node legacy-layer-to-cocos-prefab-cli.mjs <input.xml> <youle-prefab-path?> <output-dir> [--target-name <name>]');
process.exit(1);
}
const layer = readLayerXml(inputXml);
const prefabJson = emitPrefabJson(layer);
const outputName = targetName || layer.layerName;
const prefabJson = emitPrefabJson(layer, {
targetName: outputName,
atlasName: selectAtlasForLayerName(outputName),
});
if (!existsSync(outputDir)) mkdirSync(outputDir, { recursive: true });
const outputPath = join(outputDir, `${layer.layerName}.prefab`);
const outputPath = join(outputDir, `${outputName}.prefab`);
writeFileSync(outputPath, JSON.stringify(prefabJson, null, 2));
console.log(`Generated: ${outputPath}`);
@@ -32,4 +51,4 @@ if (youlePrefab && existsSync(youlePrefab)) {
const diff = execSync(`diff -u "${youlePrefab}" "${outputPath}" || true`, { encoding: 'utf8' });
writeFileSync(diffPath, diff);
console.log(`Diff: ${diffPath} (${diff.split('\n').length} lines)`);
}
}
@@ -65,6 +65,26 @@ export function readLayerXml(path) {
h: getProp(propList, 'Height'),
group: getProp(propList, 'BelongGroupID'),
indexOfLayer: getProp(propList, 'IndexOfLayer'),
text: getProp(propList, 'Text'),
sizeCalcMode: getProp(propList, 'SizeCalcMode'),
sizeParentID: getProp(propList, 'SizeParentID'),
widthCalcMode: getProp(propList, 'WidthCalcMode'),
heightCalcMode: getProp(propList, 'HeightCalcMode'),
sizeRelaWidth: getProp(propList, 'SizeRelaWidth'),
sizeRelaHeight: getProp(propList, 'SizeRelaHeight'),
coordSelfPos: getProp(propList, 'CoordSelfPos'),
coordRelaX: getProp(propList, 'CoordRelaX'),
coordRelaY: getProp(propList, 'CoordRelaY'),
coordParentID: getProp(propList, 'CoordParentID'),
belongLayerID: getProp(propList, 'BelongLayerID'),
fontSize: getProp(propList, 'FontSize'),
fontColor: getProp(propList, 'FontColor'),
alignHorz: getProp(propList, 'AlignHorz'),
alignVert: getProp(propList, 'AlignVert'),
fontBold: getProp(propList, 'FontBold'),
wordWrap: getProp(propList, 'WordWrap'),
lineSpace: getProp(propList, 'LineSpace'),
rowSpace: getProp(propList, 'RowSpace'),
};
});
@@ -97,14 +117,31 @@ export function computeLpos(spirit) {
/**
* 计算 Spirit 的 contentSize (Cocos UI 节点尺寸).
*
* 数据源权威: SizeCalcMode 缺失视作 0 (直接读 Width/Height); 非 0 抛错,
* 由上游显式决定是否逆向 gameabc.min.js (Task 5 范围, 本任务不支持).
* 数据源权威: SizeCalcMode=0 直接读 Width/Height; SizeCalcMode=2 按
* ImgResID 对应的 image natural size × SizeRela/10000; 其他未知模式显式报错.
*
* @param {{ w?: string, h?: string, sizeCalcMode?: string, id?: string, name?: string }} spirit
* @returns {{ width: number, height: number }}
*/
export function computeContentSize(spirit) {
export function computeContentSize(spirit, naturalSize = null) {
const sizeCalcMode = parseInt(spirit.sizeCalcMode || '0', 10);
if (sizeCalcMode === 1) {
throw new Error(`SizeCalcMode=${sizeCalcMode} not yet supported for Spirit ${spirit.id} (${spirit.name}) - reverse gameabc.min.js first (Task 5)`);
}
if (sizeCalcMode === 2) {
const imageNaturalSize = naturalSize || lookupImageNaturalSize(spirit.imgResID);
const widthMode = parseInt(spirit.widthCalcMode || '0', 10);
const heightMode = parseInt(spirit.heightCalcMode || '0', 10);
const widthRatio = parseFloat(spirit.sizeRelaWidth);
const heightRatio = parseFloat(spirit.sizeRelaHeight);
if (!Number.isFinite(widthRatio) || !Number.isFinite(heightRatio)) {
throw new Error(`Spirit ${spirit.id} (${spirit.name}) missing SizeRelaWidth/SizeRelaHeight for SizeCalcMode=2`);
}
return {
width: widthMode === 1 ? imageNaturalSize.width * widthRatio / 10000 : parseFloat(spirit.w || '0'),
height: heightMode === 1 ? imageNaturalSize.height * heightRatio / 10000 : parseFloat(spirit.h || '0'),
};
}
if (sizeCalcMode !== 0) {
throw new Error(`SizeCalcMode=${sizeCalcMode} not yet supported for Spirit ${spirit.id} (${spirit.name}) - reverse gameabc.min.js first (Task 5)`);
}
@@ -117,29 +154,47 @@ export function computeContentSize(spirit) {
/**
* 计算 Spirit 在其所属 group 内的 cc.Widget 锚定参数 (PoC 阶段 2 简化版).
*
* 数据源权威: 公式仅处理 Spirit.Y < 360 (上) vs > 360 (下) 与水平偏移, 完全基于
* 已知字段 (x/y/w/h). SizeCalcMode / CoordRelaX / CoordRelaY 等逆向未出的字段
* 暂不支持 — 由 buildSpiritBlock 调用方保证父 group 已经处理 SizeCalcMode ≠ 0.
*
* 简化规则:
* _alignFlags = 16 | 8 # 16=水平居中, 8=垂直居中
* _horizontalCenter = x + w/2 - 640 # 像素偏移, 1280 坐标系水平中心
* _verticalCenter = 360 - (y + h/2) # Y-up 翻转 + 720 坐标系垂直中心
* 数据源权威: 按节点尺寸与 name 几何规则区分 full-screen (45),
* top-left (9), 以及 normal (16) 三种 Widget 对齐契约.
*
* @param {{ x?: string, y?: string, w?: string, h?: string }} spirit
* @returns {object} cc.Widget 字段 (除 __type__/node/__prefab/_id 外)
*/
export function computeWidgetAlignment(spirit) {
const x = parseFloat(spirit.x || '0');
const y = parseFloat(spirit.y || '0');
const w = parseFloat(spirit.w || '0');
const h = parseFloat(spirit.h || '0');
const name = String(spirit.name || '');
const width = parseFloat(spirit.w || '0');
const height = parseFloat(spirit.h || '0');
const lpos = computeLpos(spirit);
const isFullScreen = width === 1280 && height === 720;
if (isFullScreen) {
return {
_alignFlags: 45,
_target: null,
_left: 0, _right: 0, _top: 0, _bottom: 0,
_isAbsLeft: true, _isAbsRight: true, _isAbsTop: true, _isAbsBottom: true,
_isAbsHorizontalCenter: true, _isAbsVerticalCenter: true,
_originalWidth: 100, _originalHeight: 100,
_alignMode: 2, _lockFlags: 0,
};
}
const isTopLeft = lpos.y > 0 && lpos.x < 0 || /背景|bg/i.test(name);
if (isTopLeft) {
return {
_alignFlags: 9,
_target: null,
_left: lpos.x, _top: lpos.y,
_isAbsLeft: true, _isAbsRight: true, _isAbsTop: true, _isAbsBottom: true,
_isAbsHorizontalCenter: true, _isAbsVerticalCenter: true,
_originalWidth: 0, _originalHeight: 0,
_alignMode: 2, _lockFlags: 0,
};
}
return {
_alignFlags: 16 | 8, // 水平居中 + 垂直居中
_alignFlags: 16,
_target: null,
_left: 0, _right: 0, _top: 0, _bottom: 0,
_horizontalCenter: x + w / 2 - 640, // 1280 体系半宽
_verticalCenter: 360 - (y + h / 2), // Y-up + 720 体系半高
_horizontalCenter: lpos.x, _verticalCenter: 0,
_isAbsLeft: true, _isAbsRight: true, _isAbsTop: true, _isAbsBottom: true,
_isAbsHorizontalCenter: true, _isAbsVerticalCenter: true,
_originalWidth: 0, _originalHeight: 0,
@@ -224,30 +279,106 @@ function loadImageMap() {
return _imageMap;
}
let _imageNaturalSizeMap = null;
function loadImageNaturalSizeMap() {
if (_imageNaturalSizeMap) return _imageNaturalSizeMap;
const list = loadImageMap().ImageFileList;
if (!Array.isArray(list)) {
throw new Error('gameabc_Image.json missing ImageFileList array');
}
_imageNaturalSizeMap = new Map();
for (const entry of list) {
if (!entry || entry.id == null) continue;
const width = Number(entry.w_all);
const height = Number(entry.h_all);
if (!Number.isFinite(width) || !Number.isFinite(height)) continue;
_imageNaturalSizeMap.set(String(entry.id), { width, height });
}
return _imageNaturalSizeMap;
}
function lookupImageNaturalSize(imgResId) {
const key = String(imgResId);
const naturalSize = loadImageNaturalSizeMap().get(key);
if (!naturalSize) {
throw new Error(`ImgResID "${imgResId}" missing natural size in gameabc_Image.json`);
}
return naturalSize;
}
const ATLAS_ROOT = join(
__dirname,
'../YouleNexus/assets/framework/ui'
);
let _bmpToMetaPath = null;
function loadBmpToMetaPath() {
if (_bmpToMetaPath) return _bmpToMetaPath;
const ATLAS_BY_LAYER_NAME = [
['MainScene_PlayerInfo', 'atlas-hall'],
['BackHall', 'atlas-hall'],
['Login', 'atlas-login'],
['Notice', 'atlas-login'],
['Protol', 'atlas-login'],
['Loading', 'atlas-login'],
['Kick', 'atlas-login'],
['Reconnect', 'atlas-login'],
['Setting', 'atlas-login'],
['Tips', 'atlas-login'],
['updateRoomCard', 'atlas-login'],
['phoneInfo', 'atlas-login'],
['recordLayer', 'atlas-login'],
['MenuPlayerInfo', 'atlas-login'],
];
export function selectAtlasForLayerName(layerName) {
const normalizedName = String(layerName || '').toLowerCase();
const match = ATLAS_BY_LAYER_NAME.find(([keyword]) => normalizedName.includes(keyword.toLowerCase()));
return match ? match[1] : 'atlas-login';
}
function listAtlasDirectories() {
if (!existsSync(ATLAS_ROOT)) return [];
return readdirSync(ATLAS_ROOT)
.filter(entry => entry.startsWith('atlas-') && statSync(join(ATLAS_ROOT, entry)).isDirectory());
}
const _bmpToMetaPath = new Map();
function loadBmpToMetaPath(preferredAtlas = null) {
const cacheKey = preferredAtlas || 'default';
if (_bmpToMetaPath.has(cacheKey)) return _bmpToMetaPath.get(cacheKey);
const map = new Map();
if (existsSync(ATLAS_ROOT)) {
for (const entry of readdirSync(ATLAS_ROOT)) {
if (!entry.startsWith('atlas-')) continue;
const dir = join(ATLAS_ROOT, entry);
if (!statSync(dir).isDirectory()) continue;
for (const file of readdirSync(dir)) {
if (!file.endsWith('.png.meta')) continue;
const bmpName = file.replace(/\.meta$/, '');
// 同一 bmp 出现在多个 atlas 时, 取首次发现 (字典序);
// YouleNexus 实际数据中无重复, 此为防御性 fallback.
if (!map.has(bmpName)) map.set(bmpName, join(dir, file));
const directories = listAtlasDirectories();
const orderedDirectories = [
...(preferredAtlas ? [preferredAtlas] : []),
...(preferredAtlas === 'atlas-login' || !preferredAtlas ? ['atlas-login'] : []),
...directories.filter(entry => entry !== preferredAtlas && entry !== 'atlas-login'),
];
for (const entry of orderedDirectories) {
const dir = join(ATLAS_ROOT, entry);
for (const file of readdirSync(dir)) {
if (!file.endsWith('.png.meta')) continue;
const bmpName = file.replace(/\.meta$/, '');
if (!map.has(bmpName)) map.set(bmpName, join(dir, file));
}
}
// Cocos imports multi-frame source BMPs as <bmp>_1.png, <bmp>_2.png, ...
// Keep the first frame deterministic when a legacy image has no exact BMP basename.
const imageMap = loadImageMap();
const imageList = Array.isArray(imageMap.ImageFileList) ? imageMap.ImageFileList : [];
for (const image of imageList) {
if (!image?.bmp || map.has(image.bmp)) continue;
const baseName = image.bmp.replace(/\.png$/i, '');
const frameCount = Number.isInteger(Number(image.frame_all)) && Number(image.frame_all) > 0
? Number(image.frame_all)
: 1;
for (let frame = 1; frame <= frameCount; frame += 1) {
const frameName = `${baseName}_${frame}.png`;
if (map.has(frameName)) {
map.set(image.bmp, map.get(frameName));
break;
}
}
}
_bmpToMetaPath = map;
_bmpToMetaPath.set(cacheKey, map);
return map;
}
@@ -262,9 +393,10 @@ function loadBmpToMetaPath() {
* - .meta 缺 subMetas.f9941 → throw (说明该资源未走 sprite-frame 导入)
*
* @param {string} imgResId - XML ImgResID 字符串 (如 "20", "21", "25")
* @param {string} [atlasDir] - 可选的 layer atlas 目录, 例如 atlas-login.
* @returns {string} SpriteFrame 真 UUID, 形如 "<uuid>@f9941"
*/
export function resolveSpriteFrameUuid(imgResId) {
export function resolveSpriteFrameUuid(imgResId, atlasDir = null) {
const imageMap = loadImageMap();
const list = imageMap.ImageFileList;
if (!Array.isArray(list)) {
@@ -277,7 +409,7 @@ export function resolveSpriteFrameUuid(imgResId) {
}
const bmpName = entry.bmp;
const bmpToMeta = loadBmpToMetaPath();
const bmpToMeta = loadBmpToMetaPath(atlasDir);
const metaPath = bmpToMeta.get(bmpName);
if (!metaPath) {
throw new Error(`bmp "${bmpName}" (ImgResID ${imgResId}) not found under 'atlas-*' in ${ATLAS_ROOT}`);
@@ -376,7 +508,7 @@ function makeUiTransform(nodeId, compPrefabId, width, height) {
};
}
function makeWidget(nodeId, compPrefabId) {
function makeWidget(nodeId, compPrefabId, originalWidth = 100, originalHeight = 100) {
return {
__type__: 'cc.Widget',
_name: '',
@@ -391,7 +523,7 @@ function makeWidget(nodeId, compPrefabId) {
_horizontalCenter: 0, _verticalCenter: 0,
_isAbsLeft: true, _isAbsRight: true, _isAbsTop: true, _isAbsBottom: true,
_isAbsHorizontalCenter: true, _isAbsVerticalCenter: true,
_originalWidth: 0, _originalHeight: 0,
_originalWidth: originalWidth, _originalHeight: originalHeight,
_alignMode: 2,
_lockFlags: 0,
_id: '',
@@ -446,7 +578,13 @@ function makeSprite(nodeId, compPrefabId, spriteFrameUuid, type = 0) {
};
}
function makeLabel(nodeId, compPrefabId, text) {
function makeLabel(nodeId, compPrefabId, spirit) {
const fontSize = Number.isFinite(Number(spirit.fontSize)) ? Number(spirit.fontSize) : 40;
const lineSpace = Number.isFinite(Number(spirit.lineSpace)) ? Number(spirit.lineSpace) : fontSize;
const rowSpace = Number.isFinite(Number(spirit.rowSpace)) ? Number(spirit.rowSpace) : 0;
const color = Number.isInteger(Number(spirit.fontColor)) ? Number(spirit.fontColor) : 0xffffff;
const horizontalAlign = Number.isInteger(Number(spirit.alignHorz)) ? Number(spirit.alignHorz) : 1;
const verticalAlign = Number.isInteger(Number(spirit.alignVert)) ? Number(spirit.alignVert) : 1;
return {
__type__: 'cc.Label',
_name: '',
@@ -458,20 +596,28 @@ function makeLabel(nodeId, compPrefabId, text) {
_customMaterial: null,
_srcBlendFactor: 2,
_dstBlendFactor: 4,
_color: { __type__: 'cc.Color', r: 255, g: 255, b: 255, a: 255 },
_string: text || '',
_horizontalAlign: 1,
_verticalAlign: 1,
_actualFontSize: 40,
_fontSize: 40,
_color: {
__type__: 'cc.Color',
r: (color >> 16) & 255,
g: (color >> 8) & 255,
b: color & 255,
a: 255,
},
_string: spirit.text || spirit.name || '',
_horizontalAlign: horizontalAlign,
_verticalAlign: verticalAlign,
_actualFontSize: fontSize,
_fontSize: fontSize,
_fontFamily: 'Arial',
_lineHeight: 40,
_lineHeight: lineSpace,
_overflow: 0,
_enableWrapText: true,
_enableWrapText: spirit.wordWrap !== undefined && spirit.wordWrap !== '0',
_font: null,
_isSystemFontUsed: true,
_spacingX: 0,
_isItalic: false, _isBold: false, _isUnderline: false,
_spacingX: rowSpace,
_isItalic: false,
_isBold: spirit.fontBold !== undefined && spirit.fontBold !== '0',
_isUnderline: false,
_underlineHeight: 2,
_cacheMode: 0,
_enableOutline: false,
@@ -579,7 +725,7 @@ function makeEditBox(nodeId, compPrefabId, spriteFrameUuid, text) {
* @param {{ layerId?: string, layerName: string, spirits: Array<{ id: string, name: string, type: string, imgResID: string, x: string, y: string, w: string, h: string, group: string, indexOfLayer: string }> }} layer
* @returns {Array<object>} Cocos prefab JSON 数组
*/
export function emitPrefabJson(layer) {
export function emitPrefabJson(layer, opts = {}) {
if (!layer || !layer.layerName) {
throw new Error('emitPrefabJson: layer.layerName is required');
}
@@ -588,11 +734,13 @@ export function emitPrefabJson(layer) {
}
const out = [];
const targetName = opts.targetName || layer.layerName;
const atlasName = opts.atlasName || selectAtlasForLayerName(targetName);
// ── 1. cc.Prefab 头 ─────────────────────────────────────────────
out.push({
__type__: 'cc.Prefab',
_name: layer.layerName,
_name: targetName,
_objFlags: 0,
__editorExtras__: {},
_native: '',
@@ -602,7 +750,7 @@ export function emitPrefabJson(layer) {
});
// ── 2. 根节点 cc.Node (id=1) ────────────────────────────────────
const rootNode = makeNode(layer.layerName, null);
const rootNode = makeNode(targetName, null);
out.push(rootNode);
const rootId = out.length - 1; // =1
@@ -628,7 +776,7 @@ export function emitPrefabJson(layer) {
if (!spirit) {
throw new Error(`emitPrefabJson: spirit id ${spiritId} referenced by ${gi.group.name} but not found in layer.spirits`);
}
const info = buildSpiritBlock(spirit, { __id__: gi.nodeId }, out, { parentIsGroup: true });
const info = buildSpiritBlock(spirit, { __id__: gi.nodeId }, out, { parentIsGroup: true, atlasName });
spiritInfo.set(spiritId, info);
gi.node._children.push({ __id__: info.nodeId });
}
@@ -639,7 +787,7 @@ export function emitPrefabJson(layer) {
if (!spirit) {
throw new Error(`emitPrefabJson: root child spirit id ${spiritId} not found in layer.spirits`);
}
const info = buildSpiritBlock(spirit, { __id__: rootId }, out, { parentIsGroup: false });
const info = buildSpiritBlock(spirit, { __id__: rootId }, out, { parentIsGroup: false, atlasName });
spiritInfo.set(spiritId, info);
rootNode._children.push({ __id__: info.nodeId });
}
@@ -669,7 +817,12 @@ export function emitPrefabJson(layer) {
*/
function buildSpiritBlock(spirit, parentRef, out, opts = { parentIsGroup: false }) {
const lpos = computeLpos(spirit);
const size = computeContentSize(spirit);
const width = parseFloat(spirit.w || '0');
const height = parseFloat(spirit.h || '0');
const isFullScreen = width === 1280 && height === 720;
const size = isFullScreen
? { width: 1600, height: 720 }
: computeContentSize(spirit);
const componentName = mapSpiritTypeToComponent(spirit.type);
// 1. cc.Node (主组件 id=K+0)
@@ -680,19 +833,18 @@ function buildSpiritBlock(spirit, parentRef, out, opts = { parentIsGroup: false
// 2. cc.UITransform (K+1)
const uiCompInfo = makeCompPrefabInfo();
out.push(uiCompInfo);
const uiCompInfoId = out.length - 1;
const uiTransform = makeUiTransform(nodeId, uiCompInfoId, size.width, size.height);
const uiTransform = makeUiTransform(nodeId, out.length + 1, size.width, size.height);
out.push(uiTransform);
const uiId = out.length - 1;
out.push(uiCompInfo);
const uiCompInfoId = out.length - 1;
// 3. 主组件 (K+3) — CompPrefabInfo 先 push (K+2), 主组件 __prefab → 该 CompPrefabInfo
// 3. 主组件 (K+3) — component 先 push, CompPrefabInfo follows it.
const mainCompInfo = makeCompPrefabInfo();
out.push(mainCompInfo);
const mainCompInfoId = out.length - 1;
const mainCompInfoId = out.length + 1;
const spriteFrameUuid = (spirit.type === '0' || spirit.type === '3' || spirit.type === '5')
? resolveSpriteFrameUuid(spirit.imgResID)
? resolveSpriteFrameUuid(spirit.imgResID, opts.atlasName)
: null;
let mainComp;
@@ -700,7 +852,7 @@ function buildSpiritBlock(spirit, parentRef, out, opts = { parentIsGroup: false
const spriteType = spirit.type === '5' ? 1 : 0; // 5=SLICED, else SIMPLE
mainComp = makeSprite(nodeId, mainCompInfoId, spriteFrameUuid, spriteType);
} else if (componentName === 'cc.Label') {
mainComp = makeLabel(nodeId, mainCompInfoId, spirit.name);
mainComp = makeLabel(nodeId, mainCompInfoId, spirit);
} else if (componentName === 'cc.ProgressBar') {
mainComp = makeProgressBar(nodeId, mainCompInfoId, spriteFrameUuid);
} else if (componentName === 'cc.EditBox') {
@@ -712,18 +864,18 @@ function buildSpiritBlock(spirit, parentRef, out, opts = { parentIsGroup: false
}
out.push(mainComp);
const mainCompId = out.length - 1;
out.push(mainCompInfo);
// 4. cc.Widget (K+5) — CompPrefabInfo 先 push (K+4)
// 4. cc.Widget (K+5) — component 先 push, CompPrefabInfo follows it.
const widgetCompInfo = makeCompPrefabInfo();
out.push(widgetCompInfo);
const widgetCompInfoId = out.length - 1;
// parentIsGroup 时走 computeWidgetAlignment (水平+垂直居中); Layer 根直挂时
// 用默认 _alignFlags=45 (全屏铺满占位, 后续 PoC 阶段再细化).
// parentIsGroup 时走 computeWidgetAlignment; Layer 根直挂时使用全屏 Widget.
const widget = opts.parentIsGroup
? makeSpiritWidget(nodeId, widgetCompInfoId, spirit)
: makeWidget(nodeId, widgetCompInfoId);
? makeSpiritWidget(nodeId, out.length + 1, spirit)
: makeWidget(nodeId, out.length + 1);
out.push(widget);
const widgetId = out.length - 1;
out.push(widgetCompInfo);
const widgetCompInfoId = out.length - 1;
// 5. cc.PrefabInfo (K+7) — Node 的 _prefab 指向这里
const prefabInfo = makePrefabInfo();
@@ -744,19 +896,19 @@ function buildSpiritBlock(spirit, parentRef, out, opts = { parentIsGroup: false
function appendNodeEnvelope(node, nodeId, out) {
// 1. UITransform + CompPrefabInfo
const uiCompInfo = makeCompPrefabInfo();
out.push(uiCompInfo);
const uiCompInfoId = out.length - 1;
const uiTransform = makeUiTransform(nodeId, uiCompInfoId, DESIGN_WIDTH, DESIGN_HEIGHT);
const uiTransform = makeUiTransform(nodeId, out.length + 1, DESIGN_WIDTH, DESIGN_HEIGHT);
out.push(uiTransform);
const uiId = out.length - 1;
out.push(uiCompInfo);
const uiCompInfoId = out.length - 1;
// 2. Widget + CompPrefabInfo
const widgetCompInfo = makeCompPrefabInfo();
out.push(widgetCompInfo);
const widgetCompInfoId = out.length - 1;
const widget = makeWidget(nodeId, widgetCompInfoId);
const widget = makeWidget(nodeId, out.length + 1);
out.push(widget);
const widgetId = out.length - 1;
out.push(widgetCompInfo);
const widgetCompInfoId = out.length - 1;
// 3. PrefabInfo
const prefabInfo = makePrefabInfo();
@@ -7,15 +7,16 @@
// - 单个 target 失败 (例如 ImgResID 未导入 sprite) 不阻塞后续 target, 日志记录后继续.
// - per "下游不兜底" 原则: CLI 本身必须 throw 让错误暴露; batch 仅负责捕获 + 汇总, 不做修补.
import { execSync } from 'node:child_process';
import { join } from 'node:path';
import { ROOT } from './lib/paths.mjs';
const ROOT = process.cwd(); // assume run from cocoscreator_projects/
const OUT_DIR = `${ROOT}/YouleNexus/out/legacy-migration`;
const SOURCE = `${ROOT}/../projects/Game_Surface_3/save`;
const OUT_DIR = join(ROOT, 'YouleNexus', 'out', 'legacy-migration');
const SOURCE = join(ROOT, '..', 'projects', 'Game_Surface_3', 'save');
// 按 spec 已核实映射表 (11 个 prefab). Layer10_Protocol + Login_Layer + Layer619_BackOtherGame 三个 PoC 已存在,
// 但 batch 仍会重新生成它们 (idempotent — 覆盖 out/legacy-migration/*.prefab + .diff).
const targets = [
{ xml: 'Layer00002.xml', prefab: 'YouleNexus/assets/framework/ui/prefabs/views/Login_Layer.prefab', name: 'Login_Layer', note: 'PoC 3 (Layer00002)' },
{ xml: 'Layer00002.xml', prefab: 'YouleNexus/assets/framework/ui/prefabs/Login_Layer.prefab', name: 'Login_Layer', note: 'PoC 3 (Layer00002)' },
{ xml: 'Layer00017.xml', prefab: 'YouleNexus/assets/framework/ui/prefabs/views/PlayerInfoView.prefab', name: 'MenuPlayerInfo_Layer', note: '拍板 A (Layer00017)' },
{ xml: 'Layer00029.xml', prefab: 'YouleNexus/assets/framework/ui/prefabs/widgets/Group106_PhoneVerify.prefab', name: 'phoneInfo' },
{ xml: 'Layer00010.xml', prefab: 'YouleNexus/assets/framework/ui/prefabs/widgets/Layer10_Protocol.prefab', name: 'Protol_Layer', note: 'PoC 2 (Layer00010)' },
@@ -57,4 +58,5 @@ if (results.failed.length > 0) {
console.log(` - ${f.name} (${f.xml}): ${f.error}`);
}
}
console.log('\nNext step: human reviews each .diff and decides whether to overwrite YouleNexus original prefabs.');
console.log('\nNext step: human reviews each .diff and decides whether to overwrite YouleNexus original prefabs.');
process.exitCode = results.failed.length > 0 ? 1 : 0;