Skip to content

Commit

Permalink
Merge pull request #164 from BUTR/dev
Browse files Browse the repository at this point in the history
Release 1.1.5
  • Loading branch information
Aragas authored Oct 5, 2024
2 parents b9a7549 + c35ebc0 commit b99998e
Show file tree
Hide file tree
Showing 25 changed files with 360 additions and 56 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.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
---------------------------------------------------------------------------------------------------
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.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+",
Expand Down Expand Up @@ -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": {
Expand Down
2 changes: 1 addition & 1 deletion src/collections/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }]
Expand Down
4 changes: 2 additions & 2 deletions src/collections/modOptionsData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }]
Expand Down
2 changes: 2 additions & 0 deletions src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`;
Expand Down
32 changes: 26 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -37,6 +37,7 @@ import {
getInstallPathModule,
hasPersistentBannerlordMods,
hasPersistentLoadOrder,
installedMod,
isModTypeModule,
} from './vortex';
import { version } from '../package.json';
Expand All @@ -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`,
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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);
Expand Down
17 changes: 17 additions & 0 deletions src/launcher/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { createAction } from 'redux-act';
import { EXTENSION_BASE_ID } from '../common';

export type SetUseSteamBinariesOnXboxPayload = {
useSteamBinariesOnXbox: boolean;
};

const setUseSteamBinariesOnXbox = createAction<boolean, SetUseSteamBinariesOnXboxPayload>(
`${EXTENSION_BASE_ID}_SET_USE_STEAM_BINARIES_ON_XBOX`,
(useSteamBinariesOnXbox: boolean) => ({
useSteamBinariesOnXbox,
})
);

export const actionsLauncher = {
setUseSteamBinariesOnXbox,
};
2 changes: 2 additions & 0 deletions src/launcher/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export * from './actions';
export * from './hooks';
export * from './manager';
export * from './utils';
export * from './version';
160 changes: 136 additions & 24 deletions src/launcher/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 {
Expand Down Expand Up @@ -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<types.IInstallResult> => {
public installModule = async (
files: string[],
destinationPath: string,
archivePath?: string
): Promise<types.IInstallResult> => {
const subModuleRelFilePath = files.find((x) => x.endsWith('SubModule.xml'))!;
const subModuleFilePath = path.join(destinationPath, subModuleRelFilePath);
const subModuleFile = readFileSync(subModuleFilePath, {
Expand All @@ -201,6 +214,86 @@ export class VortexLauncherManager {

const result = this.launcherManager.installModule(files, [moduleInfo]);
const subModsIds = Array<string>();
const availableStores = result.instructions.reduce<string[]>((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(`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 } }
),
},
[{ 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<types.IInstruction[]>((map, current) => {
switch (current.type) {
Expand All @@ -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;
}, []),
Expand All @@ -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);
};
Expand Down Expand Up @@ -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));
};

/**
Expand All @@ -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);
Expand Down Expand Up @@ -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;
}
Expand Down
Loading

0 comments on commit b99998e

Please sign in to comment.