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

Release 1.1.5 #164

Merged
merged 2 commits into from
Oct 5, 2024
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
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
Loading