unity-find-fault
Version:
A tool to find fault in unity project.
244 lines • 10.9 kB
JavaScript
import fg from "fast-glob";
import fs from "fs-extra";
import path from "path";
import sizeOf from "image-size";
import { toolchain } from "../toolchain.js";
import { UnityHelper } from "./UnityHelper.js";
import { standardPath } from "./vendor.js";
import normalizePath from "normalize-path";
import _ from "lodash";
export class ImageSorter {
refStatusDescMap = {
["" /* ERefStatus.None */]: '',
["\u221A" /* ERefStatus.Exclusive */]: '√',
["*" /* ERefStatus.Bad */]: '*'
};
async sortAtlas() {
// const allFormOutFile = `tmp/${toolchain.opts.projectName}.imageSorter.allForm.txt`;
// await fs.ensureFile(allFormOutFile);
// await fs.writeFile(allFormOutFile, '', 'utf-8');
// await UIRelations.build();
// const tabForms = UIRelations.getAllForms();
// tabForms.forEach(v => {
// fs.appendFileSync(allFormOutFile, `${v.tsClass.className}: ${v.children.map(v => v.tsClass.className).join(', ')}\n`);
// })
// }
// private async ttt(): Promise<void> {
const dirRefOutFile = `tmp/${toolchain.opts.projectName}.imageSorter.dirRef.txt`;
const uiRefOutFile = `tmp/${toolchain.opts.projectName}.imageSorter.uiRef.txt`;
const solutionOutFile = `tmp/${toolchain.opts.projectName}.imageSorter.solution.txt`;
if (!toolchain.opts.output) {
await fs.ensureFile(dirRefOutFile);
await fs.writeFile(dirRefOutFile, '', 'utf-8');
await fs.ensureFile(uiRefOutFile);
await fs.writeFile(uiRefOutFile, '', 'utf-8');
await fs.ensureFile(solutionOutFile);
await fs.writeFile(solutionOutFile, '', 'utf-8');
}
const infoMap = {};
// collect image infomations
const imgRoot = path.join(toolchain.opts.projectRoot, 'Assets/AssetSources');
const imgs = await fg(['**/*.png', '**/*.jpg'], { cwd: imgRoot });
for (const img of imgs) {
if (!img.startsWith('ui') && !img.startsWith('images'))
continue;
const file = path.join(imgRoot, img);
const guid = await UnityHelper.readGUID(file + '.meta');
// get dimensions
const dimensions = await sizeOf(file);
if (dimensions.width == null || dimensions.height == null) {
console.error('get dimensions failed: ', img);
continue;
}
const info = { guid, width: dimensions.width, height: dimensions.height, size: dimensions.width * dimensions.height, refers: [] };
infoMap[file] = info;
}
// get references
const uiRoot = path.join(toolchain.opts.projectRoot, 'Assets/AssetSources/ui/system');
const referMap = await UnityHelper.collectUsedGUID(uiRoot);
// turn to Record<dir, IDirInfo>, Record<ui, dirs>
const dirMap = {}, ui2dirs = {};
for (const img in infoMap) {
const dir = path.dirname(img);
// dir map
let dinfo = dirMap[dir];
if (dinfo == null) {
dirMap[dir] = dinfo = { path: dir, imgs: [], totalSize: 0, referedPrefabs: [] };
}
dinfo.imgs.push(img);
dinfo.totalSize += infoMap[img].size;
// ui map
const info = infoMap[img];
const refers = referMap[info.guid];
if (refers == null) {
continue;
}
for (const ui of refers) {
if (!dinfo.referedPrefabs.includes(ui))
dinfo.referedPrefabs.push(ui);
let dirs = ui2dirs[ui];
if (dirs == null) {
ui2dirs[ui] = dirs = [];
}
const d = dirs.find(v => v.path == dir);
if (d != null) {
d.imgs.push(img);
}
else {
dirs.push({ path: dir, imgs: [img] });
}
}
}
const allLowerCaseDirs = Object.keys(dirMap).map(v => v.toLowerCase());
// check ui
const uiReports = [];
for (const ui in ui2dirs) {
const dirs = ui2dirs[ui];
if (dirs.length <= 1)
continue;
const referDirs = [];
for (const d of dirs) {
// if (!d.path.includes('atlas\\common_')) {
let s = 0;
d.imgs.forEach(v => s += infoMap[v].size);
const dinfo = dirMap[d.path];
const sizePct = Math.round(s / dinfo.totalSize * 100);
let status = "" /* ERefStatus.None */;
if (dinfo.referedPrefabs.length == 1) {
// 专属目录
status = "\u221A" /* ERefStatus.Exclusive */;
}
else if (sizePct < 30) {
status = "*" /* ERefStatus.Bad */;
}
referDirs.push({
...d,
cntPct: d.imgs.length / dinfo.imgs.length,
cntPctDesc: `${d.imgs.length}/${dinfo.imgs.length}`,
sizePct,
status
});
// }
}
if (referDirs.length > 0) {
const score = _.meanBy(referDirs, 'sizePct');
uiReports.push({ prefab: path.basename(ui, '.prefab'), refs: referDirs, score });
}
}
uiReports.sort((a, b) => a.score - b.score);
if (!toolchain.opts.output) {
uiReports.forEach(v => {
const refLines = v.refs.map(v => ` ${this.refStatusDescMap[v.status]} ${normalizePath(path.relative(imgRoot, v.path))}(${v.cntPctDesc}, ${v.sizePct}%): ${v.imgs.map(v => path.basename(v)).join(', ')}`);
fs.appendFileSync(uiRefOutFile, `${v.prefab} (${v.refs.length}): \n${refLines.join('\n')}\n`);
});
}
// check dirs
const dirReports = [];
for (const dir in dirMap) {
// if (dir.includes('atlas\\common_')) {
// continue;
// }
const dinfo = dirMap[dir];
if (dinfo.referedPrefabs.length > 1) {
dirReports.push(dinfo);
}
}
dirReports.sort((a, b) => b.referedPrefabs.length - a.referedPrefabs.length);
if (!toolchain.opts.output) {
dirReports.forEach(d => {
fs.appendFileSync(dirRefOutFile, `${normalizePath(path.relative(imgRoot, d.path))} (${d.referedPrefabs.length}): ${d.referedPrefabs.map(v => path.basename(v, '.prefab')).join(', ')}\n`);
});
}
// suggestion
// // 检查是否被多个ui使用(相互不重叠),这样可以直接拆分
// const splitMap: Record<string, string> = {};
// for (const dir in dirMap) {
// if (dir.includes('atlas\\common_')) {
// continue;
// }
// const dinfo = dirMap[dir];
// if (dinfo.referedPrefabs.length > 1) {
// for (const p of dinfo.referedPrefabs) {
// const uiDirs = ui2dirs[p];
// const ud = uiDirs.find(v => v.path == dir);
// for (const img of ud!.imgs) {
// if (splitMap[img] == null) {
// splitMap[img] = p;
// } else {
// splitMap[img] = '__SHARED__';
// }
// }
// }
// }
// }
// // 生成split solution
// const splitSolutions: ISplitSolution[] = [];
// for (const img in splitMap) {
// const prefab = splitMap[img];
// if (prefab == '__SHARED__') {
// continue;
// }
// let solution = splitSolutions.find(v => v.prefab == prefab);
// if (solution == null) {
// solution = { prefab, imgs: [] };
// splitSolutions.push(solution);
// }
// solution.imgs.push(img);
// }
// // 执行split solution
// for (const ss of splitSolutions) {
// const prefabName = path.basename(ss.prefab, '.prefab');
// const uiDirs = ui2dirs[ss.prefab];
// const td = uiDirs.find(v => dirMap[v.path].referedPrefabs.length == 1);
// let target = '', newTarget = false;
// if (td) {
// target = td.path;
// } else {
// const tryDir = path.join(specialRoot, prefabName);
// target = tryDir;
// let tryCnt = 0;
// while (allLowerCaseDirs.includes(target.toLowerCase())) {
// target = tryDir + tryCnt;
// tryCnt++;
// }
// newTarget = true;
// }
// for (const img of ss.imgs) {
// const bdn = path.basename(path.dirname(img));
// if (bdn.toLowerCase() == prefabName.toLowerCase()) {
// continue;
// }
// if (newTarget) {
// await fs.appendFile(solutionOutFile, `mkdir "${standardPath(target)}"\n`);
// await fs.appendFile(solutionOutFile, `svn add "${standardPath(target)}"\n`);
// newTarget = false;
// }
// await fs.appendFile(solutionOutFile, `svn mv "${standardPath(img)}" "${standardPath(path.join(target, path.basename(img)))}"\n`);
// }
// }
// output
if (toolchain.opts.output) {
await fs.ensureDir(toolchain.opts.output);
let op = toolchain.opts.output;
if (!path.isAbsolute(op)) {
op = path.join(toolchain.opts.projectRoot, op);
}
uiReports.forEach(v => {
v.refs.forEach(r => {
r.path = normalizePath(standardPath(r.path));
r.imgs = r.imgs.map(i => normalizePath(standardPath(i)));
});
});
const uiRefOutJson = path.join(op, '__uiRef.json');
await fs.writeJSON(uiRefOutJson, { uis: uiReports });
dirReports.forEach(v => {
v.path = normalizePath(standardPath(v.path));
v.imgs = v.imgs.map(i => normalizePath(standardPath(i)));
v.referedPrefabs = v.referedPrefabs.map(r => normalizePath(standardPath(r)));
});
const dirRefOutJson = path.join(op, '__dirRef.json');
await fs.writeJSON(dirRefOutJson, { dirs: dirReports });
}
}
}
//# sourceMappingURL=ImageSorter.js.map