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
@@ -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();