Skip to content

Commit

Permalink
Fix profile import silently leaving some mods uninstalled
Browse files Browse the repository at this point in the history
To reduce time spent on the splash screen, recent changes skip
refreshing the online mod list if older version is found in IndexedDB
cache. This means the user might have outdated mod list when importing
a profile, and the manager can't import mods it doesn't know about. To
mitigate the issue, a warning was added to profile preview step with an
option for the user to update the mod list.

However, the warning uses the mod list in Vuex to check if a mod is
available, which is a mistake since that only checks if the mod itself
is known, and not whether the mod version in the profile is known. This
caused recently updated mods to not always trigger the warning. The
download part uses IndexedDB as its source of truth, and silently
ignored the unknown mods/versions, since user has explicitly decided to
import a partial profile.

To fix the issue, both parts now use IndexedDB as the source of truth.

The implementation now somewhat akwardly handles the mod information as
ExportMods (read from mods.yml and required to know which mods are
disabled), ThunderstoreMods (required to use existing UI components),
and ThunderstoreCombos (required to download a specific version of the
mod). This could potentially be simplified but is outside the scope of
this PR.
  • Loading branch information
anttimaki committed Feb 27, 2025
1 parent 998d1c8 commit 6b13aa2
Show file tree
Hide file tree
Showing 4 changed files with 48 additions and 45 deletions.
59 changes: 28 additions & 31 deletions src/components/profiles-modals/ImportProfileModal.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
<script lang="ts">
import { mixins } from "vue-class-component";
import { Component } from 'vue-property-decorator';
import { Component, Watch } from 'vue-property-decorator';
import ManagerInformation from "../../_managerinf/ManagerInformation";
import R2Error from "../../model/errors/R2Error";
import ExportFormat from "../../model/exports/ExportFormat";
import ExportMod from "../../model/exports/ExportMod";
import ThunderstoreCombo from "../../model/ThunderstoreCombo";
import ThunderstoreMod from "../../model/ThunderstoreMod";
import ThunderstoreDownloaderProvider from "../../providers/ror2/downloading/ThunderstoreDownloaderProvider";
import InteractionProvider from "../../providers/ror2/system/InteractionProvider";
Expand All @@ -30,6 +31,7 @@ export default class ImportProfileModal extends mixins(ProfilesMixin) {
private targetProfileName: string = '';
private profileImportFilePath: string | null = null;
private profileImportContent: ExportFormat | null = null;
private profileMods: {known: ThunderstoreCombo[], unknown: string[]} = {known: [], unknown: []};
private activeStep:
'FILE_CODE_SELECTION'
| 'IMPORT_FILE'
Expand All @@ -56,31 +58,32 @@ export default class ImportProfileModal extends mixins(ProfilesMixin) {
this.profileImportCode = '';
this.profileImportFilePath = null;
this.profileImportContent = null;
this.profileMods = {known: [], unknown: []};
this.$store.commit('closeImportProfileModal');
}
get profileMods(): {known: ThunderstoreMod[], unknown: string[]} {
const profileMods = this.profileImportContent ? this.profileImportContent.getMods() : [];
const known: ThunderstoreMod[] = [];
const unknown: string[] = [];
for (const mod of profileMods) {
const tsMod = this.$store.getters['tsMods/tsMod'](mod);
if (tsMod) {
known.push(tsMod);
} else {
unknown.push(mod.getName());
}
}
unknown.sort();
return {known, unknown};
get knownProfileMods(): ThunderstoreMod[] {
return this.profileMods.known.map((combo) => combo.getMod());
}
get unknownProfileModNames(): string {
return this.profileMods.unknown.join(', ');
}
// Required to trigger a re-render of the modlist in preview step
// when the online modlist is refreshed.
@Watch('$store.state.tsMods.mods')
async updateProfileModsOnOnlineModListRefresh() {
if (this.profileImportContent === null) {
return;
}
this.profileMods = await ProfileUtils.exportModsToCombos(
this.profileImportContent.getMods(),
this.$store.state.activeGame
);
}
get isProfileCodeValid(): boolean {
return VALID_PROFILE_CODE_REGEX.test(this.profileImportCode);
}
Expand Down Expand Up @@ -126,9 +129,13 @@ export default class ImportProfileModal extends mixins(ProfilesMixin) {
return;
}
let read: string = '';
try {
read = await ProfileUtils.readProfileFile(files[0]);
const yamlContent = await ProfileUtils.readProfileFile(files[0]);
this.profileImportContent = await ProfileUtils.parseYamlToExportFormat(yamlContent);
this.profileMods = await ProfileUtils.exportModsToCombos(
this.profileImportContent.getMods(),
this.$store.state.activeGame
);
} catch (e: unknown) {
const err = R2Error.fromThrownValue(e);
this.$store.commit('error/handleError', err);
Expand All @@ -137,15 +144,6 @@ export default class ImportProfileModal extends mixins(ProfilesMixin) {
}
this.profileImportFilePath = files[0];
try {
this.profileImportContent = await ProfileUtils.parseYamlToExportFormat(read);
} catch (e: unknown) {
const err = R2Error.fromThrownValue(e);
this.$store.commit('error/handleError', err)
this.closeModal();
return;
}
this.activeStep = 'REVIEW_IMPORT';
}
Expand Down Expand Up @@ -215,13 +213,12 @@ export default class ImportProfileModal extends mixins(ProfilesMixin) {
const progressCallback = (progress: number|string) => typeof progress === "number"
? this.importPhaseDescription = `Downloading mods: ${Math.floor(progress)}%`
: this.importPhaseDescription = progress;
const game = this.$store.state.activeGame;
const settings = this.$store.getters['settings'];
const ignoreCache = settings.getContext().global.ignoreCache;
const isUpdate = this.importUpdateSelection === 'UPDATE';
try {
const comboList = await ProfileUtils.exportModsToCombos(mods, game);
const comboList = this.profileMods.known;
await ThunderstoreDownloaderProvider.instance.downloadImportedMods(comboList, ignoreCache, progressCallback);
await ProfileUtils.populateImportedProfile(comboList, mods, targetProfileName, isUpdate, zipPath, progressCallback);
} catch (e) {
Expand Down Expand Up @@ -305,7 +302,7 @@ export default class ImportProfileModal extends mixins(ProfilesMixin) {
<h2 class="modal-title">Packages to be installed</h2>
</template>
<template v-slot:body>
<OnlineModList :paged-mod-list="profileMods.known" :read-only="true" />
<OnlineModList :paged-mod-list="knownProfileMods" :read-only="true" />
<div v-if="profileMods.known.length === 0 || profileMods.unknown.length > 0" class="notification is-warning margin-top">
<p v-if="profileMods.known.length === 0">
None of the packages in the profile were found on Thunderstore:
Expand Down
3 changes: 3 additions & 0 deletions src/model/ThunderstoreCombo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,7 @@ export default class ThunderstoreCombo {
this.version = version;
}

public getDependencyString(): string {
return `${this.mod.getFullName()}-${this.version.getVersionNumber().toString()}`;
}
}
5 changes: 2 additions & 3 deletions src/store/modules/TsModsModule.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { ActionTree, GetterTree, MutationTree } from 'vuex';

import { State as RootState } from '../index';
import ExportMod from '../../model/exports/ExportMod';
import ManifestV2 from '../../model/ManifestV2';
import ThunderstoreMod from '../../model/ThunderstoreMod';
import VersionNumber from '../../model/VersionNumber';
Expand Down Expand Up @@ -81,7 +80,7 @@ export const TsModsModule = {
* time this happens slows down the LocalModList, cache the
* data in a Map.
*/
cachedMod: (state) => (mod: ExportMod|ManifestV2): CachedMod => {
cachedMod: (state) => (mod: ManifestV2): CachedMod => {
const cacheKey = `${mod.getName()}-${mod.getVersionNumber()}`;

if (state.cache.get(cacheKey) === undefined) {
Expand Down Expand Up @@ -119,7 +118,7 @@ export const TsModsModule = {
},

/*** Return ThunderstoreMod representation of a ManifestV2 */
tsMod: (_state, getters) => (mod: ExportMod|ManifestV2): ThunderstoreMod | undefined => {
tsMod: (_state, getters) => (mod: ManifestV2): ThunderstoreMod | undefined => {
return getters.cachedMod(mod).tsMod;
},

Expand Down
26 changes: 15 additions & 11 deletions src/utils/ProfileUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,23 @@ import ProfileInstallerProvider from "../providers/ror2/installing/ProfileInstal
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());
const combos = await PackageDb.getCombosByDependencyStrings(game, dependencyStrings);

if (combos.length === 0) {
throw new R2Error(
'No importable mods found',
'None of the mods or versions listed in the shared profile are available on Thunderstore.',
'Make sure the shared profile is meant for the currently selected game.'
);
export async function exportModsToCombos(
exportMods: ExportMod[],
game: Game
): Promise<{known: ThunderstoreCombo[], unknown: string[]}> {
const allDependencyStrings = exportMods.map((m) => m.getDependencyString());
const known = await PackageDb.getCombosByDependencyStrings(game, allDependencyStrings);
const knownDependencyStrings = known.map((c) => c.getDependencyString());
let unknown: string[] = [];

for (const dependencyString of allDependencyStrings) {
if (!knownDependencyStrings.includes(dependencyString)) {
unknown.push(dependencyString);
}
}

return combos;
unknown.sort();
return {known, unknown};
}

async function extractConfigsToImportedProfile(
Expand Down

0 comments on commit 6b13aa2

Please sign in to comment.