From a00e10b4913a49946e72b89afc6efd22a53a76df Mon Sep 17 00:00:00 2001 From: "J.T. Sage" Date: Mon, 30 Jan 2023 20:09:23 -0500 Subject: [PATCH 01/12] Note even close to working. Version2.0 --- DEVELOP.md | 4 +- lib/modCheckLib.js | 1195 +++++++++++++++++++++ modAssist_main.js | 522 +++------ package.json | 5 +- renderer/main.html | 6 +- renderer/preload/preload-confirmMulti.js | 5 +- renderer/preload/preload-loadingWindow.js | 4 +- renderer/renderJS/assist_ui.js | 192 ++-- renderer/renderJS/confirm_copy_move_ui.js | 81 +- renderer/renderJS/confirm_multi_ui.js | 26 +- renderer/renderJS/fsg_util.js | 12 +- 11 files changed, 1501 insertions(+), 551 deletions(-) create mode 100644 lib/modCheckLib.js diff --git a/DEVELOP.md b/DEVELOP.md index 0d3c2a98..7178b6b0 100644 --- a/DEVELOP.md +++ b/DEVELOP.md @@ -64,13 +64,13 @@ This package provides a number of test cases: See if translation files match keys -`# npm run langtest` +`# npm run lang_test` ### Fix translations If translation files are mismatched, this will fix it (english is master) -`# npm run langfix` +`# npm run lang_fix` ### Check Dependencies diff --git a/lib/modCheckLib.js b/lib/modCheckLib.js new file mode 100644 index 00000000..d9da29ec --- /dev/null +++ b/lib/modCheckLib.js @@ -0,0 +1,1195 @@ +/* _______ __ _______ __ __ + | | |.-----.--| | _ |.-----.-----.|__|.-----.| |_ + | || _ | _ | ||__ --|__ --|| ||__ --|| _| + |__|_|__||_____|_____|___|___||_____|_____||__||_____||____| + (c) 2022-present FSG Modding. MIT License. */ + +// Mod Checker Class + +const fs = require('fs') +const path = require('path') +const admZip = require('adm-zip') +const glob = require('glob') +const xml2js = require('xml2js') +const crypto = require('crypto') +const { decodeDXT, parseDDS } = require('./ddsLibrary') +const PNG = require('pngjs').PNG + + +class modFileCollection { + #ignoreList = [ + '^npm-debug\\.log$', + '^\\..*\\.swp$', + '^Thumbs\\.db$', + '^thumbs\\.db$', + '^ehthumbs\\.db$', + '^Desktop\\.ini$', + '^desktop\\.ini$', + '@eaDir$', + ] + #junkRegex = new RegExp(this.#ignoreList.join('|')) + + #bindConflict = {} + #map_CollectionToFolder = {} + #map_CollectionToFolderRelative = {} + #map_CollectionToFullName = {} + #map_CollectionToName = {} + #map_FolderContents = {} + #map_FolderToCollection = {} + #map_ModUUIDToShortName = {} + #set_Collections = new Set() + + #modHubList = { 'mods' : {}, 'last' : [] } + #modHubVersion = {} + + #list_allMods = {} + #list_newMods = new Set() + + #log = null + #loadingWindow = {} + #localeFunction = null + #map_CollectionNotes = null + #modCache = {} + #modCacheStore = {} + + #userHome = '' + #useSyncSafeMode = false + #skipCache = false + + #scanPromise = [] + + constructor(logger, notes, cache, homeDir, loadingWindow, locale, skipCache = false) { + this.#modCache = cache + this.#log = logger + this.#map_CollectionNotes = notes + this.#userHome = homeDir + this.#skipCache = skipCache + this.#loadingWindow = loadingWindow + this.#localeFunction = locale + } + + mapFolderToCollection(folder) { + return this.#map_FolderToCollection[folder] || null + } + mapCollectionToFolder(collectKey) { + return this.#map_CollectionToFolder[collectKey] + } + mapCollectionToName(collectKey) { + return this.#map_CollectionToName[collectKey] + } + + get bindConflict() { return this.#bindConflict } + get collections() { return this.#set_Collections } + + getModCollection(collectKey) { + return this.#list_allMods[collectKey] + } + + get modHubList() { return this.#modHubList } + set modHubList(newList) { this.#modHubList = newList } + get modHubVersion() { return this.#modHubVersion } + set modHubVersion(newList) { this.#modHubVersion = newList } + + modHubFullRecord(thisMod, asArray = true) { + /* Return [ID, Version, Recent] */ + const modHubID = this.modHubModRecord(thisMod) + + if ( asArray ) { + return [ + modHubID, + this.modHubVersionModHubId(modHubID), + this.#modHubList.last.includes(modHubID) + ] + } + + return { + id : modHubID, + version : this.modHubVersionModHubId(modHubID), + recent : this.#modHubList.last.includes(modHubID), + } + } + modHubModRecord(thisMod) { + return this.#modHubList.mods[thisMod.fileDetail.shortName] || null + } + modHubModUUID(modUUID) { + return this.#modHubList.mods[this.#map_ModUUIDToShortName[modUUID]] || null + } + modHubVersionUUID(modUUID) { + return this.#modHubVersion[this.modHubModUUID(modUUID)] || null + } + modHubVersionModRecord(thisMod) { + return this.#modHubVersion[this.modHubModRecord(thisMod)] || null + } + modHubVersionModHubId(modHubID) { + return this.#modHubVersion[modHubID] || null + } + + modColUUIDToRecord(ID) { + const idParts = ID.split('--') + return this.#list_allMods?.[idParts[0]]?.mods?.[idParts[1]] || null + } + + modColUUIDsToRecords(IDs) { + const theseMods = [] + IDs.forEach((thisColUUID) => { theseMods.push(this.modColUUIDToRecord(thisColUUID)) }) + return theseMods + } + + clearAll() { + this.#scanPromise = [] + this.#map_FolderContents = {} + this.#map_CollectionToFolder = {} + this.#map_CollectionToFolderRelative = {} + this.#map_FolderToCollection = {} + this.#map_CollectionToName = {} + this.#map_CollectionToFullName = {} + this.#map_ModUUIDToShortName = {} + this.#list_allMods = {} + this.#list_newMods = new Set() + this.#set_Collections.clear() + } + + set syncSafe(mode) { this.#useSyncSafeMode = mode } + + addCollection(folder) { + const goodFolderContents = [] + const thisFolderKey = this.#getMD5Hash(folder, 'col_') + const thisRealPath = path.normalize(folder) + const thisShortName = path.basename(thisRealPath) + const thisFullName = this.#populateFullName(thisFolderKey, thisShortName) + + try { + const folderContents = fs.readdirSync(thisRealPath, {withFileTypes : true}) + + folderContents.forEach((thisFile) => { + if ( !this.#junkRegex.test(thisFile.name) ) { goodFolderContents.push(thisFile) } + }) + } catch (e) { + this.#log.log.danger(`Couldn't read folder: ${thisRealPath} :: ${e}`, 'collection-reader') + return null + } + + this.#set_Collections.add(thisFolderKey) + this.#list_allMods[thisFolderKey] = { + alphaSort : [], + dependSet : new Set(), + folderSize : 0, + fullName : thisFullName, + mods : {}, + modSet : new Set(), + name : thisShortName, + } + + this.#map_FolderContents [thisFolderKey] = goodFolderContents + this.#map_CollectionToFolder [thisFolderKey] = thisRealPath + this.#map_CollectionToFolderRelative [thisFolderKey] = this.#toHomeDir(thisRealPath) + this.#map_FolderToCollection [thisRealPath] = thisFolderKey + this.#map_CollectionToName [thisFolderKey] = thisShortName + this.#map_CollectionToFullName [thisFolderKey] = thisFullName + + return { + collectKey : thisFolderKey, + fileCount : goodFolderContents.length, + } + } + + async processMods() { + this.#modCacheStore = this.#modCache.store + + this.#set_Collections.forEach((collectKey) => { + this.#map_FolderContents[collectKey].forEach((thisFile) => { + this.#scanPromise.push(this.#addMod(collectKey, thisFile)) + }) + }) + + Promise.all(this.#scanPromise).then(() => { + this.#modCache.store = this.#modCacheStore + this.#doAlphaSort() + this.#processBindConflicts() + console.log('Promises Finished') + }) + console.log(this.#scanPromise.length) + } + + get processPromise() { return Promise.all(this.#scanPromise) } + + #doAlphaSort() { + this.#set_Collections.forEach((collectKey) => { + this.#list_allMods[collectKey].alphaSort.sort() + }) + } + + #cacheHit(thisFileStats) { + if ( thisFileStats.folder || this.#skipCache || thisFileStats.hashString === null ) { return false } + + return this.#modCacheStore[thisFileStats.hashString] ?? false + } + + #fileStats(collectKey, thisFile) { + const folderLoc = this.#map_CollectionToFolder[collectKey] + const fullPath = path.join(folderLoc, thisFile.name) + const fileStats = { + isFolder : null, + date : null, + birthDate : null, + size : null, + error : false, + hashString : null, + fullPath : fullPath, + } + + try { + if ( thisFile.isSymbolicLink() ) { + const thisSymLink = fs.readlinkSync(fullPath) + const thisSymLinkStat = fs.lstatSync(path.join(folderLoc, thisSymLink)) + + fileStats.isFolder = thisSymLinkStat.isDirectory() + fileStats.date = thisSymLinkStat.ctime + fileStats.birthDate = thisSymLinkStat.birthtime + + if ( !fileStats.isFolder ) { fileStats.size = thisSymLinkStat.size } + } else { + const theseStats = fs.statSync(fullPath) + + fileStats.isFolder = thisFile.isDirectory() + fileStats.date = theseStats.ctime + fileStats.birthDate = theseStats.birthtime + + if ( !fileStats.isFolder ) { fileStats.size = theseStats.size } + } + + if ( fileStats.isFolder ) { + let bytes = 0 + glob.sync('**', { cwd : path.join(fullPath) }).forEach((file) => { + try { + const stats = fs.statSync(path.join(fullPath, file)) + if ( stats.isFile() ) { bytes += stats.size } + } catch { /* Do Nothing if we can't read it. */ } + }) + fileStats.size = bytes + } else { + fileStats.hashString = this.#getMD5Hash(`${thisFile.name}-${fileStats.size}-${(this.#useSyncSafeMode)?fileStats.birthDate.toISOString():fileStats.date.toISOString()}`) + } + } catch (e) { + this.#log.log.warning(`Unable to stat file ${thisFile.name} in ${folderLoc} : ${e}`, 'file-stat') + fileStats.isFolder = false + fileStats.size = 0 + fileStats.date = new Date(1969, 1, 1, 0, 0, 0, 0) + fileStats.birthDate = new Date(1969, 1, 1, 0, 0, 0, 0) + fileStats.error = true + } + return fileStats + } + + #addModToData(collectKey, modRecord) { + modRecord.currentCollection = collectKey + modRecord.colUUID = `${collectKey}--${modRecord.uuid}` + modRecord.modHub = this.modHubFullRecord(modRecord, false) + + this.#map_ModUUIDToShortName[modRecord.uuid] = modRecord.fileDetail.shortName + this.#list_allMods[collectKey].mods[modRecord.uuid] = modRecord + this.#list_allMods[collectKey].folderSize += modRecord.fileDetail.fileSize + this.#list_allMods[collectKey].modSet.add(modRecord.uuid) + this.#list_allMods[collectKey].dependSet.add(modRecord.fileDetail.shortName) + this.#list_allMods[collectKey].alphaSort.push(`${modRecord.fileDetail.shortName}::${modRecord.uuid}`) + } + + #processBindConflicts() { + this.#bindConflict = {} + + this.#set_Collections.forEach((collectKey) => { + this.#bindConflict[collectKey] = {} + + const collectionBinds = {} + + this.#list_allMods[collectKey].modSet.forEach((modUUID) => { + const thisMod = this.#list_allMods[collectKey].mods[modUUID] + + Object.keys(thisMod.modDesc.binds).forEach((actName) => { + thisMod.modDesc.binds[actName].forEach((keyCombo) => { + if ( keyCombo === '' ) { return } + + const safeCat = thisMod.modDesc.actions[actName] || 'UNKNOWN' + const thisCombo = `${safeCat}--${keyCombo}` + + collectionBinds[thisCombo] ??= [] + collectionBinds[thisCombo].push(thisMod.fileDetail.shortName) + }) + }) + }) + + Object.keys(collectionBinds).forEach((keyCombo) => { + if ( collectionBinds[keyCombo].length > 1 ) { + collectionBinds[keyCombo].forEach((modName) => { + this.#bindConflict[collectKey][modName] ??= {} + this.#bindConflict[collectKey][modName][keyCombo] = collectionBinds[keyCombo].filter((w) => w !== modName) + if ( this.#bindConflict[collectKey][modName][keyCombo].length === 0 ) { + delete this.#bindConflict[collectKey][modName][keyCombo] + } + }) + } + }) + }) + + Object.keys(this.#bindConflict).forEach((collectKey) => { + Object.keys(this.#bindConflict[collectKey]).forEach((modName) => { + if ( Object.keys(this.#bindConflict[collectKey][modName]).length === 0 ) { + delete this.#bindConflict[collectKey][modName] + } + }) + }) + } + + async #addMod(collectKey, thisFile) { + const thisFileStats = this.#fileStats(collectKey, thisFile) + + // Check cache + const modInCache = this.#cacheHit(thisFileStats) + if ( modInCache ) { + this.#log.log.debug(`Adding mod FROM cache: ${modInCache.fileDetail.shortName}`, `mod-${modInCache.uuid}`) + this.#addModToData(collectKey, modInCache) + this.#loadingWindow.count() + return true + } + + if ( !thisFileStats.isFolder && !thisFile.name.endsWith('.zip') ) { + const thisModRecord = new notModFileChecker( + thisFileStats.fullPath, + false, + thisFileStats.size, + thisFileStats.date, + this.#log + ) + this.#addModToData(collectKey, thisModRecord) + this.#loadingWindow.count() + return true + } + + try { + const thisModRecord = new modFileChecker( + thisFileStats.fullPath, + thisFileStats.isFolder, + thisFileStats.size, + (this.#useSyncSafeMode) ? thisFileStats.birthDate : thisFileStats.date, + thisFileStats.hashString, + this.#log, + this.#localeFunction + ) + + const thisModRecordStore = thisModRecord.storable + + this.#addModToData(collectKey, thisModRecordStore) + + if ( thisFileStats.hashString !== null ) { + this.#log.log.info('Adding mod to cache', `mod-${thisModRecordStore.uuid}`) + this.#list_newMods.add(thisFileStats.hashString) + this.#modCacheStore[thisFileStats.hashString] = thisModRecordStore + } + this.#loadingWindow.count() + } catch (e) { + this.#log.log.danger(`Couldn't process file: ${thisFileStats.fullPath} :: ${e}`, 'collection-reader') + const thisBadMod = new notModFileChecker( + thisFileStats.fullPath, + false, + thisFileStats.size, + thisFileStats.date, + this.#log + ) + this.#addModToData(collectKey, thisBadMod) + this.#loadingWindow.count() + } + } + + async toRenderer(extra = null) { + this.#log.log.debug('Collection Render Return Called', 'collection-reader') + + return Promise.all(this.#scanPromise).then(() => { + this.#log.log.debug('Collection Render Return Firing', 'collection-reader') + + return { + opts : extra, + bindConflict : this.#bindConflict, + modList : this.#list_allMods, + set_Collections : this.#set_Collections, + collectionToFolder : this.#map_CollectionToFolder, + collectionToFolderRelative : this.#map_CollectionToFolderRelative, + folderToCollection : this.#map_FolderToCollection, + collectionToName : this.#map_CollectionToName, + collectionToFullName : this.#map_CollectionToFullName, + collectionNotes : this.#map_CollectionNotes.store, + newMods : this.#list_newMods, + modHub : { + list : this.#modHubList, + version : this.#modHubVersion, + }, + } + }) + } + + #toHomeDir(folder) { + return folder.replaceAll(this.#userHome, '~') + } + + #populateFullName(collectKey, shortName) { + const tagLine = this.#map_CollectionNotes.get(`${collectKey}.notes_tagline`, null) + + return `${shortName}${tagLine === null ? '' : ` [${tagLine}]`}` + } + + #getMD5Hash(text, prefix = '') { + return `${prefix}${crypto.createHash('md5').update(text).digest('hex')}` + } +} + +class modFileChecker { + #maxFilesType = { grle : 10, png : 128, txt : 2, pdf : 1 } + + #fileSizeMap = { + dds : ( 12 * 1024 * 1024 ), + xml : ( 0.25 * 1024 * 1024 ), + shapes : ( 256 * 1024 * 1024 ), + cache : ( 10 * 1024 * 1024 ), + gdm : ( 18 * 1024 * 1024 ), + } + + #failFlags = { + first_digit : false, + probable_copy : false, + probable_zippack : false, + other_archive : false, + name_failed : false, + garbage_file : false, + bad_zip : false, + no_modDesc : false, + bad_modDesc : false, + bad_modDesc_no_rec : false, + bad_modDesc_ver : false, + no_modVer : false, + no_modIcon : false, + folder_needs_zip : false, + might_be_crack : false, + has_extra_files : false, + png_texture : false, + dds_too_big : false, // 12MB + xml_too_big : false, // 0.25MB + i3d_too_big : false, // 10MB + shapes_too_big : false, // 256MB + gdm_too_big : false, // 18MB + grle_too_many : false, // 10 + png_too_many : false, // 128 + pdf_too_many : false, + txt_too_many : false, + space_in_file : false, // (internal files) + l10n_not_set : false, // set on processL10n if either null + } + #failMessages = { + first_digit : 'FILE_ERROR_NAME_STARTS_DIGIT', + probable_copy : 'FILE_ERROR_LIKELY_COPY', + probable_zippack : 'FILE_ERROR_LIKELY_ZIP_PACK', + other_archive : 'FILE_ERROR_UNSUPPORTED_ARCHIVE', + name_failed : 'FILE_ERROR_NAME_INVALID', + garbage_file : 'FILE_ERROR_GARBAGE_FILE', + bad_zip : 'FILE_ERROR_UNREADABLE_ZIP', + no_modDesc : 'NOT_MOD_MODDESC_MISSING', + bad_modDesc : 'MOD_ERROR_MODDESC_DAMAGED_RECOVERABLE', + bad_modDesc_no_rec : 'NOT_MOD_MODDESC_PARSE_ERROR', + bad_modDesc_ver : 'NOT_MOD_MODDESC_VERSION_OLD_OR_MISSING', + no_modVer : 'MOD_ERROR_NO_MOD_VERSION', + no_modIcon : 'MOD_ERROR_NO_MOD_ICON', + folder_needs_zip : 'INFO_NO_MULTIPLAYER_UNZIPPED', + might_be_crack : 'INFO_MIGHT_BE_PIRACY', + has_extra_files : 'PERF_HAS_EXTRA', + png_texture : 'PREF_PNG_TEXTURE', + dds_too_big : 'PERF_DDS_TOO_BIG', // 12MB + xml_too_big : 'PERF_XML_TOO_BIG', // 0.25MB + i3d_too_big : 'PERF_I3D_TOO_BIG', // 10MB + shapes_too_big : 'PERF_SHAPES_TOO_BIG', // 256MB + gdm_too_big : 'PERF_GDM_TOO_BIG', // 18MB + grle_too_many : 'PERF_GRLE_TOO_MANY', // 10 + pdf_too_many : 'PERF_PDF_TOO_MANY', // 1 + txt_too_many : 'PERF_TXT_TOO_MANY', // 2 + png_too_many : 'PERF_PNG_TOO_MANY', // 128 + space_in_file : 'PERF_SPACE_IN_FILE', // (internal files) + l10n_not_set : 'PERF_L10N_NOT_SET', // set on processL10n if either null + } + + #flags_broken = [ + 'first_digit', 'probable_copy', 'probable_zippack', + 'other_archive', 'name_failed', 'garbage_file', + 'bad_zip', 'no_modDesc', 'bad_modDesc_no_rec', + 'bad_modDesc_ver', 'no_modVer', + ] + + #flags_problem = [ + 'might_be_crack', 'bad_modDesc', 'dds_too_big', 'xml_too_big', + 'i3d_too_big', 'shapes_too_big', 'gdm_too_big', 'grle_too_many', + 'png_too_many', 'space_in_file', 'l10n_not_set', 'has_extra_files', + 'png_texture', 'pdf_too_many', 'txt_too_many', 'no_modIcon', + ] + + modDesc = { + actions : {}, + binds : {}, + descVersion : 0, + version : '0.0.0.0', + author : 'n/a', + storeItems : 0, + scriptFiles : 0, + iconFileName : false, + iconImageCache : null, + multiPlayer : false, + xmlDoc : false, + xmlParsed : false, + depend : [], + } + + issues = [] + + l10n = { + title : null, + description : null, + } + + md5Sum = null + uuid = null + currentCollection = null + + fileDetail = { + isFolder : false, + fullPath : false, + shortName : false, + fileSize : 0, + fileDate : null, + copyName : false, + imageNonDDS : [], + imageDDS : [], + i3dFiles : [], + extraFiles : [], + tooBigFiles : [], + spaceFiles : [], + pngTexture : [], + } + + badges = '' + canNotUse = false + currentLocale = null + + #locale = false + #log = null + #logUUID = null + + constructor( filePath, isFolder, size, date, md5Pre = null, log = null, locale = null ) { + this.fileDetail.fullPath = filePath + this.fileDetail.isFolder = isFolder + this.fileDetail.fileSize = size + this.fileDetail.fileDate = date.toISOString() + + this.uuid = crypto.createHash('md5').update(filePath).digest('hex') + + this.#locale = locale + this.#log = log + this.#logUUID = `mod-${this.uuid}` + this.md5Sum = md5Pre + + this.fileDetail.shortName = path.parse(this.fileDetail.fullPath).name + + this.#log.log.info(`Adding Mod File: ${this.fileDetail.shortName}`, this.#logUUID) + + this.#failFlags.folder_needs_zip = this.fileDetail.isFolder + + if ( ! this.#isFileNameBad() ) { + if ( ! this.fileDetail.isFolder ) { + this.#testZip() + + if ( this.#failFlags.no_modDesc ) { this.md5Sum = null } + + } else { + this.#testFolder() + } + } + + this.populateL10n() + this.badgeArray = this.#getBadges() + this.issues = this.#populateIssues() + this.currentLocal = this.#locale() + } + + get debugDump() { + const retObj = this.storable + if ( retObj.modDesc.iconImageCache !== null ) { + retObj.modDesc.iconImageCache = `${retObj.modDesc.iconImageCache.length} base64 bytes` + } + retObj.flags = this.#failFlags + return retObj + } + + get storable() { + const storable = { + canNotUse : this.canNotUse, + md5Sum : this.md5Sum, + fileDetail : this.fileDetail, + issues : this.issues, + badgeArray : this.badgeArray, + l10n : this.l10n, + modDesc : {}, + uuid : this.uuid, + currentCollection : this.currentCollection, + } + Object.keys(this.modDesc).forEach((key) => { + if ( ! key.startsWith('xml') ) { + storable.modDesc[key] = this.modDesc[key] + } + }) + return storable + } + + populateL10n() { + this.l10n.title = this.#getLocalString('title') + this.l10n.description = this.#getLocalString('description') + + if ( this.l10n.title === null || this.l10n.description === null ) { + this.l10n.title = 'n/a' + this.l10n.description = '' + this.#failFlags.l10n_not_set = true + } + } + + #populateIssues() { + const issues = [] + Object.keys(this.#failFlags).forEach((flag) => { + if ( this.#failFlags[flag] === true ) { + issues.push(this.#failMessages[flag]) + } + }) + return issues + } + + #getLocalString(key) { + if ( this.modDesc.xmlParsed === false ) { return null } + + if ( ! this.#nestedXMLProperty(`moddesc.${key.toLowerCase()}`) ) { + return null + } + const searchTree = this.modDesc.xmlParsed.moddesc[key.toLowerCase()][0] + + if ( Object.prototype.hasOwnProperty.call(searchTree, this.#locale()) ) { + return searchTree[this.#locale()][0].trim() + } + if ( Object.prototype.hasOwnProperty.call(searchTree, 'en') ) { + return searchTree.en[0].trim() + } + if ( Object.prototype.hasOwnProperty.call(searchTree, 'de') ) { + return searchTree.de[0].trim() + } + return null + } + + #getBadges() { + const badges = { + broken : false, + problem : false, + noMP : ! this.modDesc.multiPlayer, + PCOnly : (this.modDesc.scriptFiles > 0), + folder : this.fileDetail.isFolder, + notmod : this.#failFlags.no_modDesc, + } + + if ( this.fileDetail.isFolder ) { badges.noMP = true } + + this.#flags_broken.forEach((flag) => { + if ( this.#failFlags[flag] ) { badges.broken = true; this.canNotUse = true } + }) + this.#flags_problem.forEach((flag) => { + if ( this.#failFlags[flag] ) { badges.problem = true } + }) + + const badgeCollection = [] + Object.keys(badges).forEach((badge) => { + if ( badges[badge] === true ) { + badgeCollection.push(badge) + } + }) + + return badgeCollection + } + + #isFileNameBad() { + const fullModPath = this.fileDetail.fullPath + const shortName = this.fileDetail.shortName + + if ( ! this.fileDetail.isFolder && ! fullModPath.endsWith('.zip') ) { + if ( fullModPath.endsWith('.rar') || fullModPath.endsWith('.7z') ) { + this.#failFlags.other_archive = true + } else { + this.#failFlags.garbage_file = true + } + this.#failFlags.name_failed = true + } + + if ( shortName.match(/unzip/i) ) { + this.#failFlags.probable_zippack = true + } + + if ( shortName.match(/^[0-9]/) ) { + this.#failFlags.first_digit = true + this.#failFlags.name_failed = true + } + + if ( ! shortName.match(/^[a-zA-Z][a-zA-Z0-9_]+$/) ) { + this.#failFlags.name_failed = true + + if ( ! this.#failFlags.first_digit && ! this.#failFlags.garbage_file ) { + const winCopy = shortName.match(/^([a-zA-Z][a-zA-Z0-9_]+) - .+$/) + const dlCopy = shortName.match(/^([a-zA-Z][a-zA-Z0-9_]+) \(.+$/) + + if ( winCopy !== null ) { + this.#failFlags.probable_copy = true + this.fileDetail.copyName = winCopy[1] + } + if ( dlCopy !== null ) { + this.#failFlags.probable_copy = true + this.fileDetail.copyName = dlCopy[1] + } + } + } + return this.#failFlags.name_failed + } + + #processModDesc() { + const XMLOptions = {strict : true, async : false, normalizeTags : true, attrNameProcessors : [function(name) { return name.toUpperCase() }] } + const strictXMLParser = new xml2js.Parser(XMLOptions) + + /* Read modDesc.xml */ + strictXMLParser.parseString(this.modDesc.xmlDoc, (err, result) => { + if ( err !== null ) { + /* XML Parse failed, lets try to recover */ + this.#log.log.warning(`Caught XML Parse error: ${err}`, this.#logUUID) + this.#failFlags.bad_modDesc = true + XMLOptions.strict = false + const looseXMLParser = new xml2js.Parser(XMLOptions) + + looseXMLParser.parseString(this.modDesc.xmlDoc, (err, result) => { + if ( err !== null ) { + /* Couldn't recover */ + this.#log.log.warning(`Caught unrecoverable XML Parse error: ${err}`, this.#logUUID) + this.#failFlags.bad_modDesc_no_rec = true + return false + } + this.modDesc.xmlParsed = result + }) + } else { + this.modDesc.xmlParsed = result + } + }) + + /* Get modDesc.xml version */ + if ( this.#nestedXMLProperty('moddesc.$.DESCVERSION') ) { + this.modDesc.descVersion = parseInt(this.modDesc.xmlParsed.moddesc.$.DESCVERSION) + if ( this.modDesc.descVersion < 60 ) { + this.#failFlags.bad_modDesc_ver = true + } + } else { + this.#failFlags.bad_modDesc_ver = true + return false + } + + /* Get MOD Version */ + if ( this.#nestedXMLProperty('moddesc.version') ) { + this.modDesc.version = this.modDesc.xmlParsed.moddesc.version.toString() + } else { + this.#failFlags.no_modVer = true + return false + } + + /* Set the mod author (safe fail, I think) */ + if ( this.#nestedXMLProperty('moddesc.author') ) { + this.modDesc.author = this.modDesc.xmlParsed.moddesc.author.toString() + } + + if ( this.#nestedXMLProperty('moddesc.multiplayer') ) { + try { + if ( this.modDesc.xmlParsed.moddesc.multiplayer[0].$.SUPPORTED === 'true' ) { + this.modDesc.multiPlayer = true + } + } catch { + this.modDesc.multiPlayer = false + } + } + + /* Count storeitems */ + if ( this.#nestedXMLProperty('moddesc.storeitems') ) { + try { + this.modDesc.storeItems = this.modDesc.xmlParsed.moddesc.storeitems[0].storeitem.length + } catch { + this.modDesc.storeItems = 0 + } + } + + /* Get icon filename */ + if ( this.#nestedXMLProperty('moddesc.iconfilename') ) { + // NOTE: don't attempt to load png, if it's there. We can't read it anyway + let tempIcon = this.modDesc.xmlParsed.moddesc.iconfilename[0].toString() + if ( ! tempIcon.endsWith('.dds') ) { + tempIcon = `${tempIcon.slice(0, -4)}.dds` + } + if ( this.fileDetail.imageDDS.includes(tempIcon) ) { + this.modDesc.iconFileName = tempIcon + } else { + this.#failFlags.no_modIcon = true + } + } else { + this.#failFlags.no_modIcon = true + return false + } + + if ( this.#nestedXMLProperty('moddesc.productid') ) { + this.#failFlags.might_be_crack = true + } + + try { + if ( this.#nestedXMLProperty('moddesc.dependencies') ) { + this.modDesc.xmlParsed.moddesc.dependencies[0].dependency.forEach((dep) => { + this.modDesc.depend.push(dep) + }) + } + } catch (e) { + this.#log.log.warning(`Dependency processing failed : ${e}`, this.#logUUID) + } + + try { + if ( this.#nestedXMLProperty('moddesc.actions') ) { + this.modDesc.xmlParsed.moddesc.actions[0].action.forEach((action) => { + this.modDesc.actions[action.$.NAME] = action.$.CATEGORY || 'ALL' + }) + } + if ( this.#nestedXMLProperty('moddesc.inputbinding')) { + this.modDesc.xmlParsed.moddesc.inputbinding[0].actionbinding.forEach((action) => { + const thisActionName = action.$.ACTION + + action.binding.forEach((binding) => { + if ( binding.$.DEVICE === 'KB_MOUSE_DEFAULT' ) { + this.modDesc.binds[thisActionName] ??= [] + this.modDesc.binds[thisActionName].push(binding.$.INPUT) + } + }) + }) + } + } catch (e) { + this.#log.log.warning(`Key binding read failed : ${e}`, this.#logUUID) + } + return true + } + + + #testZip() { + let zipFile = null + let zipEntries = null + + try { + zipFile = new admZip(this.fileDetail.fullPath) + zipEntries = zipFile.getEntries() + } catch (e) { + this.#failFlags.bad_zip = true + this.#log.log.warning(`Zip file failure: ${e}`, this.#logUUID) + return + } + + zipEntries.forEach((entry) => { + this.#checkInternalFile( + path.extname(entry.entryName), + entry.entryName, + entry.header.size + ) + }) + + this.#failFlags.grle_too_many = ( this.#maxFilesType.grle < 1 ) + this.#failFlags.png_too_many = ( this.#maxFilesType.png < 1 ) + this.#failFlags.pdf_too_many = ( this.#maxFilesType.pdf < 1 ) + this.#failFlags.txt_too_many = ( this.#maxFilesType.txt < 1 ) + + try { + if ( zipFile.getEntry('modDesc.xml') === null ) { + throw 'File does not exist' + } + this.modDesc.xmlDoc = zipFile.readAsText('modDesc.xml') + } catch (e) { + this.#failFlags.no_modDesc = true + this.#log.log.notice(`Zip file missing modDesc.xml: ${e}`, this.#logUUID) + } + + if ( ! this.#failFlags.no_modDesc ) { + this.#processModDesc() + } + + if ( this.#failFlags.bad_zip || this.#failFlags.no_modIcon ) { return } + + try { + if ( zipFile.getEntry(this.modDesc.iconFileName) === null ) { + throw 'File does not Exist' + } + const iconFile = zipFile.readFile(this.modDesc.iconFileName) + this.#processIcon(iconFile.buffer) + } catch (e) { + this.#failFlags.no_modIcon = true + this.#log.log.notice(`Caught icon fail: ${e}`, this.#logUUID) + } + + zipFile = null + } + + + #testFolder() { + if ( ! fs.existsSync(path.join(this.fileDetail.fullPath, 'modDesc.xml')) ) { + this.#failFlags.no_modDesc = true + return false + } + + try { + const data = fs.readFileSync(path.join(this.fileDetail.fullPath, 'modDesc.xml'), 'utf8') + this.modDesc.xmlDoc = data + } catch (e) { + this.#log.log.warning(`Couldn't read modDesc.xml: ${e}`, this.#logUUID) + this.#failFlags.bad_modDesc = true + return false + } + + const allFileList = glob.sync('**', { cwd : this.fileDetail.fullPath, mark : true }) + + for ( const checkFile of allFileList ) { + const fileStats = fs.statSync(path.join(this.fileDetail.fullPath, checkFile)) + + this.#checkInternalFile( + path.extname(checkFile), + checkFile, + fileStats.size + ) + } + + this.#failFlags.grle_too_many = ( this.#maxFilesType.grle < 1 ) + this.#failFlags.png_too_many = ( this.#maxFilesType.png < 1 ) + this.#failFlags.pdf_too_many = ( this.#maxFilesType.pdf < 1 ) + this.#failFlags.txt_too_many = ( this.#maxFilesType.txt < 1 ) + + if ( ! this.#failFlags.no_modDesc ) { + this.#processModDesc() + } + + try { + const ddsBuffer = fs.readFileSync(path.join(this.fileDetail.fullPath, this.modDesc.iconFileName), null) + this.#processIcon(ddsBuffer.buffer) + } catch (e) { + this.#failFlags.no_modIcon = true + this.#log.log.notice(`Caught icon fail: ${e}`, this.#logUUID) + } + } + + + #nestedXMLProperty (propertyPath, passedObj = false) { + if (!propertyPath) { return false } + + const properties = propertyPath.split('.') + let obj = ( passedObj === false ? this.modDesc.xmlParsed : passedObj ) + + for (let i = 0; i < properties.length; i++) { + const prop = properties[i] + + if (!obj || !Object.prototype.hasOwnProperty.call(obj, prop)) { + return false + } + + obj = obj[prop] + } + + return true + } + + #checkInternalFile(suffix, fileName, size) { + if ( fileName.includes(' ') ) { + this.fileDetail.spaceFiles.push(fileName) + this.#failFlags.space_in_file = true + } + + if ( !fileName.endsWith('/') && !fileName.endsWith('\\') ) { + switch (suffix) { + case '.png' : + this.#maxFilesType.png-- + this.fileDetail.imageNonDDS.push(fileName) + this.fileDetail.pngTexture.push(fileName) + break + case '.dds' : + this.fileDetail.imageDDS.push(fileName) + if ( size > this.#fileSizeMap.dds ) { + this.fileDetail.tooBigFiles.push(fileName) + this.#failFlags.dds_too_big = true + } + break + case '.i3d' : + this.fileDetail.i3dFiles.push(fileName) + break + case '.shapes' : + if ( size > this.#fileSizeMap.shapes ) { + this.fileDetail.tooBigFiles.push(fileName) + this.#failFlags.i3d_too_big = true + } + break + case '.lua' : + this.modDesc.scriptFiles++ + break + case '.gdm' : + if ( size > this.#fileSizeMap.gdm ) { + this.fileDetail.tooBigFiles.push(fileName) + this.#failFlags.gdm_too_big = true + } + break + case '.cache' : + if ( size > this.#fileSizeMap.cache ) { + this.fileDetail.tooBigFiles.push(fileName) + this.#failFlags.i3d_too_big = true + } + break + case '.xml' : + if ( size > this.#fileSizeMap.xml ) { + this.fileDetail.tooBigFiles.push(fileName) + this.#failFlags.xml_too_big = true + } + break + case '.grle' : + this.#maxFilesType.grle-- + break + case '.pdf' : + this.#maxFilesType.pdf-- + break + case '.txt' : + this.#maxFilesType.txt-- + break + case '.l64' : + case '.dat' : + this.fileDetail.extraFiles.push(fileName) + this.#failFlags.might_be_crack = true + break + case '.gls' : + case '.anim' : + case '.ogg' : + break + default : + this.fileDetail.extraFiles.push(fileName) + this.#failFlags.has_extra_files = true + } + } + } + + #processIcon(buffer) { + if ( buffer === null ) { + this.modDesc.iconImageCache = null + return true + } + try { + const ddsData = parseDDS(buffer) + + // get the first mipmap texture + const image = ddsData.images[0] + const imageWidth = image.shape[0] + const imageHeight = image.shape[1] + const imageDataView = new DataView(buffer, image.offset, image.length) + + // convert the DXT texture to an Uint8Array containing RGBA data + const rgbaData = decodeDXT(imageDataView, imageWidth, imageHeight, ddsData.format) + + // make a new PNG image of same width and height, pipe in raw RGBA data + const pngData = new PNG({ width : imageWidth, height : imageHeight }) + + pngData.data = rgbaData + + try { + // Dump out PNG, base64 encode it. + const pngBuffer = PNG.sync.write(pngData) + + this.modDesc.iconImageCache = `data:image/png;base64, ${pngBuffer.toString('base64')}` + } catch { + this.modDesc.iconImageCache = null + return false + } + + return false + } catch (err) { + this.#log.log.notice(this.fileDetail.shortName, `Unknown icon processing error: ${err}`) + return true + } + } +} + +class notModFileChecker { + modDesc = { + actions : {}, + binds : {}, + descVersion : 0, + version : 'n/a', + author : 'n/a', + storeItems : 0, + scriptFiles : 0, + iconFileName : false, + iconImageCache : null, + multiPlayer : false, + xmlDoc : false, + xmlParsed : false, + } + + issues = [ + 'FILE_ERROR_NAME_INVALID', + 'FILE_ERROR_GARBAGE_FILE', + ] + + l10n = { + title : 'n/a', + description : 'n/a', + } + + md5Sum = null + uuid = null + currentCollection = null + + fileDetail = { + isFolder : false, + fullPath : false, + shortName : false, + fileSize : 0, + fileDate : null, + copyName : false, + imageNonDDS : [], + imageDDS : [], + i3dFiles : [], + extraFiles : [], + tooBigFiles : [], + spaceFiles : [], + pngTexture : [], + } + + badgeArray = ['broken', 'notmod'] + canNotUse = true + currentLocale = null + + #log = null + #logUUID = null + + constructor( filePath, isFolder, size, date, log = null ) { + this.fileDetail.fullPath = filePath + this.fileDetail.size = size + this.fileDetail.fileDate = date.toISOString() + + this.#log = log + this.uuid = crypto.createHash('md5').update(filePath).digest('hex') + this.#logUUID = `mod-${this.uuid}` + + this.#log.log.info(`Adding NON Mod File: ${filePath}`, this.#logUUID) + + this.fileDetail.shortName = path.basename(this.fileDetail.fullPath) + } +} + +module.exports = { + modFileCollection : modFileCollection, + modFileChecker : modFileChecker, + notModFileChecker : notModFileChecker, +} + + diff --git a/modAssist_main.js b/modAssist_main.js index f7be1837..6ac88c8a 100644 --- a/modAssist_main.js +++ b/modAssist_main.js @@ -31,43 +31,26 @@ log.log.info(` - Electron Version: ${process.versions.electron}`) log.log.info(` - Chrome Version: ${process.versions.chrome}`) - -process.on('uncaughtException', (err, origin) => { +function handleUnhandled(type, err, origin) { const rightNow = new Date() fs.appendFileSync( crashLog, - `Exception Timestamp : ${rightNow.toISOString()}\n\nCaught exception: ${err}\n\nException origin: ${origin}\n\n${err.stack}` + `${type} Timestamp : ${rightNow.toISOString()}\n\nCaught ${type}: ${err}\n\nOrigin: ${origin}\n\n${err.stack}` ) if ( !isNetworkError(err) ) { dialog.showMessageBoxSync(null, { - title : 'Uncaught Error - Quitting', - message : `Caught exception: ${err}\n\nException origin: ${origin}\n\n${err.stack}\n\n\nCan't Continue, exiting now!\n\nTo send file, please see ${crashLog}`, + title : `Uncaught ${type} - Quitting`, + message : `Caught ${type}: ${err}\n\nOrigin: ${origin}\n\n${err.stack}\n\n\nCan't Continue, exiting now!\n\nTo send file, please see ${crashLog}`, type : 'error', }) - gameLogFile.close() + if ( gameLogFile ) { gameLogFile.close() } app.quit() } else { - log.log.debug(`Network error: ${err}`, 'net-error-exception') + log.log.debug(`Network error: ${err}`, `net-error-${type}`) } -}) -process.on('unhandledRejection', (err, origin) => { - const rightNow = new Date() - fs.appendFileSync( - crashLog, - `Rejection Timestamp : ${rightNow.toISOString()}\n\nCaught rejection: ${err}\n\nRejection origin: ${origin}\n\n${err.stack}` - ) - if ( !isNetworkError(err) ) { - dialog.showMessageBoxSync(null, { - title : 'Uncaught Error - Quitting', - message : `Caught rejection: ${err}\n\nRejection origin: ${origin}\n\n${err.stack}\n\n\nCan't Continue, exiting now!\n\nTo send file, please see ${crashLog}`, - type : 'error', - }) - gameLogFile.close() - app.quit() - } else { - log.log.debug(`Network error: ${err}`, 'net-error-rejection') - } -}) +} +process.on('uncaughtException', (err, origin) => { handleUnhandled('exception', err, origin) }) +process.on('unhandledRejection', (err, origin) => { handleUnhandled('rejection', err, origin) }) const translator = require('./lib/translate.js') const myTranslator = new translator.translator(translator.getSystemLocale()) @@ -106,7 +89,7 @@ if ( process.platform === 'win32' && app.isPackaged && gotTheLock && !isPortable dialog.showMessageBox(windows.main, dialogOpts).then((returnValue) => { if (returnValue.response === 0) { if ( tray ) { tray.destroy() } - gameLogFile.close() + if ( gameLogFile ) { gameLogFile.close() } Object.keys(windows).forEach((thisWin) => { if ( thisWin !== 'main' && windows[thisWin] !== null ) { windows[thisWin].destroy() @@ -124,10 +107,7 @@ if ( process.platform === 'win32' && app.isPackaged && gotTheLock && !isPortable }, ( 30 * 60 * 1000)) } -const glob = require('glob') -const fxml = require('fast-xml-parser') -const crypto = require('crypto') - +const fxml = require('fast-xml-parser') const userHome = require('os').homedir() const pathRender = path.join(app.getAppPath(), 'renderer') const pathPreload = path.join(pathRender, 'preload') @@ -151,14 +131,10 @@ const gameGuesses = [ 'C:\\Program Files (x86)\\Steam\\steamapps\\common\\Farming Simulator 22' ] const pathGuesses = [ + path.join(app.getPath('documents'), 'My Games', 'FarmingSimulator2022'), path.join(userHome, 'OneDrive', 'Documents', 'My Games', 'FarmingSimulator2022'), path.join(userHome, 'Documents', 'My Games', 'FarmingSimulator2022') ] -try { - const winUtil = require('windows') - const userFolder = winUtil.registry('HKEY_CURRENT_USER/SOFTWARE/Microsoft/Windows/CurrentVersion/Explorer/User Shell Folders').Personal.value - pathGuesses.unshift(path.join(userFolder, 'My Games', 'FarmingSimulator2022')) -} catch { /* do nothing */ } gameGuesses.forEach((testPath) => { if ( fs.existsSync(path.join(testPath, gameExeName)) ) { @@ -173,7 +149,7 @@ pathGuesses.forEach((testPath) => { } }) -const { modFileChecker, notModFileChecker } = require('./lib/single-mod-checker.js') +const { modFileCollection } = require('./lib/modCheckLib.js') const winDef = (w, h) => { return { x : { type : 'number', default : -1 }, @@ -234,32 +210,24 @@ const mcStore = new Store({schema : settingsSchema, migrations : settingsMig, cl const maCache = new Store({name : 'mod_cache', clearInvalidConfig : true}) const modNote = new Store({name : 'col_notes', clearInvalidConfig : true}) -const newModsList = [] - -let modFolders = new Set() -let modFoldersMap = {} -let modList = {} -let bindConflict = {} -let countTotal = 0 -let countMods = 0 -let modHubList = {'mods' : {}, 'last' : []} -let modHubVersion = {} -let lastFolderLoc = null -let lastGameSettings = {} - +const modCollect = new modFileCollection( + log, + modNote, + maCache, + app.getPath('home'), + { + hide : loadingWindow_hide, + count : loadingWindow_current, + }, + myTranslator.deferCurrentLocale, + skipCache +) -const ignoreList = [ - '^npm-debug\\.log$', - '^\\..*\\.swp$', - '^Thumbs\\.db$', - '^thumbs\\.db$', - '^ehthumbs\\.db$', - '^Desktop\\.ini$', - '^desktop\\.ini$', - '@eaDir$', -] +const loadWindowCount = { total : 0, current : 0} -const junkRegex = new RegExp(ignoreList.join('|')) +let modFolders = new Set() +let lastFolderLoc = null +let lastGameSettings = {} let tray = null const windows = { @@ -332,14 +300,14 @@ mcStore.set('cache_version', app.getVersion()) function debugDangerCallback() { - if ( windows.main !== null ) { - windows.main.webContents.send('fromMain_debugLogDanger') - } + if ( windows.main !== null ) { windows.main.webContents.send('fromMain_debugLogDanger') } } + /* _ _ ____ _ _ ____ _____ _ _ ___ ( \/\/ )(_ _)( \( )( _ \ ( _ )( \/\/ )/ __) ) ( _)(_ ) ( )(_) ) )(_)( ) ( \__ \ (__/\__)(____)(_)\_)(____/ (_____)(__/\__)(___/ */ + function destroyAndFocus(winName) { windows[winName] = null if ( windows.main !== null ) { windows.main.focus() } @@ -535,14 +503,15 @@ function createMainWindow () { }) } -function createConfirmFav(mods, destinations) { - if ( mods.length < 1 ) { return } +function createConfirmFav(windowArgs) { if ( windows.confirm ) { windows.confirm.focus(); return } windows.confirm = createSubWindow('confirm', { parent : 'main', preload : 'confirmMulti', fixed : true }) - windows.confirm.webContents.on('did-finish-load', async (event) => { - event.sender.send('fromMain_confirmList', mods, destinations, modList) + windows.confirm.webContents.on('did-finish-load', async () => { + sendModList(windowArgs, 'fromMain_confirmList', 'confirm', false) + + if ( devDebug ) { windows.confirm.webContents.openDevTools() } }) windows.confirm.loadFile(path.join(pathRender, 'confirm-multi.html')) @@ -560,13 +529,17 @@ function createConfirmWindow(type, modRecords, origList) { windows.confirm = createSubWindow('confirm', { parent : 'main', preload : file_JS, fixed : true }) - windows.confirm.webContents.on('did-finish-load', async (event) => { - event.sender.send('fromMain_confirmList', { - records : modRecords, - list : modList, - foldersMap : modFoldersMap, - collection : collection, - }) + windows.confirm.webContents.on('did-finish-load', async () => { + sendModList( + { + records : modRecords, + originCollectKey : collection, + }, + 'fromMain_confirmList', + 'confirm', + false + ) + if ( devDebug ) { windows.confirm.webContents.openDevTools() } }) windows.confirm.loadFile(path.join(pathRender, file_HTML)) @@ -605,18 +578,19 @@ function createFolderWindow() { function createDetailWindow(thisModRecord) { if ( thisModRecord === null ) { return } - const modhubRecord = modRecordToModHub(thisModRecord) + //const modhubRecord = modCollect.modHubFullRecord(thisModRecord) if ( windows.detail ) { windows.detail.focus() - windows.detail.webContents.send('fromMain_modRecord', thisModRecord, modhubRecord, bindConflict, myTranslator.currentLocale) + // TODO: this is wrong! Get rid of modhubRecord + windows.detail.webContents.send('fromMain_modRecord', thisModRecord, modhubRecord, modCollect.bindConflict, myTranslator.currentLocale) return } windows.detail = createSubWindow('detail', { parent : 'main', preload : 'detailWindow' }) windows.detail.webContents.on('did-finish-load', async (event) => { - event.sender.send('fromMain_modRecord', thisModRecord, modhubRecord, bindConflict, myTranslator.currentLocale) + event.sender.send('fromMain_modRecord', thisModRecord, modhubRecord, modCollect.bindConflict, myTranslator.currentLocale) if ( devDebug ) { windows.detail.webContents.openDevTools() } }) @@ -796,19 +770,18 @@ function loadingWindow_open(l10n) { return } } -function loadingWindow_total(amount, reset = false, inMB = false) { - countTotal = ( reset ) ? amount : amount + countTotal +function loadingWindow_doCount(whichCount, amount, reset, inMB) { + loadWindowCount[whichCount] = ( reset ) ? amount : amount + loadWindowCount[whichCount] if ( ! windows.load.isDestroyed() ) { - windows.load.webContents.send('fromMain_loadingTotal', countTotal, inMB) + windows.load.webContents.send(`fromMain_loading_${whichCount}`, loadWindowCount.total, inMB) } } +function loadingWindow_total(amount, reset = false, inMB = false) { + loadingWindow_doCount('total', amount, reset, inMB) +} function loadingWindow_current(amount = 1, reset = false, inMB = false) { - countMods = ( reset ) ? amount : amount + countMods - - if ( ! windows.load.isDestroyed() ) { - windows.load.webContents.send('fromMain_loadingCurrent', countMods, inMB) - } + loadingWindow_doCount('current', amount, reset, inMB) } function loadingWindow_hide(time = 1250) { setTimeout(() => { @@ -861,7 +834,7 @@ ipcMain.on('toMain_openMods', (event, mods) => { }) ipcMain.on('toMain_openHub', (event, mods) => { const thisMod = modIdToRecord(mods[0]) - const thisModId = modHubList.mods[thisMod.fileDetail.shortName] || null + const thisModId = modHubData.modHubList.mods[thisMod.fileDetail.shortName] || null if ( thisModId !== null ) { shell.openExternal(`https://www.farming-simulator.com/mod.php?mod_id=${thisModId}`) @@ -869,31 +842,43 @@ ipcMain.on('toMain_openHub', (event, mods) => { }) ipcMain.on('toMain_copyFavorites', () => { - const favCols = [] - const sourceFiles = [] - let destCols = Object.keys(modList) + const sourceCollections = [] + const destinationCollections = [] + const sourceFiles = [] + + modCollect.collections.forEach((collectKey) => { + const isFavorite = modNote.get(`${collectKey}.notes_favorite`, false) - Object.keys(modNote.store).forEach((collection) => { - if ( modNote.get(`${collection}.notes_favorite`, false) === true ) { favCols.push(collection) } + if ( isFavorite ) { + sourceCollections.push(collectKey) + } else { + destinationCollections.push(collectKey) + } }) - favCols.forEach((collection) => { - destCols = destCols.filter((item) => item !== collection ) - modList[collection].mods.forEach((thisMod) => { - sourceFiles.push([ - thisMod.fileDetail.fullPath, - collection, - thisMod.fileDetail.shortName, - thisMod.l10n.title - ]) + sourceCollections.forEach((collectKey) => { + const thisCollection = modCollect.getModCollection(collectKey) + thisCollection.modSet.forEach((modKey) => { + sourceFiles.push({ + fullPath : thisCollection.mods[modKey].fileDetail.fullPath, + collectKey : collectKey, + shortName : thisCollection.mods[modKey].fileDetail.shortName, + title : thisCollection.mods[modKey].l10n.title, + }) }) }) - createConfirmFav(sourceFiles, destCols) + if ( sourceFiles.length > 0 ) { + createConfirmFav({ + sourceFiles : sourceFiles, + destinations : destinationCollections, + sources : sourceCollections, + }) + } }) -ipcMain.on('toMain_deleteMods', (event, mods) => { createConfirmWindow('delete', modIdsToRecords(mods), mods) }) -ipcMain.on('toMain_moveMods', (event, mods) => { createConfirmWindow('move', modIdsToRecords(mods), mods) }) -ipcMain.on('toMain_copyMods', (event, mods) => { createConfirmWindow('copy', modIdsToRecords(mods), mods) }) +ipcMain.on('toMain_deleteMods', (event, mods) => { createConfirmWindow('delete', modCollect.modColUUIDsToRecords(mods), mods) }) +ipcMain.on('toMain_moveMods', (event, mods) => { createConfirmWindow('move', modCollect.modColUUIDsToRecords(mods), mods) }) +ipcMain.on('toMain_copyMods', (event, mods) => { createConfirmWindow('copy', modCollect.modColUUIDsToRecords(mods), mods) }) ipcMain.on('toMain_realFileDelete', (event, fileMap) => { fileOperation('delete', fileMap) }) ipcMain.on('toMain_realFileMove', (event, fileMap) => { fileOperation('move', fileMap) }) ipcMain.on('toMain_realFileCopy', (event, fileMap) => { fileOperation('copy', fileMap) }) @@ -1087,7 +1072,7 @@ ipcMain.on('toMain_showChangelog', () => { createChangeLogWindow() } ) ipcMain.on('toMain_modContextMenu', async (event, modID) => { const thisMod = modIdToRecord(modID) - const thisModId = modHubList.mods[thisMod.fileDetail.shortName] || null + const thisModId = modHubData.mods[thisMod.fileDetail.shortName] || null const template = [ { label : thisMod.fileDetail.shortName}, @@ -1369,15 +1354,13 @@ ipcMain.on('toMain_downloadList', (event, collection) => { dialog.showMessageBoxSync(windows.main, { title : myTranslator.syncStringLookup('download_title'), - message : `${myTranslator.syncStringLookup('download_started')} :: ${modList[collection].name}\n${myTranslator.syncStringLookup('download_finished')}`, + message : `${myTranslator.syncStringLookup('download_started')} :: ${modCollect.mapCollectionToName(collection)}\n${myTranslator.syncStringLookup('download_finished')}`, type : 'info', }) log.log.info(`Downloading Collection : ${collection}`, 'mod-download') log.log.info(`Download Link : ${thisLink}`, 'mod-download') - - const dlReq = net.request(thisLink) dlReq.on('response', (response) => { @@ -1431,7 +1414,7 @@ ipcMain.on('toMain_downloadList', (event, collection) => { processModFolders() }) - zipReadStream.pipe(unzip.Extract({ path : modList[collection].fullPath })) + zipReadStream.pipe(unzip.Extract({ path : modCollect.mapCollectionToFolder(collection) })) } catch (e) { log.log.warning(`Download failed : (${response.statusCode}) ${e}`, 'mod-download') loadingWindow_hide() @@ -1610,59 +1593,38 @@ ipcMain.on('toMain_versionResolve', (event, shortName) => { /** Utility & Convenience Functions */ ipcMain.on('toMain_closeSubWindow', (event, thisWin) => { windows[thisWin].close() }) -ipcMain.on('toMain_homeDirRevamp', (event, thisPath) => { event.returnValue = thisPath.replaceAll(userHome, '~') }) -function refreshClientModList() { - windows.main.webContents.send( - 'fromMain_modList', +function sendModList(extraArgs = {}, eventName = 'fromMain_modList', toWindow = 'main', closeLoader = true) { + modCollect.toRenderer(extraArgs).then((modCollection) => { + windows[toWindow].webContents.send(eventName, modCollection) + + if ( toWindow === 'main' && windows.version && windows.version.isVisible() ) { + windows.version.webContents.send(eventName, modCollection) + } + if ( closeLoader ) { loadingWindow_hide(1500) } + }) +} + +function refreshClientModList(closeLoader = true) { + sendModList( { currentLocale : myTranslator.deferCurrentLocale(), - modList : modList, l10n : { disable : myTranslator.syncStringLookup('override_disabled'), unknown : myTranslator.syncStringLookup('override_unknown'), }, activeCollection : overrideIndex, - foldersMap : modFoldersMap, - newMods : newModsList, - modHub : { - list : modHubList, - version : modHubVersion, - }, - bindConflict : bindConflict, - notes : modNote.store, - } + }, + 'fromMain_modList', + 'main', + closeLoader ) } -function modRecordToModHub(mod) { - const modId = modHubList.mods[mod.fileDetail.shortName] || null - return [modId, (modHubVersion[modId] || null), modHubList.last.includes(modId)] -} -function modIdToRecord(id) { - const idParts = id.split('--') - let foundMod = null - let foundCol = null - - modList[idParts[0]].mods.forEach((mod) => { - if ( foundMod === null && mod.uuid === idParts[1] ) { - foundMod = mod - foundCol = idParts[0] - } - }) - foundMod.currentCollection = foundCol - return foundMod -} - -function modIdsToRecords(mods) { - const theseMods = [] - mods.forEach((inMod) => { theseMods.push(modIdToRecord(inMod)) }) - return theseMods -} /** END: Utility & Convenience Functions */ - +/** Business Functions */ function parseGameXML(devMode = null) { const gameXMLFile = gameSettings.replace('gameSettings.xml', 'game.xml') @@ -1708,7 +1670,7 @@ function parseGameXML(devMode = null) { parseGameXML(null) } } -/** Business Functions */ + function parseSettings({disable = null, newFolder = null, userName = null, serverName = null, password = null } = {}) { if ( ! gameSettings.endsWith('.xml') ) { log.log.danger(`Game settings is not an xml file ${gameSettings}, fixing`, 'game-settings') @@ -1759,10 +1721,7 @@ function parseSettings({disable = null, newFolder = null, userName = null, serve if ( overrideActive === 'false' || overrideActive === false ) { overrideIndex = '0' } else { - overrideIndex = '999' - Object.keys(modFoldersMap).forEach((cleanName) => { - if ( modFoldersMap[cleanName] === overrideFolder ) { overrideIndex = cleanName } - }) + overrideIndex = modCollect.mapFolderToCollection(overrideFolder) || '999' } if ( disable !== null || newFolder !== null || userName !== null || password !== null || serverName !== null ) { @@ -1813,7 +1772,6 @@ function parseSettings({disable = null, newFolder = null, userName = null, serve parseSettings() refreshClientModList() - loadingWindow_hide(1500) } } @@ -1833,15 +1791,14 @@ function fileOperation(type, fileMap, srcWindow = 'confirm') { }, 250) } - function fileOperation_post(type, fileMap) { const fullPathMap = [] fileMap.forEach((file) => { const thisFileName = path.basename(file[2]) fullPathMap.push([ - path.join(modFoldersMap[file[1]], thisFileName), // source - path.join(modFoldersMap[file[0]], thisFileName), // dest + path.join(modCollect.mapCollectionToFolder(file[1]), thisFileName), // source + path.join(modCollect.mapCollectionToFolder(file[0]), thisFileName), // dest ]) }) @@ -1879,67 +1836,10 @@ function fileOperation_post(type, fileMap) { }) processModFolders() - if ( windows.version && windows.version.isVisible() ) { - windows.version.webContents.send('fromMain_modList', modList) - } - -} - -function fileGetStats(folder, thisFile) { - let isFolder = null - let date = null - let b_date = null - let size = null - let error = false - - try { - if ( thisFile.isSymbolicLink() ) { - const thisSymLink = fs.readlinkSync(path.join(folder, thisFile.name)) - const thisSymLinkStat = fs.lstatSync(path.join(folder, thisSymLink)) - isFolder = thisSymLinkStat.isDirectory() - date = thisSymLinkStat.ctime - b_date = thisSymLinkStat.birthtime - - if ( !isFolder ) { size = thisSymLinkStat.size } - } else { - isFolder = thisFile.isDirectory() - } - - if ( ! thisFile.isSymbolicLink() ) { - const theseStats = fs.statSync(path.join(folder, thisFile.name)) - if ( !isFolder ) { size = theseStats.size } - date = theseStats.ctime - b_date = theseStats.birthtime - - } - if ( isFolder ) { - let bytes = 0 - glob.sync('**', { cwd : path.join(folder, thisFile.name) }).forEach((file) => { - try { - const stats = fs.statSync(path.join(folder, thisFile.name, file)) - if ( stats.isFile() ) { bytes += stats.size } - } catch { /* Do Nothing if we can't read it. */ } - }) - size = bytes - } - } catch (e) { - log.log.warning(`Unable to stat file ${thisFile.name} in ${folder} : ${e}`, 'file-stat') - isFolder = false - size = 0 - date = new Date(1969, 1, 1, 0, 0, 0, 0) - error = true - } - return { - folder : isFolder, - size : size, - date : date, - b_date : b_date, - error : error, - } } let loadingWait = null -async function processModFolders(newFolder) { +async function processModFolders() { if ( !foldersDirty ) { loadingWindow_hide(); return } loadingWindow_open('mods', 'main') @@ -1949,206 +1849,54 @@ async function processModFolders(newFolder) { loadingWait = setInterval(() => { if ( windows.load.isVisible() ) { clearInterval(loadingWait) - const localStore = maCache.store - const processPromises = processModFolders_post(newFolder, localStore) - - Promise.all(processPromises).then(() => { - maCache.store = localStore - foldersDirty = false - - processModBindConflict() - processModFolders_post_after() - }) + processModFoldersOnDisk() } }, 250) } -function processModFolders_post(newFolder = false, localStore) { - const useOneDrive = mcStore.get('use_one_drive', false) - const readingPromises = [] - if ( newFolder === false ) { modList = {}; modFoldersMap = {}} +function processModFoldersOnDisk() { + modCollect.syncSafe = mcStore.get('use_one_drive', false) + modCollect.clearAll() // Cleaner for no-longer existing folders, count contents of others modFolders.forEach((folder) => { - if ( ! fs.existsSync(folder) ) { - modFolders.delete(folder) - } else { - try { - const folderSize = fs.readdirSync(folder, {withFileTypes : true}) - loadingWindow_total(folderSize.length) - } catch (e) { - log.log.danger(`Couldn't count folder: ${folder} :: ${e}`, 'folder-reader') - } - } + if ( ! fs.existsSync(folder) ) { modFolders.delete(folder) } }) mcStore.set('modFolders', Array.from(modFolders)) modFolders.forEach((folder) => { - const cleanName = `col_${crypto.createHash('md5').update(folder).digest('hex')}` - const shortName = path.basename(folder) - - if ( folder === newFolder || newFolder === false ) { - modFoldersMap[cleanName] = folder - modList[cleanName] = { name : shortName, fullPath : folder, mods : [] } - - try { - const folderContents = fs.readdirSync(folder, {withFileTypes : true}) - - folderContents.forEach((thisFile) => { - if ( junkRegex.test(thisFile.name) ) { - loadingWindow_current() - return - } - - const thisFileStats = fileGetStats(folder, thisFile) - - if ( thisFileStats.error ) { - loadingWindow_current() - return - } + const thisCollectionStats = modCollect.addCollection(folder) - readingPromises.push( - new Promise((resolve) => { - setTimeout(() => { - processModFileSingleton(folder, thisFile, localStore, cleanName, thisFileStats, useOneDrive) - resolve(true) - }, 10) - }) - ) - }) - - } catch (e) { - log.log.danger(`Couldn't process folder: ${folder} :: ${e}`, 'folder-reader') - } - } + loadingWindow_total(thisCollectionStats.fileCount) }) - return readingPromises -} - -async function processModFileSingleton (folder, thisFile, localStore, cleanName, thisFileStats, useOneDrive) { - if ( !thisFileStats.folder && !skipCache ) { - const hashString = `${thisFile.name}-${thisFileStats.size}-${(useOneDrive)?thisFileStats.b_date.toISOString():thisFileStats.date.toISOString()}` - const thisMD5Sum = crypto.createHash('md5').update(hashString).digest('hex') + modCollect.processMods() - if ( typeof localStore[thisMD5Sum] !== 'undefined') { - modList[cleanName].mods.push(localStore[thisMD5Sum]) - log.log.debug(`Adding mod FROM cache: ${localStore[thisMD5Sum].fileDetail.shortName}`, `mod-${localStore[thisMD5Sum].uuid}`) - loadingWindow_current() - return - } - } - - if ( !thisFileStats.folder && !thisFile.name.endsWith('.zip') ) { - modList[cleanName].mods.push(new notModFileChecker( - path.join(folder, thisFile.name), - false, - thisFileStats.size, - thisFileStats.date, - log - )) - loadingWindow_current() - return - } - - try { - const thisModDetail = new modFileChecker( - path.join(folder, thisFile.name), - thisFileStats.folder, - thisFileStats.size, - (useOneDrive) ? thisFileStats.b_date : thisFileStats.date, - log, - myTranslator.deferCurrentLocale - ) - const storable = thisModDetail.storable - modList[cleanName].mods.push(thisModDetail) + modCollect.processPromise.then(() => { + parseSettings() + parseGameXML() + refreshClientModList() - if ( thisModDetail.md5Sum !== null ) { - log.log.info('Adding mod to cache', `mod-${thisModDetail.uuid}`) - newModsList.push(thisModDetail.md5Sum) - localStore[thisModDetail.md5Sum] = storable + if ( mcStore.get('rel_notes') !== app.getVersion() ) { + mcStore.set('rel_notes', app.getVersion() ) + log.log.info('New version detected, show changelog') + createChangeLogWindow() } - } catch (e) { - log.log.danger(`Couldn't process file: ${thisFile.name} :: ${e}`, 'folder-reader') - modList[cleanName].mods.push(new notModFileChecker( - path.join(folder, thisFile.name), - false, - thisFileStats.size, - thisFileStats.date, - log - )) - } - - loadingWindow_current() - -} - -function processModBindConflict() { - bindConflict = {} - - Object.keys(modList).forEach((collection) => { - bindConflict[collection] = {} - const collectionBinds = {} - - modList[collection].mods.forEach((thisMod) => { - Object.keys(thisMod.modDesc.binds).forEach((actName) => { - thisMod.modDesc.binds[actName].forEach((keyCombo) => { - if ( keyCombo === '' ) { return } - - const safeCat = thisMod.modDesc.actions[actName] || 'UNKNOWN' - const thisCombo = `${safeCat}--${keyCombo}` - - collectionBinds[thisCombo] ??= [] - collectionBinds[thisCombo].push(thisMod.fileDetail.shortName) - }) - }) - }) - Object.keys(collectionBinds).forEach((keyCombo) => { - if ( collectionBinds[keyCombo].length > 1 ) { - collectionBinds[keyCombo].forEach((modName) => { - bindConflict[collection][modName] ??= {} - bindConflict[collection][modName][keyCombo] = collectionBinds[keyCombo].filter((w) => w !== modName) - if ( bindConflict[collection][modName][keyCombo].length === 0 ) { - delete bindConflict[collection][modName][keyCombo] - } - }) - } - }) - Object.keys(bindConflict).forEach((collection) => { - Object.keys(bindConflict[collection]).forEach((modName) => { - if ( Object.keys(bindConflict[collection][modName]).length === 0 ) { - delete bindConflict[collection][modName] - } - }) - }) }) } -function processModFolders_post_after() { - parseSettings() - parseGameXML() - refreshClientModList() - loadingWindow_hide() - - if ( mcStore.get('rel_notes') !== app.getVersion() ) { - mcStore.set('rel_notes', app.getVersion() ) - log.log.info('New version detected, show changelog') - createChangeLogWindow() - } -} - function loadSaveFile(filename) { try { - const rawData = fs.readFileSync(path.join(app.getPath('userData'), filename)) + const rawData = fs.readFileSync(path.join(app.getPath('userData'), filename)) const jsonData = JSON.parse(rawData) switch (filename) { case 'modHubData.json' : - modHubList = jsonData + modCollect.modHubList = jsonData break case 'modHubVersion.json' : - modHubVersion = jsonData + modCollect.modHubVersion = jsonData break default : break diff --git a/package.json b/package.json index f5bb0e8f..ae48875e 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,8 @@ "pack": "electron-builder --dir", "dist": "electron-builder", "test": "node ./test/mod-reader-test.js && node ./test/save-reader-test.js && node ./test/translate-check.js", - "langtest": "node ./test/translate-check.js", - "langfix": "node ./test/translate-fix.js", + "lang_test": "node ./test/translate-check.js", + "lang_fix": "node ./test/translate-fix.js", "depends": "node ./test/outdated-deps.js" }, "devDependencies": { @@ -40,7 +40,6 @@ "pngjs": "^6.0.0", "semver": "^7.3.8", "unzip-stream": "^0.3.1", - "windows": "^0.1.2", "xml2js": "^0.4.23", "yargs": "^17.5.1" }, diff --git a/renderer/main.html b/renderer/main.html index 1d34715e..4ce48f3c 100644 --- a/renderer/main.html +++ b/renderer/main.html @@ -20,9 +20,9 @@ if (evt.keyCode == 116) { window.mods.refreshFolders() } } - window.onerror = function (message, file, line, col, error) { - window.log.debug(error.message, 'main'); return false - } + // window.onerror = function (message, file, line, col, error) { + // window.log.debug(error.message, 'main'); return false + // }