From 4f22676fb23f03ff8931fbe75017c57611d37fef Mon Sep 17 00:00:00 2001 From: Vili Manninen Date: Tue, 26 Nov 2024 16:48:22 +0200 Subject: [PATCH] WIP: Callback stack refactoring and installation after download --- src/components/views/DownloadModModal.vue | 54 +++++---- .../ThunderstoreDownloaderProvider.ts | 14 +-- .../BetterThunderstoreDownloader.ts | 103 ++++++++++------- src/store/modules/ModDownloadModule.ts | 104 +++++++++++++++--- 4 files changed, 189 insertions(+), 86 deletions(-) diff --git a/src/components/views/DownloadModModal.vue b/src/components/views/DownloadModModal.vue index d83d63da..3f869f79 100644 --- a/src/components/views/DownloadModModal.vue +++ b/src/components/views/DownloadModModal.vue @@ -11,6 +11,15 @@ :value='activeDownloadProgress' :className="['is-dark']" /> +
+

Installing {{activeDownloadModName}}

+

{{Math.floor(activeInstallProgress)}}% complete

+ +
@@ -145,6 +154,10 @@ let assignId = 0; return this.$store.getters['modDownload/activeDownloadModName']; } + get activeInstallProgress(): number { + return this.$store.getters['modDownload/activeInstallProgress']; + } + get activeGame(): Game { return this.$store.state.activeGame; } @@ -175,8 +188,8 @@ let assignId = 0; failed: false, }; DownloadModModal.allVersions.push([currentAssignId, progressObject]); - setTimeout(() => { - ThunderstoreDownloaderProvider.instance.download(profile.asImmutableProfile(), tsMod, tsVersion, ignoreCache, (progress: number, modName: string, status: number, err: R2Error | null) => { + setTimeout(async () => { + const downloadedMods = await ThunderstoreDownloaderProvider.instance.download(profile.asImmutableProfile(), tsMod, tsVersion, ignoreCache, (progress: number, modName: string, status: number, err: R2Error | null) => { const assignIndex = DownloadModModal.allVersions.findIndex(([number, val]) => number === currentAssignId); if (status === StatusEnum.FAILURE) { if (err !== null) { @@ -196,28 +209,29 @@ let assignId = 0; } DownloadModModal.allVersions[assignIndex] = [currentAssignId, obj]; } - }, async (downloadedMods: ThunderstoreCombo[]) => { - ProfileModList.requestLock(async () => { - for (const combo of downloadedMods) { - try { - await DownloadModModal.installModAfterDownload(profile, combo.getMod(), combo.getVersion()); - } catch (e) { - return reject( - R2Error.fromThrownValue(e, `Failed to install mod [${combo.getMod().getFullName()}]`) - ); - } + }); + await ProfileModList.requestLock(async () => { + for (const combo of downloadedMods) { + try { + await DownloadModModal.installModAfterDownload(profile, combo.getMod(), combo.getVersion()); + } catch (e) { + return reject( + R2Error.fromThrownValue(e, `Failed to install mod [${combo.getMod().getFullName()}]`) + ); } - const modList = await ProfileModList.getModList(profile.asImmutableProfile()); - if (!(modList instanceof R2Error)) { - const err = await ConflictManagementProvider.instance.resolveConflicts(modList, profile); - if (err instanceof R2Error) { - return reject(err); - } + } + const modList = await ProfileModList.getModList(profile.asImmutableProfile()); + if (!(modList instanceof R2Error)) { + const err = await ConflictManagementProvider.instance.resolveConflicts(modList, profile); + if (err instanceof R2Error) { + return reject(err); } - return resolve(); - }); + } + return resolve(); }); }, 1); + + }); } diff --git a/src/providers/ror2/downloading/ThunderstoreDownloaderProvider.ts b/src/providers/ror2/downloading/ThunderstoreDownloaderProvider.ts index 1b44a060..842a8653 100644 --- a/src/providers/ror2/downloading/ThunderstoreDownloaderProvider.ts +++ b/src/providers/ror2/downloading/ThunderstoreDownloaderProvider.ts @@ -54,12 +54,12 @@ export default abstract class ThunderstoreDownloaderProvider { * @param modVersion The version of the mod to download. * @param ignoreCache Download mod even if it already exists in the cache. * @param callback Callback to show the current state of the downloads. - * @param completedCallback Callback to perform final actions against. Only called if {@param callback} has not returned a failed status. */ - public abstract download(profile: ImmutableProfile, mod: ThunderstoreMod, modVersion: ThunderstoreVersion, - ignoreCache: boolean, - callback: (progress: number, modName: string, status: number, err: R2Error | null) => void, - completedCallback: (modList: ThunderstoreCombo[]) => void): void; + public abstract download( + profile: ImmutableProfile, mod: ThunderstoreMod, modVersion: ThunderstoreVersion, + ignoreCache: boolean, + callback: (progress: number, modName: string, status: number, err: R2Error | null) => void + ): Promise; /** * A top-level method to download exact versions of exported mods. @@ -87,11 +87,11 @@ export default abstract class ThunderstoreDownloaderProvider { * Iterate the {@class ThunderstoreCombo} array to perform the download for each mod. * Progress to the next one recursively once the callback received has been successful. * - * @param entries IterableIterator of entries for {@class ThunderstoreCombo} mods to download. + * @param entries The {@class ThunderstoreCombo} mods to download. * @param ignoreCache Should mod be downloaded even if it already exists in the cache? * @param callback See {@method download} */ - public abstract queueDownloadDependencies(entries: IterableIterator<[number, ThunderstoreCombo]>, ignoreCache: boolean, callback: (progress: number, modName: string, status: number, err: R2Error | null) => void): void + public abstract queueDownloadDependencies(entries: ThunderstoreCombo[], ignoreCache: boolean, callback: (progress: number, modName: string, status: number, err: R2Error | null) => void): void /** * Generate the total count of mods to be downloaded. Cached mods are not included in this count unless download cache is disabled. diff --git a/src/r2mm/downloading/BetterThunderstoreDownloader.ts b/src/r2mm/downloading/BetterThunderstoreDownloader.ts index 5bbb5151..b083a48a 100644 --- a/src/r2mm/downloading/BetterThunderstoreDownloader.ts +++ b/src/r2mm/downloading/BetterThunderstoreDownloader.ts @@ -66,7 +66,7 @@ export default class BetterThunderstoreDownloader extends ThunderstoreDownloader let downloadCount = 0; const downloadableDependencySize = this.calculateInitialDownloadSize(dependencies); - await this.queueDownloadDependencies(dependencies.entries(), ignoreCache, (progress: number, modName: string, status: number, err: R2Error | null) => { + await this.queueDownloadDependencies(dependencies, ignoreCache, (progress: number, modName: string, status: number, err: R2Error | null) => { if (status === StatusEnum.FAILURE) { callback(0, modName, status, err); } else if (status === StatusEnum.PENDING) { @@ -82,10 +82,12 @@ export default class BetterThunderstoreDownloader extends ThunderstoreDownloader }); } - public async download(profile: ImmutableProfile, mod: ThunderstoreMod, modVersion: ThunderstoreVersion, - ignoreCache: boolean, - callback: (progress: number, modName: string, status: number, err: R2Error | null) => void, - completedCallback: (modList: ThunderstoreCombo[]) => void) { + + public async download( + profile: ImmutableProfile, mod: ThunderstoreMod, modVersion: ThunderstoreVersion, + ignoreCache: boolean, + callback: (progress: number, modName: string, status: number, err: R2Error | null) => void + ): Promise { let dependencies: ThunderstoreCombo[] = []; await this.buildDependencySet(modVersion, dependencies, DependencySetBuilderMode.USE_EXACT_VERSION); this.sortDependencyOrder(dependencies); @@ -94,9 +96,11 @@ export default class BetterThunderstoreDownloader extends ThunderstoreDownloader combo.setVersion(modVersion); let downloadCount = 0; + let downloadedModsList: ThunderstoreCombo[] = [combo]; + const modList = await ProfileModList.getModList(profile); if (modList instanceof R2Error) { - return callback(0, mod.getName(), StatusEnum.FAILURE, modList); + throw modList; } let downloadableDependencySize = this.calculateInitialDownloadSize(dependencies); @@ -122,30 +126,32 @@ export default class BetterThunderstoreDownloader extends ThunderstoreDownloader callback(this.generateProgressPercentage(progress, downloadCount, downloadableDependencySize + 1), mod.getName(), status, err); } else if (status === StatusEnum.SUCCESS) { downloadCount += 1; - // If no dependencies, end here. - if (dependencies.length === 0) { - callback(100, mod.getName(), StatusEnum.PENDING, err); - completedCallback([combo]); - return; - } + } + }); - // If dependencies, queue and download. - await this.queueDownloadDependencies(dependencies.entries(), ignoreCache, (progress: number, modName: string, status: number, err: R2Error | null) => { - if (status === StatusEnum.FAILURE) { - callback(0, modName, status, err); - } else if (status === StatusEnum.PENDING) { - callback(this.generateProgressPercentage(progress, downloadCount, downloadableDependencySize + 1), modName, status, err); - } else if (status === StatusEnum.SUCCESS) { - callback(this.generateProgressPercentage(progress, downloadCount, downloadableDependencySize + 1), modName, StatusEnum.PENDING, err); - downloadCount += 1; - if (downloadCount >= dependencies.length + 1) { - callback(100, modName, StatusEnum.PENDING, err); - completedCallback([...dependencies, combo]); - } - } - }); + // If no dependencies, end here. + if (dependencies.length === 0) { + callback(100, mod.getName(), StatusEnum.SUCCESS, null); + return downloadedModsList; + } + + // If dependencies, queue and download. + let downloadedDependencies = await this.queueDownloadDependencies(dependencies, ignoreCache, (progress: number, modName: string, status: number, err: R2Error | null) => { + if (status === StatusEnum.FAILURE) { + callback(0, modName, status, err); + } else if (status === StatusEnum.PENDING) { + callback(this.generateProgressPercentage(progress, downloadCount, downloadableDependencySize + 1), modName, status, err); + } else if (status === StatusEnum.SUCCESS) { + callback(this.generateProgressPercentage(progress, downloadCount, downloadableDependencySize + 1), modName, StatusEnum.PENDING, err); + downloadCount += 1; + if (downloadCount >= dependencies.length + 1) { + callback(100, modName, StatusEnum.PENDING, err); + } } - }) + }); + + downloadedModsList.push(...downloadedDependencies); + return downloadedModsList; } public async downloadImportedMods( @@ -197,20 +203,35 @@ export default class BetterThunderstoreDownloader extends ThunderstoreDownloader return completedProgress + (progress * 1/total); } - public async queueDownloadDependencies(entries: IterableIterator<[number, ThunderstoreCombo]>, ignoreCache: boolean, callback: (progress: number, modName: string, status: number, err: R2Error | null) => void) { - const entry = entries.next(); - if (!entry.done) { - await this.downloadAndSave(entry.value[1] as ThunderstoreCombo, ignoreCache, async (progress: number, status: number, err: R2Error | null) => { - if (status === StatusEnum.FAILURE) { - callback(0, (entry.value[1] as ThunderstoreCombo).getMod().getName(), status, err); - } else if (status === StatusEnum.PENDING) { - callback(progress, (entry.value[1] as ThunderstoreCombo).getMod().getName(), status, err); - } else if (status === StatusEnum.SUCCESS) { - callback(100, (entry.value[1] as ThunderstoreCombo).getMod().getName(), status, err); - await this.queueDownloadDependencies(entries, ignoreCache, callback); - } + public async queueDownloadDependencies( + thunderstoreCombos: ThunderstoreCombo[], + ignoreCache: boolean, + callback: (progress: number, modName: string, status: number, err: R2Error | null) => void + ): Promise { + const downloadTasks: Promise[] = []; + const mods: ThunderstoreCombo[] = []; + + thunderstoreCombos.forEach((mod) => { + const task = new Promise((resolve, reject) => { + this.downloadAndSave(mod, ignoreCache, (progress: number, status: number, err: R2Error | null) => { + if (status === StatusEnum.FAILURE) { + callback(0, mod.getMod().getName(), status, err); + reject(err); // Reject the promise if there's a failure + } else if (status === StatusEnum.PENDING) { + callback(progress, mod.getMod().getName(), status, err); + } else if (status === StatusEnum.SUCCESS) { + callback(100, mod.getMod().getName(), status, err); + mods.push(mod); + resolve(); + } + }); }); - } + + downloadTasks.push(task); + }); + + await Promise.all(downloadTasks); + return mods; } public calculateInitialDownloadSize(list: ThunderstoreCombo[]): number { diff --git a/src/store/modules/ModDownloadModule.ts b/src/store/modules/ModDownloadModule.ts index b73e6db4..354e7f0d 100644 --- a/src/store/modules/ModDownloadModule.ts +++ b/src/store/modules/ModDownloadModule.ts @@ -1,11 +1,14 @@ -import {ActionTree, GetterTree} from 'vuex'; +import { ActionTree, GetterTree } from 'vuex'; import StatusEnum from '../../model/enums/StatusEnum'; -import {ImmutableProfile} from '../../model/Profile'; +import R2Error from "../../model/errors/R2Error"; +import ManifestV2 from "../../model/ManifestV2"; +import { ImmutableProfile } from '../../model/Profile'; import ThunderstoreCombo from '../../model/ThunderstoreCombo'; import ThunderstoreDownloaderProvider from '../../providers/ror2/downloading/ThunderstoreDownloaderProvider'; -import {State as RootState} from '../index'; -import R2Error from "../../model/errors/R2Error"; +import ProfileInstallerProvider from "../../providers/ror2/installing/ProfileInstallerProvider"; +import ProfileModList from "../../r2mm/mods/ProfileModList"; +import { State as RootState } from '../index'; interface ProgressItem { modName: string; @@ -48,9 +51,9 @@ export default { return state.downloads.slice(-1)[0].modName; // Last element of the array } }, - activeDownloadProgressItem(state): ProgressItem | undefined { + activeInstallProgress(state): number | undefined { if (state.downloads.length > 0) { - return state.downloads.slice(-1)[0]; // Last element of the array + return state.downloads.slice(-1)[0].installProgress; // Last element of the array } }, }, @@ -89,11 +92,24 @@ export default { downloadMod.error = params.err; } } + }, + updateInstallProgress(state: State, params: { progress: number, modName: string, status: number, err: R2Error | null }) { + let installMod = state.downloads.find((progressItem) => progressItem.modName === params.modName); + + if (!installMod) { + installMod = state.dependencyDownloads.find((progressItem) => progressItem.modName === params.modName); + } + + if (installMod) { + installMod.installProgress = params.progress; + installMod.status = params.status; + if (params.status === StatusEnum.FAILURE && params.err) { + installMod.error = params.err; + } + } } }, actions: >{ - - //TODO: Do the installation async downloadAndInstallMod( {dispatch, getters, state, commit}, params: { @@ -109,23 +125,75 @@ export default { error: null }); - ThunderstoreDownloaderProvider.instance.download( + const mods: ThunderstoreCombo[] = await ThunderstoreDownloaderProvider.instance.download( params.profile, params.mod.getMod(), params.mod.getVersion(), true, (progress, modName, status, err) => { - commit('updateDownloadProgress', { progress, modName, status, err }); - }, - (mods) => { - mods.forEach((mod) => { - commit( - 'updateDownloadProgress', - { progress: 100, modName: mod.getMod().getName(), status: StatusEnum.SUCCESS, err: null } - ); - }); + commit('updateDownloadProgress', {progress, modName, status, err}); } ); + for (const mod of mods) { + await commit( + 'updateDownloadProgress', + { progress: 100, modName: mod.getMod().getName(), status: StatusEnum.SUCCESS, err: null } + ); + await dispatch('installModAfterDownload', { profile: params.profile, mod: params.mod }).then(() => { + commit( + 'updateInstallProgress', + { progress: 100, modName: mod.getMod().getName(), status: StatusEnum.SUCCESS, err: null } + ); + }); + } }, + async installModAfterDownload( + {dispatch, getters, state, commit}, + params: { + profile: ImmutableProfile, + mod: ThunderstoreCombo, + } + ): Promise { + return new Promise(async (resolve, reject) => { + const manifestMod: ManifestV2 = new ManifestV2().fromThunderstoreMod(params.mod.getMod(), params.mod.getVersion()); + const profileModList = await ProfileModList.getModList(params.profile); + if (profileModList instanceof R2Error) { + return reject(profileModList); + } + const modAlreadyInstalled = profileModList.find( + value => value.getName() === params.mod.getMod().getFullName() + && value.getVersionNumber().isEqualTo(params.mod.getVersion().getVersionNumber()) + ); + + if (modAlreadyInstalled === undefined || !modAlreadyInstalled) { + const resolvedAuthorModNameString = `${manifestMod.getAuthorName()}-${manifestMod.getDisplayName()}`; + const olderInstallOfMod = profileModList.find(value => `${value.getAuthorName()}-${value.getDisplayName()}` === resolvedAuthorModNameString); + if (manifestMod.getName().toLowerCase() !== 'bbepis-bepinexpack') { + const result = await ProfileInstallerProvider.instance.uninstallMod(manifestMod, params.profile); + if (result instanceof R2Error) { + return reject(result); + } + } + const installError: R2Error | null = await ProfileInstallerProvider.instance.installMod(manifestMod, params.profile); + if (!(installError instanceof R2Error)) { + const newModList: ManifestV2[] | R2Error = await ProfileModList.addMod(manifestMod, params.profile); + if (newModList instanceof R2Error) { + return reject(newModList); + } + } else { + return reject(installError); + } + if (olderInstallOfMod !== undefined) { + if (!olderInstallOfMod.isEnabled()) { + await ProfileModList.updateMod(manifestMod, params.profile, async mod => { + mod.disable(); + }); + await ProfileInstallerProvider.instance.disableMod(manifestMod, params.profile); + } + } + } + return resolve(); + }); + } }, }