diff --git a/src/components/views/DownloadModModal.vue b/src/components/views/DownloadModModal.vue index 6b3c7539..93406438 100644 --- a/src/components/views/DownloadModModal.vue +++ b/src/components/views/DownloadModModal.vue @@ -107,6 +107,7 @@ import ConflictManagementProvider from '../../providers/generic/installing/Confl import { MOD_LOADER_VARIANTS } from '../../r2mm/installing/profile_installers/ModLoaderVariantRecord'; import ModalCard from '../ModalCard.vue'; import * as PackageDb from '../../r2mm/manager/PackageDexieStore'; +import { installModsAfterDownload } from '../../utils/ProfileUtils'; interface DownloadProgress { assignId: number; @@ -370,24 +371,20 @@ let assignId = 0; async downloadCompletedCallback(downloadedMods: ThunderstoreCombo[]) { ProfileModList.requestLock(async () => { - for (const combo of downloadedMods) { - try { - await DownloadModModal.installModAfterDownload(this.profile, combo.getMod(), combo.getVersion()); - } catch (e) { - this.downloadingMod = false; - const err = R2Error.fromThrownValue(e, `Failed to install mod [${combo.getMod().getFullName()}]`); - this.$store.commit('error/handleError', err); - return; - } - } - this.downloadingMod = false; - const modList = await ProfileModList.getModList(this.profile.asImmutableProfile()); - if (!(modList instanceof R2Error)) { + const profile = this.profile.asImmutableProfile(); + + try { + const modList = await installModsAfterDownload(downloadedMods, profile); await this.$store.dispatch('profile/updateModList', modList); + const err = await ConflictManagementProvider.instance.resolveConflicts(modList, this.profile); if (err instanceof R2Error) { - this.$store.commit('error/handleError', err); + throw err; } + } catch (e) { + this.$store.commit('error/handleError', R2Error.fromThrownValue(e)); + } finally { + this.downloadingMod = false; } }); } diff --git a/src/model/ManifestV2.ts b/src/model/ManifestV2.ts index e2bcedfb..309f6688 100644 --- a/src/model/ManifestV2.ts +++ b/src/model/ManifestV2.ts @@ -282,4 +282,8 @@ export default class ManifestV2 implements ReactiveObjectConverterInterface { public setInstalledAtTime(installedAtTime: number) { this.installedAtTime = installedAtTime; } + + public getDependencyString(): string { + return `${this.getName()}-${this.getVersionNumber().toString()}`; + } } diff --git a/src/utils/ProfileUtils.ts b/src/utils/ProfileUtils.ts index 96ace792..094683e4 100644 --- a/src/utils/ProfileUtils.ts +++ b/src/utils/ProfileUtils.ts @@ -15,6 +15,7 @@ import FsProvider from "../providers/generic/file/FsProvider"; import ZipProvider from "../providers/generic/zip/ZipProvider"; import ProfileInstallerProvider from "../providers/ror2/installing/ProfileInstallerProvider"; import * as PackageDb from '../r2mm/manager/PackageDexieStore'; +import ProfileModList from "../r2mm/mods/ProfileModList"; export async function exportModsToCombos(exportMods: ExportMod[], game: Game): Promise { const dependencyStrings = exportMods.map((m) => m.getDependencyString()); @@ -65,6 +66,66 @@ async function extractConfigsToImportedProfile( } } +/** + * Install mods to target profile and sync the changes to mods.yml file + * This is more performant than calling ProfileModList.addMod() on a + * loop, as that causes multiple disc operations per mod. + */ +export async function installModsAfterDownload( + comboList: ThunderstoreCombo[], + profile: ImmutableProfile +): Promise { + const profileMods = await ProfileModList.getModList(profile); + if (profileMods instanceof R2Error) { + throw profileMods; + } + + const installedVersions = profileMods.map((m) => m.getDependencyString()); + const disabledMods = profileMods.filter((m) => !m.isEnabled()).map((m) => m.getName()); + + try { + for (const comboMod of comboList) { + const manifestMod = new ManifestV2().fromThunderstoreMod(comboMod.getMod(), comboMod.getVersion()); + + if (installedVersions.includes(manifestMod.getDependencyString())) { + continue; + } + + // Uninstall possible different version of the mod before installing the target version. + throwForR2Error(await ProfileInstallerProvider.instance.uninstallMod(manifestMod, profile)); + throwForR2Error(await ProfileInstallerProvider.instance.installMod(manifestMod, profile)); + + if (disabledMods.includes(manifestMod.getName())) { + throwForR2Error(await ProfileInstallerProvider.instance.disableMod(manifestMod, profile)); + manifestMod.disable(); + } + + manifestMod.setInstalledAtTime(Number(new Date())); + ProfileModList.setIconPath(manifestMod, profile); + + const positionInProfile = profileMods.findIndex((m) => m.getName() === manifestMod.getName()); + if (positionInProfile >= 0) { + profileMods[positionInProfile] = manifestMod; + } else { + profileMods.push(manifestMod); + } + } + } catch (e) { + const originalError = R2Error.fromThrownValue(e); + throw new R2Error( + 'Installing downloaded mods to profile failed', + ` + The mod and its dependencies might not be installed properly. + The original error was: ${originalError.name}: ${originalError.message} + `, + 'The original error might provide hints about what went wrong.' + ); + } + + throwForR2Error(await ProfileModList.saveModList(profile, profileMods)); + return profileMods; +} + /** * Install mods to target profile without syncing changes to mods.yml file. * Syncing is futile, as the mods.yml is copied from the imported profile.