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); }, }