2025-04-24 17:03:28 +08:00

541 lines
22 KiB
JavaScript

const Fs = require('fs');
const Path = require('path');
const FsExtra = require('fs-extra');
const crc16 = require('./pannel/crc16.js');
const crc32 = require('./pannel/crc32.js');
const _ = require('lodash');
const crcMethod = false ? crc16 : crc32;
module.exports = {
// 预制体内存缓存.
caches: {}, // 缓存所有预制体解析对象: {prefab_uuid:{prefab_obj}};
uuidMap: {},// 缓存预制体内引用的资源uuid-> (预制体+节点索引) {res_uuid:{prefab_uuid:[0,2,64]}}
// tsUuidMap: {},// 缓存预制体中脚本中引用的资源uuid: {res_uuid:{prefab_uuid:["0.abc.1","1.spriteframe"]}}
updatedMap: {}, // 缓存已经被修改了的预制体内容.
fileGroups: {}, // 存储重复资源列表. {res_crc:[]}
checkProgress: 0,
fontMap: {}, // 缓存项目中的bmpFont信息. 资源剔除时,需要忽略bmpFont内容.
animMap: {}, // 动画资源中的图片资源引用, {"spriteframe_uuid":anim_uuid}; 同一张图片只记一个引用即可, 表示有动画引用了,就可以排除清理.
spineMap: {}, // 缓存项目中spine 的信息, 资源剔除时,忽略spine引用的图片资源.
plistMap: {}, // plist 资源引用. 剔除时忽略plist 资源.
filters: [],
depend0s: [],
moveSwitch: true,
moveNumber: 3,
messages: {
'open'() {
Editor.Panel.open('res-remove');
// 面板加载时,直接显示缓存数据.
setTimeout(() => {
Editor.Ipc.sendToPanel("res-remove", "res-search-finished", this.fileGroups, this.depend0s);
}, 2000);
},
'make-cache'(event) {
// 当打开界面时,手动建立一份所有预制体的cache,以便进行资源查找.
this.makeCache();
if (event.reply) {
event.reply(null);
}
},
'exchange-uuid'(event, savedRes, dropped) {
const newUUid = savedRes ? savedRes.subMetas[0].uuid : null;
let cannotDropList = {};// 当未选择保留时,记录那些即将被删除,但是仍有引用的资源.
let differentFiles = 0; // 统计某个资源被引用的次数,当引用次数大于3时,选中资源将会移动到texture目录中.
dropped.forEach(u => {
const subMetauuid = u.subMetas[0].uuid;
const prefabsKeys = this.uuidMap[subMetauuid];
if (!prefabsKeys) return;
differentFiles++;
for (const pId in prefabsKeys) {
const prefab = this.caches[pId];
if (!prefab) continue;
if (newUUid) {
this.updatedMap[pId] = true;
const updatedInds = prefabsKeys[pId];
for (const id of updatedInds) {
_.set(prefab, id, newUUid);
Editor.log("更新预制体:", pId, id, newUUid);
}
} else {
cannotDropList[u.uuid] = true;
}
}
});
if (cannotDropList.length > 0) {
Editor.warn("有资源仍存在引用,但未勾选默认替换资源,忽略删除")
}
const droppedUrls = dropped.filter(v => {
return Editor.assetdb.existsByUuid(v.uuid) && !cannotDropList[v.uuid];
}).map(v => {
return v.url;
});
if (this.moveSwitch && differentFiles >= this.moveNumber) {
if (!savedRes.url.startsWith("db://assets/Texture/")) {
const dstUrl = savedRes.url.replace('db://assets/', 'db://assets/Texture/');
const dir = Editor.url(dstUrl);
FsExtra.ensureDir(Path.dirname(dir), 0o2775, (err) => {
this.addLog("准备移动资源:", err, dir);
Editor.assetdb.move(savedRes.url, dstUrl, (err, result) => {
if (err) {
Editor.warn("移动资源失败:", savedRes.url, err);
}
Editor.assetdb.refresh(dstUrl, function (err, results) {
});
});
});
}
}
if (droppedUrls && droppedUrls.length > 0) {
Editor.assetdb.delete(droppedUrls, err => {
if (err) {
Editor.error("资源清理异常:", err);
}
if (event.reply) {
event.reply(null, "");
}
});
} else {
if (event.reply) {
event.reply(null, "");
}
}
},
'exchange-uuid-finish'(event) {
Editor.log("准备保存更新...");
let count = Object.keys(this.updatedMap).length - 1;
// 保存更新.
for (const uuid in this.updatedMap) {
const url = Editor.assetdb.uuidToUrl(uuid);
Editor.assetdb.saveExists(url, JSON.stringify(this.caches[uuid], null, 2), function (err, meta) {
if (err) {
Editor.warn("资源更新异常:", url, err);
}
count--;
if (count <= 0) {
Editor.log("引用更新完成!");
if (event.reply) {
event.reply(null, "");
}
}
});
}
},
'remove-unused'(event, unused) {
this.updateProgress(0)
let i = 0;
for (let item of unused) {
Editor.assetdb.delete(item, (err) => {
i++;
if (i >= unused.length) {
this.addLog("清理未引用资源完成:" + unused.length);
if (event.reply) {
event.reply(null, "");
}
}
this.updateProgress(i * 100 / unused.length);
});
}
},
'print-depends'(event, metaUuid) {
Editor.log("查询资源引用...");
const deps = this.uuidMap[metaUuid];
if (!deps) {
this.addLog("未找到引用资源.")
} else {
const len = Object.keys(deps).length;
this.addLog(`共找到${len}处引用`);
for (let uuid in deps) {
const url = Editor.assetdb.uuidToUrl(uuid);
this.addLog(`-->${url}:${deps[uuid].length}处引用`);
}
}
if (event.reply) {
event.reply(null);
}
},
'setting-changed'(event, settings) {
this._settingChanged(settings);
if (event.reply) {
event.reply(null);
}
}
},
load() {
this.caches = {};
this.uuidMap = {};
// this.tsUuidMap = {};
this.updatedMap = {};
this.fileGroups = {};
this.fontMap = {};
this.depend0s = [];
this.animMap = {};
this.spineMap = {};
this.plistMap = {};
Fs.readFile(Editor.url('packages://res-remove/.setting.json'), 'utf-8', (err, res) => {
if (err) {
return;
}
const settingObj = res ? JSON.parse(res) : {};
this._settingChanged(settingObj);
});
},
unload() {
},
addLog(...str) {
Editor.Ipc.sendToPanel("res-remove", "addLog", str.join(':'));
},
_settingChanged(settings) {
Editor.log("设置更新:", settings);
const filters = settings.filters;
if (filters && filters.length > 0) {
this.filters = filters.split(',').map(v => {
return v.trim();
}).filter(v => {
return v.length > 0;
});
} else {
this.filters = [];
}
this.addLog("当前过滤规则", this.filters);
this.moveSwitch = settings.moveSwitch || false;
this.moveNumber = settings.moveNumber || 3;
},
makeCache() {
this.caches = {};
this.uuidMap = {};
// this.tsUuidMap = {};
this.updatedMap = {};
this.fileGroups = {};
this.fontMap = {};
this.depend0s = [];
this.animMap = {};
this.spineMap = {};
this.plistMap = {};
this.updateProgress(0);
// 统计字体文件引用.
const p0 = new Promise(resolve => {
Editor.assetdb.queryMetas("db://assets/**\/*", "bitmap-font", (err, results) => {
this.addLog("查找BmpFont资源引用:", results.length);
results.forEach(value => {
if (value.textureUuid) {
this.fontMap[value.textureUuid] = value.uuid;
}
});
resolve();
});
});
// 查找动画资源引用.
const p1 = new Promise(resolve => {
Editor.assetdb.queryAssets('db://assets/**\/*', ["animation-clip"], (err, results) => {
this._dealAnimClipRefs(results);
resolve();
});
});
//??? 在我的项目中竟然查不到resources中的 anim 资源, 必须要带上resources路径才行,这是什么騒操作.
const p2 = new Promise(resolve => {
Editor.assetdb.queryAssets('db://assets/resources/**\/*', ["animation-clip"], (err, results) => {
this._dealAnimClipRefs(results);
resolve();
});
});
// spine 动画引用.
const p3 = new Promise(resolve => {
Editor.assetdb.queryMetas('db://assets/**\/*', "spine", (err, results) => {
this._dealSpineResRefs(results);
resolve();
});
});
// dragbone动画引用.
const p4 = new Promise(resolve => {
Editor.assetdb.queryAssets('db://assets/**\/*', "dragonbones", (err, results) => {
this._dealDragboneResRefs(results);
resolve();
});
});
// plist引用.
const p5 = new Promise(resolve => {
Editor.assetdb.queryMetas('db://assets/**\/*.plist', null, (err, results) => {
this._dealPlistResRefs(results);
resolve();
});
});
//labelatlas 引用。
const p6 = new Promise(resolve => {
Editor.assetdb.queryMetas('db://assets/**\/*.labelatlas', null, (err, results) => {
this._dealLabelAtlasRefs(results);
resolve();
});
});
// 查找场景/预制体中的资源引用.
Promise.all([p0, p1, p2, p3, p4, p5, p6]).then(() => {
Editor.assetdb.queryAssets("db://assets/**", ["prefab", "scene"], (err, results) => {
const p = 100 / results.length;
results.forEach((prefab, index) => {
// Editor.log("开始索引:", prefab.url);
const tmp = this.caches[prefab.uuid] = JSON.parse(Fs.readFileSync(prefab.path));
if (!tmp || tmp.length <= 0) return;
this.checkPrefabResRef(prefab, tmp);
this.updateProgress(p * index + p);
});
this.onRefresh();
});
})
},
// 设置资源被引用的pref以及key路径.
_updateUuidMap(resUuid, prefUuid, key) {
let tmp = this.uuidMap[resUuid] || {};
this.uuidMap[resUuid] = tmp;
let prf = tmp[prefUuid] || [];
tmp[prefUuid] = prf;
prf.push(key);
},
checkPrefabResRef(prefabMeta, prefabContent) {
// 预制体资源实际存储格式为jsonArray.
for (const [i, v] of prefabContent.entries()) {
// 查找sprite 对应的资源引用,其他包含spriteframe的类型有: Partical, Mask等.
if (v["_spriteFrame"] && v["__type__"].startsWith('cc.')) {
const uuid = v["_spriteFrame"]["__uuid__"];
if (!uuid || uuid.length <= 0) continue;
// 同一个资源uuid,可能对应着多个预制体中的多个引用
this._updateUuidMap(uuid, prefabMeta.uuid, `[${i}]._spriteFrame.__uuid__`);
} else if (v.node) { // 系统脚本或者自定义脚本,都有node属性,这里识别每一个属性,判断里边是否有图片引用,并记录.
for (let p in v) {
if (!v.hasOwnProperty(p)) continue;
if (!v[p] || typeof v[p] != 'object') continue; // 只处理{}|[] 两种情况
if (Array.isArray(v[p])) {
for (let j = 0; j < v[p].length; j++) {
if (v[p][j] && v[p][j].__uuid__) {
const meta = Editor.assetdb.loadMetaByUuid(v[p][j].__uuid__);
// 判断这是不是一个spriteframe的uuid.
if (!meta) continue;
if (meta.rawTextureUuid) { // 判断为图片资源.
// Editor.log(">>>1", p, v.__type__, Object.keys(meta));
const key = `[${i}].${p}.[${j}].__uuid__`;
this._updateUuidMap(meta.uuid, prefabMeta.uuid, key);
}
}
}
} else if (v[p].__uuid__) {
const meta = Editor.assetdb.loadMetaByUuid(v[p].__uuid__);
if (!meta) continue;
if (meta.rawTextureUuid) { // 判断为图片资源.
// Editor.log(">>>", p, v.__type__, Object.keys(meta));
const key = `[${i}].${p}.__uuid__`;
this._updateUuidMap(meta.uuid, prefabMeta.uuid, key);
}
}
}
}
}
},
updateProgress(p) {
Editor.Ipc.sendToPanel("res-remove", "update-progress", p)
},
// 计算艺术字图集资源引用。
async _dealLabelAtlasRefs(res) {
this.addLog("查找艺术字图集引用:",res.length);
for(let i = 0; i< res.length; i++){
const item = res[i];
if(item && item.rawTextureUuid){
this.plistMap[item.rawTextureUuid] = item.uuid;
}
}
},
// 计算动画资源中的图片引用.
async _dealAnimClipRefs(res) {
this.addLog("查找动画anim资源引用:", res.length);
for (let i = 0; i < res.length; i++) {
const animContent = JSON.parse(Fs.readFileSync(res[i].path));
if (animContent.curveData
&& animContent.curveData.comps
&& animContent.curveData.comps["cc.Sprite"]
&& animContent.curveData.comps["cc.Sprite"]["spriteFrame"]) {
const frames = animContent.curveData.comps["cc.Sprite"]["spriteFrame"];
frames.forEach(v => {
if (v.value["__uuid__"]) {
this.animMap[v.value["__uuid__"]] = res[i].url;
}
});
}
}
// Editor.log("anim 资源列表:",this.animMap);
},
async _dealSpineResRefs(res) {
this.addLog("查找动画Spine资源引用:", res.length);
for (let i = 0; i < res.length; i++) {
const item = res[i];
item["textures"].forEach(v => {
this.spineMap[v] = item.uuid;
});
}
},
async _dealDragboneResRefs(res) {
this.addLog("查找动画dragonbone资源引用:", res.length);
for (let i = 0; i < res.length; i++) {
const item = res[i];
// 查询本目录的图片资源,然后加入忽略列表.
Editor.assetdb.queryAssets(Path.dirname(item.url) + "/**", "texture", (err, results) => {
results.forEach(v => {
this.spineMap[v.uuid] = item.uuid;
});
});
}
},
async _dealPlistResRefs(res) {
this.addLog("查找Plist资源引用:", res.length);
for (let i = 0; i < res.length; i++) {
const item = res[i];
if (item.rawTextureUuid) {
this.plistMap[item.rawTextureUuid] = item.uuid;
}
}
},
_insertRow(res) {
if (!res) {
Editor.warn("插入资源为空");
return false;
}
const t = this.fileGroups[res.crc] || [];
const url = res.url;
let findFilter = false;
for (let filter of this.filters) {
if (url.indexOf(filter) >= 0) {
// Editor.warn("资源被过滤:", url, filter);
findFilter = true;
break;
}
}
// 未被过滤掉,则加入排重队列中去.
if (!findFilter && this._filterResRef(res)) {
t.push(res);
// 只有插入了元素后,才对这个key 赋值. 否则不处理.
this.fileGroups[res.crc] = t;
return true;
} else {
// Editor.log("过滤资源:", res.url);
return false;
}
},
async _getFileCrc(path) {
return new Promise(resolve => {
let crc = undefined;
const stream = Fs.createReadStream(path);
stream.on('data', (chunk) => {
crc = crcMethod(chunk, crc)
});
stream.on('end', () => {
stream.close();
resolve(crc);
});
});
},
onRefresh() {
this.addLog("开始查找重复资源...");
this.checkProgress = 0;
Editor.assetdb.queryAssets('db://assets/**\/*', 'texture', function (err, results) {
if (err) {
this.addLog("异常:" + err)
return;
}
this.checkRes(results);
}.bind(this));
},
async checkRes(results) {
this.addLog("开始计算资源重复度", results.length);
const p = 100 / results.length;
this.fileGroups = {};
this.checkProgress = 0;
this.updateProgress(0);
let counter = 0;
for (let i = 0; i < results.length; i++) {
const value = results[i];
const crc = await this._getFileCrc(value.path);
value.crc = (typeof crc === 'number' ? crc : crc.result);
// dealResMeta
const meta = Editor.assetdb.loadMetaByUuid(value.uuid);
if (meta && meta["__subMetas__"]) {
value.crc = `${meta.width}_${meta.height}_${value.crc}`;
const json = meta["__subMetas__"];
value.subMetas = Object.values(json).map(it => {
return {uuid: it.uuid}
});
const subMetauuid = (value.subMetas && value.subMetas[0]) ? value.subMetas[0].uuid : null;
const dp = subMetauuid ? this.uuidMap[subMetauuid] : null;
value.depends = dp ? Object.keys(dp).length : 0;
const ret = this._insertRow(value);
if (ret) {
counter++;
}
} else {
Editor.warn("资源meta为空:", value.url);
value.depends = 0;
}
this.checkProgress += p;
this.updateProgress(this.checkProgress);
}
Editor.log("资源总数:", results.length, counter, Object.keys(this.fileGroups).length,)
this.removeSingles();
},
// 判断资源是否被引用了. 被引用返回false, 未被引用返回true.
_filterResRef(value) {
if (this.fontMap.hasOwnProperty(value.uuid)) {
this.addLog("BmpFont资源中引用图片,排除清理:", value.url);
return false;
}
if (this.spineMap.hasOwnProperty(value.uuid)) {
this.addLog("Spine资源中引用图片,排除清理:", value.url);
return false;
}
if (this.plistMap.hasOwnProperty(value.uuid)) {
this.addLog("Plist资源中引用图片,排除清理:", value.url);
return false;
}
if (value.subMetas) {
// spriteFrame 在动画资源中有使用.
for (let i = 0; i < value.subMetas.length; i++) {
if (this.animMap.hasOwnProperty(value.subMetas[i].uuid)) {
Editor.info("动画资源中引用图片,排除清理:", value.url);
return false;
}
}
}
return true;
},
removeSingles() {
this.addLog("资源计算完成!");
const depend0s = []; // 引用为0的资源,统一归类到一个分类下.
for (let crc in this.fileGroups) {
// 移除仅有非重复资源.
if (this.fileGroups[crc].length <= 1) {
const file = this.fileGroups[crc][0];
if (file && file.depends <= 0) {
depend0s.push(file)
}
delete this.fileGroups[crc];
}
}
// 准备筛选非动画引用/非字体引用资源.
this.depend0s = depend0s;
this.addLog("重复资源数量:" + Object.keys(this.fileGroups).length)
this.addLog("无引用资源数量:" + this.depend0s.length)
Editor.Ipc.sendToPanel("res-remove", "res-search-finished", this.fileGroups, this.depend0s);
},
}