diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 7df01ec..7283b6d 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,65 +1,69 @@ { - "name": "NodeJS & Powershell & 7z", - "build": { - "dockerfile": "Dockerfile", - "cacheFrom": [ - "ghcr.io/butr/game-mount-and-blade2-devcontainer:latest" - ] - }, - "features": { - "ghcr.io/butr/devcontainer/upgrade:1": { - - }, - "ghcr.io/devcontainers/features/git:1": { - "version": "latest", - "ppa": "false" + "name": "NodeJS & Powershell & 7z", + "build": { + "dockerfile": "Dockerfile", + "cacheFrom": [ + "ghcr.io/butr/game-mount-and-blade2-devcontainer:latest" + ] }, - "ghcr.io/butr/devcontainer/7z:1": { + "features": { + "ghcr.io/butr/devcontainer/upgrade:1": { + + }, + "ghcr.io/devcontainers/features/git:1": { + "version": "latest", + "ppa": "false" + }, + "ghcr.io/butr/devcontainer/7z:1": { - }, - "ghcr.io/devcontainers/features/node:1": { - "version": "18.17.1", - "nodeGypDependencies": "true" + }, + "ghcr.io/devcontainers/features/node:1": { + "version": "18.17.1", + "nodeGypDependencies": "true" + }, + "ghcr.io/devcontainers/features/python:1": { + + }, + "ghcr.io/devcontainers/features/powershell:1": { + "version": "7.4.2" + } }, - "ghcr.io/devcontainers/features/powershell:1": { - "version": "latest" - } - }, - "overrideFeatureInstallOrder": [ - "ghcr.io/butr/devcontainer/upgrade", - "ghcr.io/devcontainers/features/git", - "ghcr.io/butr/devcontainer/7z", - "ghcr.io/devcontainers/features/node", - "ghcr.io/devcontainers/features/powershell" - ], + "overrideFeatureInstallOrder": [ + "ghcr.io/butr/devcontainer/upgrade", + "ghcr.io/devcontainers/features/git", + "ghcr.io/butr/devcontainer/7z", + "ghcr.io/devcontainers/features/node", + "ghcr.io/devcontainers/features/python", + "ghcr.io/devcontainers/features/powershell" + ], "mounts": [ { "source":"${localEnv:HOME}${localEnv:USERPROFILE}/AppData/Roaming/Vortex/plugins", "target":"/vortex-plugins", "type":"bind" } - ], - "postAttachCommand": "yarn install", - "customizations": { - "vscode": { - "extensions": [ - "editorconfig.editorconfig", - "dbaeumer.vscode-eslint", - "github.vscode-github-actions", - "ms-vscode.powershell", - "ms-azuretools.vscode-docker" - ], - "settings": { - "terminal.integrated.defaultProfile.linux": "pwsh", - "terminal.integrated.profiles.linux": { - "path": { - "path": "/usr/local/bin/pwsh" - } - }, - "powershell.powerShellAdditionalExePaths": { - "pwsh": "/usr/local/bin/pwsh" - } - } - } - } + ], + "postAttachCommand": "yarn install", + "customizations": { + "vscode": { + "extensions": [ + "editorconfig.editorconfig", + "dbaeumer.vscode-eslint", + "github.vscode-github-actions", + "ms-vscode.powershell", + "ms-azuretools.vscode-docker" + ], + "settings": { + "terminal.integrated.defaultProfile.linux": "pwsh", + "terminal.integrated.profiles.linux": { + "path": { + "path": "/usr/local/bin/pwsh" + } + }, + "powershell.powerShellAdditionalExePaths": { + "pwsh": "/usr/local/bin/pwsh" + } + } + } + } } diff --git a/.editorconfig b/.editorconfig index f7b23f8..4d53096 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,6 +1,5 @@ -# Project-specific editor formatting options [*] charset = utf-8 insert_final_newline = true indent_style = space -indent_size = 2 \ No newline at end of file +indent_size = 2 diff --git a/.eslintrc.json b/.eslintrc.json index a9e8fcd..39ca31c 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -5,7 +5,8 @@ "sourceType": "module", "ecmaFeatures": { "jsx": true - } + }, + "project": "./tsconfig.json" }, "env": { "browser": true, @@ -24,7 +25,7 @@ "plugin:prettier/recommended", "plugin:@typescript-eslint/recommended" ], - "plugins": ["prettier", "react", "react-hooks", "@typescript-eslint"], + "plugins": ["prettier", "react", "react-hooks", "@typescript-eslint", "import"], "rules": { "eqeqeq": "error", "no-console": "warn", @@ -38,6 +39,50 @@ "react-hooks/rules-of-hooks": "error", "react-hooks/exhaustive-deps": "warn", + "require-await": "off", + "@typescript-eslint/require-await": "error", + "@typescript-eslint/await-thenable": "error", + "@typescript-eslint/no-floating-promises": "error", + "@typescript-eslint/explicit-function-return-type": "error", + "@typescript-eslint/strict-boolean-expressions": + [ + "error", + { + "allowString": true, + "allowNumber": true, + "allowNullableObject": true, + "allowNullableBoolean": false, + "allowNullableString": false, + "allowNullableNumber": false, + "allowNullableEnum": false, + "allowAny": false + } + ], + + "sort-imports": + [ + "error", + { + "ignoreCase": true, + "ignoreDeclarationSort": true + } + ], + + "import/order": [ + 1, + { + "groups": + [ + "external", + "builtin", + "internal", + "sibling", + "parent", + "index" + ] + } + ], + "@typescript-eslint/naming-convention": [ "warn", { @@ -48,7 +93,19 @@ "match": true } } + // , + // { + // "selector": "method", + // "format": ["camelCase"], + // "leadingUnderscore": "allow", + // "trailingUnderscore": "allow", + // "custom": { + // "regex": "^.*Async$", + // "match": true + // } + // } ], + "no-restricted-imports": [ "warn", { diff --git a/changelog.txt b/changelog.txt index e5abb2b..489d6b4 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,4 +1,10 @@ --------------------------------------------------------------------------------------------------- +Version: 1.1.0 +* Added back collection support +* Old collections will now set the Load Order on install +* New collections will be able to include MCM settings +* The extension should now see symbolic links +--------------------------------------------------------------------------------------------------- Version: 1.0.13 * Added mandatory folders for manual game path hint * Fixed undefined tools bug diff --git a/package.json b/package.json index 2f54216..902c1a7 100644 --- a/package.json +++ b/package.json @@ -31,19 +31,18 @@ "@types/react-redux": "^7.1.9", "@types/redux": "^3.6.0", "@types/webpack": "^5.28.5", - "@typescript-eslint/eslint-plugin": "7.0.0", - "@typescript-eslint/parser": "^6", + "@typescript-eslint/eslint-plugin": "7.2.0", + "@typescript-eslint/parser": "7.2.0", "bluebird": "^3.7.2", "collections": "git+https://github.com/Nexus-Mods/extension-collections", "copy-webpack-plugin": "^12.0.1", "eslint": "^8.34.0", "eslint-config-prettier": "^9", - "eslint-plugin-import": "^2.25.4", + "eslint-plugin-import": "^2.29.1", "eslint-plugin-prettier": "^5", - "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react": "^7.34.3", "eslint-plugin-react-hooks": "^4.6.2", "i18next": "^19.0.1", - "lint-staged": "^12.3.5", "native-addon-loader": "^2.0.1", "prettier": "^3", "react": "^16.12.0", @@ -55,10 +54,11 @@ "redux-act": "^1.7.7", "ts-loader": "^9.2.6", "ts-node": "^10.7.0", - "typescript": "^5.3.3", + "turbowalk": "git+https://github.com/Nexus-Mods/node-turbowalk", + "typescript": "^5.4.5", "vortex-api": "git+https://github.com/Nexus-Mods/vortex-api", "vortex-ext-common": "^0.4.0", - "webpack": "^5.76.0", + "webpack": "^5.92.1", "webpack-cli": "^5.1.4", "webpack-node-externals": "^3.0.0" }, diff --git a/src/blse/events.ts b/src/blse/events.ts new file mode 100644 index 0000000..9d28ab9 --- /dev/null +++ b/src/blse/events.ts @@ -0,0 +1,47 @@ +import { actions, types } from 'vortex-api'; +import { findBLSEMod } from './utils'; +import { hasSettingsInterfacePrimaryTool } from '../vortex'; +import { GAME_ID } from '../common'; + +export const didDeployBLSE = (api: types.IExtensionApi): Promise => { + const state = api.getState(); + + if (!hasSettingsInterfacePrimaryTool(state.settings.interface)) { + return Promise.resolve(); + } + + const primaryTool = state.settings.interface.primaryTool.mountandblade2bannerlord; + + const blseMod = findBLSEMod(state); + if (blseMod && primaryTool === undefined) { + api.store?.dispatch(actions.setPrimaryTool(GAME_ID, 'blse-cli')); + } + if (!blseMod && primaryTool === 'blse-cli') { + api.store?.dispatch(actions.setPrimaryTool(GAME_ID, undefined!)); + } + + return Promise.resolve(); +}; + +/** + * Event function, be careful + */ +export const didPurgeBLSE = (api: types.IExtensionApi): Promise => { + const state = api.getState(); + + if (!hasSettingsInterfacePrimaryTool(state.settings.interface)) { + return Promise.resolve(); + } + + const primaryTool = state.settings.interface.primaryTool.mountandblade2bannerlord; + if (primaryTool !== 'blse-cli') { + return Promise.resolve(); + } + + const blseMod = findBLSEMod(state); + if (blseMod) { + api.store?.dispatch(actions.setPrimaryTool(GAME_ID, undefined!)); + } + + return Promise.resolve(); +}; diff --git a/src/utils/blse/index.ts b/src/blse/index.ts similarity index 81% rename from src/utils/blse/index.ts rename to src/blse/index.ts index e1222cc..34325dd 100644 --- a/src/utils/blse/index.ts +++ b/src/blse/index.ts @@ -1,4 +1,5 @@ export * from './events'; export * from './installer'; export * from './modType'; +export * from './utils'; export * from './vortex'; diff --git a/src/utils/blse/installer.ts b/src/blse/installer.ts similarity index 56% rename from src/utils/blse/installer.ts rename to src/blse/installer.ts index 81d145e..b299127 100644 --- a/src/utils/blse/installer.ts +++ b/src/blse/installer.ts @@ -1,11 +1,11 @@ -import path from 'path'; import { selectors, types } from 'vortex-api'; -import { isStoreXbox } from '..'; -import { GAME_ID, BLSE_CLI_EXE, BINARY_FOLDER_XBOX, BINARY_FOLDER_STANDARD } from '../../common'; +import path from 'path'; +import { isStoreXbox } from '../vortex'; +import { BINARY_FOLDER_STANDARD, BINARY_FOLDER_XBOX, BLSE_CLI_EXE, GAME_ID } from '../common'; -export const installBLSE = async (api: types.IExtensionApi, files: string[]): Promise => { - const discovery = selectors.currentGameDiscovery(api.getState()); - if (!discovery) { +export const installBLSE = (api: types.IExtensionApi, files: string[]): Promise => { + const discovery: types.IDiscoveryResult | undefined = selectors.currentGameDiscovery(api.getState()); + if (discovery === undefined) { return Promise.resolve({ instructions: [], }); @@ -21,13 +21,13 @@ export const installBLSE = async (api: types.IExtensionApi, files: string[]): Pr source: file, destination: file, })); - return { + return Promise.resolve({ instructions: instructions, - }; + }); }; export const testBLSE = (files: string[], gameId: string): Promise => { - const supported = gameId === GAME_ID && !!files.find((file) => path.basename(file) === BLSE_CLI_EXE); + const supported = gameId === GAME_ID && files.find((file) => path.basename(file) === BLSE_CLI_EXE) !== undefined; return Promise.resolve({ supported, requiredFiles: [], diff --git a/src/utils/blse/modType.ts b/src/blse/modType.ts similarity index 67% rename from src/utils/blse/modType.ts rename to src/blse/modType.ts index 658cb44..8909e01 100644 --- a/src/utils/blse/modType.ts +++ b/src/blse/modType.ts @@ -1,5 +1,5 @@ import { selectors, types } from 'vortex-api'; -import { BLSE_CLI_EXE } from '../../common'; +import { BLSE_CLI_EXE } from '../common'; export const getInstallPathBLSE = (api: types.IExtensionApi, game: types.IGame): string => { const discovery: types.IDiscoveryResult | undefined = selectors.discoveryByGame(api.getState(), game.id); @@ -7,5 +7,7 @@ export const getInstallPathBLSE = (api: types.IExtensionApi, game: types.IGame): }; export const isModTypeBLSE = (instructions: types.IInstruction[]): boolean => { - return instructions.some((inst) => inst.type === 'copy' && inst.source && inst.source.endsWith(BLSE_CLI_EXE)); + return instructions.some( + (inst) => inst.type === 'copy' && inst.source !== undefined && inst.source.endsWith(BLSE_CLI_EXE) + ); }; diff --git a/src/utils/blse/shared.ts b/src/blse/utils.ts similarity index 67% rename from src/utils/blse/shared.ts rename to src/blse/utils.ts index 186bd2c..48e0365 100644 --- a/src/utils/blse/shared.ts +++ b/src/blse/utils.ts @@ -1,19 +1,22 @@ import { gte } from 'semver'; import { actions, selectors, types, util } from 'vortex-api'; import { IFileInfo } from '@nexusmods/nexus-api/lib'; -import { GAME_ID, BLSE_MOD_ID, BLSE_URL } from '../../common'; -import { GetLocalizationManager, IBannerlordMod, IBannerlordModStorage } from '../../types'; +import { BLSE_MOD_ID, BLSE_URL, GAME_ID } from '../common'; +import { hasPersistentBannerlordMods } from '../vortex'; +import { LocalizationManager } from '../localization'; +import { IBannerlordMod } from '../types'; export const isModActive = (profile: types.IProfile, mod: IBannerlordMod): boolean => { return profile.modState[mod.id]?.enabled ?? false; }; -const isModBLSE = (mod: IBannerlordMod) => { +const isModBLSE = (mod: IBannerlordMod): boolean => { return mod.type === `bannerlord-blse` || (mod.attributes?.modId === 1 && mod.attributes?.source === `nexus`); }; -export const findBLSEMod = (api: types.IExtensionApi): IBannerlordMod | undefined => { - const state = api.getState(); - const mods = (state.persistent.mods[GAME_ID] as IBannerlordModStorage) ?? {}; +export const findBLSEMod = (state: types.IState): IBannerlordMod | undefined => { + if (!hasPersistentBannerlordMods(state.persistent)) return undefined; + + const mods = state.persistent.mods.mountandblade2bannerlord ?? {}; const blseMods: IBannerlordMod[] = Object.values(mods).filter((mod: IBannerlordMod) => isModBLSE(mod)); if (blseMods.length === 0) return undefined; @@ -21,7 +24,7 @@ export const findBLSEMod = (api: types.IExtensionApi): IBannerlordMod | undefine if (blseMods.length === 1) return blseMods[0]; return blseMods.reduce((prev: IBannerlordMod | undefined, iter: IBannerlordMod) => { - if (prev === undefined) { + if (!prev) { return iter; } return gte(iter.attributes?.version ?? '0.0.0', prev.attributes?.version ?? '0.0.0') ? iter : prev; @@ -31,14 +34,14 @@ export const findBLSEMod = (api: types.IExtensionApi): IBannerlordMod | undefine export const findBLSEDownload = (api: types.IExtensionApi): string | undefined => { const state = api.getState(); const downloadedFiles = state.persistent.downloads.files; - if (!downloadedFiles) { + if (downloadedFiles === undefined) { return undefined; } const blseFiles = Object.entries(downloadedFiles) - .filter((x) => x[1].game.includes(GAME_ID)) - .filter((x) => x[1].modInfo?.['nexus']?.modInfo?.mod_id === 1) - .sort((x, y) => x[1].fileTime - y[1].fileTime); + .filter(([, download]) => download.game.includes(GAME_ID)) + .filter(([, download]) => download.modInfo?.['nexus']?.modInfo?.mod_id === 1) + .sort(([, downloadA], [, downloadB]) => downloadA.fileTime - downloadB.fileTime); if (blseFiles.length === 0) { return undefined; @@ -51,14 +54,17 @@ export const findBLSEDownload = (api: types.IExtensionApi): string | undefined = export const isActiveBLSE = (api: types.IExtensionApi): boolean => { const state = api.getState(); - const mods = (state.persistent.mods[GAME_ID] as IBannerlordModStorage) ?? {}; + + if (!hasPersistentBannerlordMods(state.persistent)) return false; + + const mods = state.persistent.mods.mountandblade2bannerlord ?? {}; const blseMods: IBannerlordMod[] = Object.values(mods).filter((mod: IBannerlordMod) => isModBLSE(mod)); if (blseMods.length === 0) { return false; } - const profile = selectors.activeProfile(state); + const profile: types.IProfile | undefined = selectors.activeProfile(state); return blseMods.filter((x) => isModActive(profile, x)).length >= 1; }; @@ -66,38 +72,31 @@ export const deployBLSE = async (api: types.IExtensionApi): Promise => { await util.toPromise((cb) => api.events.emit('deploy-mods', cb)); await util.toPromise((cb) => api.events.emit('start-quick-discovery', () => cb(null))); - const discovery = selectors.currentGameDiscovery(api.getState()); + const discovery: types.IDiscoveryResult | undefined = selectors.currentGameDiscovery(api.getState()); const tool = discovery?.tools?.['blse-cli']; if (tool) { api.store?.dispatch(actions.setPrimaryTool(GAME_ID, tool.id)); } }; -export const downloadBLSE = async ( - api: types.IExtensionApi, - getLocalizationManager: GetLocalizationManager, - update?: boolean -): Promise => { - const localizationManager = getLocalizationManager(); - const t = localizationManager.localize; +export const downloadBLSE = async (api: types.IExtensionApi, shouldUpdate: boolean = false): Promise => { + const { localize: t } = LocalizationManager.getInstance(api); api.dismissNotification?.('blse-missing'); api.sendNotification?.({ id: 'blse-installing', - message: update ? t('Updating BLSE') : t('Installing BLSE'), + message: shouldUpdate ? t('Updating BLSE') : t('Installing BLSE'), type: 'activity', noDismiss: true, allowSuppress: false, }); - if (api.ext?.ensureLoggedIn) { - await api.ext.ensureLoggedIn(); - } + await api.ext?.ensureLoggedIn?.(); try { const modFiles = (await api.ext.nexusGetModFiles?.(GAME_ID, BLSE_MOD_ID)) ?? []; - const fileTime = (input: IFileInfo) => Number.parseInt(input.uploaded_time, 10); + const fileTime = (input: IFileInfo): number => Number.parseInt(input.uploaded_time, 10); const file = modFiles.filter((file) => file.category_id === 1).sort((lhs, rhs) => fileTime(lhs) - fileTime(rhs))[0]; @@ -117,7 +116,7 @@ export const downloadBLSE = async ( const modId = await util.toPromise((cb) => api.events.emit('start-install-download', dlId, { allowAutoEnable: false }, cb) ); - const profile = selectors.activeProfile(api.getState()); + const profile: types.IProfile | undefined = selectors.activeProfile(api.getState()); await actions.setModsEnabled(api, profile.id, [modId], true, { allowAutoDeploy: false, installed: true, diff --git a/src/blse/vortex.ts b/src/blse/vortex.ts new file mode 100644 index 0000000..2bef9fd --- /dev/null +++ b/src/blse/vortex.ts @@ -0,0 +1,105 @@ +import { actions, selectors, types } from 'vortex-api'; +import { deployBLSE, downloadBLSE, findBLSEDownload, findBLSEMod, isModActive } from './utils'; +import { LocalizationManager } from '../localization'; + +const sendNotification = ( + api: types.IExtensionApi, + title: string, + actionTitle: string, + action: (dismiss: types.NotificationDismiss) => void +): void => { + const { localize: t } = LocalizationManager.getInstance(api); + + api.sendNotification?.({ + id: 'blse-missing', + type: 'warning', + title: title, + message: t('BLSE is recommended to mod Bannerlord.'), + actions: [ + { + title: actionTitle, + action: action, + }, + ], + }); +}; + +export const recommendBLSE = (api: types.IExtensionApi): void => { + const { localize: t } = LocalizationManager.getInstance(api); + + const state = api.getState(); + + const profile: types.IProfile | undefined = selectors.activeProfile(state); + + const blseMod = findBLSEMod(state); + if (blseMod) { + // Found but not enabled + const blseIsActive = isModActive(profile, blseMod); + if (!blseIsActive) { + const action = (dismiss: types.NotificationDismiss): void => { + api.store?.dispatch(actions.setModEnabled(profile.id, blseMod.id, true)); + deployBLSE(api) + .catch(() => {}) + .finally(() => dismiss()); + }; + sendNotification(api, t('BLSE is not enabled'), t('Enable'), action); + return; + } + } else { + const blseDownload = findBLSEDownload(api); + if (blseDownload !== undefined) { + // Downloaded but not installed + const action = (dismiss: types.NotificationDismiss): void => { + api.events.emit('start-install-download', blseDownload, { + allowAutoEnable: true, + }); + deployBLSE(api) + .catch(() => {}) + .finally(() => dismiss()); + }; + sendNotification(api, t('BLSE is not installed'), t('Install'), action); + } else { + // Non existent + const action = (dismiss: types.NotificationDismiss): void => { + downloadBLSE(api) + .catch(() => {}) + .finally(() => dismiss()); + }; + sendNotification(api, t('BLSE is not installed via Vortex'), t('Get BLSE'), action); + } + } +}; + +export const forceInstallBLSE = async (api: types.IExtensionApi): Promise => { + const { localize: t } = LocalizationManager.getInstance(api); + + api.sendNotification?.({ + id: 'blse-required', + type: 'info', + title: t('BLSE Required'), + message: t('BLSE is required by the collection. Ensuring it is installed...'), + }); + + const state = api.getState(); + + const profile: types.IProfile | undefined = selectors.activeProfile(state); + + const blseMod = findBLSEMod(state); + 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); + } + } +}; diff --git a/src/butr/const.ts b/src/butr/const.ts new file mode 100644 index 0000000..79d7c71 --- /dev/null +++ b/src/butr/const.ts @@ -0,0 +1 @@ +export const BUTR_HOST = 'sitenexusmods.butr.link'; diff --git a/src/butr/index.ts b/src/butr/index.ts new file mode 100644 index 0000000..c6a366c --- /dev/null +++ b/src/butr/index.ts @@ -0,0 +1,4 @@ +export * from './const'; +export * from './modAnalyzerProxy'; +export * from './types'; +export * from './utils'; diff --git a/src/butr/modAnalyzerProxy.ts b/src/butr/modAnalyzerProxy.ts new file mode 100644 index 0000000..2ebcb10 --- /dev/null +++ b/src/butr/modAnalyzerProxy.ts @@ -0,0 +1,45 @@ +import { log } from 'vortex-api'; +import { request, RequestOptions } from 'https'; +import { BUTR_HOST } from './const'; +import { IModAnalyzerRequestQuery, IModAnalyzerResult } from './types'; + +export class ModAnalyzerProxy { + private options: RequestOptions; + constructor() { + this.options = { + host: BUTR_HOST, + method: 'POST', + protocol: 'https:', + path: '/api/v1/ModsAnalyzer/GetCompatibilityScore', + headers: { + Tenant: '1', // Bannerlord + 'Content-Type': 'application/json', + }, + }; + } + + public async analyze(query: IModAnalyzerRequestQuery): Promise { + return new Promise((resolve, reject) => { + const req = request(this.options, (res) => { + let body = Buffer.from([]); + res + .on('error', (err) => reject(err)) + .on('data', (chunk) => { + body = Buffer.concat([body, chunk]); + }) + .on('end', () => { + const textual = body.toString('utf8'); + try { + const parsed = JSON.parse(textual); + resolve(parsed); + } catch (err) { + log('error', 'failed to parse butr mod analyzer response', textual); + reject(err); + } + }); + }).on('error', (err) => reject(err)); + req.write(JSON.stringify(query)); + req.end(); + }); + } +} diff --git a/src/butr/types.ts b/src/butr/types.ts new file mode 100644 index 0000000..3b67741 --- /dev/null +++ b/src/butr/types.ts @@ -0,0 +1,30 @@ +export interface IModAnalyzerRequestModule { + moduleId: string; + moduleVersion?: string; +} + +export interface IModAnalyzerRequestQuery { + gameVersion: string; + modules: IModAnalyzerRequestModule[]; +} + +export interface IModAnalyzerResultModule { + moduleId: string; + compatibility: number; + recommendedCompatibility: number | null; + recommendedModuleVersion: string | null; +} + +export interface IModAnalyzerResult { + modules: IModAnalyzerResultModule[]; +} + +export interface IModuleCompatibilityInfoCache { + [moduleId: string]: IModuleCompatibilityInfo; +} + +export interface IModuleCompatibilityInfo { + score: number; + recommendedScore: number | null; + recommendedVersion: string | null; +} diff --git a/src/butr/utils.ts b/src/butr/utils.ts new file mode 100644 index 0000000..f5be00d --- /dev/null +++ b/src/butr/utils.ts @@ -0,0 +1,29 @@ +import { types } from 'vortex-api'; +import { IModAnalyzerRequestModule, IModAnalyzerRequestQuery, IModuleCompatibilityInfoCache } from './types'; +import { ModAnalyzerProxy } from './modAnalyzerProxy'; +import { versionToString, VortexLauncherManager } from '../launcher'; + +export const getCompatibilityScores = async (api: types.IExtensionApi): Promise => { + const launcherManager = VortexLauncherManager.getInstance(api); + + const allModules = launcherManager.getAllModules(); + const gameVersion = launcherManager.getGameVersionVortex(); + + const proxy = new ModAnalyzerProxy(); + const query: IModAnalyzerRequestQuery = { + gameVersion: gameVersion, + modules: Object.values(allModules).map((x) => ({ + moduleId: x.id, + moduleVersion: versionToString(x.version), + })), + }; + const result = await proxy.analyze(query); + return result.modules.reduce((map, curr) => { + map[curr.moduleId] = { + score: curr.compatibility, + recommendedScore: curr.recommendedCompatibility, + recommendedVersion: curr.recommendedModuleVersion, + }; + return map; + }, {}); +}; diff --git a/src/collections/collectionUtil.ts b/src/collections/collectionUtil.ts deleted file mode 100644 index 3d179b0..0000000 --- a/src/collections/collectionUtil.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* -import Bluebird, { Promise, method as toBluebird } from 'bluebird'; - -import { types, util } from 'vortex-api'; -import { ILoadOrder, IMods } from '../types'; - -import { OFFICIAL_MODULES } from '../common'; - -export class CollectionGenerateError extends Error { - constructor(why: string) { - super(`Failed to generate game specific data for collection: ${why}`); - this.name = `CollectionGenerateError`; - } -} - -export class CollectionParseError extends Error { - constructor(collectionName: string, why: string) { - super(`Failed to parse game specific data for collection ${collectionName}: ${why}`); - this.name = `CollectionGenerateError`; - } -} - -const isValidSubMod = (subModId: string, mods: IMods): boolean => { - if (OFFICIAL_MODULES.has(subModId)) { - // official modules are always included. - return true; - } - - // The mods map should only include mods that have been included in the - // collection or this won't work. - const modIds = Object.keys(mods); - const subModIds = modIds.reduce((accum, id) => accum.concat([id], mods[id]?.attributes?.subModIds ?? []), Array()); - - return subModIds.map((id) => id.toLowerCase()).includes(subModId.toLowerCase()); -}; - -export const isValidMod = (mod: types.IMod): boolean => (mod?.type !== `collection`); - -export const isModInCollection = (collectionMod: types.IMod, mod: types.IMod): boolean => { - if (collectionMod.rules === undefined) { - return false; - } - - return collectionMod.rules.find((rule) => util.testModReference(mod, rule.reference)) !== undefined; -}; - -export const genCollectionLoadOrder = (loadOrder: ILoadOrder, mods: IMods, collection?: types.IMod) : ILoadOrder => { - const filteredMods = (collection !== undefined) - ? Object.keys(mods) - .filter((id) => isValidMod(mods[id]) && isModInCollection(collection, mods[id])) - .reduce((accum: IMods, iter) => { - accum[iter] = mods[iter]; - return accum; - }, {}) - : mods; - - const sortedMods = Object.keys(loadOrder) - .filter((id) => isValidSubMod(id, filteredMods)) - .sort((lhs, rhs) => loadOrder[lhs].pos - loadOrder[rhs].pos) - .reduce((accum: ILoadOrder, iter, idx) => { - accum[iter] = { - ...loadOrder[iter], - pos: idx, - }; - return accum; - }, {}); - return sortedMods; -}; -*/ diff --git a/src/collections/collections.ts b/src/collections/collections.ts deleted file mode 100644 index 301a4b4..0000000 --- a/src/collections/collections.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* -import Bluebird, { Promise, method as toBluebird } from 'bluebird'; - -import { selectors, util } from 'vortex-api'; - -import { IExtensionContext } from "vortex-api/lib/types/api"; - -import { IMods } from '../types'; -import { ICollectionMB } from './types'; - -import { exportLoadOrder, importLoadOrder } from './loadOrder'; - -import { CollectionParseError } from './collectionUtil'; - -export const genCollectionsData = toBluebird(async (context: IExtensionContext, gameId: string, includedMods: string[]): Promise => { - const { api } = context; - const state = api.getState(); - const mods = util.getSafe(state, [`persistent`, `mods`, gameId], {}); - const loadOrder = await exportLoadOrder(api.getState(), includedMods, mods) ?? {}; - const collectionData: ICollectionMB = { - loadOrder, - info: { - author: ``, - authorUrl: ``, - name: ``, - description: ``, - domainName: ``, - gameVersions: [], - }, - mods: [], - modRules: [], - }; - return collectionData; -}); - -export const parseCollectionsData = toBluebird(async (context: IExtensionContext, gameId: string, collection: ICollectionMB): Promise => { - const { api } = context; - const state = api.getState(); - const profileId: string = selectors.lastActiveProfileForGame(state, gameId); - const profile = selectors.profileById(state, profileId); - if (profile?.gameId !== gameId) { - const collectionName = collection.info?.name !== undefined ? collection.info.name : `Bannerlord Collection`; - throw new CollectionParseError(collectionName, `Last active profile is missing`); - } - await importLoadOrder(api, collection); -}); -*/ diff --git a/src/collections/errors.ts b/src/collections/errors.ts new file mode 100644 index 0000000..31f7af9 --- /dev/null +++ b/src/collections/errors.ts @@ -0,0 +1,13 @@ +export class CollectionGenerateError extends Error { + constructor(why: string) { + super(`Failed to generate game specific data for collection: ${why}`); + this.name = 'CollectionGenerateError'; + } +} + +export class CollectionParseError extends Error { + constructor(collectionName: string, why: string) { + super(`Failed to parse game specific data for collection ${collectionName}: ${why}`); + this.name = 'CollectionGenerateError'; + } +} diff --git a/src/collections/events.ts b/src/collections/events.ts new file mode 100644 index 0000000..6573aab --- /dev/null +++ b/src/collections/events.ts @@ -0,0 +1,51 @@ +import { types } from 'vortex-api'; +import { GAME_ID } from '../common'; +import { hasBackupModOptions, removeOriginalModOptions, restoreOriginalModOptions } from '../modoptions'; +import { LocalizationManager } from '../localization'; + +/** + * Event function, be careful + */ +export const willRemoveModCollections = async (api: types.IExtensionApi, modId: string): Promise => { + const mod = api.getState().persistent.mods[GAME_ID]?.[modId]; + if (!mod) { + return; + } + if (mod.type !== 'collection') { + return; + } + + if (!(await hasBackupModOptions(mod))) { + return; + } + + const localizationManager = LocalizationManager.getInstance(api); + const { localize: t } = localizationManager; + + const deleteOriginals = t('Delete Originals'); + const restoreOriginals = t('Restore Originals'); + const cancel = t('Cancel'); + const result = await api.showDialog?.( + 'question', + t('Restore Original Mod Options'), + { + 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?` + ), + }, + [{ label: deleteOriginals }, { label: restoreOriginals }, { label: cancel }] + ); + + if (!result || result.action === cancel) { + return; + } + + if (result.action === deleteOriginals) { + await removeOriginalModOptions(mod); + } + + if (result.action === restoreOriginals) { + await restoreOriginalModOptions(mod); + } +}; diff --git a/src/collections/generalData.ts b/src/collections/generalData.ts new file mode 100644 index 0000000..07cbf2b --- /dev/null +++ b/src/collections/generalData.ts @@ -0,0 +1,93 @@ +import { selectors, types } from 'vortex-api'; +import { ICollectionData, ICollectionDataWithGeneralData, ICollectionGeneralData } from './types'; +import { genCollectionGeneralLoadOrder, parseCollectionGeneralLoadOrder } from './loadOrder'; +import { CollectionParseError } from './errors'; +import { GAME_ID } from '../common'; +import { hasPersistentBannerlordMods, hasPersistentLoadOrder } from '../vortex'; +import { findBLSEMod, forceInstallBLSE, isModActive } from '../blse'; +import { vortexToPersistence } from '../loadOrder'; +import { VortexLauncherManager } from '../launcher'; + +/** + * Assumes that the correct Game ID is active and that the profile is set up correctly. + */ +export const genCollectionGeneralData = ( + api: types.IExtensionApi, + includedModIds: string[] +): Promise => { + const state = api.getState(); + + const profile: types.IProfile | undefined = selectors.activeProfile(state); + + const loadOrder = hasPersistentLoadOrder(state.persistent) ? state.persistent.loadOrder[profile.id] ?? [] : []; + const mods = hasPersistentBannerlordMods(state.persistent) ? state.persistent.mods.mountandblade2bannerlord : {}; + + const includedMods = Object.values(mods).filter((mod) => includedModIds.includes(mod.id)); + const collectionLoadOrder = genCollectionGeneralLoadOrder(loadOrder, includedMods); + + const blseMod = findBLSEMod(state); + const hasBLSE = blseMod !== undefined && isModActive(profile, blseMod); + + return Promise.resolve({ + hasBLSE: hasBLSE, + suggestedLoadOrder: vortexToPersistence(collectionLoadOrder), + }); +}; + +/** + * Assumes that the correct Game ID is active and that the profile is set up correctly. + */ +export const parseCollectionGeneralData = async ( + api: types.IExtensionApi, + collection: ICollectionData +): Promise => { + if (!hasGeneralData(collection)) { + return; + } + + const state = api.getState(); + const profileId: string | undefined = selectors.lastActiveProfileForGame(state, GAME_ID); + const profile: types.IProfile | undefined = selectors.profileById(state, profileId ?? ''); + if (profile?.gameId !== GAME_ID) { + const collectionName = collection.info.name !== undefined ? collection.info.name : 'Bannerlord Collection'; + throw new CollectionParseError(collectionName, 'Last active profile is missing'); + } + const { hasBLSE } = collection; + + const launcherManager = VortexLauncherManager.getInstance(api); + const modules = launcherManager.getAllModules(); + await parseCollectionGeneralLoadOrder(api, modules, collection); + + if (hasBLSE) { + await forceInstallBLSE(api); + } +}; + +/** + * Assumes that the correct Game ID is active and that the profile is set up correctly. + */ +export const cloneCollectionGeneralData = ( + api: types.IExtensionApi, + gameId: string, + collection: ICollectionData, + from: types.IMod, + to: types.IMod +): Promise => { + if (!hasGeneralData(collection)) { + return Promise.resolve(); + } + + // we don't need to do anything, since it's based on the LO + return Promise.resolve(); +}; + +const hasGeneralData = (collection: ICollectionData): collection is ICollectionDataWithGeneralData => { + const collectionData = collection as ICollectionDataWithGeneralData; + if (!collectionData.hasBLSE) { + return false; + } + if (collectionData.suggestedLoadOrder === undefined) { + return false; + } + return true; +}; diff --git a/src/collections/index.ts b/src/collections/index.ts new file mode 100644 index 0000000..9f22127 --- /dev/null +++ b/src/collections/index.ts @@ -0,0 +1,8 @@ +export * from './errors'; +export * from './events'; +export * from './generalData'; +export * from './legacyData'; +export * from './loadOrder'; +export * from './modOptionsData'; +export * from './types'; +export * from './utils'; diff --git a/src/collections/legacyData.ts b/src/collections/legacyData.ts new file mode 100644 index 0000000..98fb4c6 --- /dev/null +++ b/src/collections/legacyData.ts @@ -0,0 +1,72 @@ +import { selectors, types } from 'vortex-api'; +import { ICollectionData, ICollectionDataWithLegacyData } from './types'; +import { CollectionParseError } from './errors'; +import { GAME_ID, SUB_MODS_IDS } from '../common'; +import { actionsLoadOrder, orderCurrentLoadOrderByExternalLoadOrder } from '../loadOrder'; +import { VortexLauncherManager } from '../launcher'; +import { PersistenceLoadOrderStorage } from '../types'; +import { hasPersistentBannerlordMods } from '../vortex'; + +export const parseCollectionLegacyData = async ( + api: types.IExtensionApi, + collection: ICollectionData +): Promise => { + if (!hasLegacyData(collection)) { + return; + } + + await parseLegacyLoadOrder(api, collection); +}; + +const parseLegacyLoadOrder = async ( + api: types.IExtensionApi, + collection: ICollectionDataWithLegacyData +): Promise => { + const state = api.getState(); + + const profileId: string | undefined = selectors.lastActiveProfileForGame(state, GAME_ID); + if (profileId === undefined) { + throw new CollectionParseError(collection.info.name ?? '', 'Invalid profile id'); + } + + if (!hasPersistentBannerlordMods(state.persistent)) { + throw new CollectionParseError(collection.info.name ?? '', 'No mods were found'); + } + + const launcherManager = VortexLauncherManager.getInstance(api); + const allModules = launcherManager.getAllModules(); + + const suggestedLoadOrderEntries = Object.entries(collection.loadOrder); + const suggestedLoadOrder = suggestedLoadOrderEntries.reduce((arr, [id, entry], idx) => { + if (!allModules[id] && !state.persistent.mods[GAME_ID]?.[id]) { + return arr; + } + + const mod = state.persistent.mods[GAME_ID]?.[id]; + const modIds: string[] = mod?.attributes?.[SUB_MODS_IDS] !== undefined ? mod.attributes[SUB_MODS_IDS] ?? [] : [id]; + modIds.forEach((modId) => { + if (allModules[modId]) { + arr.push({ + id: modId, + name: entry.name ?? id, + isSelected: entry.enabled, + isDisabled: entry.locked !== undefined && (entry.locked === `true` || entry.locked === `always`), + index: idx, + }); + } + }); + return arr; + }, []); + + const loadOrder = await orderCurrentLoadOrderByExternalLoadOrder(api, allModules, suggestedLoadOrder); + + api.store?.dispatch(actionsLoadOrder.setFBLoadOrder(profileId, loadOrder)); +}; + +const hasLegacyData = (collection: ICollectionData): collection is ICollectionDataWithLegacyData => { + const collectionData = collection as ICollectionDataWithLegacyData; + if (collectionData.loadOrder === undefined) { + return false; + } + return true; +}; diff --git a/src/collections/loadOrder.ts b/src/collections/loadOrder.ts index ac020e4..63ddc3d 100644 --- a/src/collections/loadOrder.ts +++ b/src/collections/loadOrder.ts @@ -1,61 +1,70 @@ -/* -import Bluebird, { Promise, method as toBluebird } from 'bluebird'; - -import { - actions, selectors, util, types, -} from 'vortex-api'; +import { selectors, types, util } from 'vortex-api'; +import { ICollectionDataWithGeneralData } from './types'; +import { CollectionParseError } from './errors'; import { GAME_ID } from '../common'; -import { ILoadOrder, IMods } from '../types'; +import { IBannerlordMod, IModuleCache, VortexLoadOrderStorage } from '../types'; +import { actionsLoadOrder, orderCurrentLoadOrderByExternalLoadOrder } from '../loadOrder'; -import { CollectionGenerateError, CollectionParseError, genCollectionLoadOrder } from './collectionUtil'; -import { ICollectionMB } from "./types"; +const isValidMod = (mod: types.IMod): boolean => { + return mod !== undefined && mod.type !== 'collection'; +}; -export const exportLoadOrder = toBluebird(async (state: types.IState, modIds: string[], mods: IMods) : Promise => { - const profileId = selectors.lastActiveProfileForGame(state, GAME_ID); - if (profileId === undefined) { - throw new CollectionGenerateError(`Invalid profile id`); +const isModInCollection = (collectionMod: types.IMod, mod: IBannerlordMod): boolean => { + if (!collectionMod.rules) { + return false; } - const loadOrder = util.getSafe(state, [`persistent`, `loadOrder`, profileId], undefined); - if (loadOrder === undefined) { - // This is theoretically "fine" - the user may have simply - // downloaded the mods and immediately created the collection - // without actually setting up a load order. Alternatively - // the game extension itself might be handling the presort functionality - // erroneously. Regardless, the collection creation shouldn't be blocked - // by the inexistance of a loadOrder. - return undefined; - } + return collectionMod.rules.find((rule) => util.testModReference(mod, rule.reference)) !== undefined; +}; - const includedMods = modIds.reduce((accum, iter) => { - if (mods[iter] !== undefined) { - accum[iter] = mods[iter]; - } - return accum; - }, {}); +export const genCollectionGeneralLoadOrder = ( + loadOrder: VortexLoadOrderStorage, + mods: IBannerlordMod[], + collectionMod?: types.IMod +): VortexLoadOrderStorage => { + // We get the current load order the user has + // And the mods that are tied to the collection + // And we return the load order with the mods that are in the collection + const filteredLoadOrder = loadOrder + .filter((entry) => { + if (entry.modId === undefined) { + // We add the non existent LO entries as optionals + return entry.data ? entry.enabled : false; + } - const filteredLO = genCollectionLoadOrder(loadOrder, includedMods); - return filteredLO; -}); + const mod = mods.find((x) => x.attributes?.modId === parseInt(entry.modId ?? '0')); + if (!mod) { + return false; + } -export const importLoadOrder = toBluebird(async (api: types.IExtensionApi, collection: ICollectionMB) : Promise => { + if (collectionMod) { + return isValidMod(mod) && isModInCollection(collectionMod, mod); + } + + return isValidMod(mod); + }) + .reduce((accum, iter) => { + accum.push(iter); + return accum; + }, []); + return filteredLoadOrder; +}; + +export const parseCollectionGeneralLoadOrder = async ( + api: types.IExtensionApi, + modules: Readonly, + collection: ICollectionDataWithGeneralData +): Promise => { const state = api.getState(); - const profileId = selectors.lastActiveProfileForGame(state, GAME_ID); + const profileId: string | undefined = selectors.lastActiveProfileForGame(state, GAME_ID); if (profileId === undefined) { - throw new CollectionParseError(collection?.info?.name ?? ``, `Invalid profile id`); + throw new CollectionParseError(collection.info.name ?? '', 'Invalid profile id'); } - // The mods need to be deployed in order for the load order to be imported correctly. - return new Promise((resolve, reject) => { - api.events.emit(`deploy-mods`, (err: any) => { - if (err) { - reject(err); - return; - } - api.store?.dispatch(actions.setLoadOrder(profileId, [collection.loadOrder])); - resolve(); - }); - }); -}); -*/ + const suggestedLoadOrder = collection.suggestedLoadOrder; + + const loadOrder = await orderCurrentLoadOrderByExternalLoadOrder(api, modules, suggestedLoadOrder); + + api.store?.dispatch(actionsLoadOrder.setFBLoadOrder(profileId, loadOrder)); +}; diff --git a/src/collections/modOptionsData.ts b/src/collections/modOptionsData.ts new file mode 100644 index 0000000..23c9432 --- /dev/null +++ b/src/collections/modOptionsData.ts @@ -0,0 +1,118 @@ +import { actions, types, util } from 'vortex-api'; +import { + ICollectionData, + ICollectionDataWithSettingsData, + ICollectionSettingsData, + IModAttributesWithCollection, + IncludedModOptions, +} from './types'; +import { hasIncludedModOptions, hasModAttributeCollection } from './utils'; +import { nameof } from '../nameof'; +import { getGlobalSettings, getSpecialSettings, overrideModOptions } from '../modoptions'; +import { LocalizationManager } from '../localization'; + +/** + * Assumes that the correct Game ID is active and that the profile is set up correctly. + */ +export const genCollectionModOptionsData = ( + api: types.IExtensionApi, + collectionMod: types.IMod +): Promise => { + if (!hasIncludedModOptions(collectionMod)) { + return Promise.resolve({ + includedModOptions: [], + }); + } + + const includedModOptions = collectionMod.attributes?.collection?.includedModOptions ?? []; + + return Promise.resolve({ + includedModOptions: includedModOptions, + }); +}; + +/** + * Assumes that the correct Game ID is active and that the profile is set up correctly. + */ +export const cloneCollectionModOptionsData = async ( + api: types.IExtensionApi, + gameId: string, + collection: ICollectionData, + from: types.IMod, + to: types.IMod +): Promise => { + if (!hasModOptionsData(collection)) { + return; + } + + if (!hasModAttributeCollection(to) || to.attributes?.collection === undefined || to.attributes.collection === null) { + return; + } + + const includedModOptions = collection.includedModOptions; + + const availableModOptions = Object.values({ ...getSpecialSettings(), ...(await getGlobalSettings()) }); + const availableIncludedModOptions = includedModOptions.filter((modOption) => { + return availableModOptions.some((iter) => iter.name === modOption.name); + }); + + const attributes = util.setSafe( + to.attributes.collection, + [nameof('includedModOptions')], + availableIncludedModOptions + ); + api.store?.dispatch( + actions.setModAttribute(gameId, to.id, nameof('collection'), attributes) + ); +}; + +/** + * Assumes that the correct Game ID is active and that the profile is set up correctly. + */ +export const parseCollectionModOptionsData = async ( + api: types.IExtensionApi, + collection: ICollectionData, + mod: types.IMod +): Promise => { + if (!hasModOptionsData(collection)) { + return; + } + + const includedModOptions = collection.includedModOptions; + + if (includedModOptions === undefined || !includedModOptions.length) { + return; + } + + const localizationManager = LocalizationManager.getInstance(api); + const { localize: t } = localizationManager; + + const no = t('No'); + const yes = t('Yes'); + const result = await api.showDialog?.( + 'question', + t('Override Mod Options'), + { + 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.` + ), + }, + [{ label: no }, { label: yes }] + ); + + if (!result || result.action === no) { + return; + } + + await overrideModOptions(mod, includedModOptions); +}; + +const hasModOptionsData = (collection: ICollectionData): collection is ICollectionDataWithSettingsData => { + const collectionData = collection as ICollectionDataWithSettingsData; + if (collectionData.includedModOptions === undefined) { + return false; + } + return true; +}; diff --git a/src/collections/types.ts b/src/collections/types.ts index 201bb8d..1a236a5 100644 --- a/src/collections/types.ts +++ b/src/collections/types.ts @@ -1,32 +1,63 @@ -/* -import Bluebird, { Promise, method as toBluebird } from 'bluebird'; - import { types } from 'vortex-api'; +import { ICollection as ICollectionDataToExport } from 'collections/src/types/ICollection'; +import { IExtensionFeature } from 'collections/src/util/extension'; +import { IModAttributes, IStatePersistent, PersistenceLoadOrderStorage } from '../types'; +import { PersistentModOptionsEntry } from '../modoptions'; -import { ICollection } from 'collections/src/types/ICollection'; -import { IExtendedInterfaceProps } from "collections/src/types/IExtendedInterfaceProps"; -import { ILoadOrder } from "../types"; +export interface ICollectionData extends ICollectionDataToExport {} -export interface ICollectionMB extends ICollection { - loadOrder: ILoadOrder; -} +export type IncludedModOptions = { + includedModOptions?: PersistentModOptionsEntry[]; +}; -export interface IExtensionContextCollectionFeature { - registerCollectionFeature: - ( - id: string, +export type IModAttributesWithCollection = IModAttributes & { + collection?: T; +}; - generate: (gameId: string, includedMods: string[]) => Bluebird, +export interface IModWithCollection extends types.IMod { + attributes?: IModAttributesWithCollection; +} - parse: (gameId: string, collection: ICollection) => Bluebird, +export interface IModWithIncludedModOptions extends IModWithCollection {} - clone: (gameId: string, collection: ICollection, from: types.IMod, to: types.IMod) => Bluebird, +export interface IStatePersistentWithModsWithIncludedModOptions extends IStatePersistent { + mods: { + [gameId: string]: { + [modId: string]: IModWithCollection; + }; + }; +} - title: (t: types.TFunction) => string, +export interface ICollectionFeature { + registerCollectionFeature: ( + id: IExtensionFeature['id'], - condition?: (state: types.IState, gameId: string) => boolean, + generate: IExtensionFeature['generate'], + parse: IExtensionFeature['parse'], + clone: IExtensionFeature['clone'], - editComponent?: React.ComponentType + title: IExtensionFeature['title'], + condition?: IExtensionFeature['condition'], + editComponent?: IExtensionFeature['editComponent'] ) => void; } -*/ + +export interface IExtensionContextWithCollectionFeature extends types.IExtensionContext { + optional: ICollectionFeature; +} + +export interface ICollectionGeneralData { + hasBLSE: boolean; + suggestedLoadOrder: PersistenceLoadOrderStorage; +} +export interface ICollectionDataWithGeneralData extends ICollectionData, ICollectionGeneralData {} + +export interface ICollectionLegacyData { + loadOrder: types.ILoadOrderEntry[]; +} +export interface ICollectionDataWithLegacyData extends ICollectionData, ICollectionLegacyData {} + +export interface ICollectionSettingsData { + includedModOptions: PersistentModOptionsEntry[]; +} +export interface ICollectionDataWithSettingsData extends ICollectionData, ICollectionSettingsData {} diff --git a/src/collections/utils.ts b/src/collections/utils.ts new file mode 100644 index 0000000..bc0b90a --- /dev/null +++ b/src/collections/utils.ts @@ -0,0 +1,64 @@ +import { types } from 'vortex-api'; +import { + IExtensionContextWithCollectionFeature, + IModWithCollection, + IModWithIncludedModOptions, + IncludedModOptions, + IStatePersistentWithModsWithIncludedModOptions, +} from './types'; +import { GAME_ID } from '../common'; +import { IStatePersistent } from '../types'; + +export const hasContextWithCollectionFeature = ( + context: types.IExtensionContext +): context is IExtensionContextWithCollectionFeature => { + return context.optional.registerCollectionFeature; +}; + +export const hasStatePersistentCollectionModWithIncludedModOptions = ( + statePersistent: IStatePersistent, + collectionId: string +): statePersistent is IStatePersistentWithModsWithIncludedModOptions => { + if (!statePersistent.mods[GAME_ID]) { + return false; + } + + if (!statePersistent.mods[GAME_ID][collectionId]) { + return false; + } + + return hasIncludedModOptions(statePersistent.mods[GAME_ID][collectionId]!); +}; + +export const hasModAttributeCollection = (mod: types.IMod): mod is IModWithCollection => { + const modWithIncludedModOptions = mod as IModWithCollection; + if (!modWithIncludedModOptions.attributes) { + return false; + } + + if (modWithIncludedModOptions.attributes.collection === undefined) { + return false; + } + + return true; +}; + +export const hasIncludedModOptions = (mod: types.IMod): mod is IModWithIncludedModOptions => { + if (!hasModAttributeCollection(mod)) { + return false; + } + + if (!mod.attributes) { + return false; + } + + if (!mod.attributes.collection) { + return false; + } + + if (!mod.attributes.collection.includedModOptions) { + return false; + } + + return true; +}; diff --git a/src/common.ts b/src/common.ts index e338722..87351b5 100644 --- a/src/common.ts +++ b/src/common.ts @@ -11,6 +11,8 @@ export const I18N_NAMESPACE = `game-mount-and-blade2`; export const SUBMODULE_FILE = `SubModule.xml`; +export const SUB_MODS_IDS = `subModsIds`; + export const BINARY_FOLDER_STANDARD = `Win64_Shipping_Client`; export const BINARY_FOLDER_STANDARD_MODDING_KIT = `Win64_Shipping_wEditor`; export const BINARY_FOLDER_XBOX = `Gaming.Desktop.x64_Shipping_Client`; diff --git a/src/game.ts b/src/game.ts index 5a6d76f..6cc0b15 100644 --- a/src/game.ts +++ b/src/game.ts @@ -2,13 +2,11 @@ import Bluebird, { Promise, method as toBluebird } from 'bluebird'; import { types } from 'vortex-api'; import { EPICAPP_ID, GAME_ID, GOG_IDS, MODULES, STEAMAPP_ID, XBOX_ID } from './common'; -import { findGame, getBannerlordMainExe, requiresLauncher, setup } from './utils'; -import { GetLauncherManager, GetLocalizationManager } from './types'; +import { findGame, getBannerlordMainExe, setup } from './vortex'; +import { VortexLauncherManager } from './launcher'; export class BannerlordGame implements types.IGame { - private _api: types.IExtensionApi; - private _getLauncherManager: GetLauncherManager; - private _getLocalizationManager: GetLocalizationManager; + private api: types.IExtensionApi; public id: string = GAME_ID; public name = `Mount & Blade II: Bannerlord (BUTR)`; @@ -31,14 +29,8 @@ export class BannerlordGame implements types.IGame { customOpenModsPath: MODULES, }; - constructor( - api: types.IExtensionApi, - getLauncherManager: GetLauncherManager, - getLocalizationManager: GetLocalizationManager - ) { - this._api = api; - this._getLauncherManager = getLauncherManager; - this._getLocalizationManager = getLocalizationManager; + constructor(api: types.IExtensionApi) { + this.api = api; } public queryPath = toBluebird(async (): Promise => { @@ -49,13 +41,13 @@ export class BannerlordGame implements types.IGame { return `.`; }; public getGameVersion = (_gamePath: string, _exePath: string): PromiseLike => { - return this._getLauncherManager().getGameVersionVortexAsync(); + return VortexLauncherManager.getInstance(this.api).getGameVersionVortexAsync(); }; public executable = (discoveredPath?: string): string => { - return getBannerlordMainExe(discoveredPath, this._api); + return getBannerlordMainExe(discoveredPath, this.api); }; public setup = toBluebird(async (discovery: types.IDiscoveryResult) => { - await setup(this._api, discovery, this._getLauncherManager, this._getLocalizationManager); + await setup(this.api, discovery); }); //public requiresLauncher = toBluebird(async (_gamePath: string, store?: string) => { // return await requiresLauncher(store); diff --git a/src/index.ts b/src/index.ts index 009f88f..ca65d4f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,54 +1,51 @@ // eslint-disable-next-line no-restricted-imports -import Bluebird, { Promise, method as toBluebird } from 'bluebird'; +import Bluebird, { method as toBluebird } from 'bluebird'; +import { log, selectors, types } from 'vortex-api'; +import { TFunction } from 'vortex-api/lib/util/i18n'; import path from 'path'; -import { selectors, types, log } from 'vortex-api'; import { GAME_ID } from './common'; -import { BannerlordGame } from './game'; import { - LoadOrderManager, - VortexLauncherManager, - getInstallPathModule, - isModTypeModule, - isModTypeBLSE, - getInstallPathBLSE, - testBLSE, - installBLSE, - didPurgeEvent, - didDeployEvent, - addedFiles, - SaveManager, - reducer, - actionsSettings, - LocalizationManager, -} from './utils'; -import { SaveList, SavePageOptions, Settings } from './views'; + BannerlordGeneralDataPage, + ModOptionsDataPage, + SavePage, + SavePageOptions, + Settings, + SettingsProps, +} from './views'; +import { BannerlordGame } from './game'; import { IAddedFiles } from './types'; +import { reducer } from './react'; +import { actionsSettings } from './settings'; +import { + cloneCollectionGeneralData, + cloneCollectionModOptionsData, + genCollectionGeneralData, + genCollectionModOptionsData, + hasContextWithCollectionFeature, + ICollectionData, + parseCollectionGeneralData, + parseCollectionLegacyData, + parseCollectionModOptionsData, + willRemoveModCollections, +} from './collections'; +import { didDeployLoadOrder, gamemodeActivatedLoadOrder, LoadOrderManager } from './loadOrder'; +import { didDeployBLSE, didPurgeBLSE, getInstallPathBLSE, installBLSE, isModTypeBLSE, testBLSE } from './blse'; +import { VortexLauncherManager } from './launcher'; +import { gamemodeActivatedSave } from './save'; +import { addedFilesEvent, getInstallPathModule, isModTypeModule } from './vortex'; import { version } from '../package.json'; -import { ISettingsProps } from './views/Settings/Settings'; + +// TODO: Better dialogs with settings const main = (context: types.IExtensionContext): boolean => { log('info', `Extension Version: ${version}`); - const getLocalizationManager = () => { - return LocalizationManager.getInstance(context.api); - }; - const getLauncherManager = () => { - return VortexLauncherManager.getInstance(context.api, getLocalizationManager); - }; - const getLoadOrderManager = () => { - return LoadOrderManager.getInstance(context.api, getLauncherManager, getLocalizationManager); - }; - const getSaveManager = () => { - return SaveManager.getInstance(context.api, getLauncherManager); - }; - context.registerReducer(/*path:*/ [`settings`, GAME_ID], /*spec:*/ reducer); context.registerSettings( /*title:*/ `Interface`, /*element:*/ Settings, - /*props?:*/ (): ISettingsProps => ({ - getLocalizationManager: getLocalizationManager, + /*props?:*/ (): SettingsProps => ({ onSetSortOnDeploy: (profileId: string, sort: boolean) => context.api.store?.dispatch(actionsSettings.setSortOnDeploy(profileId, sort)), onSetFixCommonIssues: (profileId: string, fixCommonIssues: boolean) => @@ -62,31 +59,80 @@ const main = (context: types.IExtensionContext): boolean => { /*priority?:*/ 51 ); - context.registerGame(new BannerlordGame(context.api, getLauncherManager, getLocalizationManager)); - - /* - // Register Collection Feature - const collectionFeature: IExtensionContextCollectionFeature = context.optional; - if (collectionFeature.registerCollectionFeature) { - collectionFeature.registerCollectionFeature( - `mountandblade2_collection_data`, - (gameId: string, includedMods: string[]) => genCollectionsData(context, gameId, includedMods), - (gameId: string, collection: ICollection) => parseCollectionsData(context, gameId, collection as ICollectionMB), - () => Promise.resolve(), - (t: TFunction) => t(`Mount and Blade 2 Data`), - (_state: types.IState, gameId: string) => gameId === GAME_ID, - CollectionsDataView as React.ComponentType, + context.registerGame(new BannerlordGame(context.api)); + + if (hasContextWithCollectionFeature(context)) { + context.optional.registerCollectionFeature( + /*id:*/ `${GAME_ID}_load_order`, + /*generate:*/ async (gameId: string, includedMods: string[], _mod: types.IMod) => { + if (GAME_ID !== gameId) { + return {}; + } + return await genCollectionGeneralData(context.api, includedMods); + }, + /*parse:*/ async (gameId: string, collection: ICollectionData, _mod: types.IMod) => { + if (GAME_ID !== gameId) { + return; + } + + await parseCollectionLegacyData(context.api, collection); + await parseCollectionGeneralData(context.api, collection); + }, + /*clone:*/ + async (gameId: string, collection: ICollectionData, from: types.IMod, to: types.IMod) => { + if (GAME_ID !== gameId) { + return; + } + await cloneCollectionGeneralData(context.api, gameId, collection, from, to); + }, + /*title:*/ (t: TFunction) => { + return t(`Requirements & Load Order`); + }, + /*condition?:*/ (_state: types.IState, gameId: string) => { + return gameId === GAME_ID; + }, + /*editComponent?:*/ BannerlordGeneralDataPage + ); + + context.optional.registerCollectionFeature( + /*id:*/ `${GAME_ID}_mod_options`, + /*generate:*/ async (gameId: string, _includedMods: string[], mod: types.IMod) => { + if (GAME_ID !== gameId) { + return {}; + } + return await genCollectionModOptionsData(context.api, mod); + }, + /*parse:*/ async (gameId: string, collection: ICollectionData, mod: types.IMod) => { + if (GAME_ID !== gameId) { + return; + } + + await parseCollectionModOptionsData(context.api, collection, mod); + }, + /*clone:*/ async (gameId: string, collection: ICollectionData, from: types.IMod, to: types.IMod) => { + if (GAME_ID !== gameId) { + return; + } + + await cloneCollectionModOptionsData(context.api, gameId, collection, from, to); + }, + /*title:*/ (t: TFunction) => { + return t(`Mod Options`); + }, + /*condition?:*/ (_state: types.IState, gameId: string) => { + return gameId === GAME_ID; + }, + /*editComponent?:*/ ModOptionsDataPage ); } - */ - context.registerLoadOrder(/*gameInfo:*/ getLoadOrderManager()); + context.registerLoadOrder(/*gameInfo:*/ LoadOrderManager.getInstance(context.api)); context.registerMainPage( /*icon:*/ 'savegame', /*title:*/ 'Saves', - /*element:*/ SaveList, - /*options:*/ new SavePageOptions(context, getLauncherManager, getSaveManager, getLocalizationManager) + /*element:*/ SavePage, + /*options:*/ new SavePageOptions(context) ); context.registerInstaller( @@ -106,13 +152,13 @@ const main = (context: types.IExtensionContext): boolean => { context.registerInstaller( /*id:*/ `bannerlord-module-installer`, /*priority:*/ 25, - /*testSupported:*/ toBluebird(async (files: string[], gameId: string) => { - const launcherManager = getLauncherManager(); - return await launcherManager.testModule(files, gameId); + /*testSupported:*/ toBluebird((files: string[], gameId: string) => { + const launcherManager = VortexLauncherManager.getInstance(context.api); + return launcherManager.testModule(files, gameId); }), - /*install:*/ toBluebird(async (files: string[], destinationPath: string) => { - const launcherManager = getLauncherManager(); - return await launcherManager.installModule(files, destinationPath); + /*install:*/ toBluebird((files: string[], destinationPath: string) => { + const launcherManager = VortexLauncherManager.getInstance(context.api); + return launcherManager.installModule(files, destinationPath); }) ); context.registerModType( @@ -130,12 +176,12 @@ const main = (context: types.IExtensionContext): boolean => { /*options:*/ {}, /*titleOrProps?:*/ `Auto Sort`, /*actionOrCondition?:*/ (_instanceIds?: string[]): boolean | void => { - const launcherManager = getLauncherManager(); + const launcherManager = VortexLauncherManager.getInstance(context.api); launcherManager.autoSort(); }, /*condition?:*/ (_instanceIds?: string[]): boolean => { const state = context.api.getState(); - const gameId = selectors.activeGameId(state); + const gameId: string | undefined = selectors.activeGameId(state); return gameId === GAME_ID; } ); @@ -148,70 +194,70 @@ const main = (context: types.IExtensionContext): boolean => { {}, `Fetch Compatibility Scores`, (_instanceIds?: string[]): boolean | void => { - const loadOrderManager = getLoadOrderManager(); + const loadOrderManager = LoadOrderManager.getInstance(context.api); loadOrderManager.updateCompatibilityScores(); }, (_instanceIds?: string[]): boolean => { const state = context.api.getState(); - const gameId = selectors.activeGameId(state); + const gameId: string | undefined = selectors.activeGameId(state); return gameId === GAME_ID; } ); */ // Register Callbacks - context.once( - toBluebird(async () => { - context.api.setStylesheet('savegame', path.join(__dirname, 'savegame.scss')); + context.once(() => { + context.api.setStylesheet('savegame', path.join(__dirname, 'savegame.scss')); - context.api.events.on('gamemode-activated', async (gameId: string) => { - if (GAME_ID !== gameId) { - return; - } - try { - const loadOrderManager = getLoadOrderManager(); - await loadOrderManager.deserializeLoadOrder(); - } catch (err) { - context.api.showErrorNotification?.('Failed to deserialize load order file', err); - return; - } - try { - const saveManager = getSaveManager(); - saveManager.reloadSave(); - } catch (err) { - context.api.showErrorNotification?.('Failed to reload the currect save file', err); - return; - } - }); - - /* - // TODO: Provide compatibility info for Game Version -> Mod Version from the BUTR Site - const proxy = new ModAnalyzerProxy(context.api); - context.api.addMetaServer(`butr.link`, { - url: '', - loopbackCB: (query: types.IQuery) => - Bluebird.resolve(proxy.find(query)).catch((err) => { - log('error', 'failed to look up butr meta info', err.message); - return Bluebird.resolve([]); - }), - cacheDurationSec: 86400, - priority: 25, - }); - */ - - context.api.onAsync(`added-files`, (profileId: string, files: IAddedFiles[]) => - addedFiles(context.api, profileId, files) - ); - - // TODO: lister to profile switch events and check for BLSE - // Set BLSE CLI as primary tool on deployment if no primary tool is set - context.api.onAsync('did-deploy', (profileId: string) => - didDeployEvent(context.api, profileId, getLocalizationManager, getLoadOrderManager) - ); - // Remove BLSE CLI as primary tool on purge if it is set - context.api.onAsync('did-purge', (profileId: string) => didPurgeEvent(context.api, profileId)); - }) - ); + context.api.events.on('gamemode-activated', async (gameId: string) => { + if (GAME_ID !== gameId) { + return; + } + + await gamemodeActivatedLoadOrder(context.api); + await gamemodeActivatedSave(context.api); + }); + + context.api.onAsync(`added-files`, async (profileId: string, files: IAddedFiles[]) => { + const state = context.api.getState(); + const profile: types.IProfile | undefined = selectors.profileById(state, profileId); + if (profile?.gameId !== GAME_ID) { + return; + } + + await addedFilesEvent(context.api, files); + }); + + // TODO: listen to profile switch events and check for BLSE + context.api.onAsync('did-deploy', async (profileId: string) => { + const state = context.api.getState(); + const profile: types.IProfile | undefined = selectors.profileById(state, profileId); + if (profile?.gameId !== GAME_ID) { + return; + } + + await didDeployLoadOrder(context.api); + await didDeployBLSE(context.api); + }); + + context.api.onAsync('did-purge', async (profileId: string) => { + const state = context.api.getState(); + const profile: types.IProfile | undefined = selectors.profileById(state, profileId); + if (profile?.gameId !== GAME_ID) { + return; + } + + await didPurgeBLSE(context.api); + }); + + context.api.onAsync('will-remove-mod', async (gameId: string, modId: string) => { + if (GAME_ID !== gameId) { + return; + } + + await willRemoveModCollections(context.api, modId); + }); + }); // Register Callbacks return true; diff --git a/src/launcher/hooks.ts b/src/launcher/hooks.ts new file mode 100644 index 0000000..fcf7633 --- /dev/null +++ b/src/launcher/hooks.ts @@ -0,0 +1,9 @@ +import { useContext } from 'react'; +import { MainContext } from 'vortex-api'; +import { VortexLauncherManager } from './manager'; + +export const useLauncher = (): VortexLauncherManager => { + const context = useContext(MainContext); + + return VortexLauncherManager.getInstance(context.api); +}; diff --git a/src/launcher/index.ts b/src/launcher/index.ts new file mode 100644 index 0000000..0e63fbf --- /dev/null +++ b/src/launcher/index.ts @@ -0,0 +1,3 @@ +export * from './hooks'; +export * from './manager'; +export * from './version'; diff --git a/src/utils/vortexLauncherManager.ts b/src/launcher/manager.ts similarity index 69% rename from src/utils/vortexLauncherManager.ts rename to src/launcher/manager.ts index 11ba43c..4e959fb 100644 --- a/src/utils/vortexLauncherManager.ts +++ b/src/launcher/manager.ts @@ -1,47 +1,41 @@ +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 { Dirent, readFileSync } from 'fs'; -import { actions, fs, selectors, types, util } from 'vortex-api'; -import { NativeLauncherManager, BannerlordModuleManager, types as vetypes } from '@butr/vortexextensionnative'; +import { hasPersistentLoadOrder } from '../vortex'; import { - vortexToLibrary, + actionsLoadOrder, + libraryToLibraryVM, + libraryToPersistence, libraryVMToVortex, - vortexToLibraryVM, persistenceToVortex, - libraryToPersistence, - writeLoadOrder, readLoadOrder, - hasPersistentLoadOrder, - libraryToLibraryVM, - filterEntryWithInvalidId, - actionsLoadOrder, - getBetaSortingFromSettings, -} from '.'; -import { GAME_ID } from '../common'; -import { GetLocalizationManager, IModuleCache, VortexLoadOrderStorage, VortexStoreIds } from '../types'; + vortexToLibrary, + vortexToLibraryVM, + writeLoadOrder, +} 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 { LocalizationManager } from '../localization'; export class VortexLauncherManager { - private static _instance: VortexLauncherManager; + private static _instance: VortexLauncherManager | undefined; - public static getInstance( - api?: types.IExtensionApi, - getLocalizationManager?: GetLocalizationManager - ): VortexLauncherManager { + public static getInstance(api: types.IExtensionApi): VortexLauncherManager { if (!VortexLauncherManager._instance) { - if (api === undefined || getLocalizationManager === undefined) { - throw new Error('IniStructure is not context aware'); - } - VortexLauncherManager._instance = new VortexLauncherManager(api, getLocalizationManager); + VortexLauncherManager._instance = new VortexLauncherManager(api); } return VortexLauncherManager._instance; } - private _launcherManager: NativeLauncherManager; - private _api: types.IExtensionApi; - private _getLocalizationManager: GetLocalizationManager; + private launcherManager: NativeLauncherManager; + private api: types.IExtensionApi; - public constructor(api: types.IExtensionApi, getLocalizationManager: GetLocalizationManager) { - this._launcherManager = new NativeLauncherManager( + public constructor(api: types.IExtensionApi) { + this.launcherManager = new NativeLauncherManager( this.setGameParameters, this.loadLoadOrder, this.saveLoadOrder, @@ -59,16 +53,15 @@ export class VortexLauncherManager { this.getState ); - this._api = api; - this._getLocalizationManager = getLocalizationManager; + this.api = api; } /** * Gets the LoadOrder from Vortex's Load Order Page */ private getLoadOrderFromVortex = (): VortexLoadOrderStorage => { - const state = this._api.getState(); - const profile = selectors.activeProfile(state); + const state = this.api.getState(); + const profile: types.IProfile | undefined = selectors.activeProfile(state); if (!hasPersistentLoadOrder(state.persistent)) { return []; } @@ -77,23 +70,23 @@ export class VortexLauncherManager { if (!Array.isArray(loadOrder)) { return []; } - return loadOrder.filter((x) => !!x && !!x.data && filterEntryWithInvalidId(x)); + return loadOrder.filter((x) => x?.data && filterEntryWithInvalidId(x)); }; public loadLoadOrderVortex = (): vetypes.LoadOrder => { - return this._launcherManager.loadLoadOrder(); + return this.launcherManager.loadLoadOrder(); }; public saveLoadOrderVortex = (loadOrder: vetypes.LoadOrder): void => { - this._launcherManager.saveLoadOrder(loadOrder); + this.launcherManager.saveLoadOrder(loadOrder); }; /** * Will trigger the LauncherManager to pull the @property {moduleViewModels} * And update the LO for the CLI. */ - public refreshGameParameters = () => { - this._launcherManager.refreshGameParameters(); + public refreshGameParameters = (): void => { + this.launcherManager.refreshGameParameters(); }; /** @@ -102,12 +95,12 @@ export class VortexLauncherManager { * Will refresh the Validation Cache */ public refreshModules = (): void => { - this._launcherManager.refreshModules(); + this.launcherManager.refreshModules(); this.refreshGameParameters(); }; public setModulesToLaunch = (loadOrder: vetypes.LoadOrder): void => { - this._launcherManager.setGameParameterLoadOrder(loadOrder); + this.launcherManager.setGameParameterLoadOrder(loadOrder); this.refreshGameParameters(); }; @@ -115,8 +108,8 @@ export class VortexLauncherManager { * Will update the CLI args with the save name * @param saveName if null will exclude if from the CLI */ - public setSaveFile = (saveName: string) => { - this._launcherManager.setGameParameterSaveFile(saveName); + public setSaveFile = (saveName: string): void => { + this.launcherManager.setGameParameterSaveFile(saveName); this.refreshGameParameters(); }; @@ -124,8 +117,8 @@ export class VortexLauncherManager { * Will update the CLI args with continuing the latest save file * @param saveName if null will exclude if from the CLI */ - public setContinueLastSaveFile = (value: boolean) => { - this._launcherManager.setGameParameterContinueLastSaveFile(value); + public setContinueLastSaveFile = (value: boolean): void => { + this.launcherManager.setGameParameterContinueLastSaveFile(value); this.refreshGameParameters(); }; @@ -135,7 +128,7 @@ export class VortexLauncherManager { * @return */ public getAllModules = (): Readonly => { - return this._launcherManager.getModules().reduce((map, current) => { + return this.launcherManager.getModules().reduce((map, current) => { map[current.id] = current; return map; }, {}); @@ -146,7 +139,7 @@ export class VortexLauncherManager { * @return */ public getAllModulesWithDuplicates = (): vetypes.ModuleInfoExtendedWithMetadata[] => { - return this._launcherManager.getAllModules(); + return this.launcherManager.getAllModules(); }; /** @@ -155,21 +148,21 @@ export class VortexLauncherManager { * @returns */ public orderByLoadOrder = (loadOrder: vetypes.LoadOrder): vetypes.OrderByLoadOrderResult => { - return this._launcherManager.orderByLoadOrder(loadOrder); + return this.launcherManager.orderByLoadOrder(loadOrder); }; /** * A simple wrapper for Vortex that returns a promise */ public getGameVersionVortexAsync = (): Promise => { - return Promise.resolve(this._launcherManager.getGameVersion()); + return Promise.resolve(this.launcherManager.getGameVersion()); }; /** * A simple wrapper for Vortex that returns a promise */ public getGameVersionVortex = (): string => { - return this._launcherManager.getGameVersion(); + return this.launcherManager.getGameVersion(); }; /** @@ -183,12 +176,11 @@ export class VortexLauncherManager { }); } - const result = this._launcherManager.testModule(files); - const transformedResult: types.ISupportedResult = { + const result = this.launcherManager.testModule(files); + return Promise.resolve({ supported: result.supported, requiredFiles: result.requiredFiles, - }; - return Promise.resolve(transformedResult); + }); }; /** @@ -207,7 +199,7 @@ export class VortexLauncherManager { )!; moduleInfo.path = subModuleRelFilePath; // TODO: fix the library - const result = this._launcherManager.installModule(files, [moduleInfo]); + const result = this.launcherManager.installModule(files, [moduleInfo]); const subModsIds = Array(); const transformedResult: types.IInstallResult = { instructions: result.instructions.reduce((map, current) => { @@ -220,7 +212,7 @@ export class VortexLauncherManager { }); break; case 'ModuleInfo': - if (current.moduleInfo !== undefined) { + if (current.moduleInfo) { subModsIds.push(current.moduleInfo.id); } break; @@ -230,7 +222,7 @@ export class VortexLauncherManager { }; transformedResult.instructions.push({ type: 'attribute', - key: 'subModsIds', + key: SUB_MODS_IDS, value: subModsIds, }); @@ -242,42 +234,42 @@ export class VortexLauncherManager { * @returns */ public isSorting = (): boolean => { - return this._launcherManager.isSorting(); + return this.launcherManager.isSorting(); }; /** * */ - public autoSort = () => { - this._launcherManager.sort(); + public autoSort = (): void => { + this.launcherManager.sort(); }; /** * */ public getSaveFiles = (): vetypes.SaveMetadata[] => { - return this._launcherManager.getSaveFiles(); + return this.launcherManager.getSaveFiles(); }; /** * Sets the game store manually, since the launcher manager is not perfect. */ - public setStore = (storeId: string) => { + public setStore = (storeId: string): void => { switch (storeId) { case VortexStoreIds.Steam: - this._launcherManager.setGameStore(`Steam`); + this.launcherManager.setGameStore(`Steam`); break; case VortexStoreIds.GOG: - this._launcherManager.setGameStore(`GOG`); + this.launcherManager.setGameStore(`GOG`); break; case VortexStoreIds.Epic: - this._launcherManager.setGameStore(`Epic`); + this.launcherManager.setGameStore(`Epic`); break; case VortexStoreIds.Xbox: - this._launcherManager.setGameStore(`Xbox`); + this.launcherManager.setGameStore(`Xbox`); break; default: - this._launcherManager.setGameStore(`Unknown`); + this.launcherManager.setGameStore(`Unknown`); break; } }; @@ -288,13 +280,13 @@ export class VortexLauncherManager { private setGameParameters = (_executable: string, gameParameters: string[]): void => { const params = gameParameters.filter((x) => x !== ' ' && x.length > 0).join(' '); - const discovery = selectors.currentGameDiscovery(this._api.getState()); + const discovery: types.IDiscoveryResult | undefined = selectors.currentGameDiscovery(this.api.getState()); const cliTools = Object.values(discovery?.tools ?? {}).filter((tool) => tool.id && tool.id.endsWith('-cli')); const batchedActions = cliTools.map((tool) => actions.addDiscoveredTool(GAME_ID, tool.id, { ...tool, parameters: [params] }, true) ); const gameParamAction = actions.setGameParameters(GAME_ID, { parameters: [params] }); - util.batchDispatch(this._api.store?.dispatch, [...batchedActions, gameParamAction]); + util.batchDispatch(this.api.store?.dispatch, [...batchedActions, gameParamAction]); }; /** * Callback @@ -303,11 +295,11 @@ export class VortexLauncherManager { private loadLoadOrder = (): vetypes.LoadOrder => { const allModules = this.getAllModules(); - const savedLoadOrder = persistenceToVortex(this._api, allModules, readLoadOrder(this._api)); + 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) === undefined) + if (!savedLoadOrder.find((x) => x.id === module.id)) savedLoadOrder.push({ id: module.id, enabled: false, @@ -327,7 +319,7 @@ export class VortexLauncherManager { * Saves the Load Order in Vortex's permantent storage */ private saveLoadOrder = (loadOrder: vetypes.LoadOrder): void => { - writeLoadOrder(this._api, libraryToPersistence(loadOrder)); + writeLoadOrder(this.api, libraryToPersistence(loadOrder)); }; /** * Callback @@ -335,7 +327,7 @@ export class VortexLauncherManager { private sendNotification = (id: string, type: vetypes.NotificationType, message: string, delayMS: number): void => { switch (type) { case 'hint': - this._api.sendNotification?.({ + this.api.sendNotification?.({ id: id, type: 'activity', message: message, @@ -343,7 +335,7 @@ export class VortexLauncherManager { }); break; case 'info': - this._api.sendNotification?.({ + this.api.sendNotification?.({ id: id, type: 'info', message: message, @@ -351,7 +343,7 @@ export class VortexLauncherManager { }); break; case 'warning': - this._api.sendNotification?.({ + this.api.sendNotification?.({ id: id, type: 'warning', message: message, @@ -359,7 +351,7 @@ export class VortexLauncherManager { }); break; case 'error': - this._api.sendNotification?.({ + this.api.sendNotification?.({ id: id, type: 'error', message: message, @@ -377,24 +369,32 @@ export class VortexLauncherManager { message: string, filters: vetypes.FileFilter[] ): Promise => { - const localizationManager = this._getLocalizationManager(); - const t = localizationManager.localize; + const { localize: t } = LocalizationManager.getInstance(this.api); switch (type) { case 'warning': { const messageFull = message.split('--CONTENT-SPLIT--', 2).join('\n'); - const result = await this._api.showDialog?.('question', title, { message: messageFull }, [ - { label: t('No'), action: () => 'false' }, - { label: t('Yes'), action: () => 'true' }, + const no = t('No'); + const yes = t('Yes'); + const result = await this.api.showDialog?.('question', title, { message: messageFull }, [ + { label: no }, + { label: yes }, ]); - return result?.action ?? ''; + switch (result?.action) { + case yes: + return 'true'; + case no: + return 'false'; + default: + return ''; + } } case 'fileOpen': { const filtersTransformed = filters.map((x) => ({ name: x.name, extensions: x.extensions, })); - const result = await this._api.selectFile({ + const result = await this.api.selectFile({ filters: filtersTransformed, }); return result; @@ -405,7 +405,7 @@ export class VortexLauncherManager { name: x.name, extensions: x.extensions, })); - const result = await this._api.saveFile({ + const result = await this.api.saveFile({ filters: filtersTransformed, defaultPath: fileName, }); @@ -417,8 +417,8 @@ export class VortexLauncherManager { * Callback */ private getInstallPath = (): string => { - const state = this._api.getState(); - const discovery = selectors.currentGameDiscovery(state); + const state = this.api.getState(); + const discovery: types.IDiscoveryResult | undefined = selectors.currentGameDiscovery(state); return discovery?.path ?? ''; }; /** @@ -427,13 +427,13 @@ export class VortexLauncherManager { private readFileContent = (filePath: string, offset: number, length: number): Uint8Array | null => { try { if (offset === 0 && length === -1) { - return fs.readFileSync(filePath); + return 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 fs.readFileSync(filePath).slice(offset, offset + length); + return readFileSync(filePath).slice(offset, offset + length); } else { return null; } @@ -446,7 +446,7 @@ export class VortexLauncherManager { */ private writeFileContent = (filePath: string, data: Uint8Array): void => { try { - return fs.writeFileSync(filePath, data); + return writeFileSync(filePath, data); } catch { /* ignore error */ } @@ -456,8 +456,7 @@ export class VortexLauncherManager { */ private readDirectoryFileList = (directoryPath: string): string[] | null => { try { - return fs - .readdirSync(directoryPath, { withFileTypes: true }) + return readdirSync(directoryPath, { withFileTypes: true }) .filter((x: Dirent) => x.isFile()) .map((x: Dirent) => path.join(directoryPath, x.name)); } catch { @@ -469,8 +468,7 @@ export class VortexLauncherManager { */ private readDirectoryList = (directoryPath: string): string[] | null => { try { - return fs - .readdirSync(directoryPath, { withFileTypes: true }) + return readdirSync(directoryPath, { withFileTypes: true }) .filter((x: Dirent) => x.isDirectory()) .map((x: Dirent) => path.join(directoryPath, x.name)); } catch { @@ -496,7 +494,7 @@ export class VortexLauncherManager { const allModules = this.getAllModules(); const existingModuleViewModels = this.getModuleViewModels() ?? []; const modulesToConvert = Object.values(allModules).filter( - (x) => existingModuleViewModels.find((y) => y.moduleInfoExtended.id === x.id) === undefined + (x) => !existingModuleViewModels.find((y) => y.moduleInfoExtended.id === x.id) ); const viewModels = libraryToLibraryVM(modulesToConvert); @@ -507,16 +505,16 @@ export class VortexLauncherManager { * Callback */ private setModuleViewModels = (moduleViewModels: vetypes.ModuleViewModel[]): void => { - const profile = selectors.activeProfile(this._api.getState()); - const loadOrder = libraryVMToVortex(this._api, moduleViewModels); - this._api.store?.dispatch(actionsLoadOrder.setFBLoadOrder(profile.id, loadOrder)); + const profile: types.IProfile | undefined = selectors.activeProfile(this.api.getState()); + const loadOrder = libraryVMToVortex(this.api, moduleViewModels); + this.api.store?.dispatch(actionsLoadOrder.setFBLoadOrder(profile.id, loadOrder)); }; /** * Callback */ private getOptions = (): vetypes.LauncherOptions => { - const profile = selectors.activeProfile(this._api.getState()); - const betaSorting = getBetaSortingFromSettings(this._api, profile.id) ?? false; + const profile: types.IProfile | undefined = selectors.activeProfile(this.api.getState()); + const betaSorting = getBetaSortingFromSettings(this.api, profile.id) ?? false; return { betaSorting: betaSorting, diff --git a/src/utils/version.ts b/src/launcher/version.ts similarity index 100% rename from src/utils/version.ts rename to src/launcher/version.ts diff --git a/src/loadOrder/actions.ts b/src/loadOrder/actions.ts new file mode 100644 index 0000000..60916a0 --- /dev/null +++ b/src/loadOrder/actions.ts @@ -0,0 +1,34 @@ +// This is a risk, since we won't notice if the API changes +// TODO: Ask IDCs to provider a proper type system +import { types } from 'vortex-api'; + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +const setFBForceUpdate = (profileId: string) => ({ + type: 'SET_FB_FORCE_UPDATE', + payload: { + profileId, + }, +}); + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +const setFBLoadOrderEntry = (profileId: string, loEntry: types.ILoadOrderEntry) => ({ + type: 'SET_FB_LOAD_ORDER_ENTRY', + payload: { + profileId, + loEntry, + }, +}); + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +const setFBLoadOrder = (profileId: string, loadOrder: types.LoadOrder) => ({ + type: 'SET_FB_LOAD_ORDER', + payload: { + profileId, + loadOrder, + }, +}); +export const actionsLoadOrder = { + setFBForceUpdate, + setFBLoadOrderEntry, + setFBLoadOrder, +}; diff --git a/src/utils/loadOrder/converter.ts b/src/loadOrder/converters.ts similarity index 74% rename from src/utils/loadOrder/converter.ts rename to src/loadOrder/converters.ts index ac4a186..2f59dfe 100644 --- a/src/utils/loadOrder/converter.ts +++ b/src/loadOrder/converters.ts @@ -1,13 +1,14 @@ import { types } from 'vortex-api'; -import { types as vetypes, BannerlordModuleManager } from '@butr/vortexextensionnative'; -import { ValidationManager, getModIds } from '../'; +import { BannerlordModuleManager, types as vetypes } from '@butr/vortexextensionnative'; +import { getModIds } from './utils'; import { - VortexLoadOrderStorage, - VortexLoadOrderEntry, - PersistenceLoadOrderStorage, IModuleCache, IPersistenceLoadOrderEntry, -} from '../../types'; + PersistenceLoadOrderStorage, + VortexLoadOrderEntry, + VortexLoadOrderStorage, +} from '../types'; +import { ValidationManager } from '../validation'; export const persistenceToVortex = ( api: types.IExtensionApi, @@ -28,11 +29,36 @@ export const persistenceToVortex = ( }, }; }) - .filter((x) => x.data !== undefined) + .filter((x) => x.data) .sort((x, y) => x.data!.index - y.data!.index); return loadOrderConverted; }; +export const persistenceToLibrary = (loadOrder: PersistenceLoadOrderStorage): vetypes.LoadOrder => { + const loadOrderConverted = loadOrder.reduce((map, curr) => { + map[curr.id] = { + id: curr.id, + name: curr.name, + isSelected: curr.isSelected, + isDisabled: curr.isDisabled, + index: curr.index, + }; + return map; + }, {}); + return loadOrderConverted; +}; + +export const vortexToPersistence = (loadOrder: VortexLoadOrderStorage): PersistenceLoadOrderStorage => { + const loadOrderConverted = Object.values(loadOrder).map((x, index) => ({ + id: x.id, + name: x.name, + isSelected: x.enabled, + isDisabled: x.locked !== undefined && (x.locked === `true` || x.locked === `always`), + index: index, + })); + return loadOrderConverted; +}; + export const libraryToPersistence = (loadOrder: vetypes.LoadOrder): PersistenceLoadOrderStorage => { const loadOrderConverted = Object.values(loadOrder).map((x) => ({ id: x.id, @@ -50,7 +76,7 @@ export const vortexToLibraryVM = ( ): vetypes.ModuleViewModel[] => { const modules = loadOrder .map((entry) => allModules[entry.id]!) - .filter((x) => x !== undefined); + .filter((x) => x); const validationManager = ValidationManager.fromVortex(loadOrder); const loadOrderConverted = loadOrder.flatMap((entry) => { @@ -58,17 +84,19 @@ export const vortexToLibraryVM = ( return entry.data && module ? { moduleInfoExtended: module, - isValid: - entry.enabled && BannerlordModuleManager.validateModule(modules, module, validationManager).length === 0, + isValid: entry.enabled && !BannerlordModuleManager.validateModule(modules, module, validationManager).length, isSelected: entry.enabled, - isDisabled: !!entry.locked && (entry.locked === `true` || entry.locked === `always`), + isDisabled: entry.locked !== undefined && (entry.locked === `true` || entry.locked === `always`), index: entry.data.index, } : []; }); return loadOrderConverted; }; -export const libraryVMToVortex = (api: types.IExtensionApi, loadOrder: vetypes.ModuleViewModel[]): types.LoadOrder => { +export const libraryVMToVortex = ( + api: types.IExtensionApi, + loadOrder: vetypes.ModuleViewModel[] +): VortexLoadOrderStorage => { const loadOrderConverted = Object.values(loadOrder).map((curr) => { const modId = getModIds(api, curr.moduleInfoExtended.id); return { @@ -121,7 +149,7 @@ export const vortexToLibrary = (loadOrder: VortexLoadOrderStorage): vetypes.Load id: curr.id, name: curr.name, isSelected: curr.enabled, - isDisabled: !!curr.locked && (curr.locked === `true` || curr.locked === `always`), + isDisabled: curr.locked !== undefined && (curr.locked === `true` || curr.locked === `always`), index: loadOrder.indexOf(curr), }; return map; @@ -135,13 +163,13 @@ export const libraryToVortex = ( ): VortexLoadOrderStorage => { const availableModules = Object.values(loadOrder) .map((curr) => allModules[curr.id]!) - .filter((x) => x !== undefined); + .filter((x) => x); const validationManager = ValidationManager.fromLibrary(loadOrder); const loadOrderConverted = Object.values(loadOrder) .map((curr) => { const module = allModules[curr.id]; - if (module === undefined) { + if (!module) { return undefined!; } @@ -154,12 +182,12 @@ export const libraryToVortex = ( modId: modId[0]?.id ?? undefined!, data: { moduleInfoExtended: module, - isValid: moduleValidation && moduleValidation.length === 0, + isValid: !moduleValidation.length, isDisabled: false, index: curr.index, }, }; }, []) - .filter((x) => x !== undefined); + .filter((x) => x); return loadOrderConverted; }; diff --git a/src/loadOrder/events.ts b/src/loadOrder/events.ts new file mode 100644 index 0000000..b5f2011 --- /dev/null +++ b/src/loadOrder/events.ts @@ -0,0 +1,29 @@ +import { types } from 'vortex-api'; +import { LoadOrderManager } from './manager'; +import { LocalizationManager } from '../localization'; + +/** + * Event function, be careful + */ +export const didDeployLoadOrder = async (api: types.IExtensionApi): Promise => { + const { localize: t } = LocalizationManager.getInstance(api); + + try { + const loadOrderManager = LoadOrderManager.getInstance(api); + await loadOrderManager.deserializeLoadOrder(); + } catch (err) { + api.showErrorNotification?.(t('Failed to deserialize load order file'), err); + } +}; + +/** + * Event function, be careful + */ +export const gamemodeActivatedLoadOrder = async (api: types.IExtensionApi): Promise => { + try { + const loadOrderManager = LoadOrderManager.getInstance(api); + await loadOrderManager.deserializeLoadOrder(); + } catch (err) { + api.showErrorNotification?.('Failed to deserialize load order file', err); + } +}; diff --git a/src/loadOrder/hooks.ts b/src/loadOrder/hooks.ts new file mode 100644 index 0000000..fe1eec7 --- /dev/null +++ b/src/loadOrder/hooks.ts @@ -0,0 +1,9 @@ +import { useContext } from 'react'; +import { MainContext } from 'vortex-api'; +import { LoadOrderManager } from './manager'; + +export const useLoadOrder = (): LoadOrderManager => { + const context = useContext(MainContext); + + return LoadOrderManager.getInstance(context.api); +}; diff --git a/src/loadOrder/index.ts b/src/loadOrder/index.ts new file mode 100644 index 0000000..a0efdc9 --- /dev/null +++ b/src/loadOrder/index.ts @@ -0,0 +1,8 @@ +export * from './actions'; +export * from './converters'; +export * from './events'; +export * from './hooks'; +export * from './manager'; +export * from './types'; +export * from './utils'; +export * from './vortex'; diff --git a/src/loadOrder/manager.ts b/src/loadOrder/manager.ts new file mode 100644 index 0000000..7332582 --- /dev/null +++ b/src/loadOrder/manager.ts @@ -0,0 +1,162 @@ +import { ComponentType } from 'react'; +import { selectors, types } from 'vortex-api'; +import { IInvalidResult, IValidationResult } from 'vortex-api/lib/extensions/file_based_loadorder/types/types'; +import { BannerlordModuleManager, Utils, types as vetypes } from '@butr/vortexextensionnative'; +import { libraryToPersistence, vortexToLibrary } from './converters'; +import { actionsLoadOrder } from './actions'; +import { orderCurrentLoadOrderByExternalLoadOrder } from './utils'; +import { readLoadOrder } from './vortex'; +import { IFBLOItemRendererProps } from './types'; +import { + IModAnalyzerRequestModule, + IModAnalyzerRequestQuery, + IModuleCompatibilityInfoCache, + ModAnalyzerProxy, +} from '../butr'; +import { GAME_ID } from '../common'; +import { LoadOrderInfoPanel, LoadOrderItemRenderer } from '../views'; +import { IVortexViewModelData, VortexLoadOrderStorage } from '../types'; +import { versionToString, VortexLauncherManager } from '../launcher'; + +export class LoadOrderManager implements types.ILoadOrderGameInfo { + private static instance: LoadOrderManager | undefined; + + public static getInstance(api: types.IExtensionApi): LoadOrderManager { + if (!LoadOrderManager.instance) { + LoadOrderManager.instance = new LoadOrderManager(api); + } + + return LoadOrderManager.instance; + } + + private api: types.IExtensionApi; + private isInitialized = false; + private allModules: vetypes.ModuleInfoExtendedWithMetadata[] = []; + private compatibilityScores: IModuleCompatibilityInfoCache = {}; + + public gameId: string = GAME_ID; + public toggleableEntries = true; + public customItemRenderer?: ComponentType<{ + className?: string; + item: IFBLOItemRendererProps; + }>; + + public usageInstructions?: ComponentType; + public noCollectionGeneration = true; + + constructor(api: types.IExtensionApi) { + this.api = api; + this.usageInstructions = (): JSX.Element => + LoadOrderInfoPanel({ + refresh: this.updateCompatibilityScores, + }); + + this.customItemRenderer = ({ className = '', item }): JSX.Element => { + const availableProviders = this.allModules + .filter((x) => x.id === item.loEntry.id) + .map((x) => x.moduleProviderType); + const compatibilityScore = this.compatibilityScores[item.loEntry.id]; + + return LoadOrderItemRenderer({ + item: item, + className: className, + availableProviders: availableProviders, + compatibilityInfo: compatibilityScore, + }); + }; + } + + public updateCompatibilityScores = (): void => { + const proxy = new ModAnalyzerProxy(); + const launcherManager = VortexLauncherManager.getInstance(this.api); + const gameVersion = launcherManager.getGameVersionVortex(); + const query: IModAnalyzerRequestQuery = { + gameVersion: gameVersion, + modules: this.allModules.map((x) => ({ + moduleId: x.id, + moduleVersion: versionToString(x.version), + })), + }; + proxy + .analyze(query) + .then((result) => { + this.compatibilityScores = result.modules.reduce((map, curr) => { + map[curr.moduleId] = { + score: curr.compatibility, + recommendedScore: curr.recommendedCompatibility, + recommendedVersion: curr.recommendedModuleVersion, + }; + return map; + }, {}); + this.forceRefresh(); + }) + .catch(() => {}); + }; + + private forceRefresh = (): void => { + const profile: types.IProfile | undefined = selectors.activeProfile(this.api.getState()); + this.api.store?.dispatch(actionsLoadOrder.setFBForceUpdate(profile.id)); + }; + + public serializeLoadOrder = (newLO: VortexLoadOrderStorage, prevLO: VortexLoadOrderStorage): Promise => { + const loadOrderConverted = vortexToLibrary(newLO); + const launcherManager = VortexLauncherManager.getInstance(this.api); + launcherManager.saveLoadOrderVortex(loadOrderConverted); + return Promise.resolve(); + }; + + private setParameters = (loadOrder: vetypes.LoadOrder): void => { + if (!this.isInitialized) { + this.isInitialized = true; + // We automatically set the modules to launch on save, but not on first load + const launcherManager = VortexLauncherManager.getInstance(this.api); + launcherManager.setModulesToLaunch(loadOrder); + } + }; + + public deserializeLoadOrder = async (): Promise => { + const launcherManager = VortexLauncherManager.getInstance(this.api); + + // Make sure the LauncherManager has the latest module list + launcherManager.refreshModules(); + this.allModules = launcherManager.getAllModulesWithDuplicates(); + + // Get the saved Load Order + const allModules = launcherManager.getAllModules(); + const savedLoadOrder = launcherManager.loadLoadOrderVortex(); + const savedLoadOrderPersistence = libraryToPersistence(savedLoadOrder); + + const loadOrder = await orderCurrentLoadOrderByExternalLoadOrder(this.api, allModules, savedLoadOrderPersistence); + this.setParameters(vortexToLibrary(loadOrder)); + return loadOrder; + }; + + public validate = (prevLO: VortexLoadOrderStorage, newLO: VortexLoadOrderStorage): Promise => { + const modules = (newLO ?? []).flatMap((entry) => + entry.data && entry.enabled ? entry.data.moduleInfoExtended : [] + ); + //const validationManager = ValidationManager.fromVortex(newLO); + + const invalidResults: IInvalidResult[] = []; + for (const enabledModule of modules) { + const loadOrderIssues = BannerlordModuleManager.validateLoadOrder(modules, enabledModule); + for (const issue of loadOrderIssues) { + const localizedIssue = Utils.renderModuleIssue(issue); + invalidResults.push({ + id: issue.target.id, + reason: localizedIssue, + }); + } + } + + // While the contract doesn't explicitly allow undefined to be returned, + // it's expecting an undefined when there are no issues. + return Promise.resolve( + invalidResults.length === 0 + ? undefined! + : { + invalid: invalidResults, + } + ); + }; +} diff --git a/src/loadOrder/types.ts b/src/loadOrder/types.ts new file mode 100644 index 0000000..518172c --- /dev/null +++ b/src/loadOrder/types.ts @@ -0,0 +1,6 @@ +import { types } from 'vortex-api'; +import { IVortexViewModelData } from '../types'; + +export type IFBLOItemRendererProps = Omit & { + loEntry: types.IFBLOLoadOrderEntry; +}; diff --git a/src/loadOrder/utils.ts b/src/loadOrder/utils.ts new file mode 100644 index 0000000..d2a7151 --- /dev/null +++ b/src/loadOrder/utils.ts @@ -0,0 +1,140 @@ +import { selectors, types } from 'vortex-api'; +import { Utils, types as vetypes } from '@butr/vortexextensionnative'; +import { libraryToVortex, libraryVMToVortex, persistenceToLibrary } from './converters'; +import { SUB_MODS_IDS } from '../common'; +import { IModuleCache, PersistenceLoadOrderStorage, RequiredProperties, VortexLoadOrderStorage } from '../types'; +import { VortexLauncherManager } from '../launcher'; +import { LocalizationManager } from '../localization'; + +type ModIdResult = { + id: string; + source: string; +}; + +/** + * 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[] => { + const state = api.getState(); + const gameId: string | undefined = selectors.activeGameId(state); + const gameMods = state.persistent.mods[gameId] ?? {}; + const modIds = Object.values(gameMods).reduce((arr, mod) => { + if (!mod.attributes || mod.attributes[SUB_MODS_IDS] === undefined) { + return arr; + } + const subModsIds: Set = new Set(mod.attributes[SUB_MODS_IDS]); + if (subModsIds.has(moduleId)) { + arr.push({ + id: mod.attributes['modId'], + source: mod.attributes['source'], + }); + } + + return arr; + }, []); + + return modIds; +}; + +const getExcludedLoadOrder = ( + loadOrder: vetypes.LoadOrder, + result: vetypes.OrderByLoadOrderResult +): vetypes.LoadOrder => { + const excludedLoadOrder = Object.entries(loadOrder).reduce((arr, curr) => { + const [id, entry] = curr; + if (result.orderedModuleViewModels?.find((x) => x.moduleInfoExtended.id === entry.id)) { + arr[id] = entry; + } + return arr; + }, {}); + return excludedLoadOrder; +}; + +const checkOrderByLoadOrderResult = ( + api: types.IExtensionApi, + autoSort: boolean, + result: vetypes.OrderByLoadOrderResult +): void => { + const { localize: t } = LocalizationManager.getInstance(api); + + if (autoSort && result.issues) { + api.sendNotification?.({ + type: 'warning', + message: t(`{=pZVVdI5d}The Load Order was re-sorted with the default algorithm!{NL}Reasons:{NL}{REASONS}`, { + NL: '\n', + REASONS: result.issues.join(`\n`), + }), + }); + } +}; + +const checkResult = ( + api: types.IExtensionApi, + autoSort: boolean, + result: vetypes.OrderByLoadOrderResult +): result is RequiredProperties => { + const { localize: t } = LocalizationManager.getInstance(api); + + if (result === undefined || !result.orderedModuleViewModels || result.result === undefined || !result.result) { + if (autoSort) { + // The user is not expecting a sort operation, so don't give the notification + api.sendNotification?.({ + type: 'error', + message: t(`{=sLf3eIpH}Failed to order the module list!`), + }); + } + return false; + } + return true; +}; + +const checkSavedLoadOrder = (api: types.IExtensionApi, autoSort: boolean, loadOrder: VortexLoadOrderStorage): void => { + const { localize: t } = LocalizationManager.getInstance(api); + + const savedLoadOrderIssues = Utils.isLoadOrderCorrect( + loadOrder.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 + api.sendNotification?.({ + type: 'warning', + message: t(`{=pZVVdI5d}The Load Order was re-sorted with the default algorithm!{NL}Reasons:{NL}{REASONS}`, { + NL: '\n', + REASONS: savedLoadOrderIssues.join(`\n`), + }), + }); + } +}; + +export const orderCurrentLoadOrderByExternalLoadOrder = ( + api: types.IExtensionApi, + allModules: Readonly, + persistenceLoadOrder: PersistenceLoadOrderStorage +): Promise => { + const autoSort = true; // TODO: get from settings + const launcherManager = VortexLauncherManager.getInstance(api); + + const savedLoadOrder = persistenceToLibrary(persistenceLoadOrder); + const savedLoadOrderVortex = libraryToVortex(api, allModules, savedLoadOrder); + + checkSavedLoadOrder(api, autoSort, savedLoadOrderVortex); + + // Apply the Load Order to the list of modules + // Useful when there are new modules or old modules are missing + // The output wil wil contain the auto sorted list of modules + const result = launcherManager.orderByLoadOrder(savedLoadOrder); + if (!checkResult(api, autoSort, result)) { + return Promise.resolve(savedLoadOrderVortex); + } + + // Not even sure this will trigger + checkOrderByLoadOrderResult(api, autoSort, result); + + // Use the sorted to closest valid state Load Order + if (autoSort) { + return Promise.resolve(libraryVMToVortex(api, result.orderedModuleViewModels)); + } + + // Do not use the sorted LO, but take the list of modules. It excludes modules that are not usable + return Promise.resolve(libraryToVortex(api, allModules, getExcludedLoadOrder(savedLoadOrder, result))); +}; diff --git a/src/utils/loadOrder/persistence.ts b/src/loadOrder/vortex.ts similarity index 77% rename from src/utils/loadOrder/persistence.ts rename to src/loadOrder/vortex.ts index c35b543..9161511 100644 --- a/src/utils/loadOrder/persistence.ts +++ b/src/loadOrder/vortex.ts @@ -1,8 +1,8 @@ -import path from 'path'; import { fs, selectors, types } from 'vortex-api'; -import { GAME_ID, LOAD_ORDER_SUFFIX } from '../../common'; -import { PersistenceLoadOrderStorage } from '../../types'; -import { filterEntryWithInvalidId } from '../util'; +import path from 'path'; +import { GAME_ID, LOAD_ORDER_SUFFIX } from '../common'; +import { PersistenceLoadOrderStorage } from '../types'; +import { filterEntryWithInvalidId } from '../utils'; const getLoadOrderFileName = (profileId: string): string => { return `${profileId}${LOAD_ORDER_SUFFIX}`; @@ -19,13 +19,13 @@ const getLoadOrderFilePath = (api: types.IExtensionApi, loadOrderFileName: strin */ export const readLoadOrder = (api: types.IExtensionApi): PersistenceLoadOrderStorage => { try { - const profile = selectors.activeProfile(api.getState()); + const profile: types.IProfile | undefined = selectors.activeProfile(api.getState()); const loFileName = getLoadOrderFileName(profile.id); const loFilePath = getLoadOrderFilePath(api, loFileName); const fileContents = fs.readFileSync(loFilePath, 'utf8'); const loadOrder: PersistenceLoadOrderStorage = JSON.parse(fileContents); - return loadOrder.filter((x) => !!x && filterEntryWithInvalidId(x)); + return loadOrder.filter((x) => x !== undefined && filterEntryWithInvalidId(x)); } catch { return []; } @@ -38,7 +38,7 @@ export const readLoadOrder = (api: types.IExtensionApi): PersistenceLoadOrderSto */ export const writeLoadOrder = (api: types.IExtensionApi, loadOrder: PersistenceLoadOrderStorage): void => { try { - const profile = selectors.activeProfile(api.getState()); + const profile: types.IProfile | undefined = selectors.activeProfile(api.getState()); const loFileName = getLoadOrderFileName(profile.id); const loFilePath = getLoadOrderFilePath(api, loFileName); //await fs.ensureDirWritableS(path.dirname(loFilePath)); diff --git a/src/localization/hooks.ts b/src/localization/hooks.ts new file mode 100644 index 0000000..aacb171 --- /dev/null +++ b/src/localization/hooks.ts @@ -0,0 +1,9 @@ +import { useContext } from 'react'; +import { MainContext } from 'vortex-api'; +import { LocalizationManager } from './manager'; + +export const useLocalization = (): LocalizationManager => { + const context = useContext(MainContext); + + return LocalizationManager.getInstance(context.api); +}; diff --git a/src/localization/index.ts b/src/localization/index.ts new file mode 100644 index 0000000..63a1e68 --- /dev/null +++ b/src/localization/index.ts @@ -0,0 +1,4 @@ +export * from './hooks'; +export * from './manager'; +export * from './types'; +export * from './utils'; diff --git a/src/localization/manager.ts b/src/localization/manager.ts new file mode 100644 index 0000000..2a334de --- /dev/null +++ b/src/localization/manager.ts @@ -0,0 +1,52 @@ +import { fs, types, util } from 'vortex-api'; +import { Utils } from '@butr/vortexextensionnative'; +import { Dirent } from 'fs'; +import { TranslateValues } from './types'; +import { i18nToBannerlord } from './utils'; +import { I18N_NAMESPACE } from '../common'; + +export class LocalizationManager { + private static instance: LocalizationManager | undefined; + + public static getInstance(api: types.IExtensionApi): LocalizationManager { + if (!LocalizationManager.instance) { + LocalizationManager.instance = new LocalizationManager(api); + } + + return LocalizationManager.instance; + } + + private readonly api: types.IExtensionApi; + private initializedLocalization = false; + + constructor(api: types.IExtensionApi) { + this.api = api; + } + + public localize = (template: string, values: TranslateValues = {}): string => { + if (template.startsWith('{=')) { + if (!this.initializedLocalization) { + this.initializeLocalization(); + this.initializedLocalization = true; + } + + return Utils.localizeString(template, values); + } + return this.api.translate(template, { + ns: I18N_NAMESPACE, + ...values, + }); + }; + + private initializeLocalization = (): void => { + fs.readdirSync(__dirname, { withFileTypes: true }).forEach((d: Dirent) => { + if (d.isFile() && d.name.startsWith('localization_') && d.name.endsWith('.xml')) { + const content: string = fs.readFileSync(`${__dirname}/${d.name}`, { + encoding: 'utf8', + }); + Utils.loadLocalization(content); + } + }); + Utils.setLanguage(i18nToBannerlord(util.getCurrentLanguage())); + }; +} diff --git a/src/localization/types.ts b/src/localization/types.ts new file mode 100644 index 0000000..e8a4378 --- /dev/null +++ b/src/localization/types.ts @@ -0,0 +1,3 @@ +export type TranslateValues = { + [value: string]: string; +}; diff --git a/src/localization/utils.ts b/src/localization/utils.ts new file mode 100644 index 0000000..f0d5ae8 --- /dev/null +++ b/src/localization/utils.ts @@ -0,0 +1,41 @@ +export const i18nToBannerlord = (languageCode: string): string => { + switch (languageCode) { + case 'pt-BR': + return 'Português (BR)'; + case 'by': // Not present + return 'Беларуская'; + //case 'byl': // Not present, no idea how to represent this one + // return 'Biełaruskaja'; + case 'zh': + return '简体中文'; + //case 'zh-Hant': // Since there's no distiction, better to use the simplified one? + // return '繁體中文'; + // break; + case 'de': + return 'Deutsch'; + case 'en': + return 'English'; + case 'fr': + return 'Français'; + case 'it': + return 'Italiano'; + case 'ja': + return '日本語'; + case 'ko': + return '한국어'; + case 'pl': + return 'Polski'; + case 'ro': // Not present + return 'Română'; + case 'ru': + return 'Русский'; + case 'es': + return 'Español (LA)'; + case 'tr': + return 'Türkçe'; + case 'uk': + return 'Українська'; + default: + return 'English'; + } +}; diff --git a/src/modoptions/index.ts b/src/modoptions/index.ts new file mode 100644 index 0000000..6d5a6ef --- /dev/null +++ b/src/modoptions/index.ts @@ -0,0 +1,2 @@ +export * from './types'; +export * from './utils'; diff --git a/src/modoptions/types.ts b/src/modoptions/types.ts new file mode 100644 index 0000000..4624428 --- /dev/null +++ b/src/modoptions/types.ts @@ -0,0 +1,18 @@ +export enum ModOptionsEntryType { + Global = 'global', + Special = 'special', +} + +export type ModOptionsEntry = { + name: string; + path: string; + type: ModOptionsEntryType; +}; + +export type ModOptionsStorage = { + [key: string]: ModOptionsEntry; +}; + +export type PersistentModOptionsEntry = ModOptionsEntry & { + contentBase64: string; +}; diff --git a/src/modoptions/utils.ts b/src/modoptions/utils.ts new file mode 100644 index 0000000..5264919 --- /dev/null +++ b/src/modoptions/utils.ts @@ -0,0 +1,169 @@ +import { fs, types, util } from 'vortex-api'; +import turbowalk from 'turbowalk'; +import path from 'path'; +import { ModOptionsEntry, ModOptionsEntryType, ModOptionsStorage, PersistentModOptionsEntry } from './types'; + +export const getSettingsPath = (): string => { + return path.join(util.getVortexPath('documents'), 'Mount and Blade II Bannerlord', 'Configs', 'ModSettings'); +}; + +export const readSettingsContentAsync = async (entry: ModOptionsEntry): Promise => { + switch (entry.type) { + case 'global': + return await fs.readFileAsync(path.join(getSettingsPath(), 'Global', entry.path), 'base64'); + case 'special': + return await fs.readFileAsync(path.join(getSettingsPath(), entry.path), 'base64'); + default: + return ''; + } +}; + +// TODO: Replace with async? +export const readSettingsContent = (entry: ModOptionsEntry): string => { + switch (entry.type) { + case 'global': + return fs.readFileSync(path.join(getSettingsPath(), 'Global', entry.path), 'base64'); + case 'special': + return fs.readFileSync(path.join(getSettingsPath(), entry.path), 'base64'); + default: + return ''; + } +}; + +export const overrideModOptions = async (mod: types.IMod, modOptions: PersistentModOptionsEntry[]): Promise => { + const id = `bak.vortex.${mod.archiveId}}`; + + for (const modOption of modOptions) { + switch (modOption.type) { + case 'global': + { + const filePath = path.join(getSettingsPath(), 'Global', modOption.path); + await fs.ensureDirAsync(path.dirname(filePath)); + try { + await fs.renameAsync(filePath, `${filePath}.${id}`); + } catch (err) { + /* empty */ + } + await fs.writeFileAsync(filePath, modOption.contentBase64, 'base64'); + } + break; + case 'special': + { + const filePath = path.join(getSettingsPath(), modOption.path); + try { + await fs.renameAsync(filePath, `${filePath}.${id}`); + } catch (err) { + /* empty */ + } + await fs.writeFileAsync(filePath, modOption.contentBase64, 'base64'); + } + break; + } + } +}; + +export const hasBackupModOptions = async (mod: types.IMod): Promise => { + const id = `bak.vortex.${mod.archiveId}}`; + + let hasBackup = false; + await turbowalk( + getSettingsPath(), + (entries) => { + const backupFiles = entries.filter((entry) => !entry.isDirectory && entry.filePath.endsWith(`.${id}`)); + hasBackup = backupFiles.length > 0; + }, + { recurse: true } + ); + return hasBackup; +}; + +export const restoreOriginalModOptions = async (mod: types.IMod): Promise => { + const id = `bak.vortex.${mod.archiveId}}`; + + const filesToRemove: { fullPath: string; originalPath: string }[] = []; + await turbowalk( + getSettingsPath(), + (entries) => { + const backupFiles = entries.filter((entry) => !entry.isDirectory && entry.filePath.endsWith(`.${id}`)); + for (const file of backupFiles) { + const fullPath = file.filePath; + const originalPath = fullPath.slice(0, fullPath.length - id.length - 1); + filesToRemove.push({ fullPath, originalPath }); + } + }, + { recurse: true } + ); + for (const file of filesToRemove) { + const { fullPath, originalPath } = file; + try { + await fs.removeAsync(originalPath); + } catch { + /* empty */ + } + try { + await fs.renameAsync(fullPath, originalPath); + } catch { + /* empty */ + } + } +}; + +export const removeOriginalModOptions = async (mod: types.IMod): Promise => { + const id = `bak.vortex.${mod.archiveId}}`; + + const filesToRemove: string[] = []; + await turbowalk( + getSettingsPath(), + (entries) => { + const backupFiles = entries.filter((entry) => !entry.isDirectory && entry.filePath.endsWith(`.${id}`)); + for (const file of backupFiles) { + const fullPath = file.filePath; + filesToRemove.push(fullPath); + } + }, + { recurse: true } + ); + for (const file of filesToRemove) { + try { + await fs.removeAsync(file); + } catch { + /* empty */ + } + } +}; + +export const getSpecialSettings = (): ModOptionsStorage => { + const specialSettingsDictionary: ModOptionsStorage = { + ['ButterLib']: { + name: 'ButterLib/Options.json', + path: 'ButterLib/Options.json', + type: ModOptionsEntryType.Special, + }, + }; + return specialSettingsDictionary; +}; + +export const getGlobalSettings = async (): Promise => { + const globalSettingsDictionary: ModOptionsStorage = {}; + const gsPath = path.join(getSettingsPath(), 'Global'); + await turbowalk( + gsPath, + (entries) => { + const settingFiles = entries.filter((entry) => !entry.isDirectory); + for (const file of settingFiles) { + const fullPath = file.filePath; + const relativePath = path.relative(gsPath, fullPath); + const name = path.basename(relativePath); + const extension = path.extname(relativePath); + const modName = name.slice(0, name.length - extension.length); + globalSettingsDictionary[modName] = { + name: relativePath, + path: relativePath, + type: ModOptionsEntryType.Global, + }; + } + }, + { recurse: true } + ); + return globalSettingsDictionary; +}; diff --git a/src/utils/nameof.ts b/src/nameof.ts similarity index 100% rename from src/utils/nameof.ts rename to src/nameof.ts diff --git a/src/react/index.ts b/src/react/index.ts new file mode 100644 index 0000000..fce8f95 --- /dev/null +++ b/src/react/index.ts @@ -0,0 +1,2 @@ +export * from './reducers'; +export * from './redux'; diff --git a/src/utils/reducers.ts b/src/react/reducers.ts similarity index 67% rename from src/utils/reducers.ts rename to src/react/reducers.ts index 9468a41..07cdc36 100644 --- a/src/utils/reducers.ts +++ b/src/react/reducers.ts @@ -1,15 +1,16 @@ import { actions, types, util } from 'vortex-api'; +import { Utils } from '@butr/vortexextensionnative'; +import { createReducer, ReducerHandler, ReducerHandlerState } from './redux'; +import { nameof as nameof2 } from '../nameof'; +import { actionsSave, SetCurrentSavePayload } from '../save'; import { actionsSettings, - actionsSave, - ReducerHandlerState, - createReducer, - nameof as nameof2, - ReducerHandler, - i18nToBannerlord, -} from '.'; -import { IBannerlordSettings } from '../types'; -import { Utils } from '@butr/vortexextensionnative'; + IBannerlordSettings, + SetBetaSortingPayload, + SetFixCommonIssuesPayload, + SetSortOnDeployPayload, +} from '../settings'; +import { i18nToBannerlord } from '../localization'; // TODO: Ask IDCs to provider a proper type system? type SetLoadOrderPayload = { @@ -21,31 +22,32 @@ type SetLanguagePayload = string; const nameof = nameof2; -const setSortOnDeploy = (state: ReducerHandlerState, payload: actionsSettings.SetSortOnDeployPayload) => { +const setSortOnDeploy = (state: ReducerHandlerState, payload: SetSortOnDeployPayload): ReducerHandlerState => { return util.setSafe(state, [nameof('sortOnDeploy'), payload.profileId], payload.sort); }; -const setFixCommonIssues = (state: ReducerHandlerState, payload: actionsSettings.SetFixCommonIssuesPayload) => { +const setFixCommonIssues = (state: ReducerHandlerState, payload: SetFixCommonIssuesPayload): ReducerHandlerState => { return util.setSafe(state, [nameof('fixCommonIssues'), payload.profileId], payload.fixCommonIssues); }; -const setBetaSorting = (state: ReducerHandlerState, payload: actionsSettings.SetBetaSortingPayload) => { +const setBetaSorting = (state: ReducerHandlerState, payload: SetBetaSortingPayload): ReducerHandlerState => { return util.setSafe(state, [nameof('betaSorting'), payload.profileId], payload.betaSorting); }; -const setCurrentSave = (state: ReducerHandlerState, payload: actionsSave.SetCurrentSavePayload) => { +const setCurrentSave = (state: ReducerHandlerState, payload: SetCurrentSavePayload): ReducerHandlerState => { return util.setSafe(state, [nameof('saveName'), payload.profileId], payload.saveId); }; -const setLoadOrder = (state: ReducerHandlerState, payload: SetLoadOrderPayload) => { +const setLoadOrder = (state: ReducerHandlerState, payload: SetLoadOrderPayload): ReducerHandlerState => { return util.setSafe(state, [payload.id], payload.order); }; -const setLanguage = (state: ReducerHandlerState, payload: SetLanguagePayload) => { +const setLanguage = (state: ReducerHandlerState, payload: SetLanguagePayload): ReducerHandlerState => { Utils.setLanguage(i18nToBannerlord(payload)); return state; }; +// 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 } = {}; @@ -60,6 +62,7 @@ const getReducers = () => { return reducers; }; +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type const getDefaults = () => ({ [nameof('sortOnDeploy')]: {}, [nameof('fixCommonIssues')]: {}, diff --git a/src/utils/redux.ts b/src/react/redux.ts similarity index 85% rename from src/utils/redux.ts rename to src/react/redux.ts index 88f3b4a..3750495 100644 --- a/src/utils/redux.ts +++ b/src/react/redux.ts @@ -10,6 +10,7 @@ export const createReducer = ( actionCreator: BaseActionCreator>, action: ReducerHandler

, reducers: { [key: string]: ReducerHandler

} + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type ) => { reducers[actionCreator.getType()] = action; }; diff --git a/src/utils/save/actions.ts b/src/save/actions.ts similarity index 59% rename from src/utils/save/actions.ts rename to src/save/actions.ts index b0c4f75..806d46b 100644 --- a/src/utils/save/actions.ts +++ b/src/save/actions.ts @@ -1,15 +1,18 @@ import { createAction } from 'redux-act'; -import { EXTENSION_BASE_ID } from '../../common'; +import { EXTENSION_BASE_ID } from '../common'; export type SetCurrentSavePayload = { profileId: string; saveId: string | null; }; -export const setCurrentSave = createAction( +const setCurrentSave = createAction( `${EXTENSION_BASE_ID}_SET_CURRENT_SAVE`, (profileId: string, saveId: string | null) => ({ profileId, saveId, }) ); +export const actionsSave = { + setCurrentSave, +}; diff --git a/src/save/events.ts b/src/save/events.ts new file mode 100644 index 0000000..5d47cc5 --- /dev/null +++ b/src/save/events.ts @@ -0,0 +1,15 @@ +import { types } from 'vortex-api'; +import { reloadSave } from './utils'; + +/** + * Event function, be careful + */ +export const gamemodeActivatedSave = (api: types.IExtensionApi): Promise => { + try { + reloadSave(api); + } catch (err) { + api.showErrorNotification?.('Failed to reload the currect save file', err); + } + + return Promise.resolve(); +}; diff --git a/src/save/index.ts b/src/save/index.ts new file mode 100644 index 0000000..c486b7c --- /dev/null +++ b/src/save/index.ts @@ -0,0 +1,3 @@ +export * from './actions'; +export * from './events'; +export * from './utils'; diff --git a/src/save/utils.ts b/src/save/utils.ts new file mode 100644 index 0000000..c8aca4d --- /dev/null +++ b/src/save/utils.ts @@ -0,0 +1,19 @@ +import { selectors, types } from 'vortex-api'; +import { actionsSave } from './actions'; +import { VortexLauncherManager } from '../launcher'; +import { getSaveFromSettings } from '../settings'; + +export const reloadSave = (api: types.IExtensionApi): void => { + const state = api.getState(); + const profile: types.IProfile | undefined = selectors.activeProfile(state); + let save = getSaveFromSettings(state, profile.id); + + if (save === 'No Save') { + save = null; + } + + api.store?.dispatch(actionsSave.setCurrentSave(profile.id, save)); + + const launcherManager = VortexLauncherManager.getInstance(api); + launcherManager.setSaveFile(save ?? ''); +}; diff --git a/src/utils/settings/actions.ts b/src/settings/actions.ts similarity index 67% rename from src/utils/settings/actions.ts rename to src/settings/actions.ts index 4ef31ef..0a0e332 100644 --- a/src/utils/settings/actions.ts +++ b/src/settings/actions.ts @@ -1,5 +1,5 @@ import { createAction } from 'redux-act'; -import { EXTENSION_BASE_ID } from '../../common'; +import { EXTENSION_BASE_ID } from '../common'; export type SetSortOnDeployPayload = { profileId: string; @@ -21,7 +21,7 @@ export type SetBetaSortingPayload = { betaSorting: boolean; }; -export const setSortOnDeploy = createAction( +const setSortOnDeploy = createAction( `${EXTENSION_BASE_ID}_SET_SORT_ON_DEPLOY`, (profileId: string, sort: boolean) => ({ profileId, @@ -29,7 +29,7 @@ export const setSortOnDeploy = createAction( +const setFixCommonIssues = createAction( `${EXTENSION_BASE_ID}_SET_FIX_COMMON_ISSUES`, (profileId: string, fixCommonIssues: boolean) => ({ profileId, @@ -37,10 +37,16 @@ export const setFixCommonIssues = createAction( +const setBetaSorting = createAction( `${EXTENSION_BASE_ID}_SET_BETA_SORTING`, (profileId: string, betaSorting: boolean) => ({ profileId, betaSorting, }) ); + +export const actionsSettings = { + setSortOnDeploy, + setFixCommonIssues, + setBetaSorting, +}; diff --git a/src/settings/index.ts b/src/settings/index.ts new file mode 100644 index 0000000..dad182a --- /dev/null +++ b/src/settings/index.ts @@ -0,0 +1,3 @@ +export * from './actions'; +export * from './utils'; +export * from './types'; diff --git a/src/settings/types.ts b/src/settings/types.ts new file mode 100644 index 0000000..2e32132 --- /dev/null +++ b/src/settings/types.ts @@ -0,0 +1,21 @@ +import { types } from 'vortex-api'; +import { GAME_ID } from '../common'; + +export interface IBannerlordSettings { + saveName: { + [profileId: string]: string | null; + }; + sortOnDeploy: { + [profileId: string]: boolean; + }; + fixCommonIssues: { + [profileId: string]: boolean; + }; + betaSorting: { + [profileId: string]: boolean; + }; +} + +export interface ISettingsWithBannerlord extends types.ISettings { + [GAME_ID]?: IBannerlordSettings; +} diff --git a/src/utils/settings/utils.ts b/src/settings/utils.ts similarity index 69% rename from src/utils/settings/utils.ts rename to src/settings/utils.ts index dc22f9d..841f96f 100644 --- a/src/utils/settings/utils.ts +++ b/src/settings/utils.ts @@ -1,6 +1,11 @@ -import { hasSettings, hasSettingsBannerlord } from '..'; +import { types } from 'vortex-api'; +import { ISettingsWithBannerlord } from './types'; +import { hasSettings } from '../vortex'; +import { GAME_ID } from '../common'; -export const getSortOnDeployFromSettings = (state: object, profileId: string) => { +const hasSettingsBannerlord = (settings: types.ISettings): settings is ISettingsWithBannerlord => GAME_ID in settings; + +export const getSortOnDeployFromSettings = (state: object, profileId: string): boolean | null => { if (!hasSettings(state)) { return null; } @@ -9,10 +14,10 @@ export const getSortOnDeployFromSettings = (state: object, profileId: string) => return null; } - return state.settings.mountandblade2bannerlord?.sortOnDeploy?.[profileId]; + return state.settings.mountandblade2bannerlord?.sortOnDeploy?.[profileId] ?? null; }; -export const getFixCommonIssuesFromSettings = (state: object, profileId: string) => { +export const getFixCommonIssuesFromSettings = (state: object, profileId: string): boolean | null => { if (!hasSettings(state)) { return null; } @@ -21,10 +26,10 @@ export const getFixCommonIssuesFromSettings = (state: object, profileId: string) return null; } - return state.settings.mountandblade2bannerlord?.fixCommonIssues?.[profileId]; + return state.settings.mountandblade2bannerlord?.fixCommonIssues?.[profileId] ?? null; }; -export const getBetaSortingFromSettings = (state: object, profileId: string) => { +export const getBetaSortingFromSettings = (state: object, profileId: string): boolean | null => { if (!hasSettings(state)) { return null; } @@ -33,10 +38,10 @@ export const getBetaSortingFromSettings = (state: object, profileId: string) => return null; } - return state.settings.mountandblade2bannerlord?.betaSorting?.[profileId]; + return state.settings.mountandblade2bannerlord?.betaSorting?.[profileId] ?? null; }; -export const getSaveFromSettings = (state: object, profileId: string) => { +export const getSaveFromSettings = (state: object, profileId: string): string | null => { if (!hasSettings(state)) { return null; } diff --git a/src/types.ts b/src/types.ts index c9f40b2..a98b6f8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,23 +1,24 @@ import { types } from 'vortex-api'; import { types as vetypes } from '@butr/vortexextensionnative'; -import { GAME_ID } from './common'; -import { LoadOrderManager, LocalizationManager, SaveManager, VortexLauncherManager } from './utils'; export type RequiredProperties = Omit & Required>; -export type GetLauncherManager = () => VortexLauncherManager; -export type GetLoadOrderManager = () => LoadOrderManager; -export type GetSaveManager = () => SaveManager; -export type GetLocalizationManager = () => LocalizationManager; +export type IStatePersistent = types.IState['persistent']; + +export type IModAttributes = types.IMod['attributes']; -export interface IModuleCompatibilityInfoCache { - [moduleId: string]: IModuleCompatibilityInfo; +export interface IBannerlordModAttributes { + modId: number; + version: string; + source: string; } -export interface IModuleCompatibilityInfo { - score: number; - recommendedScore?: number | undefined; - recommendedVersion?: string | undefined; +export interface IBannerlordMod extends types.IMod { + attributes?: IBannerlordModAttributes; +} + +export interface IBannerlordModStorage { + [modId: string]: IBannerlordMod; } export type PersistenceLoadOrderStorage = IPersistenceLoadOrderEntry[]; @@ -40,86 +41,6 @@ export interface IModuleCache { [moduleId: string]: vetypes.ModuleInfoExtendedWithMetadata; } -/** - * Vortex - */ -export interface IItemRendererProps { - className: string; - item: T; - onRef: (ref: unknown) => unknown; -} - -/** - * Vortex - */ -export interface IBannerlordModStorage { - [modId: string]: IBannerlordMod; -} - -/** - * Vortex - */ -export interface IBannerlordMod extends types.IMod { - attributes?: IBannerlordModAttributes; -} - -/** - * Vortex - */ -export interface IBannerlordModAttributes { - modId: number; - version: string; - source: string; -} - -/** - * Vortex - */ -export interface ISettingsInterfaceWithPrimaryTool extends types.ISettingsInterface { - primaryTool: { - [GAME_ID]?: string; - }; -} - -/** - * Vortex - */ -export interface ISettingsWithBannerlord extends types.ISettings { - [GAME_ID]?: IBannerlordSettings; -} - -/** - * Vortex - */ -export interface IBannerlordSettings { - saveName: { - [profileId: string]: string | null; - }; - sortOnDeploy: { - [profileId: string]: boolean; - }; - fixCommonIssues: { - [profileId: string]: boolean; - }; - betaSorting: { - [profileId: string]: boolean; - }; -} - -/** - * Vortex - */ -export type IStatePersistent = types.IState['persistent']; - -/** - * Vortex - */ -export interface IStatePersistentWithLoadOrder extends IStatePersistent { - loadOrder: { - [profileId: string]: VortexLoadOrderStorage; - }; -} - /** * Vortex */ diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..28f86f4 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,12 @@ +import { fs } from 'vortex-api'; + +export const getPathExistsAsync = async (path: string): Promise => { + return await fs + .statAsync(path) + .then(() => true) + .catch(() => false); +}; + +export const filterEntryWithInvalidId = (entry: { id: string }): boolean => { + return entry.id !== undefined && entry.id !== ''; +}; diff --git a/src/utils/blse/events.ts b/src/utils/blse/events.ts deleted file mode 100644 index 4c32049..0000000 --- a/src/utils/blse/events.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { actions, selectors, types } from 'vortex-api'; -import { findBLSEMod } from './shared'; -import { GAME_ID } from '../../common'; -import { hasSettingsInterfacePrimaryTool } from '../vortex'; -import { GetLoadOrderManager, GetLocalizationManager } from '../../types'; - -/** - * Event function, be careful - */ -export const didDeployEvent = async ( - api: types.IExtensionApi, - profileId: string, - getLocalizationManager: GetLocalizationManager, - getLoadOrderManager: GetLoadOrderManager -) => { - const localizationManager = getLocalizationManager(); - const t = localizationManager.localize; - - const state = api.getState(); - const profile = selectors.profileById(state, profileId); - if (profile.gameId !== GAME_ID) { - return Promise.resolve(); - } - - try { - await getLoadOrderManager().deserializeLoadOrder(); - } catch (err) { - api.showErrorNotification?.(t('Failed to deserialize load order file'), err); - } - - return didDeployBLSE(api, state, profile); -}; - -const didDeployBLSE = async (api: types.IExtensionApi, state: types.IState, profile: types.IProfile) => { - if (!hasSettingsInterfacePrimaryTool(state.settings.interface)) { - return Promise.resolve(); - } - - const primaryTool = state.settings.interface.primaryTool.mountandblade2bannerlord; - - const blseMod = findBLSEMod(api); - if (!!blseMod && !primaryTool) { - api.store?.dispatch(actions.setPrimaryTool(profile.gameId, 'blse-cli')); - } - if (!blseMod && primaryTool === 'blse-cli') { - api.store?.dispatch(actions.setPrimaryTool(profile.gameId, undefined!)); - } - - return Promise.resolve(); -}; - -/** - * Event function, be careful - */ -export const didPurgeEvent = async (api: types.IExtensionApi, profileId: string) => { - const state = api.getState(); - const profile = selectors.profileById(state, profileId); - if (profile.gameId !== GAME_ID) { - return Promise.resolve(); - } - - await didPurgeBLSE(api, state, profile); -}; - -const didPurgeBLSE = async (api: types.IExtensionApi, state: types.IState, profile: types.IProfile) => { - if (!hasSettingsInterfacePrimaryTool(state.settings.interface)) { - return Promise.resolve(); - } - - const primaryTool = state.settings.interface.primaryTool.mountandblade2bannerlord; - if (primaryTool !== 'blse-cli') { - return Promise.resolve(); - } - - const blseMod = findBLSEMod(api); - if (blseMod) { - api.store?.dispatch(actions.setPrimaryTool(profile.gameId, undefined!)); - } - - return Promise.resolve(); -}; diff --git a/src/utils/blse/vortex.ts b/src/utils/blse/vortex.ts deleted file mode 100644 index d5b1e5b..0000000 --- a/src/utils/blse/vortex.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { actions, selectors, types } from 'vortex-api'; -import { findBLSEMod, deployBLSE, downloadBLSE, isModActive, findBLSEDownload } from './shared'; -import { GetLocalizationManager } from '../../types'; -import { LocalizationManager } from '../localization'; - -const sendNotification = ( - api: types.IExtensionApi, - localizationManager: LocalizationManager, - title: string, - actionTitle: string, - action: (dismiss: types.NotificationDismiss) => void -) => { - const t = localizationManager.localize; - - api.sendNotification?.({ - id: 'blse-missing', - type: 'warning', - title: title, - message: t('BLSE is recommended to mod Bannerlord.'), - actions: [ - { - title: actionTitle, - action: action, - }, - ], - }); -}; - -export const recommendBLSE = (api: types.IExtensionApi, getLocalizationManager: GetLocalizationManager) => { - const localizationManager = getLocalizationManager(); - const t = localizationManager.localize; - - const profile = selectors.activeProfile(api.getState()); - - const blseMod = findBLSEMod(api); - if (blseMod) { - // Found but not enabled - const blseIsActive = isModActive(profile, blseMod); - if (!blseIsActive) { - const action = (dismiss: types.NotificationDismiss) => { - api.store?.dispatch(actions.setModEnabled(profile.id, blseMod.id, true)); - deployBLSE(api).then(() => dismiss()); - }; - sendNotification(api, localizationManager, t('BLSE is not enabled'), t('Enable'), action); - return; - } - } - - const blseDownload = findBLSEDownload(api); - if (blseDownload) { - // Downloaded but not installed - const action = (dismiss: types.NotificationDismiss) => { - api.events.emit('start-install-download', blseDownload, { - allowAutoEnable: true, - }); - deployBLSE(api).then(() => dismiss()); - }; - sendNotification(api, localizationManager, t('BLSE is not installed'), t('Install'), action); - return; - } - - // Non existent - const action = (dismiss: types.NotificationDismiss) => { - downloadBLSE(api, getLocalizationManager).then(() => dismiss()); - }; - sendNotification(api, localizationManager, t('BLSE is not installed via Vortex'), t('Get BLSE'), action); -}; diff --git a/src/utils/butr/index.ts b/src/utils/butr/index.ts deleted file mode 100644 index 719321b..0000000 --- a/src/utils/butr/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './modAnalyzerProxy'; diff --git a/src/utils/butr/modAnalyzerProxy.ts b/src/utils/butr/modAnalyzerProxy.ts deleted file mode 100644 index 8be04b1..0000000 --- a/src/utils/butr/modAnalyzerProxy.ts +++ /dev/null @@ -1,70 +0,0 @@ -import * as https from 'https'; -import { log, types } from 'vortex-api'; - -export interface IModAnalyzerRequestModule { - moduleId: string; - moduleVersion?: string; -} - -export interface IModAnalyzerRequestQuery { - gameVersion: string; - modules: IModAnalyzerRequestModule[]; -} - -export interface IModAnalyzerResultModule { - moduleId: string; - compatibility: number; - recommendedCompatibility: number | undefined; - recommendedModuleVersion?: string | undefined; -} - -export interface IModAnalyzerResult { - modules: IModAnalyzerResultModule[]; -} - -const BUTR_HOST = 'sitenexusmods.butr.link'; - -export class ModAnalyzerProxy { - private mAPI: types.IExtensionApi; - private mOptions: https.RequestOptions; - constructor(api: types.IExtensionApi) { - this.mAPI = api; - this.mOptions = { - host: BUTR_HOST, - method: 'POST', - protocol: 'https:', - path: '/api/v1/ModsAnalyzer/GetCompatibilityScore', - headers: { - Tenant: '1', // Bannerlord - 'Content-Type': 'application/json', - }, - }; - } - - public async analyze(query: IModAnalyzerRequestQuery): Promise { - return new Promise((resolve, reject) => { - const req = https - .request(this.mOptions, (res) => { - let body = Buffer.from([]); - res - .on('error', (err) => reject(err)) - .on('data', (chunk) => { - body = Buffer.concat([body, chunk]); - }) - .on('end', () => { - const textual = body.toString('utf8'); - try { - const parsed = JSON.parse(textual); - resolve(parsed); - } catch (err) { - log('error', 'failed to parse butr mod analyzer response', textual); - reject(err); - } - }); - }) - .on('error', (err) => reject(err)); - req.write(JSON.stringify(query)); - req.end(); - }); - } -} diff --git a/src/utils/index.ts b/src/utils/index.ts deleted file mode 100644 index bc56bba..0000000 --- a/src/utils/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -export * from './blse'; -export * from './butr'; -export * from './loadOrder'; -export * from './localization'; -export * from './module'; -export * from './redux'; -export * from './save'; -export * from './settings'; -export * from './events'; -export * from './game'; -export * from './moduleUtil'; -export * from './nameof'; -export * from './reducers'; -export * from './redux'; -export * from './store'; -export * from './tools'; -export * from './util'; -export * from './validationManager'; -export * from './version'; -export * from './vortex'; -export * from './vortexLauncherManager'; diff --git a/src/utils/loadOrder/actions.ts b/src/utils/loadOrder/actions.ts deleted file mode 100644 index 39f1932..0000000 --- a/src/utils/loadOrder/actions.ts +++ /dev/null @@ -1,26 +0,0 @@ -// This is a risk, since we won't notice if the API changes -// TODO: Ask IDCs to provider a proper type system -import { types } from 'vortex-api'; - -export const setFBForceUpdate = (profileId: string) => ({ - type: 'SET_FB_FORCE_UPDATE', - payload: { - profileId, - }, -}); - -export const setFBLoadOrderEntry = (profileId: string, loEntry: types.ILoadOrderEntry) => ({ - type: 'SET_FB_LOAD_ORDER_ENTRY', - payload: { - profileId, - loEntry, - }, -}); - -export const setFBLoadOrder = (profileId: string, loadOrder: types.LoadOrder) => ({ - type: 'SET_FB_LOAD_ORDER', - payload: { - profileId, - loadOrder, - }, -}); diff --git a/src/utils/loadOrder/index.ts b/src/utils/loadOrder/index.ts deleted file mode 100644 index 6a2a6c2..0000000 --- a/src/utils/loadOrder/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * as actionsLoadOrder from './actions'; -export * from './converter'; -export * from './manager'; -export * from './persistence'; diff --git a/src/utils/loadOrder/manager.tsx b/src/utils/loadOrder/manager.tsx deleted file mode 100644 index e6b0842..0000000 --- a/src/utils/loadOrder/manager.tsx +++ /dev/null @@ -1,267 +0,0 @@ -import React from 'react'; -import { types, selectors } from 'vortex-api'; -import { IInvalidResult } from 'vortex-api/lib/extensions/file_based_loadorder/types/types'; -import { BannerlordModuleManager, Utils, types as vetypes } from '@butr/vortexextensionnative'; -import { vortexToLibrary, libraryToVortex, libraryVMToVortex, libraryVMToLibrary } from '.'; -import { - IModAnalyzerRequestModule, - IModAnalyzerRequestQuery, - ModAnalyzerProxy, - versionToString, - actionsLoadOrder, -} from '..'; -import { GAME_ID } from '../../common'; -import { LoadOrderInfoPanel, BannerlordItemRenderer } from '../../views'; -import { - GetLauncherManager, - GetLocalizationManager, - IModuleCompatibilityInfoCache, - RequiredProperties, - VortexLoadOrderStorage, -} from '../../types'; - -export class LoadOrderManager implements types.ILoadOrderGameInfo { - private static instance: LoadOrderManager; - - public static getInstance( - api?: types.IExtensionApi, - getLauncherManager?: GetLauncherManager, - getLocalizationManager?: GetLocalizationManager - ): LoadOrderManager { - if (!LoadOrderManager.instance) { - if (api === undefined || getLauncherManager === undefined || getLocalizationManager === undefined) { - throw new Error('IniStructure is not context aware'); - } - LoadOrderManager.instance = new LoadOrderManager(api, getLauncherManager, getLocalizationManager); - } - - return LoadOrderManager.instance; - } - - private api: types.IExtensionApi; - private getLauncherManager: GetLauncherManager; - private getLocalizationManager: GetLocalizationManager; - private isInitialized = false; - private allModules: vetypes.ModuleInfoExtendedWithMetadata[] = []; - private compatibilityScores: IModuleCompatibilityInfoCache = {}; - - public gameId: string = GAME_ID; - public toggleableEntries = true; - public customItemRenderer?: React.ComponentType<{ - className?: string; - item: types.IFBLOItemRendererProps; - }>; - - public usageInstructions?: React.ComponentType; - public noCollectionGeneration = true; - - constructor( - api: types.IExtensionApi, - getLauncherManager: GetLauncherManager, - getLocalizationManager: GetLocalizationManager - ) { - this.api = api; - this.getLauncherManager = getLauncherManager; - this.getLocalizationManager = getLocalizationManager; - this.usageInstructions = () => ( - - ); - - this.customItemRenderer = ({ className = '', item }) => { - const availableProviders = this.allModules - .filter((x) => x.id === item.loEntry.id) - .map((x) => x.moduleProviderType); - const compatibilityScore = this.compatibilityScores[item.loEntry.id]; - - return ( - - ); - }; - } - - public updateCompatibilityScores = () => { - const proxy = new ModAnalyzerProxy(this.api); - const launcherManager = this.getLauncherManager(); - const gameVersion = launcherManager.getGameVersionVortex(); - const query: IModAnalyzerRequestQuery = { - gameVersion: gameVersion, - modules: this.allModules.map((x) => ({ - moduleId: x.id, - moduleVersion: versionToString(x.version), - })), - }; - proxy.analyze(query).then((result) => { - this.compatibilityScores = result.modules.reduce((map, curr) => { - map[curr.moduleId] = { - score: curr.compatibility, - recommendedScore: curr.recommendedCompatibility, - recommendedVersion: curr.recommendedModuleVersion, - }; - return map; - }, {}); - this.forceRefresh(); - }); - }; - - private forceRefresh = (): void => { - const profile = selectors.activeProfile(this.api.getState()); - this.api.store?.dispatch(actionsLoadOrder.setFBForceUpdate(profile.id)); - }; - - public serializeLoadOrder = (loadOrder: VortexLoadOrderStorage): Promise => { - const loadOrderConverted = vortexToLibrary(loadOrder); - const launcherManager = this.getLauncherManager(); - launcherManager.saveLoadOrderVortex(loadOrderConverted); - return Promise.resolve(); - }; - - private setParameters = (loadOrder: vetypes.LoadOrder): void => { - if (!this.isInitialized) { - this.isInitialized = true; - // We automatically set the modules to launch on save, but not on first load - const launcherManager = this.getLauncherManager(); - launcherManager.setModulesToLaunch(loadOrder); - } - }; - private checkSavedLoadOrder = (autoSort: boolean, loadOrder: VortexLoadOrderStorage): void => { - const localizationManager = this.getLocalizationManager(); - const t = localizationManager.localize; - - const savedLoadOrderIssues = Utils.isLoadOrderCorrect( - loadOrder.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 - this.api.sendNotification?.({ - type: 'warning', - message: t(`{=pZVVdI5d}The Load Order was re-sorted with the default algorithm!{NL}Reasons:{NL}{REASONS}`, { - NL: '\n', - REASONS: savedLoadOrderIssues.join(`\n`), - }), - }); - } - }; - private checkOrderByLoadOrderResult = (autoSort: boolean, result: vetypes.OrderByLoadOrderResult): void => { - const localizationManager = this.getLocalizationManager(); - const t = localizationManager.localize; - - if (autoSort && result.issues) { - this.api.sendNotification?.({ - type: 'warning', - message: t(`{=pZVVdI5d}The Load Order was re-sorted with the default algorithm!{NL}Reasons:{NL}{REASONS}`, { - NL: '\n', - REASONS: result.issues.join(`\n`), - }), - }); - } - }; - private checkResult = ( - autoSort: boolean, - result: vetypes.OrderByLoadOrderResult - ): result is RequiredProperties => { - const localizationManager = this.getLocalizationManager(); - const t = localizationManager.localize; - - if (!result || !result.orderedModuleViewModels || !result.result) { - if (autoSort) { - // The user is not expecting a sort operation, so don't give the notification - this.api.sendNotification?.({ - type: 'error', - message: t(`{=sLf3eIpH}Failed to order the module list!`), - }); - } - return false; - } - return true; - }; - private getExcludedLoadOrder = ( - loadOrder: vetypes.LoadOrder, - result: vetypes.OrderByLoadOrderResult - ): vetypes.LoadOrder => { - const excludedLoadOrder = Object.entries(loadOrder).reduce((arr, curr) => { - const [id, entry] = curr; - if (result.orderedModuleViewModels?.find((x) => x.moduleInfoExtended.id === entry.id)) { - arr[id] = entry; - } - return arr; - }, {}); - return excludedLoadOrder; - }; - public deserializeLoadOrder = (): Promise => { - const autoSort = true; // TODO: get from settings - const launcherManager = this.getLauncherManager(); - - // Make sure the LauncherManager has the latest module list - launcherManager.refreshModules(); - this.allModules = launcherManager.getAllModulesWithDuplicates(); - - // Get the saved Load Order - const allModules = launcherManager.getAllModules(); - const savedLoadOrder = launcherManager.loadLoadOrderVortex(); - const savedLoadOrderVortex = libraryToVortex(this.api, allModules, savedLoadOrder); - - this.checkSavedLoadOrder(autoSort, savedLoadOrderVortex); - - // Apply the Load Order to the list of modules - // Useful when there are new modules or old modules are missing - // The output wil wil contain the auto sorted list of modules - const result = launcherManager.orderByLoadOrder(savedLoadOrder); - if (!this.checkResult(autoSort, result)) { - this.setParameters(savedLoadOrder); - return Promise.resolve(savedLoadOrderVortex); - } - - // Not even sure this will trigger - this.checkOrderByLoadOrderResult(autoSort, result); - - // Use the sorted to closest valid state Load Order - if (autoSort) { - const loadOrderVortex = libraryVMToVortex(this.api, result.orderedModuleViewModels); - this.setParameters(libraryVMToLibrary(result.orderedModuleViewModels)); - return Promise.resolve(loadOrderVortex); - } - - // Do not use the sorted LO, but take the list of modules. It excludes modules that are not usable - const excludedSavedLoadOrder = this.getExcludedLoadOrder(savedLoadOrder, result); - this.setParameters(excludedSavedLoadOrder); - return Promise.resolve(libraryToVortex(this.api, allModules, excludedSavedLoadOrder)); - }; - - public validate = (_prev: VortexLoadOrderStorage, curr: VortexLoadOrderStorage): Promise => { - const modules = (curr ?? []).flatMap((entry) => - entry.data && entry.enabled ? entry.data.moduleInfoExtended : [] - ); - //const validationManager = ValidationManager.fromVortex(curr); - - const invalidResults = Array(); - for (const enabledModule of modules) { - const loadOrderIssues = BannerlordModuleManager.validateLoadOrder(modules, enabledModule); - for (const issue of loadOrderIssues) { - const localizedIssue = Utils.renderModuleIssue(issue); - invalidResults.push({ - id: issue.target.id, - reason: localizedIssue, - }); - } - } - - // While the contract doesn't explicitly allow undefined to be returned, - // it's expecting an undefined when there are no issues. - return Promise.resolve( - invalidResults.length === 0 - ? undefined! - : { - invalid: invalidResults, - } - ); - }; -} diff --git a/src/utils/localization/manager.ts b/src/utils/localization/manager.ts deleted file mode 100644 index 803dd35..0000000 --- a/src/utils/localization/manager.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { fs, types, util } from 'vortex-api'; -import { Utils } from '@butr/vortexextensionnative'; -import { Dirent } from 'fs'; -import { I18N_NAMESPACE } from '../../common'; - -export type TranslateValues = { - [value: string]: string; -}; - -export const i18nToBannerlord = (languageCode: string) => { - switch (languageCode) { - case 'pt-BR': - return 'Português (BR)'; - case 'by': // Not present - return 'Беларуская'; - //case 'byl': // Not present, no idea how to represent this one - // return 'Biełaruskaja'; - case 'zh': - return '简体中文'; - //case 'zh-Hant': // Since there's no distiction, better to use the simplified one? - // return '繁體中文'; - // break; - case 'de': - return 'Deutsch'; - case 'en': - return 'English'; - case 'fr': - return 'Français'; - case 'it': - return 'Italiano'; - case 'ja': - return '日本語'; - case 'ko': - return '한국어'; - case 'pl': - return 'Polski'; - case 'ro': // Not present - return 'Română'; - case 'ru': - return 'Русский'; - case 'es': - return 'Español (LA)'; - case 'tr': - return 'Türkçe'; - case 'uk': - return 'Українська'; - default: - return 'English'; - } -}; - -export class LocalizationManager { - private static instance: LocalizationManager; - - public static getInstance(api?: types.IExtensionApi): LocalizationManager { - if (!LocalizationManager.instance) { - if (api === undefined) { - throw new Error('IniStructure is not context aware'); - } - LocalizationManager.instance = new LocalizationManager(api); - } - - return LocalizationManager.instance; - } - - private readonly api: types.IExtensionApi; - private initializedLocalization = false; - - constructor(api: types.IExtensionApi) { - this.api = api; - } - - public localize = (template: string, values: TranslateValues = {}): string => { - if (template.startsWith('{=')) { - if (!this.initializedLocalization) { - this.initializeLocalization(); - this.initializedLocalization = true; - } - - return Utils.localizeString(template, { - ns: I18N_NAMESPACE, - ...values, - }); - } - this.api.translate('', {}); - return this.api.translate(template, values); - }; - - private initializeLocalization = () => { - fs.readdirSync(__dirname, { withFileTypes: true }).forEach((d: Dirent) => { - if (d.isFile() && d.name.startsWith('localization_') && d.name.endsWith('.xml')) { - const content: string = fs.readFileSync(`${__dirname}/${d.name}`, { - encoding: 'utf8', - }); - Utils.loadLocalization(content); - } - }); - Utils.setLanguage(i18nToBannerlord(util.getCurrentLanguage())); - }; -} diff --git a/src/utils/module/index.ts b/src/utils/module/index.ts deleted file mode 100644 index dc7d3f5..0000000 --- a/src/utils/module/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './modType'; diff --git a/src/utils/moduleUtil.ts b/src/utils/moduleUtil.ts deleted file mode 100644 index 20ebcab..0000000 --- a/src/utils/moduleUtil.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { selectors, types } from 'vortex-api'; - -type getModIdsResult = { - id: string; - source: string; -}; - -/** - * I have no idea what to do if we have multiple mods that provide the same Module - * @param api - * @param moduleId - * @returns - */ -export const getModIds = (api: types.IExtensionApi, moduleId: string): getModIdsResult[] => { - const state = api.getState(); - const gameId = selectors.activeGameId(state); - const gameMods = state.persistent.mods[gameId] ?? {}; - const modIds = Object.values(gameMods).reduce((arr, mod) => { - if (!mod.attributes || !mod.attributes['subModsIds']) { - return arr; - } - const subModsIds: Set = new Set(mod.attributes['subModsIds']); - if (subModsIds.has(moduleId)) { - arr.push({ - id: mod.attributes['modId'], - source: mod.attributes['source'], - }); - } - - return arr; - }, []); - - return modIds; -}; diff --git a/src/utils/save/index.ts b/src/utils/save/index.ts deleted file mode 100644 index dc653da..0000000 --- a/src/utils/save/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * as actionsSave from './actions'; -export * from './manager'; diff --git a/src/utils/save/manager.ts b/src/utils/save/manager.ts deleted file mode 100644 index 1ab198d..0000000 --- a/src/utils/save/manager.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { selectors, types } from 'vortex-api'; -import { actionsSave, getSaveFromSettings } from '..'; -import { GetLauncherManager } from '../../types'; - -export class SaveManager { - private static instance: SaveManager; - - public static getInstance(api?: types.IExtensionApi, getLauncherManager?: GetLauncherManager): SaveManager { - if (!SaveManager.instance) { - if (api === undefined || getLauncherManager === undefined) { - throw new Error('IniStructure is not context aware'); - } - SaveManager.instance = new SaveManager(api, getLauncherManager); - } - - return SaveManager.instance; - } - - private api: types.IExtensionApi; - private getLauncherManager: GetLauncherManager; - - constructor(api: types.IExtensionApi, getLauncherManager: GetLauncherManager) { - this.api = api; - this.getLauncherManager = getLauncherManager; - } - - public reloadSave(): void { - const save = this.getSave(); - this.setSave(save); - } - - public getSave = (): string | null => { - const state = this.api.getState(); - const profile = selectors.activeProfile(state); - - return getSaveFromSettings(state, profile.id); - }; - - public setSave = (saveId: string | null): void => { - if (saveId === 'No Save') { - saveId = null; - } - - const state = this.api.getState(); - const profile = selectors.activeProfile(state); - this.api.store?.dispatch(actionsSave.setCurrentSave(profile.id, saveId)); - - const launcherManager = this.getLauncherManager(); - launcherManager.setSaveFile(saveId ?? ''); - }; -} diff --git a/src/utils/settings/index.ts b/src/utils/settings/index.ts deleted file mode 100644 index 539bc34..0000000 --- a/src/utils/settings/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * as actionsSettings from './actions'; -export * from './utils'; diff --git a/src/utils/util.ts b/src/utils/util.ts deleted file mode 100644 index 4e7d707..0000000 --- a/src/utils/util.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { fs, types, util } from 'vortex-api'; -import { EPICAPP_ID, GOG_IDS, STEAMAPP_ID, XBOX_ID } from '../common'; - -export const getPathExistsAsync = async (path: string): Promise => { - return fs - .statAsync(path) - .then(() => true) - .catch(() => false); -}; - -export const findGame = async (): Promise => { - return util.GameStoreHelper.findByAppId([EPICAPP_ID, STEAMAPP_ID.toString(), ...GOG_IDS, XBOX_ID]); -}; - -type HasId = { - id: string; -}; -const hasId = (persistent: HasId): persistent is HasId => { - return !!persistent.id && persistent.id !== ''; -}; - -export const filterEntryWithInvalidId = (entry: HasId): boolean => { - return hasId(entry); -}; diff --git a/src/utils/vortex.ts b/src/utils/vortex.ts deleted file mode 100644 index d935788..0000000 --- a/src/utils/vortex.ts +++ /dev/null @@ -1,118 +0,0 @@ -import path from 'path'; -import { types, util } from 'vortex-api'; -import { - addBLSETools, - addModdingKitTool, - addOfficialCLITool, - addOfficialLauncherTool, - getBinaryPath, - getPathExistsAsync, - isStoreSteam, - isStoreXbox, - nameof, - recommendBLSE, -} from '.'; -import { BLSE_CLI_EXE, GAME_ID, XBOX_ID } from '../common'; -import { - ISettingsWithBannerlord, - ISettingsInterfaceWithPrimaryTool, - IStatePersistentWithLoadOrder, - GetLocalizationManager, - GetLauncherManager, -} from '../types'; - -type HasSettings = { - settings: types.ISettings; -}; - -type RequiresLauncherResult = { - launcher: string; - addInfo?: unknown; -}; - -export const hasPersistentLoadOrder = (persistent: object): persistent is IStatePersistentWithLoadOrder => { - return nameof('loadOrder') in persistent; -}; - -export const hasSettings = (hasSettings: object): hasSettings is HasSettings => { - return nameof('settings') in hasSettings; -}; - -export const hasSettingsBannerlord = (settings: object): settings is ISettingsWithBannerlord => { - return GAME_ID in settings; -}; - -export const hasSettingsInterfacePrimaryTool = (settings: object): settings is ISettingsInterfaceWithPrimaryTool => { - return nameof('primaryTool') in settings; -}; - -const launchGameStore = async (api: types.IExtensionApi, store: string): Promise => { - await util.GameStoreHelper.launchGameStore(api, store, undefined, true).catch(() => { - /* ignore error */ - }); -}; - -const prepareForModding = async ( - api: types.IExtensionApi, - discovery: types.IDiscoveryResult, - getLauncherManager: GetLauncherManager, - getLocalizationManager: GetLocalizationManager -): Promise => { - if (!discovery.path) { - throw new Error(`discovery.path is undefined!`); - } - - // 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); - if (!(await getPathExistsAsync(binaryPath))) { - recommendBLSE(api, getLocalizationManager); - } - - if (isStoreSteam(discovery.store)) { - await launchGameStore(api, discovery.store); - } - - if (discovery.store) { - const launcherManager = getLauncherManager(); - launcherManager.setStore(discovery.store); - } -}; - -export const setup = async ( - api: types.IExtensionApi, - discovery: types.IDiscoveryResult, - getLauncherManager: GetLauncherManager, - getLocalizationManager: GetLocalizationManager -): Promise => { - if (!discovery.path) { - throw new Error(`discovery.path is undefined!`); - } - - // Quickly ensure that the official Launcher is added. - addOfficialCLITool(api, discovery); - addOfficialLauncherTool(api, discovery); - addModdingKitTool(api, discovery); - await addBLSETools(api, discovery); - - await prepareForModding(api, discovery, getLauncherManager, getLocalizationManager); -}; - -export const requiresLauncher = async (store?: string): Promise => { - if (isStoreXbox(store)) { - return { - launcher: `xbox`, - addInfo: { - appId: XBOX_ID, - parameters: [ - { - appExecName: `bin.Gaming.Desktop.x64.Shipping.Client.Launcher.Native`, - }, - ], - }, - }; - } - // The API doesn't expect undefined, but it's allowed - return undefined!; -}; diff --git a/src/utils/localization/index.ts b/src/validation/index.ts similarity index 100% rename from src/utils/localization/index.ts rename to src/validation/index.ts diff --git a/src/utils/validationManager.ts b/src/validation/manager.ts similarity index 88% rename from src/utils/validationManager.ts rename to src/validation/manager.ts index 950ed7c..5b8e451 100644 --- a/src/utils/validationManager.ts +++ b/src/validation/manager.ts @@ -5,13 +5,13 @@ export class ValidationManager implements vetypes.IValidationManager { static fromVortex = (loadOrder: VortexLoadOrderStorage): ValidationManager => { return new ValidationManager((moduleId: string): boolean => { const module = loadOrder.find((x) => x.id === moduleId); - return !!module && module.enabled; + return module !== undefined && module.enabled; }); }; static fromLibrary = (loadOrder: vetypes.LoadOrder): ValidationManager => { return new ValidationManager((moduleId: string): boolean => { const module = loadOrder[moduleId]; - return !!module && module.isSelected; + return module !== undefined && module.isSelected; }); }; diff --git a/src/views/CollectionGeneralData/components/LoadOrderEditInfo.tsx b/src/views/CollectionGeneralData/components/LoadOrderEditInfo.tsx new file mode 100644 index 0000000..7d28d4b --- /dev/null +++ b/src/views/CollectionGeneralData/components/LoadOrderEditInfo.tsx @@ -0,0 +1,28 @@ +import React, { useContext } from 'react'; +import { FlexLayout, Icon, MainContext } from 'vortex-api'; +import { openLoadOrderPage } from '../utils'; +import { useLocalization } from '../../../localization'; + +export const LoadOrderEditInfo = (): JSX.Element => { + const { localize: t } = useLocalization(); + + const context = useContext(MainContext); + + return ( + + + + + + {t('You can make changes to this data from the ')} + openLoadOrderPage(context.api)} title={t('Go to Load Order Page')}> + {t('Load Order page.')} + + {t( + ' If you believe a load order entry is missing, please ensure the ' + + 'relevant mod is enabled and has been added to the collection.' + )} + + + ); +}; diff --git a/src/views/CollectionGeneralData/components/LoadOrderEntry.tsx b/src/views/CollectionGeneralData/components/LoadOrderEntry.tsx new file mode 100644 index 0000000..d55a965 --- /dev/null +++ b/src/views/CollectionGeneralData/components/LoadOrderEntry.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { ListGroupItem } from 'react-bootstrap'; +import { IPersistenceLoadOrderEntry, VortexLoadOrderStorage } from '../../../types'; +import { CompatibilityInfo, isOptional, ModuleIcon, OptionalBanner } from '../../Shared'; +import { IModuleCompatibilityInfoCache } from '../../../butr'; +import { versionToString } from '../../../launcher'; + +export type LoadOrderEntryProps = { + entry: IPersistenceLoadOrderEntry | undefined; + loadOrder: VortexLoadOrderStorage; + compatibilityInfoCache: IModuleCompatibilityInfoCache; +}; + +export const LoadOrderEntry = (props: LoadOrderEntryProps): JSX.Element | null => { + const { entry, loadOrder, compatibilityInfoCache } = props; + + if (!entry) { + return null; + } + + const loEntry = loadOrder.find((x) => x.id === entry.id); + if (!loEntry || !loEntry.data) { + return null; + } + + const compatibilityInfo = compatibilityInfoCache[loEntry.id]; + + const key = loEntry.id; + const name = entry.name ? `${entry.name}` : `${entry.id}`; + const version = versionToString(loEntry.data.moduleInfoExtended.version); + let classes = ['load-order-entry', 'collection-tab']; + if (isOptional(loEntry)) { + classes = classes.concat('external'); + } + + return ( + +

{entry.index + 1}

+ +

+ {name} ({version}) +

+ + +
+ + ); +}; diff --git a/src/views/CollectionGeneralData/components/OpenLoadOrderButton.tsx b/src/views/CollectionGeneralData/components/OpenLoadOrderButton.tsx new file mode 100644 index 0000000..308be8c --- /dev/null +++ b/src/views/CollectionGeneralData/components/OpenLoadOrderButton.tsx @@ -0,0 +1,22 @@ +import React, { useContext } from 'react'; +import { Button } from 'react-bootstrap'; +import { MainContext } from 'vortex-api'; +import { openLoadOrderPage } from '../utils'; +import { useLocalization } from '../../../localization'; + +export const OpenLoadOrderButton = (): JSX.Element => { + const { localize: t } = useLocalization(); + + const context = useContext(MainContext); + + return ( + + ); +}; diff --git a/src/views/CollectionGeneralData/components/Placeholder.tsx b/src/views/CollectionGeneralData/components/Placeholder.tsx new file mode 100644 index 0000000..e8b77cc --- /dev/null +++ b/src/views/CollectionGeneralData/components/Placeholder.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { EmptyPlaceholder } from 'vortex-api'; +import { OpenLoadOrderButton } from './OpenLoadOrderButton'; +import { useLocalization } from '../../../localization'; + +export const Placeholder = (): JSX.Element => { + const { localize: t } = useLocalization(); + + return ( + + ); +}; diff --git a/src/views/CollectionGeneralData/components/Requirements.tsx b/src/views/CollectionGeneralData/components/Requirements.tsx new file mode 100644 index 0000000..b5db074 --- /dev/null +++ b/src/views/CollectionGeneralData/components/Requirements.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { ListGroup } from 'react-bootstrap'; +import { useLocalization } from '../../../localization'; + +export type RequirementsProps = { + hasBLSE: boolean; +}; + +export const Requirements = (props: RequirementsProps): JSX.Element => { + const { hasBLSE } = props; + + const { localize: t } = useLocalization(); + + return ( + <> +

{t('Requirements')}

+ + {t('Require BLSE to be installed')} + {': '} + {hasBLSE ? {t('Yes')} : {t('No')}} + + + ); +}; diff --git a/src/views/CollectionGeneralData/components/index.ts b/src/views/CollectionGeneralData/components/index.ts new file mode 100644 index 0000000..f22c466 --- /dev/null +++ b/src/views/CollectionGeneralData/components/index.ts @@ -0,0 +1,5 @@ +export * from './LoadOrderEditInfo'; +export * from './LoadOrderEntry'; +export * from './OpenLoadOrderButton'; +export * from './Placeholder'; +export * from './Requirements'; diff --git a/src/views/CollectionGeneralData/index.ts b/src/views/CollectionGeneralData/index.ts new file mode 100644 index 0000000..7af5803 --- /dev/null +++ b/src/views/CollectionGeneralData/index.ts @@ -0,0 +1 @@ +export * from './pages/GeneralDataPage'; diff --git a/src/views/CollectionGeneralData/pages/GeneralDataPage.tsx b/src/views/CollectionGeneralData/pages/GeneralDataPage.tsx new file mode 100644 index 0000000..7ca8031 --- /dev/null +++ b/src/views/CollectionGeneralData/pages/GeneralDataPage.tsx @@ -0,0 +1,93 @@ +import React, { useContext, useEffect, useState } from 'react'; +import { ListGroup } from 'react-bootstrap'; +import { useSelector } from 'react-redux'; +import { MainContext, selectors, tooltip, types } from 'vortex-api'; +import { LoadOrderEditInfo, LoadOrderEntry, Placeholder, Requirements } from '../components'; +import { IBannerlordModStorage, PersistenceLoadOrderStorage, VortexLoadOrderStorage } from '../../../types'; +import { ICollectionFeatureProps } from '../../types'; +import { getCompatibilityScores, IModuleCompatibilityInfoCache } from '../../../butr'; +import { genCollectionGeneralData } from '../../../collections'; +import { useLocalization } from '../../../localization'; +import { hasPersistentBannerlordMods, hasPersistentLoadOrder } from '../../../vortex'; + +interface IFromState { + loadOrder: VortexLoadOrderStorage; + mods: IBannerlordModStorage; +} + +export type BannerlordGeneralDataPageProps = ICollectionFeatureProps; + +export const BannerlordGeneralDataPage = (props: BannerlordGeneralDataPageProps): JSX.Element => { + const [compatibilityInfoCache, setCompatibilityInfoCache] = useState({}); + const [hasBLSE, setHasBLSE] = useState(false); + const [persistentLoadOrder, setPersistentLoadOrder] = useState([]); + + const { loadOrder, mods } = useSelector(mapState); + + const context = useContext(MainContext); + + useEffect(() => { + const data = genCollectionGeneralData(context.api, Object.keys(mods)); + setHasBLSE(data.hasBLSE); + setPersistentLoadOrder(data.suggestedLoadOrder); + }, [context.api, mods]); + + const { localize: t } = useLocalization(); + + const refreshCompatibilityScores = (): void => { + getCompatibilityScores(context.api) + .then((cache) => { + setCompatibilityInfoCache(cache); + }) + .catch(() => {}); + }; + + const hint = t( + `{=zXWdahH9}Get Update Recommendations{NL}Clicking on this button will send your module list to the BUTR server to get compatibility scores and recommended versions.{NL}They are based on the crash reports from ButterLib.{NL}{NL}(Requires Internet Connection)`, + { + NL: '\n', + } + ); + + return Object.values(persistentLoadOrder).length ? ( +
+ +

{t('Load Order')}

+ + {t('Update Compatibility Score')} + +

{t('This is a snapshot of the load order information that will be exported with this collection.')}

+

+ {t( + `Enabled modules in the Load Order which are not part of the Collection will be marked as optional in the` + + `Collection load order and will not be mandatory for the user to have. + Official modules will be optional too, but the mods in your collection will require them to be installed, ` + + `which means that it's not possible for a user to install a collection without the official modules.` + )} +

+ + + {Object.values(persistentLoadOrder).map((entry) => ( + + ))} + +
+ ) : ( + + ); +}; + +const mapState = (state: types.IState): IFromState => { + const profile: types.IProfile | undefined = selectors.activeProfile(state); + const loadOrder = hasPersistentLoadOrder(state.persistent) ? state.persistent.loadOrder[profile?.id] ?? [] : []; + const mods = hasPersistentBannerlordMods(state.persistent) ? state.persistent.mods.mountandblade2bannerlord : {}; + return { + loadOrder, + mods, + }; +}; diff --git a/src/views/CollectionGeneralData/utils.ts b/src/views/CollectionGeneralData/utils.ts new file mode 100644 index 0000000..20c9e0e --- /dev/null +++ b/src/views/CollectionGeneralData/utils.ts @@ -0,0 +1,5 @@ +import { types } from 'vortex-api'; + +export const openLoadOrderPage = (api: types.IExtensionApi): void => { + api.events.emit('show-main-page', 'file-based-loadorder'); +}; diff --git a/src/views/CollectionModOptionsData/components/GlobalSettings.tsx b/src/views/CollectionModOptionsData/components/GlobalSettings.tsx new file mode 100644 index 0000000..0827a9e --- /dev/null +++ b/src/views/CollectionModOptionsData/components/GlobalSettings.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { ListGroup } from 'react-bootstrap'; +import { ModOptionsEntryView } from './ModOptionsEntryView'; +import { ModOptionsEntry, ModOptionsStorage } from '../../../modoptions'; +import { useLocalization } from '../../../localization'; + +export type GlobalSettingsProps = { + settings: ModOptionsStorage; + isToggled: (entry: ModOptionsEntry) => boolean; + toggleEntry: (newValue: boolean, entry: ModOptionsEntry) => void; +}; + +export const GlobalSettings = (props: GlobalSettingsProps): JSX.Element => { + const { settings, isToggled, toggleEntry } = props; + + const { localize: t } = useLocalization(); + + return ( +
+
{t('Global Options')}
+ + {Object.values(settings).map((entry) => ( + + ))} + +
+ ); +}; diff --git a/src/views/CollectionModOptionsData/components/LoadOrderEntry.tsx b/src/views/CollectionModOptionsData/components/LoadOrderEntry.tsx new file mode 100644 index 0000000..d55a965 --- /dev/null +++ b/src/views/CollectionModOptionsData/components/LoadOrderEntry.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { ListGroupItem } from 'react-bootstrap'; +import { IPersistenceLoadOrderEntry, VortexLoadOrderStorage } from '../../../types'; +import { CompatibilityInfo, isOptional, ModuleIcon, OptionalBanner } from '../../Shared'; +import { IModuleCompatibilityInfoCache } from '../../../butr'; +import { versionToString } from '../../../launcher'; + +export type LoadOrderEntryProps = { + entry: IPersistenceLoadOrderEntry | undefined; + loadOrder: VortexLoadOrderStorage; + compatibilityInfoCache: IModuleCompatibilityInfoCache; +}; + +export const LoadOrderEntry = (props: LoadOrderEntryProps): JSX.Element | null => { + const { entry, loadOrder, compatibilityInfoCache } = props; + + if (!entry) { + return null; + } + + const loEntry = loadOrder.find((x) => x.id === entry.id); + if (!loEntry || !loEntry.data) { + return null; + } + + const compatibilityInfo = compatibilityInfoCache[loEntry.id]; + + const key = loEntry.id; + const name = entry.name ? `${entry.name}` : `${entry.id}`; + const version = versionToString(loEntry.data.moduleInfoExtended.version); + let classes = ['load-order-entry', 'collection-tab']; + if (isOptional(loEntry)) { + classes = classes.concat('external'); + } + + return ( + +

{entry.index + 1}

+ +

+ {name} ({version}) +

+ + +
+ + ); +}; diff --git a/src/views/CollectionModOptionsData/components/ModOptionsEntryView.tsx b/src/views/CollectionModOptionsData/components/ModOptionsEntryView.tsx new file mode 100644 index 0000000..937cb6f --- /dev/null +++ b/src/views/CollectionModOptionsData/components/ModOptionsEntryView.tsx @@ -0,0 +1,37 @@ +import React, { BaseSyntheticEvent } from 'react'; +import { Checkbox, ListGroupItem } from 'react-bootstrap'; +import { ModOptionsEntry } from '../../../modoptions'; + +export type ModOptionsEntryViewProps = { + entry: ModOptionsEntry | undefined; + isToggled: (entry: ModOptionsEntry) => boolean; + toggleEntry: (newValue: boolean, entry: ModOptionsEntry) => void; +}; + +export const ModOptionsEntryView = (props: ModOptionsEntryViewProps): JSX.Element | null => { + const { entry, isToggled, toggleEntry } = props; + + if (!entry) { + return null; + } + + const key = entry.name; + const name = entry.name; + const classes = ['load-order-entry', 'collection-tab']; + + const checked = isToggled(entry); + + return ( + +

{name}

+ ) => { + toggleEntry(evt.target.checked, entry); + }} + /> +
+ + ); +}; diff --git a/src/views/CollectionModOptionsData/components/Placeholder.tsx b/src/views/CollectionModOptionsData/components/Placeholder.tsx new file mode 100644 index 0000000..12e8f03 --- /dev/null +++ b/src/views/CollectionModOptionsData/components/Placeholder.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { EmptyPlaceholder } from 'vortex-api'; +import { useLocalization } from '../../../localization'; + +export const Placeholder = (): JSX.Element => { + const { localize: t } = useLocalization(); + + return ( + + ); +}; diff --git a/src/views/CollectionModOptionsData/components/SpecialSettings.tsx b/src/views/CollectionModOptionsData/components/SpecialSettings.tsx new file mode 100644 index 0000000..1c7844f --- /dev/null +++ b/src/views/CollectionModOptionsData/components/SpecialSettings.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { ListGroup } from 'react-bootstrap'; +import { ModOptionsEntryView } from './ModOptionsEntryView'; +import { ModOptionsEntry, ModOptionsStorage } from '../../../modoptions'; +import { useLocalization } from '../../../localization'; + +export type SpecialSettingsProps = { + settings: ModOptionsStorage; + isToggled: (entry: ModOptionsEntry) => boolean; + toggleEntry: (newValue: boolean, entry: ModOptionsEntry) => void; +}; + +export const SpecialSettings = (props: SpecialSettingsProps): JSX.Element => { + const { settings, isToggled, toggleEntry } = props; + + const { localize: t } = useLocalization(); + + return ( +
+
{t('Special Options')}
+ + {Object.values(settings).map((entry) => ( + + ))} + +
+ ); +}; diff --git a/src/views/CollectionModOptionsData/components/index.ts b/src/views/CollectionModOptionsData/components/index.ts new file mode 100644 index 0000000..0ba26ac --- /dev/null +++ b/src/views/CollectionModOptionsData/components/index.ts @@ -0,0 +1,4 @@ +export * from './GlobalSettings'; +export * from './ModOptionsEntryView'; +export * from './Placeholder'; +export * from './SpecialSettings'; diff --git a/src/views/CollectionModOptionsData/index.ts b/src/views/CollectionModOptionsData/index.ts new file mode 100644 index 0000000..2218b65 --- /dev/null +++ b/src/views/CollectionModOptionsData/index.ts @@ -0,0 +1 @@ +export * from './pages/ModOptionsDataPage'; diff --git a/src/views/CollectionModOptionsData/pages/ModOptionsDataPage.tsx b/src/views/CollectionModOptionsData/pages/ModOptionsDataPage.tsx new file mode 100644 index 0000000..ce88701 --- /dev/null +++ b/src/views/CollectionModOptionsData/pages/ModOptionsDataPage.tsx @@ -0,0 +1,71 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { types } from 'vortex-api'; +import { GAME_ID } from '../../../common'; +import { GlobalSettings, Placeholder, SpecialSettings } from '../components'; +import { ICollectionFeatureProps } from '../../types'; +import { useLocalization } from '../../../localization'; +import { + getGlobalSettings, + getSpecialSettings, + ModOptionsEntry, + ModOptionsStorage, + PersistentModOptionsEntry, + readSettingsContent, +} from '../../../modoptions'; +import { hasStatePersistentCollectionModWithIncludedModOptions, IncludedModOptions } from '../../../collections'; +import { nameof } from '../../../nameof'; + +export type ModOptionsDataPageProps = ICollectionFeatureProps; + +export const ModOptionsDataPage = (props: ModOptionsDataPageProps): JSX.Element => { + const { collection, onSetCollectionAttribute } = props; + + const { localize: t } = useLocalization(); + + const [specialSettings, setSpecialSettings] = useState({}); + const [globalSettings, setGlobalSettings] = useState({}); + + const includedModOptions = useSelector((state) => { + if (!hasStatePersistentCollectionModWithIncludedModOptions(state.persistent, collection.id)) { + return []; + } + + const collectionMod = state.persistent.mods[GAME_ID]?.[collection.id]; + return collectionMod?.attributes?.collection?.includedModOptions ?? []; + }); + + const toggleEntry = useCallback( + (newValue: boolean, entry: ModOptionsEntry) => { + const newEntries: PersistentModOptionsEntry[] = newValue + ? [...includedModOptions, { ...entry, contentBase64: readSettingsContent(entry) }] + : includedModOptions.filter((x) => x.name !== entry.name); + onSetCollectionAttribute([nameof('includedModOptions')], newEntries); + }, + [includedModOptions, onSetCollectionAttribute] + ); + + const isToggled = (entry: ModOptionsEntry): boolean => { + return includedModOptions.some((x) => x.name === entry.name); + }; + + useEffect(() => { + async function setSettings(): Promise { + setSpecialSettings(getSpecialSettings()); + setGlobalSettings(await getGlobalSettings()); + } + + setSettings().catch(() => {}); + }, []); + + return Object.values(globalSettings).length ? ( +
+

{t('Mod Configuration Options')}

+

{t('This is a snapshot of the settings that can be included within the collection.')}

+ + +
+ ) : ( + + ); +}; diff --git a/src/views/CollectionsDataView/CollectionsDataView.tsx b/src/views/CollectionsDataView/CollectionsDataView.tsx deleted file mode 100644 index 8e9f293..0000000 --- a/src/views/CollectionsDataView/CollectionsDataView.tsx +++ /dev/null @@ -1,161 +0,0 @@ -/* eslint-disable prettier/prettier */ -/* -import Bluebird, { Promise, method as toBluebird } from 'bluebird'; - -import _ from 'lodash'; -import React from 'react'; -import { Button, ListGroup, ListGroupItem } from 'react-bootstrap'; -import { withTranslation } from 'react-i18next'; -import { connect } from 'react-redux'; - -import { - ComponentEx, EmptyPlaceholder, FlexLayout, selectors, types, Icon, util, -} from 'vortex-api'; - -import { Dispatch } from 'redux'; -import { IExtendedInterfaceProps } from "collections/src/types/IExtendedInterfaceProps"; -import { genCollectionLoadOrder } from '../collections/collectionUtil'; -import { ILoadOrder, IMods } from '../types'; - -const NAMESPACE = `mnb2-collections-data`; - -interface IStateProps { - gameId: string; - mods: IMods; - loadOrder: ILoadOrder; - profile: types.IProfile; -} -interface IDispatchProps { - -} -interface IOwnProps { - -} - -interface IBaseState { - sortedMods: ILoadOrder; -} - -type IComponentProps = IStateProps & IDispatchProps & IOwnProps & IExtendedInterfaceProps; -type IComponentState = IBaseState; -class CollectionsDataView extends ComponentEx { - constructor(props: IComponentProps) { - super(props); - const { loadOrder, mods, collection } = props; - this.initState({ - sortedMods: genCollectionLoadOrder(loadOrder, mods, collection) ?? {}, - }); - } - - public componentDidMount(): void { - const { loadOrder, mods, collection } = this.props; - this.nextState.sortedMods = genCollectionLoadOrder(loadOrder, mods, collection); - } - - public render(): JSX.Element { - const { t } = this.props; - const { sortedMods } = this.state; - return (!!sortedMods && Object.keys(sortedMods).length !== 0) - ? ( -
-

{t(`Load Order`)}

-

- {t(`This is a snapshot of the load order information that ` + - `will be exported with this collection.`)} -

- {this.renderLoadOrderEditInfo()} - - {Object.keys(sortedMods).map(this.renderModEntry)} - -
- ) : this.renderPlaceholder(); - } - - private renderLoadOrderEditInfo = (): JSX.Element => { - const { t } = this.props; - return ( - - - - - - {t(`You can make changes to this data from the `)} - - {t(`Load Order page.`)} - - {t(` If you believe a load order entry is missing, please ensure the ` + - `relevant mod is enabled and has been added to the collection.`)} - - - ); - }; - - private openLoadOrderPage = (): void => { - this.context.api.events.emit(`show-main-page`, `generic-loadorder`); - }; - - private renderOpenLOButton = (): JSX.Element => { - const { t } = this.props; - return (); - }; - - private renderPlaceholder = (): JSX.Element => { - const { t } = this.props; - return ( - - ); - }; - - private renderModEntry = (modId: string): JSX.Element => { - const loEntry = this.state.sortedMods[modId]; - const key = modId + JSON.stringify(loEntry); - const name = util.renderModName(this.props.mods[modId]) ?? modId; - const classes = [`load-order-entry`, `collection-tab`]; - return ( - - -

{loEntry.pos}

-

{name}

-
-
- ); - }; -} - -const mapState = (state: types.IState, _ownProps: IOwnProps): IStateProps => { - const profile = selectors.activeProfile(state); - let loadOrder: ILoadOrder = {}; - if (profile?.gameId) { - loadOrder = util.getSafe(state, [`persistent`, `loadOrder`, profile.id], {}); - } - - return { - gameId: profile?.gameId, - loadOrder, - mods: util.getSafe(state, [`persistent`, `mods`, profile.gameId], {}), - profile, - }; -}; - -const mapDispatch = (_dispatch: Dispatch): IDispatchProps => ({}); - -export default withTranslation([`common`, NAMESPACE])(connect(mapState, mapDispatch)(CollectionsDataView)); -*/ diff --git a/src/views/Controls/TooltipImage.tsx b/src/views/Controls/TooltipImage.tsx deleted file mode 100644 index 4552b31..0000000 --- a/src/views/Controls/TooltipImage.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import * as React from 'react'; -import { OverlayTrigger, Popover } from 'react-bootstrap'; -import { ITooltipProps } from 'vortex-api/lib/controls/TooltipControls'; - -export interface ITooltipImgProps { - className?: string; - fixedWidth?: boolean; - flip?: 'horizontal' | 'vertical'; - src: string; - style?: React.CSSProperties; -} -export type ImgProps = ITooltipProps & ITooltipImgProps; - -export class TooltipImage extends React.Component { - public override render() { - const { tooltip, placement, ...relayProps } = this.props; - - const classes = ['fake-link'].concat((this.props.className ?? '').split(' ')); - - if (typeof this.props.tooltip === 'string') { - return ( - - - - ); - } else { - const tooltip = {this.props.tooltip}; - - return ( - - - - - - ); - } - } -} diff --git a/src/views/Controls/index.ts b/src/views/Controls/index.ts deleted file mode 100644 index 43278e8..0000000 --- a/src/views/Controls/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { TooltipImage } from './TooltipImage'; diff --git a/src/views/LoadOrder/components/ExternalBanner.tsx b/src/views/LoadOrder/components/ExternalBanner.tsx new file mode 100644 index 0000000..4e3615c --- /dev/null +++ b/src/views/LoadOrder/components/ExternalBanner.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { Icon, types } from 'vortex-api'; +import { IVortexViewModelData } from '../../../types'; +import { isExternal } from '../utils'; +import { useLocalization } from '../../../localization'; + +export type ExternalBannerProps = { + item: types.IFBLOLoadOrderEntry; +}; + +export const ExternalBanner = (props: ExternalBannerProps): JSX.Element | null => { + const { item } = props; + + const { localize: t } = useLocalization(); + + return isExternal(item) ? ( +
+ + {t('Not managed by Vortex')} +
+ ) : null; +}; diff --git a/src/views/LoadOrderInfoPanel/LoadOrderInfoPanel.tsx b/src/views/LoadOrder/components/LoadOrderInfoPanel.tsx similarity index 88% rename from src/views/LoadOrderInfoPanel/LoadOrderInfoPanel.tsx rename to src/views/LoadOrder/components/LoadOrderInfoPanel.tsx index 5b43225..be5c94d 100644 --- a/src/views/LoadOrderInfoPanel/LoadOrderInfoPanel.tsx +++ b/src/views/LoadOrder/components/LoadOrderInfoPanel.tsx @@ -1,19 +1,17 @@ -import React from 'react'; -import { util, tooltip } from 'vortex-api'; -import { GetLocalizationManager } from '../../types'; +import React, { useCallback } from 'react'; +import { tooltip, util } from 'vortex-api'; +import { useLocalization } from '../../../localization'; -interface IBaseProps { +export type LoadOrderInfoPanelProps = { refresh: () => void; - getLocalizationManager: GetLocalizationManager; -} +}; -export function LoadOrderInfoPanel(props: IBaseProps) { - const { refresh, getLocalizationManager } = props; +export const LoadOrderInfoPanel = (props: LoadOrderInfoPanelProps): JSX.Element => { + const { refresh } = props; - const localizationManager = getLocalizationManager(); - const t = localizationManager.localize; + const { localize: t } = useLocalization(); - const openWiki = React.useCallback(() => { + const openWiki = useCallback(() => { util.opn(`https://wiki.nexusmods.com/index.php/Modding_Bannerlord_with_Vortex`).catch(() => null); }, []); const hint = t( @@ -93,4 +91,4 @@ export function LoadOrderInfoPanel(props: IBaseProps) {
); -} +}; diff --git a/src/views/LoadOrder/components/LoadOrderItemRenderer.tsx b/src/views/LoadOrder/components/LoadOrderItemRenderer.tsx new file mode 100644 index 0000000..d5d7a16 --- /dev/null +++ b/src/views/LoadOrder/components/LoadOrderItemRenderer.tsx @@ -0,0 +1,107 @@ +import React, { BaseSyntheticEvent, useCallback } from 'react'; +import { Checkbox, ListGroupItem } from 'react-bootstrap'; +import { useSelector, useStore } from 'react-redux'; +import { Icon, selectors, types } from 'vortex-api'; +import { types as vetypes } from '@butr/vortexextensionnative'; +import { ValidationError } from './ValidationError'; +import { ExternalBanner } from './ExternalBanner'; +import { ModuleDuplicates } from './ModuleDuplicates'; +import { ModuleProviderIcon } from './ModuleProviderIcon'; +import { IVortexViewModelData, VortexLoadOrderStorage } from '../../../types'; +import { CompatibilityInfo, ModuleIcon } from '../../Shared'; +import { isExternal, isLocked } from '../utils'; +import { IModuleCompatibilityInfo } from '../../../butr'; +import { versionToString } from '../../../launcher'; +import { actionsLoadOrder, IFBLOItemRendererProps } from '../../../loadOrder'; +import { hasPersistentLoadOrder } from '../../../vortex'; + +interface IFromState { + profile: types.IProfile | undefined; + loadOrder: VortexLoadOrderStorage; +} + +export type LoadOrderItemRendererProps = { + className?: string; + item: IFBLOItemRendererProps; + availableProviders: vetypes.ModuleProviderType[]; + compatibilityInfo: IModuleCompatibilityInfo | undefined; +}; + +export const LoadOrderItemRenderer = (props: LoadOrderItemRendererProps): JSX.Element => { + const { className, item, availableProviders, compatibilityInfo } = props; + const { loadOrder, profile } = useSelector(mapState); + + const key = item.loEntry.id; + const name = item.loEntry.name ? `${item.loEntry.name}` : `${item.loEntry.id}`; + const version = item.loEntry.data?.moduleInfoExtended.version + ? versionToString(item.loEntry.data.moduleInfoExtended.version) + : 'ERROR'; + + const position = + loadOrder.findIndex((entry: types.IFBLOLoadOrderEntry) => entry.id === item.loEntry.id) + 1; + + let classes = ['load-order-entry']; + if (className !== undefined) { + classes = classes.concat(className.split(' ')); + } + + if (isExternal(item.loEntry)) { + classes = classes.concat('external'); + } + + const store = useStore(); + + const onStatusChange = useCallback( + (evt: BaseSyntheticEvent) => { + const entry = { + ...item.loEntry, + enabled: evt.target.checked, + }; + if (profile) { + store.dispatch(actionsLoadOrder.setFBLoadOrderEntry(profile.id, entry)); + } + }, + [store, item, profile] + ); + + const CheckBox = (): JSX.Element | null => + item.displayCheckboxes ? ( + + ) : null; + + const Lock = (): JSX.Element | null => + isLocked(item.loEntry) ? : null; + + return ( + + +

{position}

+ + +

+ {name} ({version}) +

+ + + + + + +
+ ); + // We can render a folder icon via `icon-browse` +}; + +const mapState = (state: types.IState): IFromState => { + const profile: types.IProfile | undefined = selectors.activeProfile(state); + const loadOrder = hasPersistentLoadOrder(state.persistent) ? state.persistent.loadOrder[profile?.id] ?? [] : []; + return { + profile, + loadOrder, + }; +}; diff --git a/src/views/LoadOrder/components/ModuleDuplicates.tsx b/src/views/LoadOrder/components/ModuleDuplicates.tsx new file mode 100644 index 0000000..ad82834 --- /dev/null +++ b/src/views/LoadOrder/components/ModuleDuplicates.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { tooltip } from 'vortex-api'; +import { types as vetypes } from '@butr/vortexextensionnative'; +import { IVortexViewModelData } from '../../../types'; +import { useLocalization } from '../../../localization'; + +export type ModuleDuplicatesProps = { + availableProviders: vetypes.ModuleProviderType[]; + data: IVortexViewModelData | undefined; +}; + +export const ModuleDuplicates = (props: ModuleDuplicatesProps): JSX.Element => { + const { availableProviders, data } = props; + + const { localize: t } = useLocalization(); + + if (availableProviders.length <= 1) { + return
; + } + + if (data?.moduleInfoExtended.moduleProviderType !== undefined) { + //const redundantProviders = availableProviders.filter( + // (provider) => provider !== item.data?.moduleInfoExtended.moduleProviderType + //); + // tooltip={t(`The mod is also installed via ${redundantProviders.join(', ')}!`)} + return ( + + ); + } + + return
; +}; diff --git a/src/views/LoadOrder/components/ModuleProviderIcon.tsx b/src/views/LoadOrder/components/ModuleProviderIcon.tsx new file mode 100644 index 0000000..cc4f809 --- /dev/null +++ b/src/views/LoadOrder/components/ModuleProviderIcon.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { IVortexViewModelData } from '../../../types'; +import { TooltipImage } from '../../Shared'; +import { STEAM_LOGO } from '../../../common'; +import { isSteamWorksop } from '../utils'; +import { useLocalization } from '../../../localization'; + +export type ModuleProviderIconProps = { + data: IVortexViewModelData | undefined; +}; + +export const ModuleProviderIcon = (props: ModuleProviderIconProps): JSX.Element => { + const { data } = props; + + const { localize: t } = useLocalization(); + + if (isSteamWorksop(data)) { + return ( + + ); + } + + return
; +}; diff --git a/src/views/LoadOrder/components/ValidationError.tsx b/src/views/LoadOrder/components/ValidationError.tsx new file mode 100644 index 0000000..b025a96 --- /dev/null +++ b/src/views/LoadOrder/components/ValidationError.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { tooltip, types } from 'vortex-api'; +import { IVortexViewModelData } from '../../../types'; + +export type ValidationErrorProps = { + invalidEntries: types.IFBLOInvalidResult[] | undefined; + item: types.IFBLOLoadOrderEntry; +}; + +export const ValidationError = (props: ValidationErrorProps): JSX.Element | null => { + const { invalidEntries, item } = props; + const invalidEntryList = invalidEntries + ? invalidEntries + .filter((inv) => inv.id.toLowerCase() === item.id.toLowerCase()) + .map((x) => x.reason) + .join('\n') + : undefined; + return invalidEntryList !== undefined && invalidEntryList.length ? ( + + ) : null; +}; diff --git a/src/views/LoadOrder/components/index.ts b/src/views/LoadOrder/components/index.ts new file mode 100644 index 0000000..cfeaaaa --- /dev/null +++ b/src/views/LoadOrder/components/index.ts @@ -0,0 +1,6 @@ +export * from './ExternalBanner'; +export * from './LoadOrderInfoPanel'; +export * from './LoadOrderItemRenderer'; +export * from './ModuleDuplicates'; +export * from './ModuleProviderIcon'; +export * from './ValidationError'; diff --git a/src/views/LoadOrder/index.ts b/src/views/LoadOrder/index.ts new file mode 100644 index 0000000..487e74a --- /dev/null +++ b/src/views/LoadOrder/index.ts @@ -0,0 +1,2 @@ +export * from './components/LoadOrderInfoPanel'; +export * from './components/LoadOrderItemRenderer'; diff --git a/src/views/LoadOrder/utils.ts b/src/views/LoadOrder/utils.ts new file mode 100644 index 0000000..4fe5082 --- /dev/null +++ b/src/views/LoadOrder/utils.ts @@ -0,0 +1,14 @@ +import { types } from 'vortex-api'; +import { IVortexViewModelData } from '../../types'; + +export const isExternal = (item: types.IFBLOLoadOrderEntry): boolean => { + return item.modId === undefined; +}; + +export const isLocked = (item: types.IFBLOLoadOrderEntry): boolean => { + return [true, 'true', 'always'].includes(item.locked as types.FBLOLockState); +}; + +export const isSteamWorksop = (data: IVortexViewModelData | undefined): boolean => { + return data?.moduleInfoExtended.moduleProviderType === 'Steam'; +}; diff --git a/src/views/LoadOrderInfoPanel/index.ts b/src/views/LoadOrderInfoPanel/index.ts deleted file mode 100644 index cf6271c..0000000 --- a/src/views/LoadOrderInfoPanel/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { LoadOrderInfoPanel } from './LoadOrderInfoPanel'; diff --git a/src/views/LoadOrderItemRenderer/LoadOrderItemRenderer.tsx b/src/views/LoadOrderItemRenderer/LoadOrderItemRenderer.tsx deleted file mode 100644 index 01160dd..0000000 --- a/src/views/LoadOrderItemRenderer/LoadOrderItemRenderer.tsx +++ /dev/null @@ -1,275 +0,0 @@ -import * as React from 'react'; -import { Checkbox, ListGroupItem } from 'react-bootstrap'; -import { useSelector } from 'react-redux'; - -import { Icon, selectors, tooltip, types, util } from 'vortex-api'; -import { - GetLauncherManager, - GetLocalizationManager, - IModuleCompatibilityInfo, - IVortexViewModelData, -} from '../../types'; -import { versionToString, actionsLoadOrder } from '../../utils'; -import { MODULE_LOGO, STEAM_LOGO, TW_LOGO } from '../../common'; -import { types as vetypes, Utils } from '@butr/vortexextensionnative'; -import { TooltipImage } from '../Controls'; - -interface IBaseProps { - api: types.IExtensionApi; - getLauncherManager: GetLauncherManager; - getLocalizationManager: GetLocalizationManager; - className?: string; - item: types.IFBLOItemRendererProps; - availableProviders: vetypes.ModuleProviderType[]; - compatibilityInfo: IModuleCompatibilityInfo | undefined; -} - -interface IConnectedProps { - loadOrder: types.FBLOLoadOrder; - profile: types.IProfile; - modState: unknown; -} - -export function BannerlordItemRenderer(props: IBaseProps): JSX.Element { - const { api, className, item } = props; - const { loadOrder, profile } = useSelector(mapStateToProps); - - const name = item.loEntry.name ? `${item.loEntry.name}` : `${item.loEntry.id}`; - const version = versionToString(item.loEntry.data?.moduleInfoExtended.version); - - const position = - loadOrder.findIndex((entry: types.IFBLOLoadOrderEntry) => entry.id === item.loEntry.id) + 1; - - let classes = ['load-order-entry']; - if (className !== undefined) { - classes = classes.concat(className.split(' ')); - } - - if (isExternal(item.loEntry)) { - classes = classes.concat('external'); - } - - const onStatusChange = React.useCallback( - (evt: React.BaseSyntheticEvent) => { - const entry = { - ...item.loEntry, - enabled: evt.target.checked, - }; - api.store?.dispatch(actionsLoadOrder.setFBLoadOrderEntry(profile.id, entry)); - }, - [api, item, profile] - ); - - const checkBox = () => - item.displayCheckboxes ? ( - - ) : null; - - const lock = () => (isLocked(item.loEntry) ? : null); - - return ( - - -

{position}

- {RenderValidationError(props)} - {RenderModuleIcon(item.loEntry)} -

- {name} ({version}) -

- {RenderExternalBanner(props, item.loEntry)} - {RenderCompatibilityInfo(props)} - {RenderModuleDuplicates(props, item.loEntry)} - {RenderModuleProviderIcon(props, item.loEntry)} - {checkBox()} - {lock()} -
- ); - // We can render a folder icon via `icon-browse` -} - -function RenderModuleIcon(item: types.IFBLOLoadOrderEntry) { - const isOfficial = item.data !== undefined && item.data.moduleInfoExtended.isOfficial; - const isCommunity = item.data !== undefined && !item.data.moduleInfoExtended.isOfficial; - const dependencies = item.data !== undefined ? Utils.getDependencyHint(item.data.moduleInfoExtended) : ''; - - if (isOfficial) { - return ( - - ); - } - - if (isCommunity) { - return ( - - ); - } - - return ; -} - -function RenderValidationError(props: IBaseProps) { - const { invalidEntries, loEntry } = props.item; - const invalidEntryList = - invalidEntries !== undefined - ? invalidEntries - .filter((inv) => inv.id.toLowerCase() === loEntry.id.toLowerCase()) - .map((x) => x.reason) - .join('\n') - : undefined; - return invalidEntryList !== undefined && invalidEntryList !== '' ? ( - - ) : null; -} - -function RenderExternalBanner(props: IBaseProps, item: types.IFBLOLoadOrderEntry) { - const { getLocalizationManager } = props; - - const localizationManager = getLocalizationManager(); - const t = localizationManager.localize; - - return isExternal(item) ? ( -
- - {t('Not managed by Vortex')} -
- ) : null; -} - -function RenderModuleProviderIcon(props: IBaseProps, item: types.IFBLOLoadOrderEntry) { - const { getLocalizationManager } = props; - - const localizationManager = getLocalizationManager(); - const t = localizationManager.localize; - - if (isSteamWorksop(item)) { - return ( - - ); - } - - return
; -} - -function RenderModuleDuplicates(props: IBaseProps, item: types.IFBLOLoadOrderEntry) { - const { getLocalizationManager, availableProviders } = props; - - const localizationManager = getLocalizationManager(); - const t = localizationManager.localize; - - if (availableProviders.length <= 1) { - return
; - } - - if (item.data?.moduleInfoExtended.moduleProviderType) { - //const redundantProviders = availableProviders.filter( - // (provider) => provider !== item.data?.moduleInfoExtended.moduleProviderType - //); - // tooltip={t(`The mod is also installed via ${redundantProviders.join(', ')}!`)} - return ( - - ); - } - - return
; -} - -function RenderCompatibilityInfo(props: IBaseProps) { - const { compatibilityInfo: compatibilityScore, item, getLauncherManager, getLocalizationManager } = props; - - if (compatibilityScore === undefined) { - return
; - } - - const launcherManager = getLauncherManager(); - const localizationManager = getLocalizationManager(); - const t = localizationManager.localize; - - const hasRecommendation = - compatibilityScore.recommendedVersion !== undefined && compatibilityScore.recommendedVersion !== null; - - const hint = hasRecommendation - ? t( - `{=HdnFwgVB}Based on BUTR analytics:{NL}{NL}Compatibility Score {SCORE}%{NL}{NL}Suggesting to update to {RECOMMENDEDVERSION}.{NL}Compatibility Score {RECOMMENDEDSCORE}%{NL}{NL}{RECOMMENDEDVERSION} has a better compatibility for game {GAMEVERSION} rather than {CURRENTVERSION}!`, - { - NL: '\n', - SCORE: compatibilityScore.score.toString(), - RECOMMENDEDVERSION: compatibilityScore.recommendedVersion?.toString() ?? '', - RECOMMENDEDSCORE: compatibilityScore.recommendedScore?.toString() ?? '', - GAMEVERSION: launcherManager.getGameVersionVortex(), - CURRENTVERSION: versionToString(item.loEntry.data?.moduleInfoExtended.version), - } - ) - : t( - `{=HdnFwgVA}Based on BUTR analytics:{NL}{NL}Update is not requiured.{NL}Compatibility Score {SCORE}%{NL}{NL}{CURRENTVERSION} is one of the best version for game {GAMEVERSION}`, - { - NL: '\n', - SCORE: compatibilityScore.score.toString(), - CURRENTVERSION: versionToString(item.loEntry.data?.moduleInfoExtended.version), - GAMEVERSION: launcherManager.getGameVersionVortex(), - } - ); - - const color = compatibilityScore.score >= 75 ? 'green' : compatibilityScore.score >= 50 ? 'yellow' : 'red'; - - return ( - - ); -} - -function isLocked(item: types.IFBLOLoadOrderEntry): boolean { - return [true, 'true', 'always'].includes(item.locked as types.FBLOLockState); -} - -function isExternal(item: types.IFBLOLoadOrderEntry): boolean { - return item.modId !== undefined ? false : true; -} - -function isSteamWorksop(item: types.IFBLOLoadOrderEntry): boolean { - return item.data?.moduleInfoExtended.moduleProviderType === 'Steam'; -} - -const empty = {}; -function mapStateToProps(state: types.IState): IConnectedProps { - const profile: types.IProfile = selectors.activeProfile(state); - return { - profile, - loadOrder: util.getSafe(state, ['persistent', 'loadOrder', profile.id], []), - modState: util.getSafe(profile, ['modState'], empty), - }; -} diff --git a/src/views/LoadOrderItemRenderer/index.ts b/src/views/LoadOrderItemRenderer/index.ts deleted file mode 100644 index 08228a0..0000000 --- a/src/views/LoadOrderItemRenderer/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { BannerlordItemRenderer } from './LoadOrderItemRenderer'; diff --git a/src/views/Saves/SaveList.tsx b/src/views/Saves/SaveList.tsx deleted file mode 100644 index 8673a1d..0000000 --- a/src/views/Saves/SaveList.tsx +++ /dev/null @@ -1,578 +0,0 @@ -import React from 'react'; -import { Panel, Radio } from 'react-bootstrap'; -import ticksToDate from 'ticks-to-date'; -import { - ComponentEx, - FlexLayout, - IconBar, - ITableRowAction, - MainPage, - selectors, - Table, - ToolbarIcon, - tooltip, - types, -} from 'vortex-api'; -import { BannerlordModuleManager, types as vetypes } from '@butr/vortexextensionnative'; -import { - getModulesByName, - getLoadOrderIssues, - getMismatchedModuleVersions, - getMissingModuleNames, - getNameDuplicates, -} from './saveUtils'; -import { ISaveGame } from './types'; -import { versionToString } from '../../utils'; -import { - GetLauncherManager, - GetLocalizationManager, - GetSaveManager, - IItemRendererProps, - IModuleCache, -} from '../../types'; -import { IBaseProps as IIconBarBaseProps } from 'vortex-api/lib/controls/IconBar'; -import { IActionControlProps } from 'vortex-api/lib/controls/ActionControl'; -import { IExtensibleProps } from 'vortex-api/lib/types/IExtensionProvider'; -import { IBaseProps as ITableBaseProps } from 'vortex-api/lib/controls/Table'; -import { findBLSEMod, isModActive } from '../../utils/blse/shared'; - -type IOwnProps = IItemRendererProps & { - context: types.IExtensionContext; - getLauncherManager: GetLauncherManager; - getSaveManager: GetSaveManager; - getLocalizationManager: GetLocalizationManager; -}; - -interface IBaseState { - hasBLSE: boolean; - saves: vetypes.SaveMetadata[]; - allModules: Readonly; - // TODO: Move to the entry renderer - loadOrder: { [saveName: string]: string }; - selectedRow?: ISaveGame | undefined; - selectedSave?: ISaveGame | undefined; -} - -type IComponentProps = IOwnProps; -type IComponentState = IBaseState; - -const TableWrapper = Table as React.ComponentType; -const IconWrapper = IconBar as React.ComponentType< - IIconBarBaseProps & IActionControlProps & IExtensibleProps & React.HTMLAttributes ->; - -export type ISaveListProps = IComponentProps; - -// TODO: Reload on localization change -export class SaveList extends ComponentEx { - private mStaticButtons: types.IActionDefinition[]; - private saveGameActions: ITableRowAction[]; - private savesGames: { [name: string]: ISaveGame } = {}; - private tableAttributes: types.ITableAttribute[]; - private storedSaveGameName: string | undefined = undefined; - private sortedSaveGames: [string, ISaveGame][] = []; - - // eslint-disable-next-line max-lines-per-function - constructor(props: IComponentProps) { - super(props); - - const { context, getLauncherManager, getSaveManager, getLocalizationManager } = props; - const launcherManager = getLauncherManager(); - const saveManager = getSaveManager(); - const localizationManager = getLocalizationManager(); - const t = localizationManager.localize; - - const vortexState = context.api.getState(); - const vortexActiveProfile = selectors.activeProfile(vortexState); - - const blseMod = findBLSEMod(context.api); - const hasBLSE = !!blseMod && isModActive(vortexActiveProfile, blseMod); - const saves = launcherManager.getSaveFiles(); - const allModules = launcherManager.getAllModules(); - - // need to init the state so it saves with vortex - this.initState({ - hasBLSE, - saves, - allModules: allModules, - loadOrder: {}, - }); - - // get list of save games - this.reloadSaves(); - - this.storedSaveGameName = saveManager.getSave() ?? 'No Save'; - if (this.storedSaveGameName) { - const foundSave = Object.values(this.savesGames).find((value) => value.name === this.storedSaveGameName); - if (foundSave) { - this.setState({ selectedSave: foundSave, selectedRow: foundSave }); - } else { - this.storedSaveGameName = 'No Save'; - saveManager.setSave(null); - } - } else { - this.storedSaveGameName = 'No Save'; - saveManager.setSave(null); - } - - this.mStaticButtons = [ - { - component: ToolbarIcon, - props: () => ({ - id: `btn-refresh-list`, - key: `btn-refresh-list`, - icon: `refresh`, - text: t(`Refresh`), - className: `load-order-refresh-list`, - onClick: () => this.reloadSaves(), - }), - }, - ]; - - this.saveGameActions = []; - - // basically column data - this.tableAttributes = [ - { - id: '#', - name: '#', - customRenderer: (data: [string, ISaveGame]) => this.GetRadioCustomRenderer(data[1], this.storedSaveGameName), - placement: 'both', - edit: {}, - }, - { - id: 'name', - name: t('{=JtelOsIW}Name'), - calc: (data: [string, ISaveGame]) => data[1].name, - placement: 'both', - edit: {}, - }, - { - id: 'characterName', - name: t('{=OJsGrGVi}Character'), - calc: (data: [string, ISaveGame]) => data[1].characterName ?? '', - placement: 'both', - edit: {}, - }, - { - id: 'mainHeroLevel', - name: t('{=JxpEEQdF}Level'), - calc: (data: [string, ISaveGame]) => data[1].mainHeroLevel ?? '', - placement: 'both', - edit: {}, - }, - { - id: 'dayLong', - name: t('{=qkkTPycE}Days'), - calc: (data: [string, ISaveGame]) => data[1].dayLong?.toFixed(0) ?? '', - placement: 'both', - edit: {}, - }, - { - id: 'status', - name: t('Status'), - customRenderer: (data: [string, ISaveGame]) => this.StatusCustomRenderer(data[1]), - placement: 'both', - edit: {}, - }, - { - id: 'applicationVersion', - name: t('{=14WBFIS1}Version'), - calc: (data: [string, ISaveGame]) => - data[1].applicationVersion ? versionToString(data[1].applicationVersion) : '', - placement: 'both', - edit: {}, - }, - { - id: 'creationTime', - name: t('{=aYWWDkKX}CreatedAt'), - calc: (data: [string, ISaveGame]) => ticksToDate(data[1].creationTime)?.toLocaleString(), - placement: 'both', - edit: {}, - }, - ]; - } - - public override render(): JSX.Element { - const { context } = this.props; - - const t = context.api.translate; - - return ( - - - - - {this.renderContent(this.saveGameActions)} - - ); - } - - private renderContent(saveActions: ITableRowAction[]) { - const { getLocalizationManager } = this.props; - const { selectedRow, selectedSave } = this.state; - - const localizationManager = getLocalizationManager(); - const t = localizationManager.localize; - - return ( - - - - -

- {t( - `Instructions: Select a row to see more information and use the radio buttons to select the save to ` + - `launch the game. If you don't want to launch with a save, choose the 'No Save' option at` + - `the top.` - )} -

-

- {t(`Currently selected save: `)} - {selectedSave?.name} -

-
- - - - - this.Table_OnChangeSelection(this.sortedSaveGames[parseInt(ids[0]!)]![1]) - } - /> - - - {this.RenderSidebar(selectedRow)} - -
-
-
- ); - } - - private Table_OnChangeSelection(saveGame: ISaveGame) { - // when a row is selected - - //const selectedIndex = parseInt(ids[0]); - - // get current state object - let { selectedRow } = this.state; - - // get save game from selected row index - //const saveGame = this.sortedSaveGames[selectedIndex][1]; - - //console.log(`BANNERLORD: OnChangeSelection(${ids}) selectedIndex=${selectedIndex} saveGame=`); - //console.log(saveGame); - - // update it - selectedRow = saveGame; - - // save it - this.setState({ selectedRow }); - } - private RenderSidebar(saveGame: ISaveGame | undefined): JSX.Element { - const { getLocalizationManager } = this.props; - - const localizationManager = getLocalizationManager(); - const t = localizationManager.localize; - - // if nothing is selected - if (!saveGame) { - return <>; - } - - // something is selected - return ( - <> - {

{saveGame.name}

} - {this.GetIssueRenderSnippet( - t('{=HvvA78sZ}Load Order Issues:{NL}{LOADORDERISSUES}', { - NL: '', - LOADORDERISSUES: '', - }), - saveGame.loadOrderIssues - )} - {this.GetIssueRenderSnippet( - t('{=GtDRbC3m}Missing Modules:{NL}{MODULES}', { - NL: '', - MODULES: '', - }), - saveGame.missingModules - )} - {this.GetIssueRenderSnippet( - t('{=vCwH9226}Duplicate Module Names:{NL}{MODULENAMES}', { - NL: '', - MODULENAMES: '', - }), - saveGame.duplicateModules - )} - {this.GetIssueRenderSnippet( - t('{=BuMom4Jt}Mismatched Module Versions:{NL}{MODULEVERSIONS}', { - NL: '', - MODULEVERSIONS: '', - }), - saveGame.mismatchedModuleVersions - )} - - ); - } - private GetIssueRenderSnippet(issueHeading: string, issue: string[] | undefined): JSX.Element { - // if we have something in the issue array, then return that nicely formatted - if (issue && issue.length) { - return ( - <> -

{issueHeading}

-
    - {issue.map((object, i) => ( -
  • {object}
  • - ))} -
- - ); - } - - return <>; - } - - // Table - private GetRadioCustomRenderer(saveGame: ISaveGame, storedSaveGame: string | undefined): JSX.Element { - const { hasBLSE } = this.state; - - return hasBLSE ? ( - this.Radio_OnChange(saveGame)} - > - ) : ( -
- ); - } - private Radio_OnChange(saveGame: ISaveGame) { - // when a save is selected that we need to send to launcher - - //console.log(`BANNERLORD: Radio_OnChange(${saveGame.name}) saveGame=`); - //console.log(saveGame); - - const { getSaveManager } = this.props; - const saveManager = getSaveManager(); - - // get current state object - let { selectedSave } = this.state; - - // update it - selectedSave = saveGame; - - // save it in local state - this.setState({ selectedSave }); - - if (saveGame.index !== 0) { - saveManager.setSave(saveGame.name); - } else { - saveManager.setSave(null); - } - } - - private checkIssues( - issueArray: string[] | undefined, - iconName: string, - colorName: string, - issueMessage: string - ): [string, string, string[]] { - let newIconName = iconName; - let newColorName = colorName; - const issues: string[] = []; - - if (issueArray && issueArray.length) { - newIconName = 'feedback-warning'; - newColorName = 'var(--brand-danger)'; - issues.push(`${issueArray.length} ${issueMessage}`); - } - - return [newIconName, newColorName, issues]; - } - - // Table - private StatusCustomRenderer(saveGame: ISaveGame): JSX.Element { - const { getLocalizationManager } = this.props; - - const localizationManager = getLocalizationManager(); - const t = localizationManager.localize; - - let iconName = 'toggle-enabled'; - let colorName = 'var(--brand-success)'; - let issues: string[] = []; - - [iconName, colorName, issues] = this.checkIssues( - saveGame.loadOrderIssues, - iconName, - colorName, - t('load order issues') - ); - [iconName, colorName, issues] = this.checkIssues( - saveGame.missingModules, - iconName, - colorName, - t('missing modules') - ); - [iconName, colorName, issues] = this.checkIssues( - saveGame.duplicateModules, - iconName, - colorName, - t('duplicate modules') - ); - [iconName, colorName, issues] = this.checkIssues( - saveGame.mismatchedModuleVersions, - iconName, - colorName, - t('version mismatches') - ); - - return ; - } - - private reloadSaves() { - const { getLauncherManager, getLocalizationManager } = this.props; - - const launcherManager = getLauncherManager(); - const localizationManager = getLocalizationManager(); - const t = localizationManager.localize; - - const saves: vetypes.SaveMetadata[] = launcherManager.getSaveFiles(); - const allModules = launcherManager.getAllModules(); - - // build new ISaveGame from SaveMetadata, just to make it easier to work with - - // add starting entry to top of list (didn't work as numbers got added first, but we are setting the index anyway to sort later) - this.savesGames['nosave'] = { - index: 0, - name: t('No Save'), - modules: {}, - }; - - // add savesDict as starting object and keep adding to it - saves.reduce((prev: { [name: string]: ISaveGame }, current, currentIndex) => { - const saveGame = this.createSaveGame(allModules, current, currentIndex); - if (!saveGame) { - return prev; - } - - this.parseSave(allModules, saveGame); - prev[current.Name] = saveGame; - - return prev; - }, this.savesGames); - - // build up saves object ready for table display - - // add tempSaves built from the launcher - //this.savesDict = Object.assign(this.savesDict, tempSaves); - - this.sortedSaveGames = Object.entries(this.savesGames).sort((a, b) => a[1].index - b[1].index); - - this.setState({ - saves, - allModules: allModules, - }); - } - - private parseSave(allModules: Readonly, saveGame: ISaveGame) { - const { getLocalizationManager } = this.props; - const { loadOrder } = this.state; - - const localizationManager = getLocalizationManager(); - const t = localizationManager.localize; - - const allModulesByName = getModulesByName(allModules); - const unknownId = t('{=kxqLbSqe}(Unknown ID)'); - const newLoadOrder = { ...loadOrder }; - - Object.keys(saveGame.modules).forEach((current) => { - const moduleId = allModulesByName[current]?.id ?? `${current} ${unknownId}`; - if (moduleId !== undefined) { - newLoadOrder[saveGame.name] = t('{=sd6M4KRd}Load Order:{NL}{LOADORDER}', { - LOADORDER: moduleId, - }); - } - }); - - this.setState({ loadOrder: newLoadOrder }); - } - - private createSaveGame(allModules: Readonly, current: vetypes.SaveMetadata, currentIndex: number) { - if (!current['Modules']) { - return undefined; - } - - const { getLocalizationManager } = this.props; - - const localizationManager = getLocalizationManager(); - - const saveGame: ISaveGame = { - index: currentIndex + 1, - name: current.Name, - applicationVersion: - current['ApplicationVersion'] !== undefined - ? BannerlordModuleManager.parseApplicationVersion(current['ApplicationVersion']) - : undefined, - creationTime: current['CreationTime'] !== undefined ? parseInt(current['CreationTime']) : undefined, - characterName: current['CharacterName'], - mainHeroGold: current['MainHeroGold'] !== undefined ? parseInt(current['MainHeroGold']) : undefined, - mainHeroLevel: current['MainHeroLevel'] !== undefined ? parseInt(current['MainHeroLevel']) : undefined, - dayLong: current['DayLong'] !== undefined ? parseFloat(current['DayLong']) : undefined, - - clanBannerCode: current['ClanBannerCode'], - clanFiefs: current['ClanFiefs'] !== undefined ? parseInt(current['ClanFiefs']) : undefined, - clanInfluence: current['ClanInfluence'] !== undefined ? parseFloat(current['ClanInfluence']) : undefined, - - mainPartyFood: current['MainPartyFood'] !== undefined ? parseFloat(current['MainPartyFood']) : undefined, - mainPartyHealthyMemberCount: - current['MainPartyHealthyMemberCount'] !== undefined - ? parseInt(current['MainPartyHealthyMemberCount']) - : undefined, - mainPartyPrisonerMemberCount: - current['MainPartyPrisonerMemberCount'] !== undefined - ? parseInt(current['MainPartyPrisonerMemberCount']) - : undefined, - mainPartyWoundedMemberCount: - current['MainPartyWoundedMemberCount'] !== undefined - ? parseInt(current['MainPartyWoundedMemberCount']) - : undefined, - version: current['Version'] !== undefined ? parseInt(current['Version']) : undefined, - modules: {}, // blank dictionary for now - }; - - // build up modules dictionary? - const moduleNames = current['Modules'].split(';'); - - const saveChangeSet = saveGame.applicationVersion?.changeSet ?? 0; - for (const module of moduleNames) { - const key = module; - const moduleValue = current['Module_' + module]; - if (!moduleValue) { - continue; - } - - const version = BannerlordModuleManager.parseApplicationVersion(moduleValue); - if (version.changeSet === saveChangeSet) { - version.changeSet = 0; - } - saveGame.modules[key] = version; - } - - saveGame.duplicateModules = getNameDuplicates(allModules); - saveGame.loadOrderIssues = getLoadOrderIssues(saveGame, allModules); - saveGame.missingModules = getMissingModuleNames(saveGame, allModules); - saveGame.mismatchedModuleVersions = getMismatchedModuleVersions(saveGame, localizationManager, allModules); - - return saveGame; - } -} diff --git a/src/views/Saves/SavePageOptions.ts b/src/views/Saves/SavePageOptions.ts deleted file mode 100644 index 90ff6de..0000000 --- a/src/views/Saves/SavePageOptions.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { selectors, types } from 'vortex-api'; -import { GAME_ID } from '../../common'; -import { GetLauncherManager, GetLocalizationManager, GetSaveManager } from '../../types'; - -export class SavePageOptions implements types.IMainPageOptions { - private context: types.IExtensionContext; - private getLauncherManager: GetLauncherManager; - private getSaveManager: GetSaveManager; - private getLocalizationManager: GetLocalizationManager; - - public id = 'bannerlord-saves'; - public hotkey = 'A'; - public group: 'dashboard' | 'global' | 'per-game' | 'support' | 'hidden' = 'per-game'; - - constructor( - context: types.IExtensionContext, - getLauncherManager: GetLauncherManager, - getSaveManager: GetSaveManager, - getLocalizationManager: GetLocalizationManager - ) { - this.context = context; - this.getLauncherManager = getLauncherManager; - this.getSaveManager = getSaveManager; - this.getLocalizationManager = getLocalizationManager; - } - - public visible = () => { - if (!this.context.api.store) { - return false; - } - return selectors.activeGameId(this.context.api.getState()) === GAME_ID; - }; - public props = () => ({ - context: this.context, - getLauncherManager: this.getLauncherManager, - getSaveManager: this.getSaveManager, - getLocalizationManager: this.getLocalizationManager, - }); -} diff --git a/src/views/Saves/components/Content.tsx b/src/views/Saves/components/Content.tsx new file mode 100644 index 0000000..17f6de9 --- /dev/null +++ b/src/views/Saves/components/Content.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import { Panel } from 'react-bootstrap'; +import { FlexLayout, ITableRowAction, Table, types } from 'vortex-api'; +import { Sidebar } from './Sidebar'; +import { ISaveGame } from '../types'; +import { useLocalization } from '../../../localization'; + +export type ContentProps = { + selectedSave: ISaveGame | null; + saveActions: ITableRowAction[]; + sortedSaveGameList: [string, ISaveGame][]; + tableAttributes: types.ITableAttribute<[string, ISaveGame]>[]; + selectedRowSave: ISaveGame | null; + saveRowSelected: (save: ISaveGame) => void; +}; + +export const Content = (props: ContentProps): JSX.Element => { + const { selectedSave, saveActions, sortedSaveGameList, tableAttributes, selectedRowSave, saveRowSelected } = props; + + const { localize: t } = useLocalization(); + + return ( + + + + +

+ {t( + `Instructions: Select a row to see more information and use the radio buttons to select the save to ` + + `launch the game. If you don't want to launch with a save, choose the 'No Save' option at` + + `the top.` + )} +

+

+ {t(`Currently selected save: `)} + {selectedSave?.name} +

+
+ + + + saveRowSelected(sortedSaveGameList[parseInt(ids[0]!)]![1])} + /> + + + + + + + + + + ); +}; diff --git a/src/views/Saves/components/IssueSnippet.tsx b/src/views/Saves/components/IssueSnippet.tsx new file mode 100644 index 0000000..1a007ba --- /dev/null +++ b/src/views/Saves/components/IssueSnippet.tsx @@ -0,0 +1,25 @@ +import React from 'react'; + +export type IssueSnippetProps = { + issueHeading: string; + issue: string[] | undefined; +}; + +export const IssueSnippet = (props: IssueSnippetProps): JSX.Element => { + const { issueHeading, issue } = props; + + if (issue && issue.length) { + return ( + <> +

{issueHeading}

+
    + {issue.map((object, i) => ( +
  • {object}
  • + ))} +
+ + ); + } + + return <>; +}; diff --git a/src/views/Saves/components/RadioView.tsx b/src/views/Saves/components/RadioView.tsx new file mode 100644 index 0000000..e641949 --- /dev/null +++ b/src/views/Saves/components/RadioView.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { types } from 'vortex-api'; +import { Radio } from 'react-bootstrap'; +import { ISaveGame } from '../types'; + +export type RadioViewProps = { + api: types.IExtensionApi; + save: ISaveGame; + selectedSave: ISaveGame | null; + hasBLSE: boolean; + onChange: (save: ISaveGame) => void; +}; + +// Custom Renderer has no Context access +export const RadioView = (props: RadioViewProps): JSX.Element => { + const { save, selectedSave, hasBLSE, onChange } = props; + + return hasBLSE ? ( + onChange(save)} + /> + ) : ( +
+ ); +}; diff --git a/src/views/Saves/components/Sidebar.tsx b/src/views/Saves/components/Sidebar.tsx new file mode 100644 index 0000000..07fb726 --- /dev/null +++ b/src/views/Saves/components/Sidebar.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { IssueSnippet } from './IssueSnippet'; +import { ISaveGame } from '../types'; +import { useLocalization } from '../../../localization'; + +export type SidebarProps = { + save: ISaveGame | null; +}; + +export const Sidebar = (props: SidebarProps): JSX.Element => { + const { save } = props; + + const { localize: t } = useLocalization(); + + // if nothing is selected + if (!save) { + return <>; + } + + // something is selected + return ( + <> + {

{save.name}

} + {IssueSnippet({ + issueHeading: t('{=HvvA78sZ}Load Order Issues:{NL}{LOADORDERISSUES}', { + NL: '', + LOADORDERISSUES: '', + }), + issue: save.loadOrderIssues, + })} + {IssueSnippet({ + issueHeading: t('{=GtDRbC3m}Missing Modules:{NL}{MODULES}', { + NL: '', + MODULES: '', + }), + issue: save.missingModules, + })} + {IssueSnippet({ + issueHeading: t('{=vCwH9226}Duplicate Module Names:{NL}{MODULENAMES}', { + NL: '', + MODULENAMES: '', + }), + issue: save.duplicateModules, + })} + {IssueSnippet({ + issueHeading: t('{=BuMom4Jt}Mismatched Module Versions:{NL}{MODULEVERSIONS}', { + NL: '', + MODULEVERSIONS: '', + }), + issue: save.mismatchedModuleVersions, + })} + + ); +}; diff --git a/src/views/Saves/components/StatusView.tsx b/src/views/Saves/components/StatusView.tsx new file mode 100644 index 0000000..d5d1f57 --- /dev/null +++ b/src/views/Saves/components/StatusView.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { tooltip, types } from 'vortex-api'; +import { ISaveGame } from '../types'; +import { LocalizationManager } from '../../../localization'; + +export type StatusViewProps = { + api: types.IExtensionApi; + save: ISaveGame; +}; + +// Custom Renderer has no Context access +export const StatusView = (props: StatusViewProps): JSX.Element => { + const appendIssues = (allIssues: string[], issues: string[] | undefined, message: string): void => { + if (issues && issues.length) { + allIssues.push(`${issues.length} ${message}`); + } + }; + + const { api, save } = props; + + const { localize: t } = LocalizationManager.getInstance(api); + + const allIssues: string[] = []; + appendIssues(allIssues, save.loadOrderIssues, t('load order issues')); + appendIssues(allIssues, save.missingModules, t('missing modules')); + appendIssues(allIssues, save.duplicateModules, t('duplicate modules')); + appendIssues(allIssues, save.mismatchedModuleVersions, t('version mismatches')); + + const icon = allIssues.length === 0 ? 'toggle-enabled' : 'feedback-warning'; + const color = allIssues.length === 0 ? 'var(--brand-success)' : 'var(--brand-danger)'; + + return ; +}; diff --git a/src/views/Saves/components/index.ts b/src/views/Saves/components/index.ts new file mode 100644 index 0000000..929be42 --- /dev/null +++ b/src/views/Saves/components/index.ts @@ -0,0 +1,5 @@ +export * from './Content'; +export * from './IssueSnippet'; +export * from './RadioView'; +export * from './Sidebar'; +export * from './StatusView'; diff --git a/src/views/Saves/index.ts b/src/views/Saves/index.ts index 6311fbc..b122abf 100644 --- a/src/views/Saves/index.ts +++ b/src/views/Saves/index.ts @@ -1,2 +1,2 @@ -export { SaveList } from './SaveList'; -export { SavePageOptions } from './SavePageOptions'; +export * from './pages/SavePage'; +export * from './options'; diff --git a/src/views/Saves/options.ts b/src/views/Saves/options.ts new file mode 100644 index 0000000..065aec5 --- /dev/null +++ b/src/views/Saves/options.ts @@ -0,0 +1,25 @@ +import { selectors, types } from 'vortex-api'; +import { SavePageProps } from './pages/SavePage'; +import { GAME_ID } from '../../common'; + +export class SavePageOptions implements types.IMainPageOptions { + private context: types.IExtensionContext; + + public id = 'bannerlord-saves'; + public hotkey = 'A'; + public group: 'dashboard' | 'global' | 'per-game' | 'support' | 'hidden' = 'per-game'; + + constructor(context: types.IExtensionContext) { + this.context = context; + } + + public visible = (): boolean => { + if (!this.context.api.store) { + return false; + } + return selectors.activeGameId(this.context.api.getState()) === GAME_ID; + }; + public props = (): SavePageProps => ({ + context: this.context, + }); +} diff --git a/src/views/Saves/pages/SavePage.tsx b/src/views/Saves/pages/SavePage.tsx new file mode 100644 index 0000000..5c64e4c --- /dev/null +++ b/src/views/Saves/pages/SavePage.tsx @@ -0,0 +1,224 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import ticksToDate from 'ticks-to-date'; +import { IconBar, ITableRowAction, MainPage, selectors, ToolbarIcon, types } from 'vortex-api'; +import { useSelector, useStore } from 'react-redux'; +import { Content, RadioView, StatusView } from '../components'; +import { ISaveGame } from '../types'; +import { getSaves } from '../utils'; +import { LocalizationManager, useLocalization } from '../../../localization'; +import { actionsSave } from '../../../save'; +import { versionToString, VortexLauncherManager } from '../../../launcher'; +import { getSaveFromSettings } from '../../../settings'; +import { findBLSEMod, isModActive } from '../../../blse'; + +interface IFromState { + profile: types.IProfile | undefined; + saveName: string; + hasBLSE: boolean; +} + +export type SavePageProps = { + context: types.IExtensionContext; +}; + +export const SavePage = (props: SavePageProps): JSX.Element => { + const { context } = props; + + const localizationManager = useLocalization(); + const { localize: t } = localizationManager; + + const { profile, saveName, hasBLSE } = useSelector(mapState); + + const store = useStore(); + + const mainButtonList = [ + { + component: ToolbarIcon, + props: () => ({ + id: `btn-refresh-list`, + key: `btn-refresh-list`, + icon: `refresh`, + text: t(`Refresh`), + className: `load-order-refresh-list`, + onClick: (): void => { + reloadSaves(); + }, + }), + }, + ]; + const saveActions: ITableRowAction[] = []; + + const [selectedRowSave, setSelectedRowSave] = useState(null); + const [selectedSave, setSelectedSave] = useState(null); + + const [sortedSaveGameList, setSortedSaveGames] = useState<[string, ISaveGame][]>([]); + + const saveRowSelected = (save: ISaveGame): void => { + setSelectedRowSave(save); + }; + + const setSave = useCallback( + (api: types.IExtensionApi, saveId: string | null): void => { + if (profile) { + store.dispatch(actionsSave.setCurrentSave(profile.id, saveId)); + } + + const launcherManager = VortexLauncherManager.getInstance(api); + launcherManager.setSaveFile(saveId ?? ''); + }, + [profile, store] + ); + + const saveSelected = useCallback( + (save: ISaveGame) => { + if (save.index !== 0) { + setSave(context.api, save.name); + } else { + setSave(context.api, null); + } + + setSelectedSave(save); + }, + [context.api, setSave] + ); + + const reloadSaves = useCallback(() => { + const saveList = getSaves(context.api); + setSortedSaveGames(Object.entries(saveList).sort(([, saveA], [, saveB]) => saveA.index - saveB.index)); + + const foundSave = Object.values(saveList).find((value) => value.name === saveName); + if (foundSave) { + setSelectedSave(foundSave); + setSelectedRowSave(foundSave); + } else { + setSelectedSave(null); + setSelectedRowSave(null); + setSave(context.api, null); + } + }, [context.api, saveName, setSave]); + + useEffect(() => { + reloadSaves(); + }, [reloadSaves]); + + return ( + + + + + + {Content({ + selectedSave: selectedSave, + saveActions: saveActions, + sortedSaveGameList: sortedSaveGameList, + tableAttributes: getTableAttributes(context.api, hasBLSE, selectedSave, saveSelected), + selectedRowSave: selectedRowSave, + saveRowSelected: saveRowSelected, + })} + + + ); +}; + +const getTableAttributes = ( + api: types.IExtensionApi, + hasBLSE: boolean, + selectedSave: ISaveGame | null, + saveSelected: (save: ISaveGame) => void +): types.ITableAttribute<[string, ISaveGame]>[] => { + const { localize: t } = LocalizationManager.getInstance(api); + + const tableAttributes: types.ITableAttribute<[string, ISaveGame]>[] = [ + { + id: '#', + name: '#', + customRenderer: (data): JSX.Element => { + if (data.length && typeof data[0] === 'string' && !Array.isArray(data[1])) { + const save = data[1]; + return ( + + ); + } + return <>; + }, + placement: 'both', + edit: {}, + }, + { + id: 'name', + name: t('{=JtelOsIW}Name'), + calc: ([, save]) => save.name, + placement: 'both', + edit: {}, + }, + { + id: 'characterName', + name: t('{=OJsGrGVi}Character'), + calc: ([, save]) => save.characterName ?? '', + placement: 'both', + edit: {}, + }, + { + id: 'mainHeroLevel', + name: t('{=JxpEEQdF}Level'), + calc: ([, save]) => save.mainHeroLevel ?? '', + placement: 'both', + edit: {}, + }, + { + id: 'dayLong', + name: t('{=qkkTPycE}Days'), + calc: ([, save]) => save.dayLong?.toFixed(0) ?? '', + placement: 'both', + edit: {}, + }, + { + id: 'status', + name: t('Status'), + customRenderer: (data): JSX.Element => { + if (data.length && typeof data[0] === 'string' && !Array.isArray(data[1])) { + const save = data[1]; + return ; + } + return <>; + }, + placement: 'both', + edit: {}, + }, + { + id: 'applicationVersion', + name: t('{=14WBFIS1}Version'), + calc: ([, save]) => (save.applicationVersion ? versionToString(save.applicationVersion) : ''), + placement: 'both', + edit: {}, + }, + { + id: 'creationTime', + name: t('{=aYWWDkKX}CreatedAt'), + calc: ([, save]) => ticksToDate(save.creationTime)?.toLocaleString(), + placement: 'both', + edit: {}, + }, + ]; + return tableAttributes; +}; + +const mapState = (state: types.IState): IFromState => { + const profile: types.IProfile | undefined = selectors.activeProfile(state); + + const saveName = profile !== undefined ? getSaveFromSettings(state, profile.id) ?? 'No Save' : 'No Save'; + + const blseMod = findBLSEMod(state); + const hasBLSE = blseMod !== undefined && profile !== undefined && isModActive(profile, blseMod); + + return { + profile: profile, + saveName: saveName, + hasBLSE: hasBLSE, + }; +}; diff --git a/src/views/Saves/saveUtils.ts b/src/views/Saves/saveUtils.ts deleted file mode 100644 index 7d037e3..0000000 --- a/src/views/Saves/saveUtils.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { BannerlordModuleManager, types as vetypes, Utils } from '@butr/vortexextensionnative'; -import { ISaveGame } from './types'; -import { LocalizationManager, VortexLauncherManager, versionToString } from '../../utils'; -import { IModuleCache } from '../../types'; - -type MismatchedModule = { - name: string; - installed: vetypes.ApplicationVersion; - save: vetypes.ApplicationVersion; -}; -type MismatchedModuleMap = { - [name: string]: MismatchedModule; -}; - -type ModulesByName = { - [name: string]: vetypes.ModuleInfoExtendedWithMetadata; -}; - -export const getModulesByName = (modules: Readonly): ModulesByName => { - return Object.values(modules).reduce((map, current) => { - map[current.name] = current; - return map; - }, {}); -}; - -export const getNameDuplicates = (allModules: Readonly): string[] | undefined => { - const allModulesByName = getModulesByName(allModules); - - const moduleNames = Object.keys(allModulesByName); - const uniqueModuleNames = new Set(moduleNames); - - const duplicates = moduleNames.filter((currentValue) => { - if (uniqueModuleNames.has(currentValue)) { - uniqueModuleNames.delete(currentValue); - } - }); - - if (duplicates.length !== 0) { - return duplicates; - } - - return undefined; -}; - -export const getMissingModuleNames = (saveGame: Readonly, allModules: Readonly) => { - const allModulesByName = getModulesByName(allModules); - return Object.keys(saveGame.modules).reduce((map, current) => { - if (!allModulesByName[current]) { - map.push(current); - } - return map; - }, []); -}; - -export const getMismatchedModuleVersions = ( - saveGame: Readonly, - localizationManager: LocalizationManager, - allModules: Readonly -) => { - const t = localizationManager.localize; - - const allModulesByName = getModulesByName(allModules); - const mismatchedVersions = Object.keys(saveGame.modules).reduce((map, moduleName) => { - // is the module even installed? - if (!allModulesByName[moduleName]) { - return map; // just return the previous accumulation and move on - } - - const installedVerson = allModulesByName[moduleName]!.version; - const saveVersion = saveGame.modules[moduleName]!; - if (BannerlordModuleManager.compareVersions(installedVerson, saveVersion) !== 0) { - map[moduleName] = { - name: moduleName, - installed: installedVerson, - save: saveVersion, - }; - } - return map; - }, {}); - - const mismatchedVersionsLocalized = Object.values(mismatchedVersions).map((current) => { - const module = allModulesByName[current.name]!; - return t('{=nYVWoomO}{MODULEID}. Required {REQUIREDVERSION}. Actual {ACTUALVERSION}', { - MODULEID: module.id, - REQUIREDVERSION: versionToString(current.save), - ACTUALVERSION: versionToString(current.installed), - }); - }); - - if (mismatchedVersionsLocalized.length !== 0) { - return mismatchedVersionsLocalized; - } - - return undefined; -}; - -export const getLoadOrderIssues = (saveGame: ISaveGame, allModules: Readonly) => { - const allModulesByName = getModulesByName(allModules); - const modules = Object.keys(saveGame.modules) - .map((current) => allModulesByName[current]) - .filter((x): x is vetypes.ModuleInfoExtendedWithMetadata => !!x); - return Utils.isLoadOrderCorrect(modules); -}; - -export const getModules = ( - saveGame: ISaveGame, - manager: VortexLauncherManager -): Array => { - const allModules = manager.getAllModules(); - const allModulesByName = getModulesByName(allModules); - return Object.keys(saveGame.modules) - .map((current) => allModulesByName[current]) - .filter((x): x is vetypes.ModuleInfoExtendedWithMetadata => !!x); -}; diff --git a/src/views/Saves/types.ts b/src/views/Saves/types.ts index 381d64f..c773463 100644 --- a/src/views/Saves/types.ts +++ b/src/views/Saves/types.ts @@ -23,3 +23,20 @@ export interface ISaveGame { missingModules?: string[] | undefined; mismatchedModuleVersions?: string[] | undefined; } + +export interface ISaveList { + [name: string]: ISaveGame; +} + +export type MismatchedModule = { + name: string; + installed: vetypes.ApplicationVersion; + save: vetypes.ApplicationVersion; +}; +export type MismatchedModuleMap = { + [name: string]: MismatchedModule; +}; + +export type ModulesByName = { + [name: string]: vetypes.ModuleInfoExtendedWithMetadata; +}; diff --git a/src/views/Saves/utils.ts b/src/views/Saves/utils.ts new file mode 100644 index 0000000..4bc3151 --- /dev/null +++ b/src/views/Saves/utils.ts @@ -0,0 +1,203 @@ +import { types } from 'vortex-api'; +import { BannerlordModuleManager, Utils, types as vetypes } from '@butr/vortexextensionnative'; +import { ISaveGame, ISaveList, MismatchedModuleMap, ModulesByName } from './types'; +import { IModuleCache } from '../../types'; +import { LocalizationManager } from '../../localization'; +import { versionToString, VortexLauncherManager } from '../../launcher'; + +const createSaveGame = ( + api: types.IExtensionApi, + allModules: Readonly, + current: vetypes.SaveMetadata, + currentIndex: number +): ISaveGame | undefined => { + if (current['Modules'] === undefined) { + return undefined; + } + + const saveGame: ISaveGame = { + index: currentIndex + 1, + name: current.Name, + applicationVersion: + current['ApplicationVersion'] !== undefined + ? BannerlordModuleManager.parseApplicationVersion(current['ApplicationVersion']) + : undefined, + creationTime: current['CreationTime'] !== undefined ? parseInt(current['CreationTime']) : undefined, + characterName: current['CharacterName'], + mainHeroGold: current['MainHeroGold'] !== undefined ? parseInt(current['MainHeroGold']) : undefined, + mainHeroLevel: current['MainHeroLevel'] !== undefined ? parseInt(current['MainHeroLevel']) : undefined, + dayLong: current['DayLong'] !== undefined ? parseFloat(current['DayLong']) : undefined, + + clanBannerCode: current['ClanBannerCode'], + clanFiefs: current['ClanFiefs'] !== undefined ? parseInt(current['ClanFiefs']) : undefined, + clanInfluence: current['ClanInfluence'] !== undefined ? parseFloat(current['ClanInfluence']) : undefined, + + mainPartyFood: current['MainPartyFood'] !== undefined ? parseFloat(current['MainPartyFood']) : undefined, + mainPartyHealthyMemberCount: + current['MainPartyHealthyMemberCount'] !== undefined + ? parseInt(current['MainPartyHealthyMemberCount']) + : undefined, + mainPartyPrisonerMemberCount: + current['MainPartyPrisonerMemberCount'] !== undefined + ? parseInt(current['MainPartyPrisonerMemberCount']) + : undefined, + mainPartyWoundedMemberCount: + current['MainPartyWoundedMemberCount'] !== undefined + ? parseInt(current['MainPartyWoundedMemberCount']) + : undefined, + version: current['Version'] !== undefined ? parseInt(current['Version']) : undefined, + modules: {}, // blank dictionary for now + }; + + // build up modules dictionary? + const moduleNames = current['Modules'].split(';'); + + const saveChangeSet = saveGame.applicationVersion?.changeSet ?? 0; + for (const module of moduleNames) { + const key = module; + const moduleValue = current['Module_' + module]; + if (moduleValue === undefined) { + continue; + } + + const version = BannerlordModuleManager.parseApplicationVersion(moduleValue); + if (version.changeSet === saveChangeSet) { + version.changeSet = 0; + } + saveGame.modules[key] = version; + } + + saveGame.duplicateModules = getNameDuplicates(allModules); + saveGame.loadOrderIssues = getLoadOrderIssues(saveGame, allModules); + saveGame.missingModules = getMissingModuleNames(saveGame, allModules); + saveGame.mismatchedModuleVersions = getMismatchedModuleVersions(api, saveGame, allModules); + + return saveGame; +}; + +export const getSaves = (api: types.IExtensionApi): ISaveList => { + const { localize: t } = LocalizationManager.getInstance(api); + + const launcherManager = VortexLauncherManager.getInstance(api); + + const saveList: ISaveList = { + ['nosave']: { + index: 0, + name: t('No Save'), + modules: {}, + }, + }; + + const allModules = launcherManager.getAllModules(); + const saveMetadatas = launcherManager.getSaveFiles(); + + saveMetadatas.reduce((prev, current, currentIndex) => { + const save = createSaveGame(api, allModules, current, currentIndex); + if (!save) { + return prev; + } + + prev[current.Name] = save; + + return prev; + }, saveList); + + return saveList; +}; + +export const getModulesByName = (modules: Readonly): ModulesByName => { + return Object.values(modules).reduce((map, current) => { + map[current.name] = current; + return map; + }, {}); +}; + +export const getNameDuplicates = (allModules: Readonly): string[] | undefined => { + const allModulesByName = getModulesByName(allModules); + + const moduleNames = Object.keys(allModulesByName); + const uniqueModuleNames = new Set(moduleNames); + + const duplicates = moduleNames.filter((currentValue) => { + if (uniqueModuleNames.has(currentValue)) { + uniqueModuleNames.delete(currentValue); + } + }); + + if (duplicates.length) { + return duplicates; + } + + return undefined; +}; + +export const getMissingModuleNames = (saveGame: Readonly, allModules: Readonly): string[] => { + const allModulesByName = getModulesByName(allModules); + return Object.keys(saveGame.modules).reduce((map, current) => { + if (!allModulesByName[current]) { + map.push(current); + } + return map; + }, []); +}; + +export const getMismatchedModuleVersions = ( + api: types.IExtensionApi, + saveGame: Readonly, + allModules: Readonly +): string[] | undefined => { + const { localize: t } = LocalizationManager.getInstance(api); + + const allModulesByName = getModulesByName(allModules); + const mismatchedVersions = Object.keys(saveGame.modules).reduce((map, moduleName) => { + // is the module even installed? + if (!allModulesByName[moduleName]) { + return map; // just return the previous accumulation and move on + } + + const installedVerson = allModulesByName[moduleName]!.version; + const saveVersion = saveGame.modules[moduleName]!; + if (BannerlordModuleManager.compareVersions(installedVerson, saveVersion) !== 0) { + map[moduleName] = { + name: moduleName, + installed: installedVerson, + save: saveVersion, + }; + } + return map; + }, {}); + + const mismatchedVersionsLocalized = Object.values(mismatchedVersions).map((current) => { + const module = allModulesByName[current.name]!; + return t('{=nYVWoomO}{MODULEID}. Required {REQUIREDVERSION}. Actual {ACTUALVERSION}', { + MODULEID: module.id, + REQUIREDVERSION: versionToString(current.save), + ACTUALVERSION: versionToString(current.installed), + }); + }); + + if (mismatchedVersionsLocalized.length) { + return mismatchedVersionsLocalized; + } + + return undefined; +}; + +export const getLoadOrderIssues = (saveGame: ISaveGame, allModules: Readonly): string[] => { + const allModulesByName = getModulesByName(allModules); + const modules = Object.keys(saveGame.modules) + .map((current) => allModulesByName[current]) + .filter((x): x is vetypes.ModuleInfoExtendedWithMetadata => x !== undefined); + return Utils.isLoadOrderCorrect(modules); +}; + +export const getModules = ( + saveGame: ISaveGame, + manager: VortexLauncherManager +): Array => { + const allModules = manager.getAllModules(); + const allModulesByName = getModulesByName(allModules); + return Object.keys(saveGame.modules) + .map((current) => allModulesByName[current]) + .filter((x): x is vetypes.ModuleInfoExtendedWithMetadata => x !== undefined); +}; diff --git a/src/views/Settings/Settings.tsx b/src/views/Settings/Settings.tsx deleted file mode 100644 index 4dafa5c..0000000 --- a/src/views/Settings/Settings.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import React from 'react'; -import { useSelector } from 'react-redux'; -import { More, Toggle, selectors, types } from 'vortex-api'; -import { getBetaSortingFromSettings, getFixCommonIssuesFromSettings, getSortOnDeployFromSettings } from '../../utils'; -import { IMoreProps } from 'vortex-api/lib/controls/More'; -import { GetLocalizationManager } from '../../types'; - -export interface ISettingsProps { - getLocalizationManager: GetLocalizationManager; - onSetSortOnDeploy: (profileId: string, sort: boolean) => void; - onSetFixCommonIssues: (profileId: string, fixCommonIssues: boolean) => void; - onSetBetaSorting: (profileId: string, betaSorting: boolean) => void; -} - -interface IConnectedProps { - profileId: string; - autoSortOnDeploy: boolean; - fixCommonIssues: boolean; - betaSorting: boolean; -} - -const MoreWrapper = More as React.ComponentType; - -export const Settings = (props: ISettingsProps): JSX.Element => { - const { getLocalizationManager, onSetSortOnDeploy, onSetFixCommonIssues, onSetBetaSorting } = props; - const { profileId, autoSortOnDeploy, fixCommonIssues, betaSorting } = useSelector(mapState); - const setSortCallback = React.useCallback( - (value) => { - if (profileId !== undefined) { - onSetSortOnDeploy(profileId, value); - } - }, - [profileId, onSetSortOnDeploy] - ); - const fixCommonIssuesCallback = React.useCallback( - (value) => { - if (profileId !== undefined) { - onSetFixCommonIssues(profileId, value); - } - }, - [profileId, onSetFixCommonIssues] - ); - const betaSortingCallback = React.useCallback( - (value) => { - if (profileId !== undefined) { - onSetBetaSorting(profileId, value); - } - }, - [profileId, onSetBetaSorting] - ); - - const localizationManager = getLocalizationManager(); - const t = localizationManager.localize; - return ( -
- - {t(`Sort Bannerlord mods automatically on deployment`)} - - {t( - `Any time you deploy, Vortex will attempt to automatically sort your load order ` + - `for you to reduce game crashes caused by incorrect module order.\n\n` + - `Important: Please ensure to lock any load order entries you wish to stop from ` + - `shifting positions.` - )} - - - {/* - - {t(`{=LXlsSS8t}Fix Common Issues`)} - - {t(`{=J9VbkLW4}Fixes issues like 0Harmony.dll being in the /bin folder`)} - - - */} - - {t(`{=QJSBiZdJ}Beta Sorting`)} - - {t(`{=HVhaqeb4}Uses the new sorting algorithm after v1.12.x. Disable to use the old algorithm`)} - - -
- ); -}; - -const mapState = (state: types.IState): IConnectedProps => { - const profile = selectors.activeProfile(state); - const sortOnDeploy = getSortOnDeployFromSettings(state, profile.id) ?? true; - const fixCommonIssues = getFixCommonIssuesFromSettings(state, profile.id) ?? true; - const betaSorting = getBetaSortingFromSettings(state, profile.id) ?? false; - return { - profileId: profile.id, - autoSortOnDeploy: sortOnDeploy, - fixCommonIssues: fixCommonIssues, - betaSorting: betaSorting, - }; -}; - -export default Settings; diff --git a/src/views/Settings/index.ts b/src/views/Settings/index.ts index ec5d979..96703dc 100644 --- a/src/views/Settings/index.ts +++ b/src/views/Settings/index.ts @@ -1 +1 @@ -export { Settings } from './Settings'; +export * from './pages/Settings'; diff --git a/src/views/Settings/pages/Settings.tsx b/src/views/Settings/pages/Settings.tsx new file mode 100644 index 0000000..e927735 --- /dev/null +++ b/src/views/Settings/pages/Settings.tsx @@ -0,0 +1,98 @@ +import React, { useCallback } from 'react'; +import { useSelector } from 'react-redux'; +import { More, selectors, Toggle, types } from 'vortex-api'; +import { useLocalization } from '../../../localization'; +import { + getBetaSortingFromSettings, + getFixCommonIssuesFromSettings, + getSortOnDeployFromSettings, +} from '../../../settings'; + +interface IFromState { + profile: types.IProfile | undefined; + autoSortOnDeploy: boolean; + fixCommonIssues: boolean; + betaSorting: boolean; +} + +export type SettingsProps = { + onSetSortOnDeploy: (profileId: string, sort: boolean) => void; + onSetFixCommonIssues: (profileId: string, fixCommonIssues: boolean) => void; + onSetBetaSorting: (profileId: string, betaSorting: boolean) => void; +}; + +export const Settings = (props: SettingsProps): JSX.Element => { + const { onSetSortOnDeploy, onSetFixCommonIssues, onSetBetaSorting } = props; + + const { profile, autoSortOnDeploy, fixCommonIssues, betaSorting } = useSelector(mapState); + + const setSortCallback = useCallback( + (value) => { + if (profile) { + onSetSortOnDeploy(profile.id, value); + } + }, + [profile, onSetSortOnDeploy] + ); + const fixCommonIssuesCallback = useCallback( + (value) => { + if (profile) { + onSetFixCommonIssues(profile.id, value); + } + }, + [profile, onSetFixCommonIssues] + ); + const betaSortingCallback = useCallback( + (value) => { + if (profile) { + onSetBetaSorting(profile.id, value); + } + }, + [profile, onSetBetaSorting] + ); + + const { localize: t } = useLocalization(); + + return ( +
+ + {t(`Sort Bannerlord mods automatically on deployment`)} + + {t( + `Any time you deploy, Vortex will attempt to automatically sort your load order ` + + `for you to reduce game crashes caused by incorrect module order.\n\n` + + `Important: Please ensure to lock any load order entries you wish to stop from ` + + `shifting positions.` + )} + + + {/* + + {t(`{=LXlsSS8t}Fix Common Issues`)} + + {t(`{=J9VbkLW4}Fixes issues like 0Harmony.dll being in the /bin folder`)} + + + */} + + {t(`{=QJSBiZdJ}Beta Sorting`)} + + {t(`{=HVhaqeb4}Uses the new sorting algorithm after v1.12.x. Disable to use the old algorithm`)} + + +
+ ); +}; + +const mapState = (state: types.IState): IFromState => { + const profile: types.IProfile | undefined = selectors.activeProfile(state); + const sortOnDeploy = getSortOnDeployFromSettings(state, profile.id) ?? true; + const fixCommonIssues = getFixCommonIssuesFromSettings(state, profile.id) ?? true; + const betaSorting = getBetaSortingFromSettings(state, profile.id) ?? false; + return { + profile: profile, + autoSortOnDeploy: sortOnDeploy, + fixCommonIssues: fixCommonIssues, + betaSorting: betaSorting, + }; +}; diff --git a/src/views/Shared/components/CompatibilityInfo.tsx b/src/views/Shared/components/CompatibilityInfo.tsx new file mode 100644 index 0000000..443b23e --- /dev/null +++ b/src/views/Shared/components/CompatibilityInfo.tsx @@ -0,0 +1,55 @@ +import React, { useMemo } from 'react'; +import { tooltip } from 'vortex-api'; +import { IVortexViewModelData } from '../../../types'; +import { IModuleCompatibilityInfo } from '../../../butr'; +import { useLocalization } from '../../../localization'; +import { useLauncher, versionToString } from '../../../launcher'; + +export type CompatibilityInfoProps = { + data: IVortexViewModelData | undefined; + compatibilityInfo: IModuleCompatibilityInfo | undefined; +}; + +export const CompatibilityInfo = (props: CompatibilityInfoProps): JSX.Element => { + const { compatibilityInfo: compatibilityScore, data } = props; + + const { localize: t } = useLocalization(); + const launcherManager = useLauncher(); + + const gameVersion = useMemo(() => launcherManager.getGameVersionVortex(), [launcherManager]); + + if (!compatibilityScore || !data) { + return
; + } + + const hasRecommendation = + compatibilityScore.recommendedVersion !== null && compatibilityScore.recommendedVersion !== null; + + const hint = hasRecommendation + ? t( + `{=HdnFwgVB}Based on BUTR analytics:{NL}{NL}Compatibility Score {SCORE}%{NL}{NL}Suggesting to update to {RECOMMENDEDVERSION}.{NL}Compatibility Score {RECOMMENDEDSCORE}%{NL}{NL}{RECOMMENDEDVERSION} has a better compatibility for game {GAMEVERSION} rather than {CURRENTVERSION}!`, + { + NL: '\n', + SCORE: compatibilityScore.score.toString(), + RECOMMENDEDVERSION: compatibilityScore.recommendedVersion?.toString() ?? '', + RECOMMENDEDSCORE: compatibilityScore.recommendedScore?.toString() ?? '', + GAMEVERSION: gameVersion, + CURRENTVERSION: versionToString(data.moduleInfoExtended.version), + } + ) + : t( + `{=HdnFwgVA}Based on BUTR analytics:{NL}{NL}Update is not requiured.{NL}Compatibility Score {SCORE}%{NL}{NL}{CURRENTVERSION} is one of the best version for game {GAMEVERSION}`, + { + NL: '\n', + SCORE: compatibilityScore.score.toString(), + CURRENTVERSION: versionToString(data.moduleInfoExtended.version), + GAMEVERSION: gameVersion, + } + ); + + const color = compatibilityScore.score >= 75 ? 'green' : compatibilityScore.score >= 50 ? 'yellow' : 'red'; + + return ( + + ); +}; diff --git a/src/views/Shared/components/ModuleIcon.tsx b/src/views/Shared/components/ModuleIcon.tsx new file mode 100644 index 0000000..6b7d13d --- /dev/null +++ b/src/views/Shared/components/ModuleIcon.tsx @@ -0,0 +1,42 @@ +import React, { useMemo } from 'react'; +import { Utils } from '@butr/vortexextensionnative'; +import { TooltipImage } from './TooltipImage'; +import { IVortexViewModelData } from '../../../types'; +import { MODULE_LOGO, TW_LOGO } from '../../../common'; + +export type ModuleIconProps = { + data: IVortexViewModelData | undefined; +}; + +export const ModuleIcon = (props: ModuleIconProps): JSX.Element => { + const { data } = props; + + const isOfficial = data !== undefined && data.moduleInfoExtended.isOfficial; + const isCommunity = data !== undefined && !data.moduleInfoExtended.isOfficial; + + const dependencies = useMemo(() => (data ? Utils.getDependencyHint(data.moduleInfoExtended) : ''), [data]); + + if (isOfficial) { + return ( + + ); + } + + if (isCommunity) { + return ( + + ); + } + + return ; +}; diff --git a/src/views/Shared/components/OptionalBanner.tsx b/src/views/Shared/components/OptionalBanner.tsx new file mode 100644 index 0000000..fb7a193 --- /dev/null +++ b/src/views/Shared/components/OptionalBanner.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { Icon, types } from 'vortex-api'; +import { IVortexViewModelData } from '../../../types'; +import { useLocalization } from '../../../localization'; + +export type OptionalBannerProps = { + item: types.IFBLOLoadOrderEntry; +}; + +export const OptionalBanner = (props: OptionalBannerProps): JSX.Element | null => { + const { item } = props; + + const { localize: t } = useLocalization(); + + return isOptional(item) ? ( +
+ + {t('Not in the collection. Optional')} +
+ ) : null; +}; + +export const isOptional = (item: types.IFBLOLoadOrderEntry): boolean => { + return item.modId === undefined && item.data !== undefined && !item.data.moduleInfoExtended.isOfficial; +}; diff --git a/src/views/Shared/components/TooltipImage.tsx b/src/views/Shared/components/TooltipImage.tsx new file mode 100644 index 0000000..0f4a8dd --- /dev/null +++ b/src/views/Shared/components/TooltipImage.tsx @@ -0,0 +1,35 @@ +import React, { CSSProperties } from 'react'; +import { OverlayTrigger, Popover } from 'react-bootstrap'; +import { ITooltipProps } from 'vortex-api/lib/controls/TooltipControls'; + +export type TooltipImageProps = ITooltipProps & { + className?: string; + fixedWidth?: boolean; + flip?: 'horizontal' | 'vertical'; + src: string; + style?: CSSProperties; +}; + +export const TooltipImage = (props: TooltipImageProps): JSX.Element => { + const { tooltip, placement, ...relayProps } = props; + + const classes = ['fake-link'].concat((props.className ?? '').split(' ')); + + if (typeof props.tooltip === 'string') { + return ( + + + + ); + } else { + const tooltip = {props.tooltip}; + + return ( + + + + + + ); + } +}; diff --git a/src/views/Shared/components/index.ts b/src/views/Shared/components/index.ts new file mode 100644 index 0000000..7714bfc --- /dev/null +++ b/src/views/Shared/components/index.ts @@ -0,0 +1,4 @@ +export * from './CompatibilityInfo'; +export * from './ModuleIcon'; +export * from './OptionalBanner'; +export * from './TooltipImage'; diff --git a/src/views/Shared/index.ts b/src/views/Shared/index.ts new file mode 100644 index 0000000..edce9b5 --- /dev/null +++ b/src/views/Shared/index.ts @@ -0,0 +1,4 @@ +export * from './components/CompatibilityInfo'; +export * from './components/ModuleIcon'; +export * from './components/OptionalBanner'; +export * from './components/TooltipImage'; diff --git a/src/views/index.ts b/src/views/index.ts index 512fd82..f206c55 100644 --- a/src/views/index.ts +++ b/src/views/index.ts @@ -1,6 +1,5 @@ -//export { CollectionsDataView } from './CollectionsDataView'; -export * from './Controls'; -export * from './LoadOrderInfoPanel'; +export * from './CollectionGeneralData'; +export * from './CollectionModOptionsData'; +export * from './LoadOrder'; export * from './Saves'; export * from './Settings'; -export * from './LoadOrderItemRenderer'; diff --git a/src/views/types.ts b/src/views/types.ts new file mode 100644 index 0000000..2212d07 --- /dev/null +++ b/src/views/types.ts @@ -0,0 +1,3 @@ +import { IExtendedInterfaceProps } from 'collections/src/types/IExtendedInterfaceProps'; + +export interface ICollectionFeatureProps extends IExtendedInterfaceProps {} diff --git a/src/utils/events.ts b/src/vortex/events.ts similarity index 77% rename from src/utils/events.ts rename to src/vortex/events.ts index 2a704a0..f6cab58 100644 --- a/src/utils/events.ts +++ b/src/vortex/events.ts @@ -1,31 +1,29 @@ // eslint-disable-next-line no-restricted-imports import Bluebird from 'bluebird'; -import path from 'path'; import { fs, log, selectors, types, util } from 'vortex-api'; +import path from 'path'; import { GAME_ID } from '../common'; import { IAddedFiles } from '../types'; /** * Event function, be careful */ -export const addedFiles = async (api: types.IExtensionApi, profileId: string, files: IAddedFiles[]): Promise => { +export const addedFilesEvent = async (api: types.IExtensionApi, files: IAddedFiles[]): Promise => { const state = api.getState(); - const profile = selectors.profileById(state, profileId); - if (profile.gameId !== GAME_ID) { + const discovery: types.IDiscoveryResult | undefined = selectors.discoveryByGame(state, GAME_ID); + if (discovery?.path === undefined) { + // Can't do anything without a discovery path. return; } - const discovery: types.IDiscoveryResult | undefined = selectors.discoveryByGame(state, profile.gameId); - if (!discovery?.path) { - // Can't do anything without a discovery path. + const game = util.getGame(GAME_ID); + const modPaths = game.getModPaths?.(discovery.path) ?? {}; + const installPath: string | undefined = selectors.installPathForGame(state, game.id); + if (installPath === undefined) { + // Can't do anything without a install path. return; } - - const game = util.getGame(profile.gameId); - const modPaths = game.getModPaths ? game.getModPaths(discovery.path) : {}; - const installPath: string = selectors.installPathForGame(state, game.id); - await Bluebird.map(files, async (entry: { filePath: string; candidates: string[] }) => { // only act if we definitively know which mod owns the file if (entry.candidates.length === 1) { @@ -35,6 +33,7 @@ export const addedFiles = async (api: types.IExtensionApi, profileId: string, fi } const relPath = path.relative(modPaths[mod.type ?? ``]!, entry.filePath); const targetPath = path.join(installPath, mod.id, relPath); + // copy the new file back into the corresponding mod, then delete it. // That way, vortex will create a link to it with the correct // deployment method and not ask the user any questions diff --git a/src/utils/game.ts b/src/vortex/game.ts similarity index 69% rename from src/utils/game.ts rename to src/vortex/game.ts index 7537dc9..1001249 100644 --- a/src/utils/game.ts +++ b/src/vortex/game.ts @@ -1,12 +1,12 @@ -import path from 'path'; import { fs, selectors, types } from 'vortex-api'; -import { isStoreStandard, isStoreXbox } from '.'; +import path from 'path'; +import { isStoreStandard, isStoreXbox } from './store'; import { BANNERLORD_EXE, BANNERLORD_EXE_XBOX, BINARY_FOLDER_STANDARD, - BINARY_FOLDER_XBOX, BINARY_FOLDER_STANDARD_MODDING_KIT, + BINARY_FOLDER_XBOX, GAME_ID, } from '../common'; @@ -19,10 +19,10 @@ export const getBinaryModdingPath = (_store: string | undefined): string => { }; export const getBannerlordMainExe = (discoveryPath: string | undefined, api: types.IExtensionApi): string => { - const standard = () => path.join(`bin`, BINARY_FOLDER_STANDARD, BANNERLORD_EXE); - const xbox = () => path.join(`bin`, BINARY_FOLDER_XBOX, BANNERLORD_EXE_XBOX); + const standard = (): string => path.join(`bin`, BINARY_FOLDER_STANDARD, BANNERLORD_EXE); + const xbox = (): string => path.join(`bin`, BINARY_FOLDER_XBOX, BANNERLORD_EXE_XBOX); - const discovery = selectors.discoveryByGame(api.getState(), GAME_ID); + const discovery: types.IDiscoveryResult | undefined = selectors.discoveryByGame(api.getState(), GAME_ID); if (!discovery) { return ``; } @@ -35,7 +35,7 @@ export const getBannerlordMainExe = (discoveryPath: string | undefined, api: typ return standard(); } - if (!discovery.store && discoveryPath) { + if (discovery.store === undefined && discoveryPath !== undefined) { // Brute force the detection by manually checking the paths. try { fs.statSync(path.join(discoveryPath, BANNERLORD_EXE_XBOX)); @@ -53,10 +53,10 @@ export const getBannerlordToolExe = ( api: types.IExtensionApi, exe: string ): string => { - const standard = () => path.join(`bin`, BINARY_FOLDER_STANDARD, exe); - const xbox = () => path.join(`bin`, BINARY_FOLDER_XBOX, exe); + const standard = (): string => path.join(`bin`, BINARY_FOLDER_STANDARD, exe); + const xbox = (): string => path.join(`bin`, BINARY_FOLDER_XBOX, exe); - const discovery = selectors.discoveryByGame(api.getState(), GAME_ID); + const discovery: types.IDiscoveryResult | undefined = selectors.discoveryByGame(api.getState(), GAME_ID); if (!discovery) { return ``; } @@ -69,7 +69,7 @@ export const getBannerlordToolExe = ( return standard(); } - if (!discovery.store && discoveryPath) { + if (discovery.store === undefined && discoveryPath !== undefined) { // Brute force the detection by manually checking the paths. try { fs.statSync(path.join(discoveryPath, BANNERLORD_EXE_XBOX)); diff --git a/src/vortex/index.ts b/src/vortex/index.ts new file mode 100644 index 0000000..ae708dd --- /dev/null +++ b/src/vortex/index.ts @@ -0,0 +1,7 @@ +export * from './events'; +export * from './game'; +export * from './modType'; +export * from './store'; +export * from './tools'; +export * from './types'; +export * from './utils'; diff --git a/src/utils/module/modType.ts b/src/vortex/modType.ts similarity index 67% rename from src/utils/module/modType.ts rename to src/vortex/modType.ts index 160d628..4126d71 100644 --- a/src/utils/module/modType.ts +++ b/src/vortex/modType.ts @@ -1,5 +1,5 @@ import { selectors, types } from 'vortex-api'; -import { SUBMODULE_FILE } from '../../common'; +import { SUBMODULE_FILE } from '../common'; export const getInstallPathModule = (api: types.IExtensionApi, game: types.IGame): string => { const discovery: types.IDiscoveryResult | undefined = selectors.discoveryByGame(api.getState(), game.id); @@ -7,5 +7,6 @@ export const getInstallPathModule = (api: types.IExtensionApi, game: types.IGame }; export const isModTypeModule = (instructions: types.IInstruction[]): boolean => { - return instructions.some((instr) => instr.type === 'copy' && instr.destination?.endsWith(SUBMODULE_FILE)); + const result = instructions.some((instr) => instr.type === 'copy' && instr.destination?.endsWith(SUBMODULE_FILE)); + return result; }; diff --git a/src/utils/store.ts b/src/vortex/store.ts similarity index 100% rename from src/utils/store.ts rename to src/vortex/store.ts diff --git a/src/utils/tools.ts b/src/vortex/tools.ts similarity index 85% rename from src/utils/tools.ts rename to src/vortex/tools.ts index 5739b21..c43dc74 100644 --- a/src/utils/tools.ts +++ b/src/vortex/tools.ts @@ -1,24 +1,25 @@ // TODO: Translate tool titles? -import path from 'path'; import { actions, types } from 'vortex-api'; -import { getBinaryModdingPath, getBinaryPath, isStoreSteam } from '.'; +import path from 'path'; +import { getBinaryModdingPath, getBinaryPath } from './game'; +import { isStoreSteam } from './store'; import { - GAME_ID, + BANNERLORD_EXE, + BANNERLORD_EXE_LAUNCHER, BLSE_CLI_EXE, BLSE_LAUNCHER_EXE, BLSE_LAUNCHEREX_EXE, - BANNERLORD_EXE, - BANNERLORD_EXE_LAUNCHER, + GAME_ID, } from '../common'; // TODO: Reuse tool creation code -const addDiscoveredTool = (api: types.IExtensionApi, tool: types.IDiscoveredTool) => { - return api.store?.dispatch(actions.addDiscoveredTool(GAME_ID, tool.id, tool, false)); +const addDiscoveredTool = (api: types.IExtensionApi, tool: types.IDiscoveredTool): void => { + api.store?.dispatch(actions.addDiscoveredTool(GAME_ID, tool.id, tool, false)); }; -export const addBLSETools = async (api: types.IExtensionApi, discovery: types.IDiscoveryResult): Promise => { - if (!discovery.path) { +export const addBLSETools = (api: types.IExtensionApi, discovery: types.IDiscoveryResult): void => { + if (discovery.path === undefined) { throw new Error(`discovery.path is undefined!`); } @@ -53,7 +54,7 @@ export const addBLSETools = async (api: types.IExtensionApi, discovery: types.ID }; export const addOfficialCLITool = (api: types.IExtensionApi, discovery: types.IDiscoveryResult): void => { - if (!discovery.path) { + if (discovery.path === undefined) { throw new Error(`discovery.path is undefined!`); } @@ -74,7 +75,7 @@ export const addOfficialCLITool = (api: types.IExtensionApi, discovery: types.ID }; export const addOfficialLauncherTool = (api: types.IExtensionApi, discovery: types.IDiscoveryResult): void => { - if (!discovery.path) { + if (discovery.path === undefined) { throw new Error(`discovery.path is undefined!`); } @@ -94,9 +95,9 @@ export const addOfficialLauncherTool = (api: types.IExtensionApi, discovery: typ export const addModdingKitTool = ( api: types.IExtensionApi, discovery: types.IDiscoveryResult, - hidden?: boolean + hidden: boolean = false ): void => { - if (!discovery.path) { + if (discovery.path === undefined) { throw new Error(`discovery.path is undefined!`); } @@ -111,7 +112,7 @@ export const addModdingKitTool = ( path: path.join(discovery.path, getBinaryModdingPath(discovery.store), BANNERLORD_EXE_LAUNCHER), requiredFiles: [BANNERLORD_EXE_LAUNCHER], exclusive: true, - hidden: hidden ?? false, + hidden: hidden, custom: false, executable: () => '', }; diff --git a/src/vortex/types.ts b/src/vortex/types.ts new file mode 100644 index 0000000..b213911 --- /dev/null +++ b/src/vortex/types.ts @@ -0,0 +1,23 @@ +import { types } from 'vortex-api'; +import { GAME_ID } from '../common'; +import { IBannerlordModStorage, IStatePersistent, VortexLoadOrderStorage } from '../types'; + +export interface IStatePersistentWithLoadOrder extends IStatePersistent { + loadOrder: { + [profileId: string]: VortexLoadOrderStorage; + }; +} + +export interface IModTableWithBannerlord extends types.IModTable { + [GAME_ID]: IBannerlordModStorage; +} + +export interface IStatePersistentWithBannerlordMods extends IStatePersistent { + mods: IModTableWithBannerlord; +} + +export interface ISettingsInterfaceWithPrimaryTool extends types.ISettingsInterface { + primaryTool: { + [GAME_ID]?: string; + }; +} diff --git a/src/vortex/utils.ts b/src/vortex/utils.ts new file mode 100644 index 0000000..4c2184e --- /dev/null +++ b/src/vortex/utils.ts @@ -0,0 +1,109 @@ +import { types, util } from 'vortex-api'; +import path from 'path'; +import { + ISettingsInterfaceWithPrimaryTool, + IStatePersistentWithBannerlordMods, + IStatePersistentWithLoadOrder, +} from './types'; +import { isStoreSteam, isStoreXbox } from './store'; +import { getBinaryPath } from './game'; +import { addBLSETools, addModdingKitTool, addOfficialCLITool, addOfficialLauncherTool } from './tools'; +import { nameof } from '../nameof'; +import { getPathExistsAsync } from '../utils'; +import { recommendBLSE } from '../blse'; +import { VortexLauncherManager } from '../launcher'; +import { BLSE_CLI_EXE, EPICAPP_ID, GAME_ID, GOG_IDS, STEAMAPP_ID, XBOX_ID } from '../common'; +import { IStatePersistent } from '../types'; + +type HasSettings = { + settings: types.ISettings; +}; + +type RequiresLauncherResult = { + launcher: string; + addInfo?: unknown; +}; + +export const hasPersistentLoadOrder = ( + statePersistent: IStatePersistent +): statePersistent is IStatePersistentWithLoadOrder => + nameof('loadOrder') in statePersistent; + +export const hasPersistentBannerlordMods = ( + statePersistent: IStatePersistent +): statePersistent is IStatePersistentWithBannerlordMods => + nameof('mods') in statePersistent && GAME_ID in statePersistent.mods; + +export const hasSettings = (hasSettings: object): hasSettings is HasSettings => + nameof('settings') in hasSettings; + +export const hasSettingsInterfacePrimaryTool = ( + settings: types.ISettingsInterface +): settings is ISettingsInterfaceWithPrimaryTool => + nameof('primaryTool') in settings; + +const launchGameStore = async (api: types.IExtensionApi, store: string): Promise => { + await util.GameStoreHelper.launchGameStore(api, store, undefined, true).catch(() => { + /* ignore error */ + }); +}; + +const prepareForModding = async (api: types.IExtensionApi, discovery: types.IDiscoveryResult): Promise => { + if (discovery.path === undefined) { + throw new Error(`discovery.path is undefined!`); + } + + // 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); + if (!(await getPathExistsAsync(binaryPath))) { + recommendBLSE(api); + } + + if (isStoreSteam(discovery.store)) { + await launchGameStore(api, discovery.store); + } + + if (discovery.store !== undefined) { + const launcherManager = VortexLauncherManager.getInstance(api); + + launcherManager.setStore(discovery.store); + } +}; + +export const setup = async (api: types.IExtensionApi, discovery: types.IDiscoveryResult): Promise => { + if (discovery.path === undefined) { + throw new Error(`discovery.path is undefined!`); + } + + // Quickly ensure that the official Launcher is added. + addOfficialCLITool(api, discovery); + addOfficialLauncherTool(api, discovery); + addModdingKitTool(api, discovery); + addBLSETools(api, discovery); + + await prepareForModding(api, discovery); +}; + +export const requiresLauncher = (store?: string): RequiresLauncherResult => { + if (isStoreXbox(store)) { + return { + launcher: `xbox`, + addInfo: { + appId: XBOX_ID, + parameters: [ + { + appExecName: `bin.Gaming.Desktop.x64.Shipping.Client.Launcher.Native`, + }, + ], + }, + }; + } + // The API doesn't expect undefined, but it's allowed + return undefined!; +}; + +export const findGame = async (): Promise => { + return await util.GameStoreHelper.findByAppId([EPICAPP_ID, STEAMAPP_ID.toString(), ...GOG_IDS, XBOX_ID]); +}; diff --git a/yarn.lock b/yarn.lock index 955304b..23c4c46 100644 --- a/yarn.lock +++ b/yarn.lock @@ -71,6 +71,11 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f" integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g== +"@gar/promisify@^1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" + integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw== + "@humanwhocodes/config-array@^0.11.14": version "0.11.14" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" @@ -170,6 +175,22 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@npmcli/fs@^2.1.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-2.1.2.tgz#a9e2541a4a2fec2e69c29b35e6060973da79b865" + integrity sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ== + dependencies: + "@gar/promisify" "^1.1.3" + semver "^7.3.5" + +"@npmcli/move-file@^2.0.0": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@npmcli/move-file/-/move-file-2.0.1.tgz#26f6bdc379d87f75e55739bab89db525b06100e4" + integrity sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ== + dependencies: + mkdirp "^1.0.4" + rimraf "^3.0.2" + "@pkgr/core@^0.1.0": version "0.1.1" resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.1.tgz#1ec17e2edbec25c8306d424ecfbf13c7de1aaa31" @@ -180,6 +201,11 @@ resolved "https://registry.yarnpkg.com/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz#719df7fb41766bc143369eaa0dd56d8dc87c9958" integrity sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg== +"@tootallnate/once@2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" + integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== + "@tsconfig/node10@^1.0.7": version "1.0.11" resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.11.tgz#6ee46400685f130e278128c7b38b7e031ff5b2f2" @@ -320,16 +346,16 @@ tapable "^2.2.0" webpack "^5" -"@typescript-eslint/eslint-plugin@7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.0.0.tgz#62cda0d35bbf601683c6e58cf5d04f0275caca4e" - integrity sha512-M72SJ0DkcQVmmsbqlzc6EJgb/3Oz2Wdm6AyESB4YkGgCxP8u5jt5jn4/OBMPK3HLOxcttZq5xbBBU7e2By4SZQ== +"@typescript-eslint/eslint-plugin@7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.2.0.tgz#5a5fcad1a7baed85c10080d71ad901f98c38d5b7" + integrity sha512-mdekAHOqS9UjlmyF/LSs6AIEvfceV749GFxoBAjwAv0nkevfKHWQFDMcBZWUiIC5ft6ePWivXoS36aKQ0Cy3sw== dependencies: "@eslint-community/regexpp" "^4.5.1" - "@typescript-eslint/scope-manager" "7.0.0" - "@typescript-eslint/type-utils" "7.0.0" - "@typescript-eslint/utils" "7.0.0" - "@typescript-eslint/visitor-keys" "7.0.0" + "@typescript-eslint/scope-manager" "7.2.0" + "@typescript-eslint/type-utils" "7.2.0" + "@typescript-eslint/utils" "7.2.0" + "@typescript-eslint/visitor-keys" "7.2.0" debug "^4.3.4" graphemer "^1.4.0" ignore "^5.2.4" @@ -337,74 +363,47 @@ semver "^7.5.4" ts-api-utils "^1.0.1" -"@typescript-eslint/parser@^6": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.21.0.tgz#af8fcf66feee2edc86bc5d1cf45e33b0630bf35b" - integrity sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ== +"@typescript-eslint/parser@7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.2.0.tgz#44356312aea8852a3a82deebdacd52ba614ec07a" + integrity sha512-5FKsVcHTk6TafQKQbuIVkXq58Fnbkd2wDL4LB7AURN7RUOu1utVP+G8+6u3ZhEroW3DF6hyo3ZEXxgKgp4KeCg== dependencies: - "@typescript-eslint/scope-manager" "6.21.0" - "@typescript-eslint/types" "6.21.0" - "@typescript-eslint/typescript-estree" "6.21.0" - "@typescript-eslint/visitor-keys" "6.21.0" + "@typescript-eslint/scope-manager" "7.2.0" + "@typescript-eslint/types" "7.2.0" + "@typescript-eslint/typescript-estree" "7.2.0" + "@typescript-eslint/visitor-keys" "7.2.0" debug "^4.3.4" -"@typescript-eslint/scope-manager@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz#ea8a9bfc8f1504a6ac5d59a6df308d3a0630a2b1" - integrity sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg== - dependencies: - "@typescript-eslint/types" "6.21.0" - "@typescript-eslint/visitor-keys" "6.21.0" - -"@typescript-eslint/scope-manager@7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.0.0.tgz#15ea9abad2b56fc8f5c0b516775f41c86c5c8685" - integrity sha512-IxTStwhNDPO07CCrYuAqjuJ3Xf5MrMaNgbAZPxFXAUpAtwqFxiuItxUaVtP/SJQeCdJjwDGh9/lMOluAndkKeg== +"@typescript-eslint/scope-manager@7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.2.0.tgz#cfb437b09a84f95a0930a76b066e89e35d94e3da" + integrity sha512-Qh976RbQM/fYtjx9hs4XkayYujB/aPwglw2choHmf3zBjB4qOywWSdt9+KLRdHubGcoSwBnXUH2sR3hkyaERRg== dependencies: - "@typescript-eslint/types" "7.0.0" - "@typescript-eslint/visitor-keys" "7.0.0" + "@typescript-eslint/types" "7.2.0" + "@typescript-eslint/visitor-keys" "7.2.0" -"@typescript-eslint/type-utils@7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-7.0.0.tgz#a4c7ae114414e09dbbd3c823b5924793f7483252" - integrity sha512-FIM8HPxj1P2G7qfrpiXvbHeHypgo2mFpFGoh5I73ZlqmJOsloSa1x0ZyXCer43++P1doxCgNqIOLqmZR6SOT8g== +"@typescript-eslint/type-utils@7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-7.2.0.tgz#7be5c30e9b4d49971b79095a1181324ef6089a19" + integrity sha512-xHi51adBHo9O9330J8GQYQwrKBqbIPJGZZVQTHHmy200hvkLZFWJIFtAG/7IYTWUyun6DE6w5InDReePJYJlJA== dependencies: - "@typescript-eslint/typescript-estree" "7.0.0" - "@typescript-eslint/utils" "7.0.0" + "@typescript-eslint/typescript-estree" "7.2.0" + "@typescript-eslint/utils" "7.2.0" debug "^4.3.4" ts-api-utils "^1.0.1" -"@typescript-eslint/types@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.21.0.tgz#205724c5123a8fef7ecd195075fa6e85bac3436d" - integrity sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg== - -"@typescript-eslint/types@7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.0.0.tgz#2e5889c7fe3c873fc6dc6420aa77775f17cd5dc6" - integrity sha512-9ZIJDqagK1TTs4W9IyeB2sH/s1fFhN9958ycW8NRTg1vXGzzH5PQNzq6KbsbVGMT+oyyfa17DfchHDidcmf5cg== - -"@typescript-eslint/typescript-estree@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz#c47ae7901db3b8bddc3ecd73daff2d0895688c46" - integrity sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ== - dependencies: - "@typescript-eslint/types" "6.21.0" - "@typescript-eslint/visitor-keys" "6.21.0" - debug "^4.3.4" - globby "^11.1.0" - is-glob "^4.0.3" - minimatch "9.0.3" - semver "^7.5.4" - ts-api-utils "^1.0.1" +"@typescript-eslint/types@7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.2.0.tgz#0feb685f16de320e8520f13cca30779c8b7c403f" + integrity sha512-XFtUHPI/abFhm4cbCDc5Ykc8npOKBSJePY3a3s+lwumt7XWJuzP5cZcfZ610MIPHjQjNsOLlYK8ASPaNG8UiyA== -"@typescript-eslint/typescript-estree@7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.0.0.tgz#7ce66f2ce068517f034f73fba9029300302fdae9" - integrity sha512-JzsOzhJJm74aQ3c9um/aDryHgSHfaX8SHFIu9x4Gpik/+qxLvxUylhTsO9abcNu39JIdhY2LgYrFxTii3IajLA== +"@typescript-eslint/typescript-estree@7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.2.0.tgz#5beda2876c4137f8440c5a84b4f0370828682556" + integrity sha512-cyxS5WQQCoBwSakpMrvMXuMDEbhOo9bNHHrNcEWis6XHx6KF518tkF1wBvKIn/tpq5ZpUYK7Bdklu8qY0MsFIA== dependencies: - "@typescript-eslint/types" "7.0.0" - "@typescript-eslint/visitor-keys" "7.0.0" + "@typescript-eslint/types" "7.2.0" + "@typescript-eslint/visitor-keys" "7.2.0" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" @@ -412,33 +411,25 @@ semver "^7.5.4" ts-api-utils "^1.0.1" -"@typescript-eslint/utils@7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.0.0.tgz#e43710af746c6ae08484f7afc68abc0212782c7e" - integrity sha512-kuPZcPAdGcDBAyqDn/JVeJVhySvpkxzfXjJq1X1BFSTYo1TTuo4iyb937u457q4K0In84p6u2VHQGaFnv7VYqg== +"@typescript-eslint/utils@7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.2.0.tgz#fc8164be2f2a7068debb4556881acddbf0b7ce2a" + integrity sha512-YfHpnMAGb1Eekpm3XRK8hcMwGLGsnT6L+7b2XyRv6ouDuJU1tZir1GS2i0+VXRatMwSI1/UfcyPe53ADkU+IuA== dependencies: "@eslint-community/eslint-utils" "^4.4.0" "@types/json-schema" "^7.0.12" "@types/semver" "^7.5.0" - "@typescript-eslint/scope-manager" "7.0.0" - "@typescript-eslint/types" "7.0.0" - "@typescript-eslint/typescript-estree" "7.0.0" + "@typescript-eslint/scope-manager" "7.2.0" + "@typescript-eslint/types" "7.2.0" + "@typescript-eslint/typescript-estree" "7.2.0" semver "^7.5.4" -"@typescript-eslint/visitor-keys@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz#87a99d077aa507e20e238b11d56cc26ade45fe47" - integrity sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A== - dependencies: - "@typescript-eslint/types" "6.21.0" - eslint-visitor-keys "^3.4.1" - -"@typescript-eslint/visitor-keys@7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.0.0.tgz#83cdadd193ee735fe9ea541f6a2b4d76dfe62081" - integrity sha512-JZP0uw59PRHp7sHQl3aF/lFgwOW2rgNVnXUksj1d932PMita9wFBd3621vHQRDvHwPsSY9FMAAHVc8gTvLYY4w== +"@typescript-eslint/visitor-keys@7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.2.0.tgz#5035f177752538a5750cca1af6044b633610bf9e" + integrity sha512-c6EIQRHhcpl6+tO8EMR+kjkkV+ugUNXOmeASA1rlzkd8EPIriavpWoiEz1HR/VLhbVIdhqnV6E7JZm00cBDx2A== dependencies: - "@typescript-eslint/types" "7.0.0" + "@typescript-eslint/types" "7.2.0" eslint-visitor-keys "^3.4.1" "@ungap/structured-clone@^1.2.0": @@ -592,10 +583,15 @@ resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== -acorn-import-assertions@^1.9.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz#507276249d684797c84e0734ef84860334cfb1ac" - integrity sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA== +abbrev@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + +acorn-import-attributes@^1.9.5: + version "1.9.5" + resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef" + integrity sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ== acorn-jsx@^5.3.2: version "5.3.2" @@ -612,6 +608,20 @@ acorn@^8.4.1, acorn@^8.7.1, acorn@^8.8.2, acorn@^8.9.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== +agent-base@6, agent-base@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + +agentkeepalive@^4.2.1: + version "4.5.0" + resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.5.0.tgz#2673ad1389b3c418c5a20c5d7364f93ca04be923" + integrity sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew== + dependencies: + humanize-ms "^1.2.1" + aggregate-error@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" @@ -659,34 +669,48 @@ ajv@^8.0.0, ajv@^8.9.0: require-from-string "^2.0.2" uri-js "^4.2.2" -ansi-escapes@^4.3.0: - version "4.3.2" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" - integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== - dependencies: - type-fest "^0.21.3" +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + integrity sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA== ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== -ansi-regex@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" - integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== - -ansi-styles@^4.0.0, ansi-styles@^4.1.0: +ansi-styles@^4.1.0: version "4.3.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== dependencies: color-convert "^2.0.1" -ansi-styles@^6.0.0: - version "6.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" - integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== +aproba@^1.0.3: + version "1.2.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" + integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== + +"aproba@^1.0.3 || ^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" + integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== + +are-we-there-yet@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz#679df222b278c64f2cdba1175cdc00b0d96164bd" + integrity sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg== + dependencies: + delegates "^1.0.0" + readable-stream "^3.6.0" + +are-we-there-yet@~1.1.2: + version "1.1.7" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz#b15474a932adab4ff8a50d9adfa7e4e926f21146" + integrity sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g== + dependencies: + delegates "^1.0.0" + readable-stream "^2.0.6" arg@^4.1.0: version "4.1.3" @@ -706,7 +730,7 @@ array-buffer-byte-length@^1.0.1: call-bind "^1.0.5" is-array-buffer "^3.0.4" -array-includes@^3.1.6, array-includes@^3.1.7: +array-includes@^3.1.6, array-includes@^3.1.7, array-includes@^3.1.8: version "3.1.8" resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.8.tgz#5e370cbe172fdd5dd6530c1d4aadda25281ba97d" integrity sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ== @@ -723,7 +747,7 @@ array-union@^2.1.0: resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== -array.prototype.findlast@^1.2.4: +array.prototype.findlast@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz#3e4fbcb30a15a7f5bf64cf2faae22d139c2e4904" integrity sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ== @@ -777,15 +801,15 @@ array.prototype.toreversed@^1.1.2: es-abstract "^1.22.1" es-shim-unscopables "^1.0.0" -array.prototype.tosorted@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.3.tgz#c8c89348337e51b8a3c48a9227f9ce93ceedcba8" - integrity sha512-/DdH4TiTmOKzyQbp/eadcCVexiCb36xJg7HshYOYJnNZFDj33GEv0P7GxsynpShhq4OLYJzbGcBDkLsDt7MnNg== +array.prototype.tosorted@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz#fe954678ff53034e717ea3352a03f0b0b86f7ffc" + integrity sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA== dependencies: - call-bind "^1.0.5" + call-bind "^1.0.7" define-properties "^1.2.1" - es-abstract "^1.22.3" - es-errors "^1.1.0" + es-abstract "^1.23.3" + es-errors "^1.3.0" es-shim-unscopables "^1.0.2" arraybuffer.prototype.slice@^1.0.3: @@ -814,16 +838,20 @@ assert-plus@1.0.0, assert-plus@^1.0.0: resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" integrity sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw== -astral-regex@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" - integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== - asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== +autogypi@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/autogypi/-/autogypi-0.2.2.tgz#258bab5f7857755b09beac6a641fea130ff4622d" + integrity sha512-NkDsjbybxo98NEUpvDULvV6w4OxhnX8owBptd8/GlQLhi81TZrh7siRYX9zVEoAYpYaX5QrRuIZAtgYD9PGDXg== + dependencies: + bluebird "^3.4.0" + commander "~2.9.0" + resolve "~1.1.7" + available-typed-arrays@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" @@ -846,6 +874,11 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + bcrypt-pbkdf@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" @@ -858,7 +891,16 @@ big.js@^5.2.2: resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== -bluebird@^3.7.2: +bl@^4.0.3: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" + integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + +bluebird@^3.4.0, bluebird@^3.7.2: version "3.7.2" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== @@ -905,6 +947,38 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== +buffer@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + +cacache@^16.1.0: + version "16.1.3" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-16.1.3.tgz#a02b9f34ecfaf9a78c9f4bc16fceb94d5d67a38e" + integrity sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ== + dependencies: + "@npmcli/fs" "^2.1.0" + "@npmcli/move-file" "^2.0.0" + chownr "^2.0.0" + fs-minipass "^2.1.0" + glob "^8.0.1" + infer-owner "^1.0.4" + lru-cache "^7.7.1" + minipass "^3.1.6" + minipass-collect "^1.0.2" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.4" + mkdirp "^1.0.4" + p-map "^4.0.0" + promise-inflight "^1.0.1" + rimraf "^3.0.2" + ssri "^9.0.0" + tar "^6.1.11" + unique-filename "^2.0.0" + call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" @@ -939,6 +1013,16 @@ chalk@^4.0.0, chalk@^4.1.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +chownr@^1.1.1: + version "1.1.4" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" + integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== + +chownr@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" + integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + chrome-trace-event@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" @@ -954,29 +1038,6 @@ clean-stack@^2.0.0: resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== -cli-cursor@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" - integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== - dependencies: - restore-cursor "^3.1.0" - -cli-truncate@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7" - integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg== - dependencies: - slice-ansi "^3.0.0" - string-width "^4.2.0" - -cli-truncate@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-3.1.0.tgz#3f23ab12535e3d73e839bb43e73c9de487db1389" - integrity sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA== - dependencies: - slice-ansi "^5.0.0" - string-width "^5.0.0" - clone-deep@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" @@ -986,6 +1047,11 @@ clone-deep@^4.0.1: kind-of "^6.0.2" shallow-clone "^3.0.0" +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + integrity sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA== + "collections@git+https://github.com/Nexus-Mods/extension-collections": version "0.1.5" resolved "git+https://github.com/Nexus-Mods/extension-collections#252e69bacb2b2855b2098df47c5b7bff9c68a866" @@ -1002,7 +1068,12 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -colorette@^2.0.14, colorette@^2.0.16: +color-support@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" + integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== + +colorette@^2.0.14: version "2.0.20" resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== @@ -1024,16 +1095,23 @@ commander@^2.20.0: resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== -commander@^9.3.0: - version "9.5.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-9.5.0.tgz#bc08d1eb5cedf7ccb797a96199d41c7bc3e60d30" - integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ== +commander@~2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" + integrity sha512-bmkUukX8wAOjHdN26xj5c4ctEV22TQ7dQYhSmuckKhToXrkUn0iIaolHdIxYYqD55nhpSPA9zPQ1yP57GdXP2A== + dependencies: + graceful-readlink ">= 1.0.0" concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== +console-control-strings@^1.0.0, console-control-strings@^1.1.0, console-control-strings@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== + copy-webpack-plugin@^12.0.1: version "12.0.2" resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-12.0.2.tgz#935e57b8e6183c82f95bd937df658a59f6a2da28" @@ -1056,6 +1134,11 @@ core-util-is@1.0.2: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" integrity sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ== +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + create-require@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" @@ -1109,6 +1192,13 @@ data-view-byte-offset@^1.0.0: es-errors "^1.3.0" is-data-view "^1.0.1" +debug@4, debug@^4.3.3: + version "4.3.5" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e" + integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== + dependencies: + ms "2.1.2" + debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" @@ -1123,6 +1213,18 @@ debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: dependencies: ms "2.1.2" +decompress-response@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" + integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== + dependencies: + mimic-response "^3.1.0" + +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + deep-is@^0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" @@ -1151,6 +1253,16 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ== + +detect-libc@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700" + integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw== + diff@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" @@ -1184,11 +1296,6 @@ dom-helpers@^3.2.0, dom-helpers@^3.2.1, dom-helpers@^3.4.0: dependencies: "@babel/runtime" "^7.1.2" -eastasianwidth@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" - integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== - ecc-jsbn@~0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" @@ -1214,30 +1321,49 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== -emoji-regex@^9.2.2: - version "9.2.2" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" - integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== - emojis-list@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== -enhanced-resolve@^5.0.0, enhanced-resolve@^5.16.0: - version "5.16.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz#65ec88778083056cb32487faa9aef82ed0864787" - integrity sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA== +encoding@^0.1.13: + version "0.1.13" + resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9" + integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A== + dependencies: + iconv-lite "^0.6.2" + +end-of-stream@^1.1.0, end-of-stream@^1.4.1: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +enhanced-resolve@^5.0.0, enhanced-resolve@^5.17.0: + version "5.17.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.0.tgz#d037603789dd9555b89aaec7eb78845c49089bc5" + integrity sha512-dwDPwZL0dmye8Txp2gzFmA6sxALaSvdRDjPH0viLcKrtlOL3tw62nWWweVD1SdILDTJrbrL6tdWVN58Wo6U3eA== dependencies: graceful-fs "^4.2.4" tapable "^2.2.0" +env-paths@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" + integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== + envinfo@^7.7.3: version "7.12.0" resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.12.0.tgz#b56723b39c2053d67ea5714f026d05d4f5cc7acd" integrity sha512-Iw9rQJBGpJRd3rwXm9ft/JiGoAZmLxxJZELYDQoPRZ4USVhkKtIcNBPw6U+/K2mBpaqM25JSV6Yl4Az9vO2wJg== -es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.1, es-abstract@^1.23.2: +err-code@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9" + integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== + +es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.1, es-abstract@^1.23.2, es-abstract@^1.23.3: version "1.23.3" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.3.tgz#8f0c5a35cd215312573c5a27c87dfd6c881a0aa0" integrity sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A== @@ -1296,19 +1422,19 @@ es-define-property@^1.0.0: dependencies: get-intrinsic "^1.2.4" -es-errors@^1.1.0, es-errors@^1.2.1, es-errors@^1.3.0: +es-errors@^1.2.1, es-errors@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== -es-iterator-helpers@^1.0.17: - version "1.0.18" - resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.0.18.tgz#4d3424f46b24df38d064af6fbbc89274e29ea69d" - integrity sha512-scxAJaewsahbqTYrGKJihhViaM6DDZDDoucfvzNbK0pOren1g/daDQ3IAhzn+1G14rBG7w+i5N+qul60++zlKA== +es-iterator-helpers@^1.0.19: + version "1.0.19" + resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.0.19.tgz#117003d0e5fec237b4b5c08aded722e0c6d50ca8" + integrity sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw== dependencies: call-bind "^1.0.7" define-properties "^1.2.1" - es-abstract "^1.23.0" + es-abstract "^1.23.3" es-errors "^1.3.0" es-set-tostringtag "^2.0.3" function-bind "^1.1.2" @@ -1389,7 +1515,7 @@ eslint-module-utils@^2.8.0: dependencies: debug "^3.2.7" -eslint-plugin-import@^2.25.4: +eslint-plugin-import@^2.29.1: version "2.29.1" resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz#d45b37b5ef5901d639c15270d74d46d161150643" integrity sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw== @@ -1425,29 +1551,29 @@ eslint-plugin-react-hooks@^4.6.2: resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz#c829eb06c0e6f484b3fbb85a97e57784f328c596" integrity sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ== -eslint-plugin-react@^7.33.2: - version "7.34.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.34.1.tgz#6806b70c97796f5bbfb235a5d3379ece5f4da997" - integrity sha512-N97CxlouPT1AHt8Jn0mhhN2RrADlUAsk1/atcT2KyA/l9Q/E6ll7OIGwNumFmWfZ9skV3XXccYS19h80rHtgkw== +eslint-plugin-react@^7.34.3: + version "7.34.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.34.3.tgz#9965f27bd1250a787b5d4cfcc765e5a5d58dcb7b" + integrity sha512-aoW4MV891jkUulwDApQbPYTVZmeuSyFrudpbTAQuj5Fv8VL+o6df2xIGpw8B0hPjAaih1/Fb0om9grCdyFYemA== dependencies: - array-includes "^3.1.7" - array.prototype.findlast "^1.2.4" + array-includes "^3.1.8" + array.prototype.findlast "^1.2.5" array.prototype.flatmap "^1.3.2" array.prototype.toreversed "^1.1.2" - array.prototype.tosorted "^1.1.3" + array.prototype.tosorted "^1.1.4" doctrine "^2.1.0" - es-iterator-helpers "^1.0.17" + es-iterator-helpers "^1.0.19" estraverse "^5.3.0" jsx-ast-utils "^2.4.1 || ^3.0.0" minimatch "^3.1.2" - object.entries "^1.1.7" - object.fromentries "^2.0.7" - object.hasown "^1.1.3" - object.values "^1.1.7" + object.entries "^1.1.8" + object.fromentries "^2.0.8" + object.hasown "^1.1.4" + object.values "^1.2.0" prop-types "^15.8.1" resolve "^2.0.0-next.5" semver "^6.3.1" - string.prototype.matchall "^4.0.10" + string.prototype.matchall "^4.0.11" eslint-scope@5.1.1: version "5.1.1" @@ -1557,20 +1683,15 @@ events@^3.2.0: resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== -execa@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" - integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== - dependencies: - cross-spawn "^7.0.3" - get-stream "^6.0.0" - human-signals "^2.1.0" - is-stream "^2.0.0" - merge-stream "^2.0.0" - npm-run-path "^4.0.1" - onetime "^5.1.2" - signal-exit "^3.0.3" - strip-final-newline "^2.0.0" +expand-template@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" + integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== + +exponential-backoff@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.1.tgz#64ac7526fe341ab18a39016cd22c787d01e00bf6" + integrity sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw== extend@~3.0.2: version "3.0.2" @@ -1709,6 +1830,27 @@ form-data@~2.3.2: combined-stream "^1.0.6" mime-types "^2.1.12" +fs-constants@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" + integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== + +fs-extra@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs-minipass@^2.0.0, fs-minipass@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" + integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== + dependencies: + minipass "^3.0.0" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -1734,6 +1876,34 @@ functions-have-names@^1.2.3: resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== +gauge@^4.0.3: + version "4.0.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-4.0.4.tgz#52ff0652f2bbf607a989793d53b751bef2328dce" + integrity sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg== + dependencies: + aproba "^1.0.3 || ^2.0.0" + color-support "^1.1.3" + console-control-strings "^1.1.0" + has-unicode "^2.0.1" + signal-exit "^3.0.7" + string-width "^4.2.3" + strip-ansi "^6.0.1" + wide-align "^1.1.5" + +gauge@~2.7.3: + version "2.7.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" + integrity sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg== + dependencies: + aproba "^1.0.3" + console-control-strings "^1.0.0" + has-unicode "^2.0.0" + object-assign "^4.1.0" + signal-exit "^3.0.0" + string-width "^1.0.1" + strip-ansi "^3.0.1" + wide-align "^1.1.0" + get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" @@ -1745,11 +1915,6 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@ has-symbols "^1.0.3" hasown "^2.0.0" -get-stream@^6.0.0: - version "6.0.1" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" - integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== - get-symbol-description@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5" @@ -1766,6 +1931,11 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" +github-from-package@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" + integrity sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw== + glob-parent@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" @@ -1785,7 +1955,7 @@ glob-to-regexp@^0.4.1: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== -glob@^7.1.3: +glob@^7.1.3, glob@^7.1.4: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -1797,6 +1967,17 @@ glob@^7.1.3: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^8.0.1: + version "8.1.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" + integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^5.0.1" + once "^1.3.0" + globals@^13.19.0: version "13.24.0" resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" @@ -1842,11 +2023,16 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" -graceful-fs@^4.1.2, graceful-fs@^4.2.11, graceful-fs@^4.2.4: +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.6: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== +"graceful-readlink@>= 1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" + integrity sha512-8tLu60LgxF6XpdbK8OW3FA+IfTNBn1ZHGHKF4KQbEeSkajYw5PlYJcKluntgegDPTg8UkHjpet1T82vk6TQ68w== + graphemer@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" @@ -1899,6 +2085,11 @@ has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: dependencies: has-symbols "^1.0.3" +has-unicode@^2.0.0, has-unicode@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ== + hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" @@ -1920,6 +2111,20 @@ html-parse-stringify@^3.0.1: dependencies: void-elements "3.1.0" +http-cache-semantics@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" + integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== + +http-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" + integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w== + dependencies: + "@tootallnate/once" "2" + agent-base "6" + debug "4" + http-signature@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" @@ -1929,10 +2134,20 @@ http-signature@~1.2.0: jsprim "^1.2.2" sshpk "^1.7.0" -human-signals@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" - integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== +https-proxy-agent@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== + dependencies: + agent-base "6" + debug "4" + +humanize-ms@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" + integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ== + dependencies: + ms "^2.0.0" i18next@^19.0.1: version "19.9.2" @@ -1941,6 +2156,18 @@ i18next@^19.0.1: dependencies: "@babel/runtime" "^7.12.0" +iconv-lite@^0.6.2: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + +ieee754@^1.1.13: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + ignore@^5.2.0, ignore@^5.2.4: version "5.3.1" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" @@ -1972,6 +2199,11 @@ indent-string@^4.0.0: resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== +infer-owner@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" + integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -1980,11 +2212,16 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2: +inherits@2, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +ini@~1.3.0: + version "1.3.8" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + internal-slot@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802" @@ -2006,6 +2243,14 @@ invariant@^2.2.4: dependencies: loose-envify "^1.0.0" +ip-address@^9.0.5: + version "9.0.5" + resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-9.0.5.tgz#117a960819b08780c3bd1f14ef3c1cc1d3f3ea5a" + integrity sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g== + dependencies: + jsbn "1.1.0" + sprintf-js "^1.1.3" + is-array-buffer@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98" @@ -2074,16 +2319,18 @@ is-finalizationregistry@^1.0.2: dependencies: call-bind "^1.0.2" +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + integrity sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw== + dependencies: + number-is-nan "^1.0.0" + is-fullwidth-code-point@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== -is-fullwidth-code-point@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz#fae3167c729e7463f8461ce512b080a49268aa88" - integrity sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ== - is-generator-function@^1.0.10: version "1.0.10" resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" @@ -2098,6 +2345,11 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3: dependencies: is-extglob "^2.1.1" +is-lambda@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5" + integrity sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ== + is-map@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" @@ -2152,11 +2404,6 @@ is-shared-array-buffer@^1.0.2, is-shared-array-buffer@^1.0.3: dependencies: call-bind "^1.0.7" -is-stream@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" - integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== - is-string@^1.0.5, is-string@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" @@ -2208,6 +2455,11 @@ isarray@^2.0.5: resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -2255,6 +2507,11 @@ js-yaml@^4.1.0: dependencies: argparse "^2.0.1" +jsbn@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040" + integrity sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A== + jsbn@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" @@ -2302,6 +2559,13 @@ json5@^1.0.1, json5@^1.0.2: dependencies: minimist "^1.2.0" +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg== + optionalDependencies: + graceful-fs "^4.1.6" + jsonwebtoken@^9.0.0: version "9.0.2" resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz#65ff91f4abef1784697d40952bb1998c504caaf3" @@ -2380,45 +2644,6 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" -lilconfig@2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.5.tgz#19e57fd06ccc3848fd1891655b5a447092225b25" - integrity sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg== - -lint-staged@^12.3.5: - version "12.5.0" - resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-12.5.0.tgz#d6925747480ae0e380d13988522f9dd8ef9126e3" - integrity sha512-BKLUjWDsKquV/JuIcoQW4MSAI3ggwEImF1+sB4zaKvyVx1wBk3FsG7UK9bpnmBTN1pm7EH2BBcMwINJzCRv12g== - dependencies: - cli-truncate "^3.1.0" - colorette "^2.0.16" - commander "^9.3.0" - debug "^4.3.4" - execa "^5.1.1" - lilconfig "2.0.5" - listr2 "^4.0.5" - micromatch "^4.0.5" - normalize-path "^3.0.0" - object-inspect "^1.12.2" - pidtree "^0.5.0" - string-argv "^0.3.1" - supports-color "^9.2.2" - yaml "^1.10.2" - -listr2@^4.0.5: - version "4.0.5" - resolved "https://registry.yarnpkg.com/listr2/-/listr2-4.0.5.tgz#9dcc50221583e8b4c71c43f9c7dfd0ef546b75d5" - integrity sha512-juGHV1doQdpNT3GSTs9IUN43QJb7KHdF9uqg7Vufs/tG9VTzpFphqF4pm/ICdAABGQxsyNn9CiYA3StkI6jpwA== - dependencies: - cli-truncate "^2.1.0" - colorette "^2.0.16" - log-update "^4.0.0" - p-map "^4.0.0" - rfdc "^1.3.0" - rxjs "^7.5.5" - through "^2.3.8" - wrap-ansi "^7.0.0" - loader-runner@^4.2.0: version "4.3.0" resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" @@ -2487,16 +2712,6 @@ lodash.once@^4.0.0: resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== -log-update@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1" - integrity sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg== - dependencies: - ansi-escapes "^4.3.0" - cli-cursor "^3.1.0" - slice-ansi "^4.0.0" - wrap-ansi "^6.2.0" - loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" @@ -2504,18 +2719,38 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: dependencies: js-tokens "^3.0.0 || ^4.0.0" -lru-cache@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" - integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== - dependencies: - yallist "^4.0.0" +lru-cache@^7.7.1: + version "7.18.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" + integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== make-error@^1.1.1: version "1.3.6" resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== +make-fetch-happen@^10.0.3: + version "10.2.1" + resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz#f5e3835c5e9817b617f2770870d9492d28678164" + integrity sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w== + dependencies: + agentkeepalive "^4.2.1" + cacache "^16.1.0" + http-cache-semantics "^4.1.0" + http-proxy-agent "^5.0.0" + https-proxy-agent "^5.0.0" + is-lambda "^1.0.1" + lru-cache "^7.7.1" + minipass "^3.1.6" + minipass-collect "^1.0.2" + minipass-fetch "^2.0.3" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.4" + negotiator "^0.6.3" + promise-retry "^2.0.1" + socks-proxy-agent "^7.0.0" + ssri "^9.0.0" + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" @@ -2526,7 +2761,7 @@ merge2@^1.3.0, merge2@^1.4.1: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -micromatch@^4.0.0, micromatch@^4.0.4, micromatch@^4.0.5: +micromatch@^4.0.0, micromatch@^4.0.4: version "4.0.5" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== @@ -2546,10 +2781,10 @@ mime-types@^2.1.12, mime-types@^2.1.27, mime-types@~2.1.19: dependencies: mime-db "1.52.0" -mimic-fn@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" - integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== +mimic-response@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" + integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== minimatch@3.0.5: version "3.0.5" @@ -2572,21 +2807,102 @@ minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" -minimist@^1.2.0, minimist@^1.2.6: +minimatch@^5.0.1: + version "5.1.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== + dependencies: + brace-expansion "^2.0.1" + +minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.6: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== +minipass-collect@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617" + integrity sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA== + dependencies: + minipass "^3.0.0" + +minipass-fetch@^2.0.3: + version "2.1.2" + resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-2.1.2.tgz#95560b50c472d81a3bc76f20ede80eaed76d8add" + integrity sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA== + dependencies: + minipass "^3.1.6" + minipass-sized "^1.0.3" + minizlib "^2.1.2" + optionalDependencies: + encoding "^0.1.13" + +minipass-flush@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/minipass-flush/-/minipass-flush-1.0.5.tgz#82e7135d7e89a50ffe64610a787953c4c4cbb373" + integrity sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw== + dependencies: + minipass "^3.0.0" + +minipass-pipeline@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz#68472f79711c084657c067c5c6ad93cddea8214c" + integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A== + dependencies: + minipass "^3.0.0" + +minipass-sized@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/minipass-sized/-/minipass-sized-1.0.3.tgz#70ee5a7c5052070afacfbc22977ea79def353b70" + integrity sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g== + dependencies: + minipass "^3.0.0" + +minipass@^3.0.0, minipass@^3.1.1, minipass@^3.1.6: + version "3.3.6" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a" + integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== + dependencies: + yallist "^4.0.0" + +minipass@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" + integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== + +minizlib@^2.1.1, minizlib@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" + integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== + dependencies: + minipass "^3.0.0" + yallist "^4.0.0" + +mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" + integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== + +mkdirp@^1.0.3, mkdirp@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@^2.1.1: +ms@^2.0.0, ms@^2.1.1: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +napi-build-utils@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806" + integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg== + native-addon-loader@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/native-addon-loader/-/native-addon-loader-2.0.1.tgz#38a75ff1dd8a259f891a4e3cc8b0a94a544acc52" @@ -2599,39 +2915,98 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== +negotiator@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + neo-async@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== +node-abi@^3.3.0: + version "3.63.0" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.63.0.tgz#9bfbe68b87357f8b508554608b323e9b1052d045" + integrity sha512-vAszCsOUrUxjGAmdnM/pq7gUgie0IRteCQMX6d4A534fQCR93EJU5qgzBvU6EkFfK27s0T3HEV3BOyJIr7OMYw== + dependencies: + semver "^7.3.5" + +node-addon-api@^3.1.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" + integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== + +node-gyp@^9.0.0: + version "9.4.1" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-9.4.1.tgz#8a1023e0d6766ecb52764cc3a734b36ff275e185" + integrity sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ== + dependencies: + env-paths "^2.2.0" + exponential-backoff "^3.1.1" + glob "^7.1.4" + graceful-fs "^4.2.6" + make-fetch-happen "^10.0.3" + nopt "^6.0.0" + npmlog "^6.0.0" + rimraf "^3.0.2" + semver "^7.3.5" + tar "^6.1.2" + which "^2.0.2" + node-releases@^2.0.14: version "2.0.14" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== +nopt@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-6.0.0.tgz#245801d8ebf409c6df22ab9d95b65e1309cdb16d" + integrity sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g== + dependencies: + abbrev "^1.0.0" + normalize-path@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== -npm-run-path@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" - integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== +npmlog@^4.0.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" + integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== dependencies: - path-key "^3.0.0" + are-we-there-yet "~1.1.2" + console-control-strings "~1.1.0" + gauge "~2.7.3" + set-blocking "~2.0.0" + +npmlog@^6.0.0: + version "6.0.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-6.0.2.tgz#c8166017a42f2dea92d6453168dd865186a70830" + integrity sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg== + dependencies: + are-we-there-yet "^3.0.0" + console-control-strings "^1.1.0" + gauge "^4.0.3" + set-blocking "^2.0.0" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + integrity sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ== oauth-sign@~0.9.0: version "0.9.0" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== -object-assign@^4.1.1: +object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== -object-inspect@^1.12.2, object-inspect@^1.13.1: +object-inspect@^1.13.1: version "1.13.1" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== @@ -2651,7 +3026,7 @@ object.assign@^4.1.4, object.assign@^4.1.5: has-symbols "^1.0.3" object-keys "^1.1.1" -object.entries@^1.1.7: +object.entries@^1.1.8: version "1.1.8" resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.8.tgz#bffe6f282e01f4d17807204a24f8edd823599c41" integrity sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ== @@ -2660,7 +3035,7 @@ object.entries@^1.1.7: define-properties "^1.2.1" es-object-atoms "^1.0.0" -object.fromentries@^2.0.7: +object.fromentries@^2.0.7, object.fromentries@^2.0.8: version "2.0.8" resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.8.tgz#f7195d8a9b97bd95cbc1999ea939ecd1a2b00c65" integrity sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ== @@ -2679,7 +3054,7 @@ object.groupby@^1.0.1: define-properties "^1.2.1" es-abstract "^1.23.2" -object.hasown@^1.1.3: +object.hasown@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.4.tgz#e270ae377e4c120cdcb7656ce66884a6218283dc" integrity sha512-FZ9LZt9/RHzGySlBARE3VF+gE26TxR38SdmqOqliuTnl9wrKulaQs+4dee1V+Io8VfxqzAfHu6YuRgUy8OHoTg== @@ -2688,7 +3063,7 @@ object.hasown@^1.1.3: es-abstract "^1.23.2" es-object-atoms "^1.0.0" -object.values@^1.1.6, object.values@^1.1.7: +object.values@^1.1.6, object.values@^1.1.7, object.values@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.0.tgz#65405a9d92cee68ac2d303002e0b8470a4d9ab1b" integrity sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ== @@ -2697,20 +3072,13 @@ object.values@^1.1.6, object.values@^1.1.7: define-properties "^1.2.1" es-object-atoms "^1.0.0" -once@^1.3.0: +once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== dependencies: wrappy "1" -onetime@^5.1.0, onetime@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" - integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== - dependencies: - mimic-fn "^2.1.0" - optionator@^0.9.3: version "0.9.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" @@ -2780,7 +3148,7 @@ path-is-absolute@^1.0.0: resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== -path-key@^3.0.0, path-key@^3.1.0: +path-key@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== @@ -2815,11 +3183,6 @@ picomatch@^2.3.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== -pidtree@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/pidtree/-/pidtree-0.5.0.tgz#ad5fbc1de78b8a5f99d6fbdd4f6e4eee21d1aca1" - integrity sha512-9nxspIM7OpZuhBxPg73Zvyq7j1QMPMPsGKTqRc2XOaFQauDvoNz9fM1Wdkjmeo7l9GXOZiRs97sPkuayl39wjA== - pkg-dir@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" @@ -2832,6 +3195,25 @@ possible-typed-array-names@^1.0.0: resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== +prebuild-install@7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.0.tgz#991b6ac16c81591ba40a6d5de93fb33673ac1370" + integrity sha512-CNcMgI1xBypOyGqjp3wOc8AAo1nMhZS3Cwd3iHIxOdAUbb+YxdNuM4Z5iIrZ8RLvOsf3F3bl7b7xGq6DjQoNYA== + dependencies: + detect-libc "^2.0.0" + expand-template "^2.0.3" + github-from-package "0.0.0" + minimist "^1.2.3" + mkdirp-classic "^0.5.3" + napi-build-utils "^1.0.1" + node-abi "^3.3.0" + npmlog "^4.0.1" + pump "^3.0.0" + rc "^1.2.7" + simple-get "^4.0.0" + tar-fs "^2.0.0" + tunnel-agent "^0.6.0" + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -2845,9 +3227,27 @@ prettier-linter-helpers@^1.0.0: fast-diff "^1.1.2" prettier@^3: - version "3.2.5" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.2.5.tgz#e52bc3090586e824964a8813b09aba6233b28368" - integrity sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A== + version "3.3.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.2.tgz#03ff86dc7c835f2d2559ee76876a3914cec4a90a" + integrity sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA== + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +promise-inflight@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" + integrity sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g== + +promise-retry@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-2.0.1.tgz#ff747a13620ab57ba688f5fc67855410c370da22" + integrity sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g== + dependencies: + err-code "^2.0.2" + retry "^0.12.0" prop-types-extra@^1.0.1: version "1.1.1" @@ -2871,6 +3271,14 @@ psl@^1.1.28: resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + punycode@^2.1.0, punycode@^2.1.1: version "2.3.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" @@ -2893,6 +3301,16 @@ randombytes@^2.1.0: dependencies: safe-buffer "^5.1.0" +rc@^1.2.7: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + react-bootstrap@^0.33.0: version "0.33.1" resolved "https://registry.yarnpkg.com/react-bootstrap/-/react-bootstrap-0.33.1.tgz#e072592aa143b9792526281272eca754bc9a4940" @@ -2994,6 +3412,28 @@ react@^16.12.0: object-assign "^4.1.1" prop-types "^15.6.2" +readable-stream@^2.0.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + rechoir@^0.8.0: version "0.8.0" resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.8.0.tgz#49f866e0d32146142da3ad8f0eff352b3215ff22" @@ -3112,24 +3552,21 @@ resolve@^2.0.0-next.5: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" -restore-cursor@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" - integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== - dependencies: - onetime "^5.1.0" - signal-exit "^3.0.2" +resolve@~1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" + integrity sha512-9znBF0vBcaSN3W2j7wKvdERPwqTxSpCq+if5C0WoTCyV9n24rua28jeuQ2pL/HOf+yUe/Mef+H/5p60K0Id3bg== + +retry@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" + integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow== reusify@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== -rfdc@^1.3.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.1.tgz#2b6d4df52dffe8bb346992a10ea9451f24373a8f" - integrity sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg== - rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" @@ -3144,13 +3581,6 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -rxjs@^7.5.5: - version "7.8.1" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" - integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== - dependencies: - tslib "^2.1.0" - safe-array-concat@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.2.tgz#81d77ee0c4e8b863635227c721278dd524c20edb" @@ -3161,11 +3591,16 @@ safe-array-concat@^1.1.2: has-symbols "^1.0.3" isarray "^2.0.5" -safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2: +safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + safe-regex-test@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.3.tgz#a5b4c0f06e0ab50ea2c395c14d8371232924c377" @@ -3175,7 +3610,7 @@ safe-regex-test@^1.0.3: es-errors "^1.3.0" is-regex "^1.1.4" -safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: +"safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== @@ -3212,12 +3647,10 @@ semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.3.4, semver@^7.5.4: - version "7.6.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" - integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg== - dependencies: - lru-cache "^6.0.0" +semver@^7.3.4, semver@^7.3.5, semver@^7.5.4: + version "7.6.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" + integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== serialize-javascript@^6.0.1, serialize-javascript@^6.0.2: version "6.0.2" @@ -3226,6 +3659,11 @@ serialize-javascript@^6.0.1, serialize-javascript@^6.0.2: dependencies: randombytes "^2.1.0" +set-blocking@^2.0.0, set-blocking@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== + set-cookie-parser@^2.4.6: version "2.6.0" resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz#131921e50f62ff1a66a461d7d62d7b21d5d15a51" @@ -3282,11 +3720,25 @@ side-channel@^1.0.4, side-channel@^1.0.6: get-intrinsic "^1.2.4" object-inspect "^1.13.1" -signal-exit@^3.0.2, signal-exit@^3.0.3: +signal-exit@^3.0.0, signal-exit@^3.0.7: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== +simple-concat@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f" + integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== + +simple-get@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-4.0.1.tgz#4a39db549287c979d352112fa03fd99fd6bc3543" + integrity sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA== + dependencies: + decompress-response "^6.0.0" + once "^1.3.1" + simple-concat "^1.0.0" + slash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" @@ -3297,31 +3749,27 @@ slash@^5.1.0: resolved "https://registry.yarnpkg.com/slash/-/slash-5.1.0.tgz#be3adddcdf09ac38eebe8dcdc7b1a57a75b095ce" integrity sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg== -slice-ansi@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787" - integrity sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ== - dependencies: - ansi-styles "^4.0.0" - astral-regex "^2.0.0" - is-fullwidth-code-point "^3.0.0" +smart-buffer@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" + integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== -slice-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" - integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== +socks-proxy-agent@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz#dc069ecf34436621acb41e3efa66ca1b5fed15b6" + integrity sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww== dependencies: - ansi-styles "^4.0.0" - astral-regex "^2.0.0" - is-fullwidth-code-point "^3.0.0" + agent-base "^6.0.2" + debug "^4.3.3" + socks "^2.6.2" -slice-ansi@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-5.0.0.tgz#b73063c57aa96f9cd881654b15294d95d285c42a" - integrity sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ== +socks@^2.6.2: + version "2.8.3" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.8.3.tgz#1ebd0f09c52ba95a09750afe3f3f9f724a800cb5" + integrity sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw== dependencies: - ansi-styles "^6.0.0" - is-fullwidth-code-point "^4.0.0" + ip-address "^9.0.5" + smart-buffer "^4.2.0" source-map-support@~0.5.20: version "0.5.21" @@ -3341,6 +3789,11 @@ source-map@^0.7.4: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== +sprintf-js@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a" + integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== + sshpk@^1.7.0: version "1.18.0" resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.18.0.tgz#1663e55cddf4d688b86a46b77f0d5fe363aba028" @@ -3356,17 +3809,28 @@ sshpk@^1.7.0: safer-buffer "^2.0.2" tweetnacl "~0.14.0" -string-argv@^0.3.1: - version "0.3.2" - resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.2.tgz#2b6d0ef24b656274d957d54e0a4bbf6153dc02b6" - integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q== +ssri@^9.0.0: + version "9.0.1" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-9.0.1.tgz#544d4c357a8d7b71a19700074b6883fcb4eae057" + integrity sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q== + dependencies: + minipass "^3.1.1" string-template@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/string-template/-/string-template-1.0.0.tgz#9e9f2233dc00f218718ec379a28a5673ecca8b96" integrity sha512-SLqR3GBUXuoPP5MmYtD7ompvXiG87QjT6lzOszyXjTM86Uu7At7vNnt2xgyTLq5o9T4IxTYFyGxcULqpsmsfdg== -string-width@^4.1.0, string-width@^4.2.0: +string-width@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + integrity sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw== + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -3375,16 +3839,7 @@ string-width@^4.1.0, string-width@^4.2.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" -string-width@^5.0.0: - version "5.1.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" - integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== - dependencies: - eastasianwidth "^0.2.0" - emoji-regex "^9.2.2" - strip-ansi "^7.0.1" - -string.prototype.matchall@^4.0.10: +string.prototype.matchall@^4.0.11: version "4.0.11" resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz#1092a72c59268d2abaad76582dccc687c0297e0a" integrity sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg== @@ -3430,35 +3885,49 @@ string.prototype.trimstart@^1.0.8: define-properties "^1.2.1" es-object-atoms "^1.0.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^3.0.0, strip-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + integrity sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg== + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== dependencies: ansi-regex "^5.0.1" -strip-ansi@^7.0.1: - version "7.1.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" - integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== - dependencies: - ansi-regex "^6.0.1" - strip-bom@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== -strip-final-newline@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" - integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== - strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== + supports-color@^7.1.0: version "7.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" @@ -3473,11 +3942,6 @@ supports-color@^8.0.0: dependencies: has-flag "^4.0.0" -supports-color@^9.2.2: - version "9.4.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-9.4.0.tgz#17bfcf686288f531db3dea3215510621ccb55954" - integrity sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw== - supports-preserve-symlinks-flag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" @@ -3496,6 +3960,39 @@ tapable@^2.1.1, tapable@^2.2.0: resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== +tar-fs@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" + integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng== + dependencies: + chownr "^1.1.1" + mkdirp-classic "^0.5.2" + pump "^3.0.0" + tar-stream "^2.1.4" + +tar-stream@^2.1.4: + version "2.2.0" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" + integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== + dependencies: + bl "^4.0.3" + end-of-stream "^1.4.1" + fs-constants "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.1.1" + +tar@^6.1.11, tar@^6.1.2: + version "6.2.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a" + integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^5.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + terser-webpack-plugin@^5.3.10: version "5.3.10" resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz#904f4c9193c6fd2a03f693a2150c62a92f40d199" @@ -3522,11 +4019,6 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== -through@^2.3.8: - version "2.3.8" - resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" - integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== - ticks-to-date@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/ticks-to-date/-/ticks-to-date-1.0.6.tgz#572d290a7785a7c9cfb544a99d38e4a5ccf1a603" @@ -3592,7 +4084,7 @@ tsconfig-paths@^3.15.0: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@^2.1.0, tslib@^2.6.2: +tslib@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== @@ -3604,6 +4096,13 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" +"turbowalk@git+https://github.com/Nexus-Mods/node-turbowalk": + version "3.0.3" + resolved "git+https://github.com/Nexus-Mods/node-turbowalk#314f2cdb904a9a075c35261e8a1de10b0af20295" + dependencies: + fs-extra "^8.1.0" + winapi-bindings Nexus-Mods/node-winapi-bindings + tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" @@ -3621,11 +4120,6 @@ type-fest@^0.20.2: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== -type-fest@^0.21.3: - version "0.21.3" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" - integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== - typed-array-buffer@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz#1867c5d83b20fcb5ccf32649e5e2fc7424474ff3" @@ -3675,7 +4169,7 @@ typed-emitter@^1.3.1: resolved "https://registry.yarnpkg.com/typed-emitter/-/typed-emitter-1.4.0.tgz#38c6bf1224e764906bb20cb0b458fa914100607c" integrity sha512-weBmoo3HhpKGgLBOYwe8EB31CzDFuaK7CCL+axXhUYhn4jo6DSkHnbefboCF5i4DQ2aMFe0C/FdTWcPdObgHyg== -typescript@^5.3.3: +typescript@^5.4.5: version "5.4.5" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== @@ -3710,6 +4204,25 @@ unicorn-magic@^0.1.0: resolved "https://registry.yarnpkg.com/unicorn-magic/-/unicorn-magic-0.1.0.tgz#1bb9a51c823aaf9d73a8bfcd3d1a23dde94b0ce4" integrity sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ== +unique-filename@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-2.0.1.tgz#e785f8675a9a7589e0ac77e0b5c34d2eaeac6da2" + integrity sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A== + dependencies: + unique-slug "^3.0.0" + +unique-slug@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-3.0.0.tgz#6d347cf57c8a7a7a6044aabd0e2d74e4d76dc7c9" + integrity sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w== + dependencies: + imurmurhash "^0.1.4" + +universalify@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== + update-browserslist-db@^1.0.13: version "1.0.13" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz#3c5e4f5c083661bd38ef64b6328c26ed6c8248c4" @@ -3725,6 +4238,11 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + uuid@^3.3.2: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" @@ -3818,10 +4336,10 @@ webpack-sources@^3.2.3: resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== -webpack@^5, webpack@^5.76.0: - version "5.91.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.91.0.tgz#ffa92c1c618d18c878f06892bbdc3373c71a01d9" - integrity sha512-rzVwlLeBWHJbmgTC/8TvAcu5vpJNII+MelQpylD4jNERPwpBJOE2lEcko1zJX3QJeLjTTAnQxn/OJ8bjDzVQaw== +webpack@^5, webpack@^5.92.1: + version "5.92.1" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.92.1.tgz#eca5c1725b9e189cffbd86e8b6c3c7400efc5788" + integrity sha512-JECQ7IwJb+7fgUFBlrJzbyu3GEuNBcdqr1LD7IbSzwkSmIevTm8PF+wej3Oxuz/JFBUZ6O1o43zsPkwm1C4TmA== dependencies: "@types/eslint-scope" "^3.7.3" "@types/estree" "^1.0.5" @@ -3829,10 +4347,10 @@ webpack@^5, webpack@^5.76.0: "@webassemblyjs/wasm-edit" "^1.12.1" "@webassemblyjs/wasm-parser" "^1.12.1" acorn "^8.7.1" - acorn-import-assertions "^1.9.0" + acorn-import-attributes "^1.9.5" browserslist "^4.21.10" chrome-trace-event "^1.0.2" - enhanced-resolve "^5.16.0" + enhanced-resolve "^5.17.0" es-module-lexer "^1.2.1" eslint-scope "5.1.1" events "^3.2.0" @@ -3898,35 +4416,33 @@ which-typed-array@^1.1.14, which-typed-array@^1.1.15, which-typed-array@^1.1.9: gopd "^1.0.1" has-tostringtag "^1.0.2" -which@^2.0.1: +which@^2.0.1, which@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== dependencies: isexe "^2.0.0" +wide-align@^1.1.0, wide-align@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3" + integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== + dependencies: + string-width "^1.0.2 || 2 || 3 || 4" + wildcard@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67" integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ== -wrap-ansi@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" - integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== +winapi-bindings@Nexus-Mods/node-winapi-bindings: + version "2.6.1" + resolved "https://codeload.github.com/Nexus-Mods/node-winapi-bindings/tar.gz/3935f9603e5e8d2f18336ce0398043fd4a231d6c" dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" + autogypi "^0.2.2" + node-addon-api "^3.1.0" + node-gyp "^9.0.0" + prebuild-install "7.1.0" wrappy@1: version "1.0.2" @@ -3938,11 +4454,6 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yaml@^1.10.2: - version "1.10.2" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" - integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== - yn@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"