From 3c08f0ab23b72147a254231bc906bb0d7b8e9438 Mon Sep 17 00:00:00 2001 From: Vitalii Mikhailov Date: Sun, 6 Oct 2024 00:00:43 +0300 Subject: [PATCH 1/2] 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" From c35ebc03dc64803a4be319c69ef43343b0fbee72 Mon Sep 17 00:00:00 2001 From: Vitalii Mikhailov Date: Sun, 6 Oct 2024 00:19:50 +0300 Subject: [PATCH 2/2] Line fixes --- src/collections/events.ts | 2 +- src/collections/modOptionsData.ts | 4 ++-- src/launcher/manager.ts | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/collections/events.ts b/src/collections/events.ts index 6573aab..3d6fcb5 100644 --- a/src/collections/events.ts +++ b/src/collections/events.ts @@ -31,7 +31,7 @@ export const willRemoveModCollections = async (api: types.IExtensionApi, modId: { message: t( `The removed collection contained custom Mod Options (MCM)! - Do you want to restore your original Mod Options if they were overriden by the collection?` +Do you want to restore your original Mod Options if they were overriden by the collection?` ), }, [{ label: deleteOriginals }, { label: restoreOriginals }, { label: cancel }] diff --git a/src/collections/modOptionsData.ts b/src/collections/modOptionsData.ts index 23c9432..6a3ce2e 100644 --- a/src/collections/modOptionsData.ts +++ b/src/collections/modOptionsData.ts @@ -95,8 +95,8 @@ export const parseCollectionModOptionsData = async ( { message: t( `This collection contains custom Mod Options (MCM)! - Do you want to override your Mod Options with the custom Mod Options? - A backup of your original Mod Options will be kept and will be restored on collection removal.` +Do you want to override your Mod Options with the custom Mod Options? +A backup of your original Mod Options will be kept and will be restored on collection removal.` ), }, [{ label: no }, { label: yes }] diff --git a/src/launcher/manager.ts b/src/launcher/manager.ts index 82bf245..68da877 100644 --- a/src/launcher/manager.ts +++ b/src/launcher/manager.ts @@ -265,15 +265,15 @@ export class VortexLauncherManager { 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!`), + t(`Compatibility 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 } } ),