541 lines
22 KiB
JavaScript
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);
|
|
},
|
|
|
|
} |