feat(ui-migration): 源数据加载与校验
读取 gameabc_{Object,Layer,GroupList,Image}.json, 跳过 {} 占位槽,
校验多帧图的网格与帧数一致(不符即显式抛错, 不兜底)。
真实数据回归护栏: 991 对象 / 55 界面 / 80 组 / 439 图 / 139 多帧图。
引入 pngjs devDep 供后续切图使用(仅迁移工具, 不进框架运行时)。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { join } from 'node:path';
|
||||
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { ROOT } from '../lib/paths.mjs';
|
||||
import { loadSource, SourceError } from '../ui-migration/gameabc-source.mjs';
|
||||
|
||||
const REAL_SOURCE = join(ROOT, '..', 'projects', 'Game_Surface_3', 'output');
|
||||
|
||||
function fakeSource(overrides = {}) {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'gameabc-src-'));
|
||||
const files = {
|
||||
'gameabc_Object.json': {
|
||||
ObjectList: [
|
||||
{},
|
||||
{ Property: { ObjectID: 1, ObjectType: 2, ObjectName: '背景', Left: 0, Top: 0, Width: 1280, Height: 720, BelongLayerID: 1, IndexOfLayer: 1, ImageFileID: 1, FrameStyle: 0, FrameIndex: 0, L9: 0, T9: 0, R9: 0, B9: 0, GroupID: 1, VoiceFileID: 0, TimerInterval: 0 }, Event: { mousedown: 1, mouseup: 1 } },
|
||||
],
|
||||
},
|
||||
'gameabc_Layer.json': { LayerList: [{}, { Property: { LayerID: 1, LayerName: 'Logo_Layer' }, ObjectList: [1] }] },
|
||||
'gameabc_GroupList.json': { GroupList: [{ GroupID: 1, ObjectList: [1] }] },
|
||||
'gameabc_Image.json': { ImageFileList: [{}, { id: 1, w_all: 1280, h_all: 720, w: 1, h: 1, frame_all: 1, bmp: '00001.png', w1: 1280, h1: 720 }] },
|
||||
...overrides,
|
||||
};
|
||||
for (const [name, content] of Object.entries(files)) {
|
||||
writeFileSync(join(dir, name), JSON.stringify(content));
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
|
||||
test('loadSource 解析对象、层、组、图,并附上 events', () => {
|
||||
const dir = fakeSource();
|
||||
try {
|
||||
const s = loadSource(dir);
|
||||
assert.equal(s.objects.size, 1);
|
||||
assert.equal(s.objects.get(1).ObjectName, '背景');
|
||||
assert.deepEqual(s.objects.get(1).events, ['mousedown', 'mouseup']);
|
||||
assert.deepEqual(s.layers, [{ layerId: 1, name: 'Logo_Layer', objectIds: [1] }]);
|
||||
assert.deepEqual(s.groups, [{ groupId: 1, objectIds: [1] }]);
|
||||
assert.equal(s.images.get(1).bmp, '00001.png');
|
||||
} finally { rmSync(dir, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test('loadSource 跳过源数据里的空占位槽 {}', () => {
|
||||
const dir = fakeSource();
|
||||
try {
|
||||
const s = loadSource(dir);
|
||||
assert.equal(s.objects.size, 1, '首个 {} 占位不应变成对象');
|
||||
assert.equal(s.layers.length, 1);
|
||||
} finally { rmSync(dir, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test('loadSource 对网格与帧数不符的图显式抛错(不兜底)', () => {
|
||||
const dir = fakeSource({
|
||||
'gameabc_Image.json': { ImageFileList: [{}, { id: 1, w_all: 100, h_all: 100, w: 3, h: 4, frame_all: 11, bmp: 'bad.png', w1: 33, h1: 25 }] },
|
||||
});
|
||||
try {
|
||||
assert.throws(() => loadSource(dir), SourceError);
|
||||
assert.throws(() => loadSource(dir), /bad\.png/);
|
||||
} finally { rmSync(dir, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test('loadSource 对缺失文件显式抛错', () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'gameabc-empty-'));
|
||||
try {
|
||||
assert.throws(() => loadSource(dir), /缺少源数据文件/);
|
||||
} finally { rmSync(dir, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test('loadSource 在真实源数据上得到已知规模(回归护栏)', () => {
|
||||
const s = loadSource(REAL_SOURCE);
|
||||
assert.equal(s.objects.size, 991, '对象总数');
|
||||
assert.equal(s.layers.length, 55, '界面数');
|
||||
assert.equal(s.groups.length, 80, '分组数');
|
||||
assert.equal(s.images.size, 439, '图片数');
|
||||
const multi = [...s.images.values()].filter((i) => i.frame_all > 1);
|
||||
assert.equal(multi.length, 139, '多帧图数');
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
import { readFileSync, existsSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
|
||||
/** 源数据非法时抛出。第二准则:缺失/非法一律显式暴露,不兜底。 */
|
||||
export class SourceError extends Error {
|
||||
constructor(message) { super(message); this.name = 'SourceError'; }
|
||||
}
|
||||
|
||||
function readJson(dir, name) {
|
||||
const p = join(dir, name);
|
||||
if (!existsSync(p)) throw new SourceError(`缺少源数据文件: ${p}`);
|
||||
try {
|
||||
return JSON.parse(readFileSync(p, 'utf8'));
|
||||
} catch (e) {
|
||||
throw new SourceError(`源数据 JSON 解析失败: ${p} — ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取并校验 gameabc 导出数据。
|
||||
* 源数组用 `{}` 占位空槽(下标即 ID),须跳过。
|
||||
*/
|
||||
export function loadSource(outputDir) {
|
||||
const objects = new Map();
|
||||
for (const entry of readJson(outputDir, 'gameabc_Object.json').ObjectList ?? []) {
|
||||
if (!entry || !entry.Property) continue;
|
||||
const p = entry.Property;
|
||||
objects.set(p.ObjectID, { ...p, events: Object.keys(entry.Event ?? {}) });
|
||||
}
|
||||
|
||||
const images = new Map();
|
||||
for (const i of readJson(outputDir, 'gameabc_Image.json').ImageFileList ?? []) {
|
||||
if (!i || i.id === undefined) continue;
|
||||
if (i.w * i.h !== i.frame_all) {
|
||||
throw new SourceError(
|
||||
`图 ${i.bmp} 的网格与帧数不符: ${i.w}x${i.h}=${i.w * i.h} != frame_all=${i.frame_all}`,
|
||||
);
|
||||
}
|
||||
images.set(i.id, i);
|
||||
}
|
||||
|
||||
const layers = (readJson(outputDir, 'gameabc_Layer.json').LayerList ?? [])
|
||||
.filter((l) => l && l.Property)
|
||||
.map((l) => ({
|
||||
layerId: l.Property.LayerID,
|
||||
name: l.Property.LayerName,
|
||||
objectIds: l.ObjectList ?? [],
|
||||
}));
|
||||
|
||||
const groups = (readJson(outputDir, 'gameabc_GroupList.json').GroupList ?? [])
|
||||
.filter((g) => g && g.GroupID !== undefined)
|
||||
.map((g) => ({ groupId: g.GroupID, objectIds: g.ObjectList ?? [] }));
|
||||
|
||||
return { objects, images, layers, groups };
|
||||
}
|
||||
Reference in New Issue
Block a user