From 3c08f0ab23b72147a254231bc906bb0d7b8e9438 Mon Sep 17 00:00:00 2001
From: Vitalii Mikhailov
Date: Sun, 6 Oct 2024 00:00:43 +0300
Subject: [PATCH] Use Steam/GOG/Epic binaries on Xbox if Xbox binaries are not
available
Fixed redundant auto sort triggers
---
changelog.txt | 5 +
package.json | 4 +-
src/common.ts | 2 +
src/index.ts | 32 +++-
src/launcher/actions.ts | 17 ++
src/launcher/index.ts | 2 +
src/launcher/manager.ts | 160 +++++++++++++++---
src/launcher/utils.ts | 17 ++
src/loadOrder/converters.ts | 17 +-
src/loadOrder/utils.ts | 6 +-
src/localization/manager.ts | 2 +-
src/localization/types.ts | 2 +-
src/react/index.ts | 3 +-
src/react/reducersSession.ts | 32 ++++
.../{reducers.ts => reducersSettings.ts} | 2 +-
src/types.ts | 12 +-
.../components/LoadOrderItemRenderer.tsx | 2 +
.../components/SteamBinariesOnXbox.tsx | 26 +++
src/views/LoadOrder/components/index.ts | 1 +
src/vortex/events.ts | 34 +++-
src/vortex/types.ts | 12 +-
src/vortex/utils.ts | 12 +-
yarn.lock | 8 +-
23 files changed, 357 insertions(+), 53 deletions(-)
create mode 100644 src/launcher/actions.ts
create mode 100644 src/launcher/utils.ts
create mode 100644 src/react/reducersSession.ts
rename src/react/{reducers.ts => reducersSettings.ts} (98%)
create mode 100644 src/views/LoadOrder/components/SteamBinariesOnXbox.tsx
diff --git a/changelog.txt b/changelog.txt
index 6ef67de..a1b8f0b 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -1,4 +1,9 @@
---------------------------------------------------------------------------------------------------
+Version: 1.1.5
+* Added an option to install Steam/GOG/Epic binaries for Game Pass PC
+* Fixed Load Order validation sometimes checking non enabled mods
+* Fixed Game Version detection
+---------------------------------------------------------------------------------------------------
Version: 1.1.4
* Using Bannerlord version comparison instead of semver on installation
---------------------------------------------------------------------------------------------------
diff --git a/package.json b/package.json
index 672699b..6207a91 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "game-mount-and-blade-ii-bannerlord-butr",
- "version": "1.1.4",
+ "version": "1.1.5",
"description": "A Vortex extension for Mount and Blade II: Bannerlord mod management.",
"author": "BUTR Team & Nexus Mods",
"license": "GPL-3.0+",
@@ -63,7 +63,7 @@
"webpack-node-externals": "^3.0.0"
},
"dependencies": {
- "@butr/vortexextensionnative": "1.0.129",
+ "@butr/vortexextensionnative": "1.0.134",
"ticks-to-date": "^1.0.6"
},
"resolutions": {
diff --git a/src/common.ts b/src/common.ts
index 92dae52..4db9349 100644
--- a/src/common.ts
+++ b/src/common.ts
@@ -12,6 +12,8 @@ export const I18N_NAMESPACE = `game-mount-and-blade2`;
export const SUBMODULE_FILE = `SubModule.xml`;
export const SUB_MODS_IDS = `subModsIds`;
+export const AVAILABLE_STORES = `availableStores`;
+export const STEAM_BINARIES_ON_XBOX = `steamBinariesOnXbox`;
export const BINARY_FOLDER_STANDARD = `Win64_Shipping_Client`;
export const BINARY_FOLDER_STANDARD_MODDING_KIT = `Win64_Shipping_wEditor`;
diff --git a/src/index.ts b/src/index.ts
index bff1178..3c37fb9 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -14,7 +14,7 @@ import {
} from './views';
import { BannerlordGame } from './game';
import { IAddedFiles, IBannerlordModStorage } from './types';
-import { reducer } from './react';
+import { reducerSession, reducerSettings } from './react';
import { actionsSettings } from './settings';
import {
cloneCollectionGeneralData,
@@ -37,6 +37,7 @@ import {
getInstallPathModule,
hasPersistentBannerlordMods,
hasPersistentLoadOrder,
+ installedMod,
isModTypeModule,
} from './vortex';
import { version } from '../package.json';
@@ -46,7 +47,8 @@ import { version } from '../package.json';
const main = (context: types.IExtensionContext): boolean => {
log('info', `Extension Version: ${version}`);
- context.registerReducer(/*path:*/ [`settings`, GAME_ID], /*spec:*/ reducer);
+ context.registerReducer(/*path:*/ [`settings`, GAME_ID], /*spec:*/ reducerSettings);
+ context.registerReducer(/*path:*/ [`session`, GAME_ID], /*spec:*/ reducerSession);
context.registerSettings(
/*title:*/ `Interface`,
@@ -177,10 +179,20 @@ const main = (context: types.IExtensionContext): boolean => {
const launcherManager = VortexLauncherManager.getInstance(context.api);
return launcherManager.testModule(files, gameId);
}),
- /*install:*/ toBluebird((files: string[], destinationPath: string) => {
- const launcherManager = VortexLauncherManager.getInstance(context.api);
- return launcherManager.installModule(files, destinationPath);
- })
+ /*install:*/ toBluebird(
+ (
+ files: string[],
+ destinationPath: string,
+ _gameId: string,
+ _progressDelegate: types.ProgressDelegate,
+ _choices?: unknown,
+ _unattended?: boolean,
+ archivePath?: string
+ ) => {
+ const launcherManager = VortexLauncherManager.getInstance(context.api);
+ return launcherManager.installModule(files, destinationPath, archivePath);
+ }
+ )
);
context.registerModType(
/*id:*/ 'bannerlord-module',
@@ -265,6 +277,14 @@ const main = (context: types.IExtensionContext): boolean => {
await gamemodeActivatedSave(context.api);
});
+ context.api.events.on('did-install-mod', (gameId: string, archiveId: string, modId: string): void => {
+ if (GAME_ID !== gameId) {
+ return;
+ }
+
+ installedMod(context.api, archiveId, modId);
+ });
+
context.api.onAsync(`added-files`, async (profileId: string, files: IAddedFiles[]) => {
const state = context.api.getState();
const profile: types.IProfile | undefined = selectors.profileById(state, profileId);
diff --git a/src/launcher/actions.ts b/src/launcher/actions.ts
new file mode 100644
index 0000000..42947c4
--- /dev/null
+++ b/src/launcher/actions.ts
@@ -0,0 +1,17 @@
+import { createAction } from 'redux-act';
+import { EXTENSION_BASE_ID } from '../common';
+
+export type SetUseSteamBinariesOnXboxPayload = {
+ useSteamBinariesOnXbox: boolean;
+};
+
+const setUseSteamBinariesOnXbox = createAction(
+ `${EXTENSION_BASE_ID}_SET_USE_STEAM_BINARIES_ON_XBOX`,
+ (useSteamBinariesOnXbox: boolean) => ({
+ useSteamBinariesOnXbox,
+ })
+);
+
+export const actionsLauncher = {
+ setUseSteamBinariesOnXbox,
+};
diff --git a/src/launcher/index.ts b/src/launcher/index.ts
index 0e63fbf..9ae1ee9 100644
--- a/src/launcher/index.ts
+++ b/src/launcher/index.ts
@@ -1,3 +1,5 @@
+export * from './actions';
export * from './hooks';
export * from './manager';
+export * from './utils';
export * from './version';
diff --git a/src/launcher/manager.ts b/src/launcher/manager.ts
index 4e959fb..82bf245 100644
--- a/src/launcher/manager.ts
+++ b/src/launcher/manager.ts
@@ -2,7 +2,9 @@ import { actions, selectors, types, util } from 'vortex-api';
import { BannerlordModuleManager, NativeLauncherManager, types as vetypes } from '@butr/vortexextensionnative';
import { Dirent, readdirSync, readFileSync, writeFileSync } from 'node:fs';
import path from 'path';
-import { hasPersistentLoadOrder } from '../vortex';
+import { vortexStoreToLibraryStore } from './utils';
+import { actionsLauncher } from './actions';
+import { hasPersistentBannerlordMods, hasPersistentLoadOrder, hasSessionWithBannerlord } from '../vortex';
import {
actionsLoadOrder,
libraryToLibraryVM,
@@ -16,8 +18,15 @@ import {
} from '../loadOrder';
import { getBetaSortingFromSettings } from '../settings';
import { filterEntryWithInvalidId } from '../utils';
-import { GAME_ID, SUB_MODS_IDS } from '../common';
-import { IModuleCache, VortexLoadOrderStorage, VortexStoreIds } from '../types';
+import {
+ AVAILABLE_STORES,
+ BINARY_FOLDER_STANDARD,
+ BINARY_FOLDER_XBOX,
+ GAME_ID,
+ STEAM_BINARIES_ON_XBOX,
+ SUB_MODS_IDS,
+} from '../common';
+import { IModuleCache, VortexLoadOrderStorage } from '../types';
import { LocalizationManager } from '../localization';
export class VortexLauncherManager {
@@ -186,7 +195,11 @@ export class VortexLauncherManager {
/**
* Calls LauncherManager's installModule and converts the result to Vortex data
*/
- public installModule = (files: string[], destinationPath: string): Promise => {
+ public installModule = async (
+ files: string[],
+ destinationPath: string,
+ archivePath?: string
+ ): Promise => {
const subModuleRelFilePath = files.find((x) => x.endsWith('SubModule.xml'))!;
const subModuleFilePath = path.join(destinationPath, subModuleRelFilePath);
const subModuleFile = readFileSync(subModuleFilePath, {
@@ -201,6 +214,86 @@ export class VortexLauncherManager {
const result = this.launcherManager.installModule(files, [moduleInfo]);
const subModsIds = Array();
+ const availableStores = result.instructions.reduce((map, current) => {
+ if (current.store !== undefined) {
+ return map.includes(current.store) ? map : [...map, current.store];
+ }
+ return map;
+ }, []);
+
+ const state = this.api.getState();
+
+ let useSteamBinaries = false;
+
+ let useSteamBinariesToggle = false;
+ if (hasSessionWithBannerlord(state.session)) {
+ useSteamBinariesToggle = state.session[GAME_ID].useSteamBinariesOnXbox ?? false;
+ }
+
+ const discovery: types.IDiscoveryResult | undefined = selectors.currentGameDiscovery(state);
+ const store = vortexStoreToLibraryStore(discovery?.store ?? '');
+ if (!availableStores.includes(store) && store === 'Xbox') {
+ if (useSteamBinariesToggle) {
+ availableStores.push(store);
+ useSteamBinaries = true;
+ } else {
+ const { localize: t } = LocalizationManager.getInstance(this.api);
+
+ let modName = '';
+
+ if (archivePath !== undefined && archivePath.length > 0) {
+ if (hasPersistentBannerlordMods(state.persistent)) {
+ const archiveFileName = path.basename(archivePath!, path.extname(archivePath!));
+ const mod = state.persistent.mods.mountandblade2bannerlord[archiveFileName];
+ if (mod) {
+ modName = mod.attributes?.modName ?? '';
+ }
+ }
+ }
+ // Not sure we even can get here
+ if (modName.length === 0) {
+ modName = result.instructions
+ .filter((x) => x.moduleInfo !== undefined)
+ .filter((value, index, self) => self.indexOf(value) === index)
+ .map((x) => x.moduleInfo!)
+ .map((x) => `* ${x.name} (${x.id})`)
+ .join('\n ');
+ }
+
+ const no = t('No, remove the mods');
+ const yes = t('Install, I accept the risks');
+ const yesForAll = t(`Install, I accept the risks. Don't ask again for the current session`);
+ const dialogResult = await this.api.showDialog?.(
+ 'question',
+ t(`Compatible Issue With Game Pass PC Version of the Game!`),
+ {
+ message: t(
+ `The following mods:
+{{ modName }}
+
+Do not provide binaries for Game Pass PC (Xbox)!
+Do you want to install binaries for Steam/GOG/Epic version of the game?
+
+Warning! This can lead to issues!`,
+ { replace: { modName: modName } }
+ ),
+ },
+ [{ label: no }, { label: yes }, { label: yesForAll }]
+ );
+ switch (dialogResult?.action) {
+ case yes:
+ availableStores.push(store);
+ useSteamBinaries = true;
+ break;
+ case yesForAll:
+ availableStores.push(store);
+ useSteamBinaries = true;
+ this.api.store?.dispatch(actionsLauncher.setUseSteamBinariesOnXbox(true));
+ break;
+ }
+ }
+ }
+
const transformedResult: types.IInstallResult = {
instructions: result.instructions.reduce((map, current) => {
switch (current.type) {
@@ -216,6 +309,22 @@ export class VortexLauncherManager {
subModsIds.push(current.moduleInfo.id);
}
break;
+ case 'CopyStore':
+ if (current.store === store) {
+ map.push({
+ type: 'copy',
+ source: current.source ?? '',
+ destination: current.destination ?? '',
+ });
+ }
+ if (current.store === 'Steam' && useSteamBinaries) {
+ map.push({
+ type: 'copy',
+ source: current.source ?? '',
+ destination: current.destination?.replace(BINARY_FOLDER_STANDARD, BINARY_FOLDER_XBOX) ?? '',
+ });
+ }
+ break;
}
return map;
}, []),
@@ -225,6 +334,16 @@ export class VortexLauncherManager {
key: SUB_MODS_IDS,
value: subModsIds,
});
+ transformedResult.instructions.push({
+ type: 'attribute',
+ key: AVAILABLE_STORES,
+ value: availableStores,
+ });
+ transformedResult.instructions.push({
+ type: 'attribute',
+ key: STEAM_BINARIES_ON_XBOX,
+ value: useSteamBinaries,
+ });
return Promise.resolve(transformedResult);
};
@@ -255,23 +374,7 @@ export class VortexLauncherManager {
* Sets the game store manually, since the launcher manager is not perfect.
*/
public setStore = (storeId: string): void => {
- switch (storeId) {
- case VortexStoreIds.Steam:
- this.launcherManager.setGameStore(`Steam`);
- break;
- case VortexStoreIds.GOG:
- this.launcherManager.setGameStore(`GOG`);
- break;
- case VortexStoreIds.Epic:
- this.launcherManager.setGameStore(`Epic`);
- break;
- case VortexStoreIds.Xbox:
- this.launcherManager.setGameStore(`Xbox`);
- break;
- default:
- this.launcherManager.setGameStore(`Unknown`);
- break;
- }
+ this.launcherManager.setGameStore(vortexStoreToLibraryStore(storeId));
};
/**
@@ -293,22 +396,31 @@ export class VortexLauncherManager {
* Returns the Load Order saved in Vortex's permantent storage
*/
private loadLoadOrder = (): vetypes.LoadOrder => {
+ const state = this.api.getState();
+ if (!hasPersistentBannerlordMods(state.persistent)) {
+ return {};
+ }
+ const mods = Object.values(state.persistent.mods.mountandblade2bannerlord);
+
const allModules = this.getAllModules();
const savedLoadOrder = persistenceToVortex(this.api, allModules, readLoadOrder(this.api));
let index = savedLoadOrder.length;
for (const module of Object.values(allModules)) {
- if (!savedLoadOrder.find((x) => x.id === module.id))
+ if (!savedLoadOrder.find((x) => x.id === module.id)) {
+ const mod = mods.find((x) => x.attributes?.subModsIds?.includes(module.id));
savedLoadOrder.push({
id: module.id,
enabled: false,
name: module.name,
data: {
moduleInfoExtended: module,
+ hasSteamBinariesOnXbox: mod?.attributes?.steamBinariesOnXbox ?? false,
index: index++,
},
});
+ }
}
const loadOrderConverted = vortexToLibrary(savedLoadOrder);
@@ -427,13 +539,13 @@ export class VortexLauncherManager {
private readFileContent = (filePath: string, offset: number, length: number): Uint8Array | null => {
try {
if (offset === 0 && length === -1) {
- return readFileSync(filePath);
+ return new Uint8Array(readFileSync(filePath));
} else if (offset >= 0 && length > 0) {
// TODO: read the chunk we actually need, but there's no readFile()
//const fd = fs.openSync(filePath, 'r');
//const buffer = Buffer.alloc(length);
//fs.readSync(fd, buffer, offset, length, 0);
- return readFileSync(filePath).slice(offset, offset + length);
+ return new Uint8Array(readFileSync(filePath)).slice(offset, offset + length);
} else {
return null;
}
diff --git a/src/launcher/utils.ts b/src/launcher/utils.ts
new file mode 100644
index 0000000..af5eec4
--- /dev/null
+++ b/src/launcher/utils.ts
@@ -0,0 +1,17 @@
+import { types as vetypes } from '@butr/vortexextensionnative';
+import { VortexStoreIds } from '../types';
+
+export const vortexStoreToLibraryStore = (storeId: string): vetypes.GameStore => {
+ switch (storeId) {
+ case VortexStoreIds.Steam:
+ return 'Steam';
+ case VortexStoreIds.GOG:
+ return 'GOG';
+ case VortexStoreIds.Epic:
+ return 'Epic';
+ case VortexStoreIds.Xbox:
+ return 'Xbox';
+ default:
+ return 'Unknown';
+ }
+};
diff --git a/src/loadOrder/converters.ts b/src/loadOrder/converters.ts
index 2f59dfe..d5cd4b5 100644
--- a/src/loadOrder/converters.ts
+++ b/src/loadOrder/converters.ts
@@ -1,6 +1,6 @@
import { types } from 'vortex-api';
import { BannerlordModuleManager, types as vetypes } from '@butr/vortexextensionnative';
-import { getModIds } from './utils';
+import { getModuleAttributes } from './utils';
import {
IModuleCache,
IPersistenceLoadOrderEntry,
@@ -17,15 +17,16 @@ export const persistenceToVortex = (
): VortexLoadOrderStorage => {
const loadOrderConverted = loadOrder
.map((x) => {
- const modIds = getModIds(api, x.id);
+ const result = getModuleAttributes(api, x.id);
return {
id: x.id,
name: x.name,
enabled: x.isSelected,
- modId: modIds[0]?.id ?? undefined!,
+ modId: result[0]?.id ?? undefined!,
data: {
moduleInfoExtended: modules[x.id]!,
index: x.index,
+ hasSteamBinariesOnXbox: result[0]?.hasSteamBinariesOnXbox ?? false,
},
};
})
@@ -98,17 +99,18 @@ export const libraryVMToVortex = (
loadOrder: vetypes.ModuleViewModel[]
): VortexLoadOrderStorage => {
const loadOrderConverted = Object.values(loadOrder).map((curr) => {
- const modId = getModIds(api, curr.moduleInfoExtended.id);
+ const result = getModuleAttributes(api, curr.moduleInfoExtended.id);
return {
id: curr.moduleInfoExtended.id,
enabled: curr.isSelected,
name: curr.moduleInfoExtended.name,
- modId: modId[0]?.id ?? undefined!,
+ modId: result[0]?.id ?? undefined!,
data: {
moduleInfoExtended: curr.moduleInfoExtended,
isValid: curr.isValid,
isDisabled: curr.isDisabled,
index: curr.index,
+ hasSteamBinariesOnXbox: result[0]?.hasSteamBinariesOnXbox ?? false,
},
};
}, []);
@@ -174,17 +176,18 @@ export const libraryToVortex = (
}
const moduleValidation = BannerlordModuleManager.validateModule(availableModules, module, validationManager);
- const modId = getModIds(api, curr.id);
+ const result = getModuleAttributes(api, curr.id);
return {
id: curr.id,
enabled: curr.isSelected,
name: curr.name,
- modId: modId[0]?.id ?? undefined!,
+ modId: result[0]?.id ?? undefined!,
data: {
moduleInfoExtended: module,
isValid: !moduleValidation.length,
isDisabled: false,
index: curr.index,
+ hasSteamBinariesOnXbox: result[0]?.hasSteamBinariesOnXbox ?? false,
},
};
}, [])
diff --git a/src/loadOrder/utils.ts b/src/loadOrder/utils.ts
index c462907..37e5929 100644
--- a/src/loadOrder/utils.ts
+++ b/src/loadOrder/utils.ts
@@ -11,12 +11,13 @@ import { hasPersistentLoadOrder } from '../vortex';
type ModIdResult = {
id: string;
source: string;
+ hasSteamBinariesOnXbox: boolean;
};
/**
* I have no idea what to do if we have multiple mods that provide the same Module
*/
-export const getModIds = (api: types.IExtensionApi, moduleId: string): ModIdResult[] => {
+export const getModuleAttributes = (api: types.IExtensionApi, moduleId: string): ModIdResult[] => {
const state = api.getState();
const gameId: string | undefined = selectors.activeGameId(state);
const gameMods = state.persistent.mods[gameId] ?? {};
@@ -29,6 +30,7 @@ export const getModIds = (api: types.IExtensionApi, moduleId: string): ModIdResu
arr.push({
id: mod.attributes['modId'],
source: mod.attributes['source'],
+ hasSteamBinariesOnXbox: mod.attributes['steamBinariesOnXbox'] ?? false,
});
}
@@ -94,7 +96,7 @@ const checkSavedLoadOrder = (api: types.IExtensionApi, autoSort: boolean, loadOr
const { localize: t } = LocalizationManager.getInstance(api);
const savedLoadOrderIssues = Utils.isLoadOrderCorrect(
- loadOrder.map((x) => x.data!.moduleInfoExtended)
+ loadOrder.filter((x) => x.enabled).map((x) => x.data!.moduleInfoExtended)
);
if (autoSort && savedLoadOrderIssues.length > 0) {
// If there were any issues with the saved LO, the orderer will sort the LO to the nearest working state
diff --git a/src/localization/manager.ts b/src/localization/manager.ts
index 2a334de..07056f0 100644
--- a/src/localization/manager.ts
+++ b/src/localization/manager.ts
@@ -30,7 +30,7 @@ export class LocalizationManager {
this.initializedLocalization = true;
}
- return Utils.localizeString(template, values);
+ return Utils.localizeString(template, values as Record);
}
return this.api.translate(template, {
ns: I18N_NAMESPACE,
diff --git a/src/localization/types.ts b/src/localization/types.ts
index e8a4378..54cbee2 100644
--- a/src/localization/types.ts
+++ b/src/localization/types.ts
@@ -1,3 +1,3 @@
export type TranslateValues = {
- [value: string]: string;
+ [value: string]: string | object;
};
diff --git a/src/react/index.ts b/src/react/index.ts
index fce8f95..a87d8b8 100644
--- a/src/react/index.ts
+++ b/src/react/index.ts
@@ -1,2 +1,3 @@
-export * from './reducers';
+export * from './reducersSession';
+export * from './reducersSettings';
export * from './redux';
diff --git a/src/react/reducersSession.ts b/src/react/reducersSession.ts
new file mode 100644
index 0000000..ac50fee
--- /dev/null
+++ b/src/react/reducersSession.ts
@@ -0,0 +1,32 @@
+import { types, util } from 'vortex-api';
+import { createReducer, ReducerHandler, ReducerHandlerState } from './redux';
+import { nameof as nameof2 } from '../nameof';
+import { actionsLauncher, SetUseSteamBinariesOnXboxPayload } from '../launcher';
+import { IBannerlordSession } from '../types';
+
+const nameof = nameof2;
+
+const setUseSteamBinariesOnXbox = (
+ state: ReducerHandlerState,
+ payload: SetUseSteamBinariesOnXboxPayload
+): ReducerHandlerState => {
+ return util.setSafe(state, [nameof('useSteamBinariesOnXbox')], payload.useSteamBinariesOnXbox);
+};
+
+// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
+const getReducers = () => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const reducers: { [key: string]: ReducerHandler } = {};
+ createReducer(actionsLauncher.setUseSteamBinariesOnXbox, setUseSteamBinariesOnXbox, reducers);
+ return reducers;
+};
+
+// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
+const getDefaults = () => ({
+ [nameof('useSteamBinariesOnXbox')]: false,
+});
+
+export const reducerSession: types.IReducerSpec = {
+ reducers: getReducers(),
+ defaults: getDefaults(),
+};
diff --git a/src/react/reducers.ts b/src/react/reducersSettings.ts
similarity index 98%
rename from src/react/reducers.ts
rename to src/react/reducersSettings.ts
index 07cdc36..ac720c6 100644
--- a/src/react/reducers.ts
+++ b/src/react/reducersSettings.ts
@@ -69,7 +69,7 @@ const getDefaults = () => ({
[nameof('saveName')]: {},
});
-export const reducer: types.IReducerSpec = {
+export const reducerSettings: types.IReducerSpec = {
reducers: getReducers(),
defaults: getDefaults(),
};
diff --git a/src/types.ts b/src/types.ts
index 1454358..7b3fa62 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,18 +1,23 @@
import { types } from 'vortex-api';
import { types as vetypes } from '@butr/vortexextensionnative';
-import { SUB_MODS_IDS } from './common';
+import { AVAILABLE_STORES, STEAM_BINARIES_ON_XBOX, SUB_MODS_IDS } from './common';
export type RequiredProperties = Omit & Required>;
+export type IStateSession = types.IState['session'];
+
export type IStatePersistent = types.IState['persistent'];
export type IModAttributes = types.IMod['attributes'];
export interface IBannerlordModAttributes {
modId: number;
+ modName: string;
version: string;
source: string;
[SUB_MODS_IDS]?: string[];
+ [AVAILABLE_STORES]?: string[];
+ [STEAM_BINARIES_ON_XBOX]?: boolean;
}
export interface IBannerlordMod extends types.IMod {
@@ -23,6 +28,10 @@ export interface IBannerlordModStorage {
[modId: string]: IBannerlordMod;
}
+export interface IBannerlordSession {
+ useSteamBinariesOnXbox: boolean;
+}
+
export type PersistenceLoadOrderStorage = IPersistenceLoadOrderEntry[];
export interface IPersistenceLoadOrderEntry {
id: string;
@@ -36,6 +45,7 @@ export type VortexLoadOrderStorage = VortexLoadOrderEntry[];
export type VortexLoadOrderEntry = types.ILoadOrderEntry;
export interface IVortexViewModelData {
moduleInfoExtended: vetypes.ModuleInfoExtendedWithMetadata;
+ hasSteamBinariesOnXbox: boolean;
index: number;
}
diff --git a/src/views/LoadOrder/components/LoadOrderItemRenderer.tsx b/src/views/LoadOrder/components/LoadOrderItemRenderer.tsx
index d5d7a16..02872c9 100644
--- a/src/views/LoadOrder/components/LoadOrderItemRenderer.tsx
+++ b/src/views/LoadOrder/components/LoadOrderItemRenderer.tsx
@@ -7,6 +7,7 @@ import { ValidationError } from './ValidationError';
import { ExternalBanner } from './ExternalBanner';
import { ModuleDuplicates } from './ModuleDuplicates';
import { ModuleProviderIcon } from './ModuleProviderIcon';
+import { SteamBinariesOnXbox } from './SteamBinariesOnXbox';
import { IVortexViewModelData, VortexLoadOrderStorage } from '../../../types';
import { CompatibilityInfo, ModuleIcon } from '../../Shared';
import { isExternal, isLocked } from '../utils';
@@ -87,6 +88,7 @@ export const LoadOrderItemRenderer = (props: LoadOrderItemRendererProps): JSX.El
{name} ({version})
+
diff --git a/src/views/LoadOrder/components/SteamBinariesOnXbox.tsx b/src/views/LoadOrder/components/SteamBinariesOnXbox.tsx
new file mode 100644
index 0000000..6ed1376
--- /dev/null
+++ b/src/views/LoadOrder/components/SteamBinariesOnXbox.tsx
@@ -0,0 +1,26 @@
+import React from 'react';
+import { tooltip } from 'vortex-api';
+import { useLocalization } from '../../../localization';
+
+export type SteamBinariesOnXboxProps = {
+ hasSteamBinariesOnXbox: boolean;
+};
+
+export const SteamBinariesOnXbox = (props: SteamBinariesOnXboxProps): JSX.Element => {
+ const { hasSteamBinariesOnXbox } = props;
+
+ const { localize: t } = useLocalization();
+
+ if (hasSteamBinariesOnXbox) {
+ return (
+
+ );
+ }
+
+ return ;
+};
diff --git a/src/views/LoadOrder/components/index.ts b/src/views/LoadOrder/components/index.ts
index cfeaaaa..f7932a2 100644
--- a/src/views/LoadOrder/components/index.ts
+++ b/src/views/LoadOrder/components/index.ts
@@ -3,4 +3,5 @@ export * from './LoadOrderInfoPanel';
export * from './LoadOrderItemRenderer';
export * from './ModuleDuplicates';
export * from './ModuleProviderIcon';
+export * from './SteamBinariesOnXbox';
export * from './ValidationError';
diff --git a/src/vortex/events.ts b/src/vortex/events.ts
index f6cab58..259e077 100644
--- a/src/vortex/events.ts
+++ b/src/vortex/events.ts
@@ -1,9 +1,11 @@
// eslint-disable-next-line no-restricted-imports
import Bluebird from 'bluebird';
-import { fs, log, selectors, types, util } from 'vortex-api';
+import { actions, fs, log, selectors, types, util } from 'vortex-api';
import path from 'path';
+import { hasPersistentBannerlordMods } from './utils';
import { GAME_ID } from '../common';
import { IAddedFiles } from '../types';
+import { vortexStoreToLibraryStore } from '../launcher';
/**
* Event function, be careful
@@ -54,3 +56,33 @@ export const addedFilesEvent = async (api: types.IExtensionApi, files: IAddedFil
}
});
};
+
+export const installedMod = (api: types.IExtensionApi, archiveId: string, modId: string): void => {
+ const state = api.getState();
+
+ if (!hasPersistentBannerlordMods(state.persistent)) {
+ return;
+ }
+ const mod = state.persistent.mods.mountandblade2bannerlord[modId];
+ if (mod === undefined) {
+ return;
+ }
+
+ const supportedStores = mod.attributes?.availableStores ?? [];
+ if (supportedStores.length === 0) {
+ return;
+ }
+
+ const discovery = selectors.discoveryByGame(state, GAME_ID);
+ if (discovery === undefined) {
+ return;
+ }
+
+ const store = vortexStoreToLibraryStore(discovery.store);
+ if (supportedStores.includes(store)) {
+ return;
+ }
+
+ // uninstall mod if store is not supported by the mod
+ api.store?.dispatch(actions.removeMod(GAME_ID, modId));
+};
diff --git a/src/vortex/types.ts b/src/vortex/types.ts
index b213911..9ff7598 100644
--- a/src/vortex/types.ts
+++ b/src/vortex/types.ts
@@ -1,6 +1,16 @@
import { types } from 'vortex-api';
import { GAME_ID } from '../common';
-import { IBannerlordModStorage, IStatePersistent, VortexLoadOrderStorage } from '../types';
+import {
+ IBannerlordModStorage,
+ IBannerlordSession,
+ IStatePersistent,
+ IStateSession,
+ VortexLoadOrderStorage,
+} from '../types';
+
+export interface IStateSessionWithBannerlord extends IStateSession {
+ [GAME_ID]: IBannerlordSession;
+}
export interface IStatePersistentWithLoadOrder extends IStatePersistent {
loadOrder: {
diff --git a/src/vortex/utils.ts b/src/vortex/utils.ts
index 40aebda..5b009bb 100644
--- a/src/vortex/utils.ts
+++ b/src/vortex/utils.ts
@@ -3,6 +3,7 @@ import {
ISettingsInterfaceWithPrimaryTool,
IStatePersistentWithBannerlordMods,
IStatePersistentWithLoadOrder,
+ IStateSessionWithBannerlord,
} from './types';
import { isStoreSteam, isStoreXbox } from './store';
import { addBLSETools, addModdingKitTool, addOfficialCLITool, addOfficialLauncherTool } from './tools';
@@ -10,7 +11,11 @@ import { nameof } from '../nameof';
import { recommendBLSE } from '../blse';
import { VortexLauncherManager } from '../launcher';
import { EPICAPP_ID, GAME_ID, GOG_IDS, STEAMAPP_ID, XBOX_ID } from '../common';
-import { IStatePersistent } from '../types';
+import { IStatePersistent, IStateSession } from '../types';
+
+type HasSession = {
+ session: types.ISession;
+};
type HasSettings = {
settings: types.ISettings;
@@ -31,6 +36,11 @@ export const hasPersistentBannerlordMods = (
): statePersistent is IStatePersistentWithBannerlordMods =>
nameof('mods') in statePersistent && GAME_ID in statePersistent.mods;
+export const hasSession = (hasSession: object): hasSession is HasSession => nameof('session') in hasSession;
+
+export const hasSessionWithBannerlord = (stateSession: IStateSession): stateSession is IStateSessionWithBannerlord =>
+ nameof(GAME_ID) in stateSession;
+
export const hasSettings = (hasSettings: object): hasSettings is HasSettings =>
nameof('settings') in hasSettings;
diff --git a/yarn.lock b/yarn.lock
index bd9f1da..01b4831 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -22,10 +22,10 @@
dependencies:
regenerator-runtime "^0.14.0"
-"@butr/vortexextensionnative@1.0.129":
- version "1.0.129"
- resolved "https://registry.yarnpkg.com/@butr/vortexextensionnative/-/vortexextensionnative-1.0.129.tgz#ca450f98e3f2ec1c4282e1342775471595d71860"
- integrity sha512-5dYN/59/DXYDcY2Hrl1h2j2Lz4MMLQ0c8LY3iFzK/vKj0O8i6t5VK8jZNL9JIu3xHzS/8h3A0ekHKclSWeYfkg==
+"@butr/vortexextensionnative@1.0.134":
+ version "1.0.134"
+ resolved "https://registry.yarnpkg.com/@butr/vortexextensionnative/-/vortexextensionnative-1.0.134.tgz#24f596974f52c611e33a054a0a8a5a6664ceb6f2"
+ integrity sha512-vLAhrYflpocA4jq0dGlNPGbBWkLF0ihs8OJ0OjbmKoBxdG9w7PTH/prdZTCyafrjdX20CcsYWy4qcxcAbqfopg==
"@cspotcode/source-map-support@^0.8.0":
version "0.8.1"