import path from 'path'; import { promises as fs } from 'fs'; import fse from 'fs-extra'; import pMap from 'p-map'; import { getDirectories, normalizeModData, isMod } from '.'; import { getFileMurmurHash2 } from '../../../common/utils'; import { getAddonsByFingerprint, getAddon } from '../../../common/api'; const modsFingerprintsScan = async instancesPath => { const mapFolderToInstance = async instance => { try { const configPath = path.join( path.join(instancesPath, instance, 'config.json') ); const config = await fse.readJSON(configPath); if (!config.loader) { throw new Error(`Config for ${instance} could not be parsed`); } const modsFolder = path.join(instancesPath, instance, 'mods'); const modsFolderExists = await fse.pathExists(modsFolder); if (!modsFolderExists) return { ...config, name: instance }; // Check if config.mods has a different number of mods than the actual number of mods // Count the actual mods inside the folder const files = await fs.readdir(modsFolder); const fileNamesToRemove = []; const missingMods = {}; /* eslint-disable */ // Check for new mods in local storage that are not present in config for (const file of files) { try { const completeFilePath = path.join(modsFolder, file); const stat = await fs.lstat(completeFilePath); if (stat.isFile() && isMod(completeFilePath, instancesPath)) { // Check if file is in config if (!(config?.mods || []).find(mod => mod.fileName === file)) { const murmurHash = await getFileMurmurHash2(completeFilePath); console.log( '[MODS SCANNER] Local mod not found in config', file, murmurHash ); missingMods[file] = murmurHash; } } } catch {} } // Check for old mods in config that are not present on local storage for (const configMod of config?.mods || []) { if (!files.includes(configMod.fileName)) { fileNamesToRemove.push(configMod.fileName); console.log( `[MODS SCANNER] Removing ${configMod.fileName} from config` ); } } /* eslint-enable */ let newMods = config?.mods || []; if (Object.values(missingMods).length !== 0) { let success = false; let tries = 10; while (!success || tries > 10) { try { const data = await getAddonsByFingerprint( Object.values(missingMods) ); const matches = await Promise.all( Object.entries(missingMods).map(async ([fileName, hash]) => { const exactMatch = (data.exactMatches || []).find( v => v.file.fileFingerprint === hash ); if (exactMatch?.file) { let addonData = null; try { addonData = await getAddon(exactMatch.file.modId); return { ...normalizeModData( exactMatch.file, exactMatch.file.modId, addonData.name ), fileName }; } catch { return { fileName, displayName: fileName, packageFingerprint: hash }; } } return { fileName, displayName: fileName, packageFingerprint: hash }; }) ); newMods = [...newMods, ...matches]; success = true; } catch (err) { console.error(err); tries += 1; await new Promise(resolve => { setTimeout(() => { resolve(); }, 5000); }); } } } const filterMods = newMods .filter(_ => _) .filter(v => !fileNamesToRemove.includes(v.fileName)); const newConfig = { ...config, mods: filterMods }; if (JSON.stringify(config) !== JSON.stringify(newConfig)) { await fse.outputJson(configPath, newConfig); return { ...newConfig, name: instance }; } return { ...config, name: instance }; } catch (err) { console.error(err); } return null; }; const folders = await getDirectories(instancesPath); const instances = await pMap( folders.filter(folder => folder !== '.DS_Store'), mapFolderToInstance, { concurrency: 5 } ); const hashMap = {}; // eslint-disable-next-line for (const instance of instances) { // eslint-disable-next-line if (!instance) continue; hashMap[instance.name] = instance; } return hashMap; }; export default modsFingerprintsScan;