Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve performance of installing mods with multiple dependencies #1505

Merged
merged 1 commit into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading