Skip to content

Commit

Permalink
Merge pull request #1505 from ebkr/install-mods-disk-usage
Browse files Browse the repository at this point in the history
Improve performance of installing mods with multiple dependencies
  • Loading branch information
anttimaki authored Oct 30, 2024
2 parents 3f09e69 + ffc48ef commit 1e70fa6
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 14 deletions.
25 changes: 11 additions & 14 deletions src/components/views/DownloadModModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
});
}
Expand Down
4 changes: 4 additions & 0 deletions src/model/ManifestV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()}`;
}
}
61 changes: 61 additions & 0 deletions src/utils/ProfileUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ThunderstoreCombo[]> {
const dependencyStrings = exportMods.map((m) => m.getDependencyString());
Expand Down Expand Up @@ -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<ManifestV2[]> {
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.
Expand Down

0 comments on commit 1e70fa6

Please sign in to comment.