Skip to content

Commit

Permalink
Merge pull request #156 from BUTR/dev
Browse files Browse the repository at this point in the history
Release 1.1.3
  • Loading branch information
Aragas authored Sep 1, 2024
2 parents 7c143ea + 6059e18 commit ad8cf4d
Show file tree
Hide file tree
Showing 23 changed files with 561 additions and 153 deletions.
5 changes: 5 additions & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
---------------------------------------------------------------------------------------------------
Version: 1.1.3
* Added reload button for Mod Options Tab
* Install Harmony when installing BLSE
* Added Select/Deselect all mods
---------------------------------------------------------------------------------------------------
Version: 1.1.2
* Collection Load Order Tab was not updated correctly on Load Order Page changes
* Added French localization
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "game-mount-and-blade-ii-bannerlord-butr",
"version": "1.1.2",
"version": "1.1.3",
"description": "A Vortex extension for Mount and Blade II: Bannerlord mod management.",
"author": "BUTR Team & Nexus Mods",
"license": "GPL-3.0+",
Expand Down Expand Up @@ -58,7 +58,7 @@
"typescript": "^5.4.5",
"vortex-api": "git+https://github.com/Nexus-Mods/vortex-api",
"vortex-ext-common": "^0.4.0",
"webpack": "^5.92.1",
"webpack": "^5.94.0",
"webpack-cli": "^5.1.4",
"webpack-node-externals": "^3.0.0"
},
Expand Down
59 changes: 30 additions & 29 deletions src/blse/utils.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { gte } from 'semver';
import { actions, selectors, types, util } from 'vortex-api';
import { IFileInfo } from '@nexusmods/nexus-api/lib';
import { BLSE_MOD_ID, BLSE_URL, GAME_ID } from '../common';
import { hasPersistentBannerlordMods } from '../vortex';
import { BLSE_MOD_ID, BLSE_URL, GAME_ID, HARMONY_MOD_ID } from '../common';
import { downloadAndEnableLatestModVersion, hasPersistentBannerlordMods } from '../vortex';
import { LocalizationManager } from '../localization';
import { IBannerlordMod, IBannerlordModStorage } from '../types';

