diff --git a/src-electron/icons/icon.icns b/src-electron/icons/icon.icns deleted file mode 100644 index a2ed6aec1..000000000 Binary files a/src-electron/icons/icon.icns and /dev/null differ diff --git a/src-electron/icons/icon.ico b/src-electron/icons/icon.ico deleted file mode 100644 index 20e6f6797..000000000 Binary files a/src-electron/icons/icon.ico and /dev/null differ diff --git a/src-electron/icons/linux-512x512.png b/src-electron/icons/linux-512x512.png deleted file mode 100644 index a57354eed..000000000 Binary files a/src-electron/icons/linux-512x512.png and /dev/null differ diff --git a/src/pages/Manager.vue b/src/pages/Manager.vue index 7c115c7fb..eb90c4764 100644 --- a/src/pages/Manager.vue +++ b/src/pages/Manager.vue @@ -23,7 +23,7 @@
@@ -189,7 +189,7 @@ import ManagerSettings from '../r2mm/manager/ManagerSettings'; import GameRunner from '../r2mm/manager/GameRunner'; import * as fs from 'fs-extra'; -import { isNull } from 'util'; +import { isNull, isNullOrUndefined } from 'util'; import ModLinker from '../r2mm/manager/ModLinker'; const settings = new ManagerSettings(); @@ -291,24 +291,9 @@ export default class Manager extends Vue { const downloader: ThunderstoreDownloader = new ThunderstoreDownloader(refSelectedThunderstoreMod, Profile.getActiveProfile()); downloader.download((progress: number, status: number, error: DownloadError)=>{ if (status === StatusEnum.SUCCESS) { - const modFromManifest: Mod | R2Error = ModFromManifest.get(refSelectedThunderstoreMod.getFullName(), version.getVersionNumber()); - if (!(modFromManifest instanceof R2Error)) { - const installError: R2Error | null = ProfileInstaller.installMod(modFromManifest); - if (!(installError instanceof R2Error)) { - const newModList: Mod[] | R2Error = ProfileModList.addMod(modFromManifest); - if (!(newModList instanceof R2Error)) { - this.localModList = newModList; - this.filterModLists(); - } - } else { - // Show that installation failed - // (mod failed to be placed in /{profile} directory) - this.showError(installError); - } - } else { - // Show that mod has failed to register for profile - // (mod failed to add to mods.yml) - this.showError(modFromManifest); + const installErr = this.installModAfterDownload(refSelectedThunderstoreMod, version); + if (installErr instanceof R2Error) { + this.showError(installErr); } if (this.selectedThunderstoreMod === refSelectedThunderstoreMod) { // Close modal if no other modal has been opened. @@ -321,6 +306,66 @@ export default class Manager extends Vue { } + installModAfterDownload(mod: ThunderstoreMod, version: ThunderstoreVersion): R2Error | void { + const modFromManifest: Mod | R2Error = ModFromManifest.get(mod.getFullName(), version.getVersionNumber()); + if (!(modFromManifest instanceof R2Error)) { + const installError: R2Error | null = ProfileInstaller.installMod(modFromManifest); + if (!(installError instanceof R2Error)) { + const newModList: Mod[] | R2Error = ProfileModList.addMod(modFromManifest); + if (!(newModList instanceof R2Error)) { + this.localModList = newModList; + this.filterModLists(); + } + } else { + // (mod failed to be placed in /{profile} directory) + return installError; + } + } else { + // (mod failed to add to mods.yml) + return modFromManifest; + } + } + + downloadWithDependencies() { + const refSelectedThunderstoreMod: ThunderstoreMod | null = this.selectedThunderstoreMod; + const refSelectedVersion: string | null = this.selectedVersion; + if (refSelectedThunderstoreMod === null || refSelectedVersion === null) { + // Shouldn't happen, but shouldn't throw an error. + return; + } + const version = refSelectedThunderstoreMod.getVersions() + .find((modVersion: ThunderstoreVersion) => modVersion.getVersionNumber().toString() === refSelectedVersion); + if (version === undefined) { + return; + } + const downloader: ThunderstoreDownloader = new ThunderstoreDownloader(refSelectedThunderstoreMod, Profile.getActiveProfile()); + downloader.downloadWithDependencies((progress: number, status: number, error: R2Error | void)=>{ + if (status === StatusEnum.FAILURE) { + if (error instanceof R2Error) { + this.showError(error); + } + } else if (status === StatusEnum.SUCCESS) { + // To get to this stage, it must have already succeeded once. + const dependencies: ThunderstoreMod[] | R2Error = downloader.buildDependencyList(version, this.thunderstoreModList); + if (dependencies instanceof R2Error) { + return; + } + dependencies.forEach((mod: ThunderstoreMod) => { + const installErr = this.installModAfterDownload(mod, mod.getVersions()[0]); + if (installErr instanceof R2Error) { + this.showError(installErr); + return; + } + }) + this.installModAfterDownload(refSelectedThunderstoreMod, version); + if (this.selectedThunderstoreMod === refSelectedThunderstoreMod) { + // Close modal if no other modal has been opened. + this.closeModal(); + } + } + }, refSelectedThunderstoreMod, version, this.thunderstoreModList); + } + // eslint-disable-next-line uninstallMod(vueMod: any) { let mod: InvalidManifestError | ManifestV2 | Mod | R2Error = new ManifestV2().make(vueMod); @@ -441,7 +486,7 @@ export default class Manager extends Vue { this.gameRunning = true; GameRunner.playModded(settings.riskOfRain2Directory, ()=>{ this.gameRunning = false; - }); + }, settings); } } @@ -451,7 +496,7 @@ export default class Manager extends Vue { this.gameRunning = true; GameRunner.playVanilla(settings.riskOfRain2Directory, ()=>{ this.gameRunning = false; - }); + }, settings); } } diff --git a/src/r2mm/downloading/ThunderstoreDownloader.ts b/src/r2mm/downloading/ThunderstoreDownloader.ts index 0e1a0b8a2..a04d4e34b 100644 --- a/src/r2mm/downloading/ThunderstoreDownloader.ts +++ b/src/r2mm/downloading/ThunderstoreDownloader.ts @@ -11,6 +11,7 @@ import * as fs from 'fs-extra'; import * as path from 'path'; import ZipExtract from '../installing/ZipExtract'; import R2Error from 'src/model/errors/R2Error'; +import { isUndefined } from 'util'; const cacheDirectory: string = path.join(process.cwd(), 'mods', 'cache'); @@ -64,7 +65,72 @@ export default class ThunderstoreDownloader { } } - public saveToFile(response: Buffer, versionNumber: VersionNumber, callback: (success: boolean) => void): R2Error | null { + public downloadWithDependencies(callback: (progress: number, status: number, error: R2Error | void) => void, mod: ThunderstoreMod, version: ThunderstoreVersion, modList: ThunderstoreMod[]) { + const dependencyList = this.buildDependencyList(version, modList); + if (dependencyList instanceof R2Error) { + callback( + 0, + StatusEnum.FAILURE, + dependencyList + ); + return; + } + let listStep = 0; + const downloader = new ThunderstoreDownloader(dependencyList[listStep], Profile.getActiveProfile()); + const onFinalDownload = (progress: number, status: number, error: R2Error | void) => { + if (status === StatusEnum.SUCCESS) { + callback(100, StatusEnum.SUCCESS); + } else if (status === StatusEnum.FAILURE && error instanceof R2Error) { + callback(0, StatusEnum.FAILURE, error); + } + } + const onStepDownload = (progress: number, status: number, error: R2Error | void) => { + if (status === StatusEnum.SUCCESS) { + listStep += 1; + if (listStep < dependencyList.length) { + const downloader = new ThunderstoreDownloader(dependencyList[listStep], Profile.getActiveProfile()); + downloader.download(onStepDownload, dependencyList[listStep].getVersions()[0].getVersionNumber()); + } else { + const downloader = new ThunderstoreDownloader(mod, Profile.getActiveProfile()); + downloader.download(onFinalDownload, version.getVersionNumber()); + } + } else if (status === StatusEnum.FAILURE && error instanceof R2Error) { + callback( + 0, + StatusEnum.FAILURE, + error) + } + } + downloader.download(onStepDownload, dependencyList[listStep].getVersions()[0].getVersionNumber()); + } + + public buildDependencyList(version: ThunderstoreVersion, modList: ThunderstoreMod[]): ThunderstoreMod[] | R2Error { + const list: ThunderstoreMod[] = []; + try { + version.getDependencies().forEach((dependency: string) => { + const foundMod = modList.find((listMod: ThunderstoreMod) => dependency.startsWith(listMod.getFullName())); + if (isUndefined(foundMod)) { + throw new Error(`Unable to find Thunderstore dependency with author-name of ${dependency}`) + } else { + list.push(foundMod); + const findDependencyError = this.buildDependencyList(foundMod, modList); + if (findDependencyError instanceof R2Error) { + throw findDependencyError; + } + list.push(...findDependencyError); + } + }) + } catch(e) { + const err: Error = e; + return new R2Error( + `Failed to find all dependencies of mod ${version.getFullName()}`, + err.message + ) + } + return list; + } + + private saveToFile(response: Buffer, versionNumber: VersionNumber, callback: (success: boolean) => void): R2Error | null { try { fs.mkdirsSync(path.join(cacheDirectory, this.mod.getFullName())); fs.writeFileSync( @@ -83,9 +149,7 @@ export default class ThunderstoreDownloader { ); return extractError; } catch(e) { - console.log('Couldn\'t write file'); const err: Error = e; - console.log(err); return new FileWriteError( 'File write error', `Failed to write downloaded zip of ${this.mod.getFullName()} to profile directory of ${this.profile.getPathOfProfile()}. \nReason: ${err.message}` diff --git a/src/r2mm/installing/ProfileInstaller.ts b/src/r2mm/installing/ProfileInstaller.ts index 626ba0897..8f3afcf55 100644 --- a/src/r2mm/installing/ProfileInstaller.ts +++ b/src/r2mm/installing/ProfileInstaller.ts @@ -10,6 +10,7 @@ import Profile from 'src/model/Profile'; import FileWriteError from 'src/model/errors/FileWriteError'; import ModMode from 'src/model/enums/ModMode'; import { isNull } from 'util'; +import { lstatSync } from 'fs-extra'; const cacheDirectory: string = path.join(process.cwd(), 'mods', 'cache'); @@ -44,14 +45,16 @@ export default class ProfileInstaller { try { fs.readdirSync(bepInExLocation) .forEach((file: string) => { - fs.readdirSync(path.join(bepInExLocation, file)) - .forEach((folder: string) => { - const folderPath: string = path.join(bepInExLocation, file, folder); - if (folder === mod.getName() && fs.lstatSync(folderPath).isDirectory()) { - fs.emptyDirSync(folderPath); - fs.removeSync(folderPath); - } - }) + if (lstatSync(path.join(bepInExLocation, file)).isDirectory()) { + fs.readdirSync(path.join(bepInExLocation, file)) + .forEach((folder: string) => { + const folderPath: string = path.join(bepInExLocation, file, folder); + if (folder === mod.getName() && fs.lstatSync(folderPath).isDirectory()) { + fs.emptyDirSync(folderPath); + fs.removeSync(folderPath); + } + }) + } }); } catch(e) { const err: Error = e; diff --git a/src/r2mm/manager/GameRunner.ts b/src/r2mm/manager/GameRunner.ts index afc57f122..baa82f2a8 100644 --- a/src/r2mm/manager/GameRunner.ts +++ b/src/r2mm/manager/GameRunner.ts @@ -5,11 +5,11 @@ import * as path from 'path'; export default class GameRunner { - public static playModded(ror2Directory: string, onComplete: ()=>void) { + public static playModded(ror2Directory: string, onComplete: ()=>void, settings: ManagerSettings) { child.spawn(path.join(ror2Directory, 'Risk of Rain 2.exe'), ['--doorstop-enable', 'true', '--doorstop-target', 'r2modman\\BepInEx\\core\\BepInEx.Preloader.dll']).on('exit', onComplete); } - public static playVanilla(ror2Directory: string, onComplete: ()=>void) { + public static playVanilla(ror2Directory: string, onComplete: ()=>void, settings: ManagerSettings) { child.spawn(path.join(ror2Directory, 'Risk of Rain 2.exe'), ['--doorstop-enable', 'false']).on('exit', onComplete); } diff --git a/src/statics/icons/icon-256x256.png b/src/statics/icons/icon-256x256.png index 9ab309bbc..e8b9808b7 100644 Binary files a/src/statics/icons/icon-256x256.png and b/src/statics/icons/icon-256x256.png differ