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

Fix profile import silently leaving some mods uninstalled #1647

Merged
merged 1 commit into from
Feb 27, 2025
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
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
Loading