export const isModActive = (profile: types.IProfile, mod: IBannerlordMod): boolean => {
const isModActive = (profile: types.IProfile, mod: IBannerlordMod): boolean => {
return profile.modState[mod.id]?.enabled ?? false;
};
const isModBLSE = (mod: IBannerlordMod): boolean => {
Expand Down Expand Up @@ -91,39 +90,41 @@ export const downloadBLSE = async (api: types.IExtensionApi, shouldUpdate: boole
await api.ext?.ensureLoggedIn?.();

try {
const modFiles = (await api.ext.nexusGetModFiles?.(GAME_ID, BLSE_MOD_ID)) ?? [];
await downloadAndEnableLatestModVersion(api, BLSE_MOD_ID);
await downloadAndEnableLatestModVersion(api, HARMONY_MOD_ID);

const fileTime = (input: IFileInfo): number => Number.parseInt(input.uploaded_time, 10);
await deployBLSE(api);
} catch (err) {
api.showErrorNotification?.(t('Failed to download/install BLSE'), err);
util.opn(BLSE_URL).catch(() => null);
} finally {
api.dismissNotification?.('blse-installing');
}
};

const file = modFiles.filter((file) => file.category_id === 1).sort((lhs, rhs) => fileTime(lhs) - fileTime(rhs))[0];
export const downloadHarmony = async (api: types.IExtensionApi, shouldUpdate: boolean = false): Promise<void> => {
const { localize: t } = LocalizationManager.getInstance(api);

if (!file) {
throw new util.ProcessCanceled('No BLSE main file found');
}
api.dismissNotification?.('harmony-missing');
api.sendNotification?.({
id: 'harmony-installing',
message: shouldUpdate ? t('Updating Harmony') : t('Installing Harmony'),
type: 'activity',
noDismiss: true,
allowSuppress: false,
});

const dlInfo = {
game: GAME_ID,
name: 'BLSE',
};

const nxmUrl = `nxm://${GAME_ID}/mods/${BLSE_MOD_ID}/files/${file.file_id}`;
const dlId = await util.toPromise<string>((cb) =>
api.events.emit('start-download', [nxmUrl], dlInfo, undefined, cb, undefined, { allowInstall: false })
);
const modId = await util.toPromise<string>((cb) =>
api.events.emit('start-install-download', dlId, { allowAutoEnable: false }, cb)
);
const profile: types.IProfile | undefined = selectors.activeProfile(api.getState());
await actions.setModsEnabled(api, profile.id, [modId], true, {
allowAutoDeploy: false,
installed: true,
});
await api.ext?.ensureLoggedIn?.();

try {
await downloadAndEnableLatestModVersion(api, BLSE_MOD_ID);
await downloadAndEnableLatestModVersion(api, HARMONY_MOD_ID);

await deployBLSE(api);
} catch (err) {
api.showErrorNotification?.(t('Failed to download/install BLSE'), err);
api.showErrorNotification?.(t('Failed to download/install Harmony'), err);
util.opn(BLSE_URL).catch(() => null);
} finally {
api.dismissNotification?.('blse-installing');
api.dismissNotification?.('harmony-installing');
}
};
188 changes: 133 additions & 55 deletions src/blse/vortex.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
import { actions, selectors, types } from 'vortex-api';
import { deployBLSE, downloadBLSE, findBLSEDownload, findBLSEMod, isModActive } from './utils';
import { selectors, types } from 'vortex-api';
import path from 'path';
import { LocalizationManager } from '../localization';
import { hasPersistentBannerlordMods } from '../vortex';
import {
checkBLSEDeploy,
checkHarmonyDeploy,
DeployModResult,
DeployModStatus,
getBinaryPath,
hasPersistentBannerlordMods,
installBLSE,
installHarmony,
} from '../vortex';
import { BLSE_CLI_EXE } from '../common';
import { getPathExistsAsync } from '../utils';

const sendNotification = (
const sendBLSENotification = (
api: types.IExtensionApi,
title: string,
actionTitle: string,
Expand All @@ -25,84 +36,151 @@ const sendNotification = (
});
};

export const recommendBLSE = (api: types.IExtensionApi): void => {
const sendHarmonyNotification = (
api: types.IExtensionApi,
title: string,
actionTitle: string,
action: (dismiss: types.NotificationDismiss) => void
): void => {
const { localize: t } = LocalizationManager.getInstance(api);

const state = api.getState();
api.sendNotification?.({
id: 'harmony-missing',
type: 'warning',
title: title,
message: t('Harmony is required for BLSE.'),
actions: [
{
title: actionTitle,
action: action,
},
],
});
};

const profile: types.IProfile | undefined = selectors.activeProfile(state);
const doBLSEDeploy = (
api: types.IExtensionApi,
profile: types.IProfile,
harmonyDeployResult: DeployModResult,
blseResult: DeployModResult
): void => {
const { localize: t } = LocalizationManager.getInstance(api);

const mods = hasPersistentBannerlordMods(state.persistent) ? state.persistent.mods.mountandblade2bannerlord : {};
const blseMod = findBLSEMod(mods);
if (blseMod) {
// Found but not enabled
const blseIsActive = isModActive(profile, blseMod);
if (!blseIsActive) {
switch (blseResult.status) {
case DeployModStatus.OK:
return;
case DeployModStatus.NOT_DOWNLOADED: {
const action = (dismiss: types.NotificationDismiss): void => {
installHarmony(api, profile, harmonyDeployResult)
.catch(() => {})
.finally(() => {
installBLSE(api, profile, blseResult)
.catch(() => {})
.finally(() => dismiss());
});
};
sendBLSENotification(api, t('BLSE is not installed via Vortex'), t('Get BLSE'), action);
return;
}
case DeployModStatus.NOT_INSTALLED: {
const action = (dismiss: types.NotificationDismiss): void => {
if (blseResult.downloadId === undefined) {
return;
}
installHarmony(api, profile, harmonyDeployResult)
.catch(() => {})
.finally(() => {
installBLSE(api, profile, blseResult)
.catch(() => {})
.finally(() => dismiss());
});
};
sendBLSENotification(api, t('BLSE is not installed'), t('Install'), action);
return;
}
case DeployModStatus.NOT_ENABLED: {
const action = (dismiss: types.NotificationDismiss): void => {
api.store?.dispatch(actions.setModEnabled(profile.id, blseMod.id, true));
deployBLSE(api)
installHarmony(api, profile, harmonyDeployResult)
.catch(() => {})
.finally(() => {
if (blseResult.modId === undefined) {
return;
}
installBLSE(api, profile, blseResult)
.catch(() => {})
.finally(() => dismiss());
});
};
sendBLSENotification(api, t('BLSE is not enabled'), t('Enable'), action);
return;
}
}
};

const doHarmonyDeploy = (api: types.IExtensionApi, profile: types.IProfile, result: DeployModResult): void => {
const { localize: t } = LocalizationManager.getInstance(api);

switch (result.status) {
case DeployModStatus.OK:
return;
case DeployModStatus.NOT_DOWNLOADED: {
const action = (dismiss: types.NotificationDismiss): void => {
installHarmony(api, profile, result)
.catch(() => {})
.finally(() => dismiss());
};
sendNotification(api, t('BLSE is not enabled'), t('Enable'), action);
sendHarmonyNotification(api, t('Harmony is not installed via Vortex'), t('Get Harmony'), action);
return;
}
} else {
const blseDownload = findBLSEDownload(api);
if (blseDownload !== undefined) {
// Downloaded but not installed
case DeployModStatus.NOT_INSTALLED: {
const action = (dismiss: types.NotificationDismiss): void => {
api.events.emit('start-install-download', blseDownload, {
if (result.downloadId === undefined) {
return;
}
api.events.emit('start-install-download', result.downloadId, {
allowAutoEnable: true,
});
deployBLSE(api)
installHarmony(api, profile, result)
.catch(() => {})
.finally(() => dismiss());
};
sendNotification(api, t('BLSE is not installed'), t('Install'), action);
} else {
// Non existent
sendHarmonyNotification(api, t('Harmony is not installed'), t('Install'), action);
return;
}
case DeployModStatus.NOT_ENABLED: {
const action = (dismiss: types.NotificationDismiss): void => {
downloadBLSE(api)
installHarmony(api, profile, result)
.catch(() => {})
.finally(() => dismiss());
};
sendNotification(api, t('BLSE is not installed via Vortex'), t('Get BLSE'), action);
sendHarmonyNotification(api, t('Harmony is not enabled'), t('Enable'), action);
return;
}
}
};

export const forceInstallBLSE = async (api: types.IExtensionApi): Promise<void> => {
const { localize: t } = LocalizationManager.getInstance(api);
export const recommendBLSE = async (api: types.IExtensionApi, discovery: types.IDiscoveryResult): Promise<void> => {
const state = api.getState();
const profile: types.IProfile | undefined = selectors.activeProfile(state);
const mods = hasPersistentBannerlordMods(state.persistent) ? state.persistent.mods.mountandblade2bannerlord : {};

api.sendNotification?.({
id: 'blse-required',
type: 'info',
title: t('BLSE Required'),
message: t('BLSE is required by the collection. Ensuring it is installed...'),
});
if (discovery.path === undefined) {
throw new Error(`discovery.path is undefined!`);
}

const state = api.getState();
const harmonyDeployResult = checkHarmonyDeploy(api, profile, mods);
const blseDeployResult = checkBLSEDeploy(api, profile, mods);

const profile: types.IProfile | undefined = selectors.activeProfile(state);
if (harmonyDeployResult.status !== DeployModStatus.OK && blseDeployResult.status === DeployModStatus.OK) {
doHarmonyDeploy(api, profile, harmonyDeployResult);
}

const mods = hasPersistentBannerlordMods(state.persistent) ? state.persistent.mods.mountandblade2bannerlord : {};
const blseMod = findBLSEMod(mods);
if (blseMod) {
// Found but not enabled
const blseIsActive = isModActive(profile, blseMod);
if (!blseIsActive) {
api.store?.dispatch(actions.setModEnabled(profile.id, blseMod.id, true));
await deployBLSE(api);
}
} else {
const blseDownload = findBLSEDownload(api);
if (blseDownload !== undefined) {
// Downloaded but not installed
await deployBLSE(api);
} else {
// Non existent
await downloadBLSE(api);
}
// skip if BLSE found
// question: if the user incorrectly deleted BLSE and the binary is left, what should we do?
// maybe just ask the user to always install BLSE via Vortex?
const binaryPath = path.join(discovery.path, getBinaryPath(discovery.store), BLSE_CLI_EXE);
const binaryExists = await getPathExistsAsync(binaryPath);
if (!binaryExists || blseDeployResult.status !== DeployModStatus.OK) {
doBLSEDeploy(api, profile, harmonyDeployResult, blseDeployResult);
}
};
2 changes: 2 additions & 0 deletions src/butr/modAnalyzerProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { log } from 'vortex-api';
import { request, RequestOptions } from 'https';
import { BUTR_HOST } from './const';
import { IModAnalyzerRequestQuery, IModAnalyzerResult } from './types';
import { version } from '../../package.json';

export class ModAnalyzerProxy {
private options: RequestOptions;
Expand All @@ -14,6 +15,7 @@ export class ModAnalyzerProxy {
headers: {
Tenant: '1', // Bannerlord
'Content-Type': 'application/json',
'User-Agent': `Vortex BUTR Extension v${version}`,
},
};
}
Expand Down
10 changes: 5 additions & 5 deletions src/collections/generalData.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { selectors, types } from 'vortex-api';
import { profile } from 'node:console';
import { ICollectionData, ICollectionDataWithGeneralData, ICollectionGeneralData } from './types';
import { genCollectionGeneralLoadOrder, parseCollectionGeneralLoadOrder } from './loadOrder';
import { CollectionParseError } from './errors';
import { collectionInstallBLSE } from './utils';
import { GAME_ID } from '../common';
import { hasPersistentBannerlordMods, hasPersistentLoadOrder } from '../vortex';
import { findBLSEMod, forceInstallBLSE, isModActive } from '../blse';
import { isModActive } from '../vortex';
import { findBLSEMod } from '../blse';
import { vortexToPersistence } from '../loadOrder';
import { VortexLauncherManager } from '../launcher';
import { IBannerlordMod, IBannerlordModStorage, VortexLoadOrderStorage } from '../types';
import { IBannerlordModStorage, VortexLoadOrderStorage } from '../types';

/**
* Assumes that the correct Game ID is active and that the profile is set up correctly.
Expand Down Expand Up @@ -54,7 +54,7 @@ export const parseCollectionGeneralData = async (
await parseCollectionGeneralLoadOrder(api, modules, collection);

if (hasBLSE) {
await forceInstallBLSE(api);
await collectionInstallBLSE(api);
}
};

Expand Down
Loading

0 comments on commit ad8cf4d

Please sign in to comment.