From 42f1a90f8c4aa4156a67bef04913c7930f56b1fc Mon Sep 17 00:00:00 2001 From: IDCs Date: Wed, 5 Jun 2024 06:24:46 +0100 Subject: [PATCH 01/20] Adding CK tool --- CHANGELOG.md | 4 ++++ package.json | 2 +- src/assets/CK.png | Bin 0 -> 1558 bytes src/index.ts | 9 +++++++++ 4 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 src/assets/CK.png diff --git a/CHANGELOG.md b/CHANGELOG.md index e9341dd..e074df7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [0.7.0] - 2024-xx-xx + +- Added CK tool + ## [0.6.7] - 2024-03-19 - Removed all logic pertaining to the sPhotoModeFolder ini entry (Please add/remove/modify the ini entry manually if needed) diff --git a/package.json b/package.json index b894cd0..89a71cf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "starfield", - "version": "0.6.7", + "version": "0.7.0", "description": "Vortex Extension for Starfield", "author": "Nexus Mods", "private": true, diff --git a/src/assets/CK.png b/src/assets/CK.png new file mode 100644 index 0000000000000000000000000000000000000000..e77769a6684097025e682f2b20b5145f3d714231 GIT binary patch literal 1558 zcmV+x2I={UP)+iT-kIsV+S?9O2U-dhXiIFZRLesOt3oB4>|#W87bRuc#6+@{(rA6GyTn9U zb+fRtxCEBfNFoi^MG>WlSp!&<7VIMsV0EX7yy^^Xr=3S<=H7X5_K&$VppDDA`_Cr* zCcoUAb8^q``}_WW=ll+gV;tic$2k5^umQMWGMkf=rxuH8rDdYDyh2PYoSXuDz-xfz zhiokUoAqVD*5c_UlYacba_+2M!j$RLv0Act>8VDB14FpnZv1_h=xRR}Os7)M06YFy z1fG^8>BWa1U1M3bb{)AB^5{9CaXOH7`#9qOy9nXnOHw zVYA!Qz+?X{flI3&ZxAQ^0kM7WezE4MXGa}fXObioMHwBpv~`GI|KU{ugn{fEaU_p1 z_nVm&m4z$T{EAEGzh>aFkA9zznnkrp(zh}u1nG1d07;TLd7^_~)UV^V*>kgjO+PSr z{@JwQ6>->mUNryxQ_+6WFB*RPys$f*-&J4?d$jRMv2o`v0R(QunF$b+i;F2Jn!>qW zFTp^7FOD7|8VO^wJJ|Zt3v_oJW9p0&Ty8hfNQBefU6?HvT<&~)mo8FTUV+2u1WDrX zN3Hn3>BD3;yWAd+C@P*tbR>fJ%o(gUD^4{R-^B~KJrmL5F--TbtYiJ=Eu)N<&8_B- zyLOL``+a@LCWUS9?jxaV0L+<`PyKW2sjF{b)xGyHt*neyYu8a+Qo=c}m*SEV61q;q z^DoeTj*pwh;-1*z{I258t(b`yN@zIjab$j_tU=MSQ5aRA=_ z>kb~fe<_vo=A&zI;?WrI?%c`CO;0fIuDhwLZ{YBO{XFygjqKaqOlwPq^z!+00Jyb! zHqM-Ee)q&WW>(%x(akrH237ZXBeUk!5DABn8&*6-UHw|R28P+vvIj*`04Trx4*v4~ zhgd8YWLZWr$t+*Jn$n6&{`}5f-rT&2&bA{&M)V+%XD$ekBuNr#o{MZ%WoU4aWJ0HE?wxdg{u$|X3N*NfF=N0udG(I`1; zF1n^+5<tE%|q!w&#x-oA}~zmJwTUI%yt@BkA4)$nBj6dAArP9P7+2lkyj zaeTmPwNqMAL9S~8HH#KAv7itDXLb$+MMc*U2#10I9BSQ9?b7=)%4ixnxhmmc5FiIA zFi5z_+>#bRmJFv(V^RfjfrWO5v+;qtRp#SuM^H>Ap4<2*zB+xHWI|`q_YKY4x1#I% zD2cL)3TD^*i1&Bz5+mX8LEsdi0~#<0gveacL14)EMijt7=0Y+V>97EyWHO<6&z>!r zzo=GDB@&n{S^To05l=xO^X|Tf+zI)d?e4>YJCPu>HZlK%9wFZm; zF(7h{#f`P3LZ%;FEsi9RHiGI)C6nPVPo11$&dSPiI2;TQ4FM31M3`|)Ia6koB8e+} z-Rq_8XqejcE0&4vhfQz!gA9T>mJM3^FD%C&OkGkZRP3 z?Ha 'CreationKit.exe', + logo: 'CK.png', + requiredFiles: [ + 'CreationKit.exe', + ], + }, ]; const gameFinderQuery = { From a8878f1318c558e3db6eb5e9b5872e6e0c6e8554 Mon Sep 17 00:00:00 2001 From: IDCs Date: Wed, 5 Jun 2024 14:20:11 +0100 Subject: [PATCH 02/20] modified extension to support new CK release See changelog for all modifications --- CHANGELOG.md | 6 ++ src/actions/settings.ts | 7 +- src/common.ts | 6 ++ src/index.ts | 18 +++-- src/loadOrder/StarFieldLoadOrder.tsx | 30 +++++-- src/migrations/migrations.ts | 51 ++++++++++-- src/reducers/settings.ts | 5 ++ src/tests.ts | 6 +- src/types.ts | 1 + src/util.ts | 51 +++++++++++- src/views/InfoPanel.tsx | 2 +- src/views/InfoPanelCK.tsx | 62 +++++++++++++++ src/views/Settings.tsx | 114 +++++++++++++++++++-------- 13 files changed, 302 insertions(+), 57 deletions(-) create mode 100644 src/views/InfoPanelCK.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index e074df7..f89c9a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p ## [0.7.0] - 2024-xx-xx - Added CK tool +- Added ability to change LO functionality based on game version +- Added setting allowing to switch between dnd and auto-sort (classic gamebryo/loot) +- Added new mid-master plugins +- Fixed game version resolution issues for XBox Game Pass +- Setting controls will now hide/show depending on the game version. +- Code cleanup ## [0.6.7] - 2024-03-19 diff --git a/src/actions/settings.ts b/src/actions/settings.ts index 49f7397..a676a20 100644 --- a/src/actions/settings.ts +++ b/src/actions/settings.ts @@ -1,9 +1,14 @@ import { createAction } from 'redux-act'; +import { LoadOrderManagementType } from '../types'; + export const setDirectoryJunctionEnabled = createAction('SET_STARFIELD_JUNCTION_ENABLED', (enabled: boolean) => enabled); export const setDirectoryJunctionSuppress = createAction('SET_STARFIELD_JUNCTION_SUPPRESS', (suppress: boolean) => suppress); export const setMigrationVersion = createAction('SET_STARFIELD_MIGRATION_VERSION', (version: string) => version); -export const setPluginsEnabler = createAction('SET_STARFIELD_PLUGIN_ENABLER', (enabled: boolean) => enabled); \ No newline at end of file +export const setPluginsEnabler = createAction('SET_STARFIELD_PLUGIN_ENABLER', (enabled: boolean) => enabled); + +export const setLoadOrderManagementType = createAction('SET_STARFIELD_LOAD_ORDER_MANAGEMENT_TYPE', + (profileId: string, type: LoadOrderManagementType) => ({ profileId, type })); \ No newline at end of file diff --git a/src/common.ts b/src/common.ts index e957d6d..64741aa 100644 --- a/src/common.ts +++ b/src/common.ts @@ -8,6 +8,7 @@ export const PLUGINS_ENABLER_FILENAME = 'SFPluginsTxtEnabler'; export const NS = 'game-starfield'; export const GAME_ID = 'starfield'; export const XBOX_ID = 'BethesdaSoftworks.ProjectGold'; +export const XBOX_APP_X_MANIFEST = 'appxmanifest.xml'; export const STEAMAPP_ID = '1716740'; export const MOD_TYPE_ASI_MOD = 'starfield-asi-mod'; @@ -16,8 +17,13 @@ export const JUNCTION_NOTIFICATION_ID = 'starfield-junction-notif'; export const MISSING_PLUGINS_NOTIFICATION_ID = 'starfield-missing-plugins'; export const MY_GAMES_DATA_WARNING = 'starfield-my-games-data-warning'; +// Below constraint is used for the GAME version. Not the extension. +export const PLUGIN_ENABLER_CONSTRAINT = '<1.10.0'; + // This is the order we expect the native plugins to be arranged. export const NATIVE_PLUGINS = ['starfield.esm', 'blueprintships-starfield.esm', 'oldmars.esm', 'constellation.esm']; +export const NATIVE_MID_PLUGINS = ['sfbgs006.esm', 'sfbgs007.esm', 'sfbgs008.esm']; +export const ALL_NATIVE_PLUGINS = [].concat(NATIVE_PLUGINS, NATIVE_MID_PLUGINS); export const DATA_SUBFOLDERS = [ 'Meshes', diff --git a/src/index.ts b/src/index.ts index cc33a6d..1fb2a57 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,9 @@ /* eslint-disable */ import path from 'path'; +import semver from 'semver'; -import { actions, fs, types, selectors, util } from 'vortex-api'; +import { fs, types, selectors, util } from 'vortex-api'; import { getDataPath, testDataPath } from './modTypes/dataPath'; import { getASIPluginsPath, testASIPluginsPath } from './modTypes/asiMod'; @@ -13,7 +14,7 @@ import { testASILoaderSupported, installASILoader, testASIModSupported, installA import { mergeASIIni, testASIMergeIni } from './merges/iniMerge'; import { isStarfield, openAppDataPath, openSettingsPath, dismissNotifications, linkAsiLoader, - walkPath, removePluginsFile, forceRefresh } from './util'; + walkPath, removePluginsFile, forceRefresh, getGameVersionAsync, getGameVersionSync } from './util'; import { toggleJunction, setup } from './setup'; import { raiseJunctionDialog, testFolderJunction, testLooseFiles, testDeprecatedFomod, testPluginsEnabler } from './tests'; @@ -29,7 +30,7 @@ import StarFieldLoadOrder from './loadOrder/StarFieldLoadOrder'; import { GAME_ID, SFSE_EXE, MOD_TYPE_DATAPATH, MOD_TYPE_ASI_MOD, STEAMAPP_ID, XBOX_ID, TARGET_ASI_LOADER_NAME, - ASI_LOADER_BACKUP, PLUGINS_TXT, PLUGINS_BACKUP } from './common'; + ASI_LOADER_BACKUP, PLUGINS_TXT, PLUGINS_BACKUP, PLUGIN_ENABLER_CONSTRAINT } from './common'; const supportedTools: types.ITool[] = [ { @@ -102,6 +103,7 @@ function main(context: types.IExtensionContext) { requiredFiles: ['Starfield.exe'], setup: (discovery) => setup(context.api, discovery) as any, supportedTools, + getGameVersion: () => getGameVersionAsync(context.api), requiresLauncher: requiresLauncher as any, details: { supportsSymlinks: false, @@ -122,6 +124,10 @@ function main(context: types.IExtensionContext) { raiseJunctionDialog(context.api, true); } }, + needsEnabler: () => { + const version = getGameVersionSync(context.api); + return semver.satisfies(version, PLUGIN_ENABLER_CONSTRAINT); + } }), () => selectors.activeGameId(context.api.getState()) === GAME_ID, 150 @@ -139,6 +145,9 @@ function main(context: types.IExtensionContext) { context.registerAction('mod-icons', 500, 'open-ext', {}, 'Open Game Application Data Folder', openAppDataPath, (gameId?: string[]) => isStarfield(context, gameId)); context.registerAction('fb-load-order-icons', 150, 'open-ext', {}, 'View Plugins File', openAppDataPath, (gameId?: string[]) => isStarfield(context, gameId)); context.registerAction('fb-load-order-icons', 500, 'remove', {}, 'Reset Plugins File', () => removePluginsWrap(context.api), (gameId?: string[]) => isStarfield(context, gameId)); + context.registerAction('fb-load-order-icons', 600, 'loot-sort', {}, 'Sort via LOOT', () => { + context.api.showErrorNotification('Not Implemented Yet', 'Soon TM'); + }); context.registerLoadOrder(new StarFieldLoadOrder(context.api)); @@ -179,9 +188,6 @@ function main(context: types.IExtensionContext) { context.api.onAsync('did-deploy', (profileId: string, deployment: types.IDeploymentManifest) => onDidDeployEvent(context.api, profileId, deployment)); context.api.onAsync('will-purge', (profileId: string) => onWillPurgeEvent(context.api, profileId)); context.api.onAsync('did-purge', (profileId: string) => onDidPurgeEvent(context.api, profileId)); - // context.api.onAsync('intercept-file-changes', (intercepted: types.IFileChange[], cb: (result: types.IFileChange[]) => void) => { - // return onInterceptFileChanges(context.api, intercepted, cb); - // }); }); return true; diff --git a/src/loadOrder/StarFieldLoadOrder.tsx b/src/loadOrder/StarFieldLoadOrder.tsx index 8805ad5..afc8146 100644 --- a/src/loadOrder/StarFieldLoadOrder.tsx +++ b/src/loadOrder/StarFieldLoadOrder.tsx @@ -1,19 +1,22 @@ /* eslint-disable */ import React from 'react'; import path from 'path'; +import semver from 'semver'; import { fs, selectors, types, util } from 'vortex-api'; import { IPluginRequirement } from '../types'; import { - GAME_ID, PLUGINS_TXT, PLUGINS_BACKUP, MOD_TYPE_ASI_MOD, PLUGINS_ENABLER_FILENAME, NATIVE_PLUGINS, + GAME_ID, PLUGINS_TXT, PLUGINS_BACKUP, MOD_TYPE_ASI_MOD, PLUGINS_ENABLER_FILENAME, ALL_NATIVE_PLUGINS, DLL_EXT, ASI_EXT, MOD_TYPE_DATAPATH, SFSE_EXE, TARGET_ASI_LOADER_NAME, DATA_PLUGINS, MISSING_PLUGINS_NOTIFICATION_ID, + PLUGIN_ENABLER_CONSTRAINT, } from '../common'; import { download } from '../downloader'; -import InfoPanel from '../views/InfoPanel'; -import { walkPath, findModByFile, forceRefresh } from '../util'; +import { walkPath, findModByFile, forceRefresh, requiresPluginEnabler, getGameVersionSync } from '../util'; import { migrateTestFiles } from './testFileHandler'; import { setPluginsEnabler } from '../actions/settings'; +import { InfoPanel } from '../views/InfoPanel'; +import { InfoPanelCK } from '../views/InfoPanelCK'; type PluginRequirements = { [gameStore: string]: IPluginRequirement[] }; export const PLUGIN_REQUIREMENTS: PluginRequirements = { @@ -68,7 +71,16 @@ class StarFieldLoadOrder implements types.ILoadOrderGameInfo { this.clearStateOnPurge = true; this.toggleableEntries = true; this.noCollectionGeneration = true; - this.usageInstructions = () => (); + this.usageInstructions = (() => { + const gameVersion = getGameVersionSync(api); + const needsEnabler = semver.satisfies(gameVersion, PLUGIN_ENABLER_CONSTRAINT); + return needsEnabler ? ( + + ) : ( + + ); + }) as any; + this.mApi = api; this.deserializeLoadOrder = this.deserializeLoadOrder.bind(this); this.serializeLoadOrder = this.serializeLoadOrder.bind(this); @@ -178,7 +190,7 @@ class StarFieldLoadOrder implements types.ILoadOrderGameInfo { let nativeIdx = 0; const nextNativeIdx = () => nativeIdx++; - for (const plugin of NATIVE_PLUGINS) { + for (const plugin of ALL_NATIVE_PLUGINS) { // Make sure the native plugin is deployed to the game's data folder. let idx = plugins.findIndex(entry => path.basename(entry.filePath.toLowerCase()) === plugin); if (idx === -1) { @@ -259,7 +271,7 @@ class StarFieldLoadOrder implements types.ILoadOrderGameInfo { private async serializePluginsFile(plugins: types.ILoadOrderEntry[]): Promise { const data = plugins.map(plugin => { // Strip the native plugins from whatever we write to the file as it's uneccessary. - if (NATIVE_PLUGINS.includes(plugin.name.toLowerCase())) { + if (ALL_NATIVE_PLUGINS.includes(plugin.name.toLowerCase())) { return ''; } const invalid = plugin.data?.isInvalid ? '#' : ''; @@ -283,9 +295,11 @@ class StarFieldLoadOrder implements types.ILoadOrderGameInfo { } } - private isLOManagedByVortex(): boolean { + private async isLOManagedByVortex(): Promise { const state = this.mApi.getState(); - return util.getSafe(state, ['settings', 'starfield', 'pluginEnabler'], false); + const needsEnabler = await requiresPluginEnabler(this.mApi); + const enablerStatus = util.getSafe(state, ['settings', 'starfield', 'pluginEnabler'], false); + return needsEnabler ? enablerStatus : true; } } diff --git a/src/migrations/migrations.ts b/src/migrations/migrations.ts index 65c90d5..7830af2 100644 --- a/src/migrations/migrations.ts +++ b/src/migrations/migrations.ts @@ -3,24 +3,24 @@ import path from 'path'; import semver from 'semver'; import { actions, fs, selectors, types, util } from 'vortex-api'; import { setMigrationVersion } from '../actions/settings'; -import { DATA_SUBFOLDERS, GAME_ID, MOD_TYPE_DATAPATH, NS } from '../common'; -import { deploy, nuclearPurge, getExtensionVersion } from '../util'; - -const DEBUG = false; +import { DATA_SUBFOLDERS, GAME_ID, MOD_TYPE_DATAPATH, NS, PLUGIN_ENABLER_CONSTRAINT } from '../common'; +import { deploy, nuclearPurge, getExtensionVersion, requiresPluginEnabler } from '../util'; // Migrations should be self contained - do not let any errors escape from them. // if a migration fails, it should allow the user to fallback to his previous state, // or to retry the migration. export async function migrateExtension(api: types.IExtensionApi) { - if (DEBUG) debugger; const state = api.getState(); if (selectors.activeGameId(state) !== GAME_ID) { return Promise.resolve(); } - const currentVersion = util.getSafe(state, ['settings', 'starfield', 'migrationVersion'], '0.0.0'); + const currentVersion = '0.0.0'; + // const currentVersion = util.getSafe(state, ['settings', 'starfield', 'migrationVersion'], '0.0.0'); const newVersion = await getExtensionVersion(); - if (semver.gt('0.6.0', currentVersion)) { + if (semver.gt('0.7.0', currentVersion)) { + await migrate070(api, newVersion); + } else if (semver.gt('0.6.0', currentVersion)) { await migrate060(api, newVersion); } else if (semver.gt('0.5.0', currentVersion)) { await migrate050(api, newVersion); @@ -29,6 +29,43 @@ export async function migrateExtension(api: types.IExtensionApi) { api.store?.dispatch(setMigrationVersion(newVersion)); } +export async function migrate070(api: types.IExtensionApi, version: string) { + const enablePlugins = await requiresPluginEnabler(api); + if (enablePlugins) { + return migrate060(api, version); + } + + const notificationId = 'starfield-update-notif-0.7.0'; + const t = api.translate; + api.sendNotification({ + message: t('Starfield extension has been updated to v{{newVersion}}', { replace: { newVersion: version }, ns: NS }), + noDismiss: true, + allowSuppress: false, + type: 'success', + id: notificationId, + actions: [ + { + title: t('More', { ns: NS }), + action: () => api.showDialog('success', 'Starfield Extension Update v{{newVersion}}', { + parameters: { + newVersion: version + }, + bbcode: t('As of version "{{cutoff}}" of the game, the plugin enabler is no longer required as the game ' + + 'now fully supports plugins on its own.', { replace: { cutoff: PLUGIN_ENABLER_CONSTRAINT.slice(1) } }), + }, [ + { + label: t('Close', { ns: NS }), + action: () => { + api.dismissNotification(notificationId); + api.suppressNotification(notificationId, true); + } + } + ], 'starfield-update-0.7.0') + } + ], + }); +} + export async function migrate060(api: types.IExtensionApi, version: string) { const notificationId = 'starfield-update-notif-0.6.0'; const t = api.translate; diff --git a/src/reducers/settings.ts b/src/reducers/settings.ts index 6fc5b98..1017b25 100644 --- a/src/reducers/settings.ts +++ b/src/reducers/settings.ts @@ -1,5 +1,6 @@ import { types, util } from 'vortex-api'; import { setDirectoryJunctionEnabled, setDirectoryJunctionSuppress, + setLoadOrderManagementType, setMigrationVersion, setPluginsEnabler } from '../actions/settings'; export const settingsReducer: types.IReducerSpec = { @@ -8,6 +9,10 @@ export const settingsReducer: types.IReducerSpec = { [setDirectoryJunctionSuppress as any]: (state, payload) => util.setSafe(state, ['suppressDirectoryJunctionTest'], payload), [setMigrationVersion as any]: (state, payload) => util.setSafe(state, ['migrationVersion'], payload), [setPluginsEnabler as any]: (state, payload) => util.setSafe(state, ['pluginEnabler'], payload), + [setLoadOrderManagementType as any]: (state, payload) => { + const { profileId, type } = payload; + return util.setSafe(state, ['loadOrderManagementType', profileId], type); + }, }, defaults: { pluginEnabler: false, diff --git a/src/tests.ts b/src/tests.ts index 736cd1e..8aa74c3 100644 --- a/src/tests.ts +++ b/src/tests.ts @@ -5,7 +5,7 @@ import { PLUGIN_REQUIREMENTS } from './loadOrder/StarFieldLoadOrder'; import { actions, fs, types, log, selectors, util } from 'vortex-api'; import { parse, stringify } from 'ini-comments'; -import { isJunctionDir, purge, deploy, migrateMod, sanitizeIni } from './util'; +import { isJunctionDir, purge, deploy, migrateMod, sanitizeIni, requiresPluginEnabler } from './util'; import { toggleJunction } from './setup'; import { setDirectoryJunctionSuppress, setDirectoryJunctionEnabled, setPluginsEnabler } from './actions/settings'; @@ -154,6 +154,10 @@ export async function testPluginsEnabler(api: types.IExtensionApi): Promise util.getSafe(profile, ['modState', mod.id, 'enabled'], false); const discovery = selectors.discoveryByGame(state, GAME_ID); const gameStore = discovery?.store === 'xbox' ? 'xbox' : 'steam'; diff --git a/src/types.ts b/src/types.ts index 132340f..773987e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,6 +3,7 @@ import { IMod } from "vortex-api/lib/types/IState"; export type LockedState = "true" | "false" | "always" | "never"; export type LoadOrder = ILoadOrderEntry[]; +export type LoadOrderManagementType = 'gamebryo' | 'dnd'; export interface IGithubDownload { fileName: string; diff --git a/src/util.ts b/src/util.ts index 72695f3..3d445cc 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,8 +1,11 @@ /* eslint-disable */ +import { getFileVersion } from 'exe-version'; import { fs, log, selectors, types, util } from 'vortex-api'; -import { PLUGINS_TXT, LOCAL_APP_DATA, GAME_ID, MY_GAMES_DATA_WARNING, MISSING_PLUGINS_NOTIFICATION_ID, JUNCTION_NOTIFICATION_ID, PLUGINS_BACKUP } from './common'; +import { PLUGINS_TXT, LOCAL_APP_DATA, GAME_ID, MY_GAMES_DATA_WARNING, MISSING_PLUGINS_NOTIFICATION_ID, JUNCTION_NOTIFICATION_ID, PLUGINS_BACKUP, XBOX_APP_X_MANIFEST, PLUGIN_ENABLER_CONSTRAINT } from './common'; import turbowalk, { IWalkOptions, IEntry } from 'turbowalk'; +import { parseStringPromise } from 'xml2js'; import path from 'path'; +import semver from 'semver'; import { getStopPatterns } from './stopPatterns'; export async function mergeFolderContents(source: string, destination: string, overwrite = false): Promise { @@ -202,6 +205,50 @@ export async function purgeDeployedFiles(basePath: string, return Promise.resolve(); } +export async function requiresPluginEnabler(api: types.IExtensionApi): Promise { + try { + const gameVersion = await getGameVersionAsync(api); + return (semver.satisfies(semver.coerce(gameVersion).version, PLUGIN_ENABLER_CONSTRAINT)); + } catch (err) { + // Assume it's required. + log('error', 'failed to check plugin enabler constraint', err); + return false; + } +} + +export function getGameVersionSync(api: types.IExtensionApi): string { + const state = api.getState(); + const gameVersion = util.getSafe(state, ['persistent', 'gameMode', 'versions', GAME_ID], '0.0.0'); + return semver.coerce(gameVersion).raw; +} + +export async function getGameVersionAsync(api: types.IExtensionApi): Promise { + const state = api.getState(); + const discovery = selectors.discoveryByGame(state, GAME_ID); + if (!discovery?.path) { + return Promise.reject(new util.GameNotFound(GAME_ID)); + } + if (discovery.store === 'xbox') { + try { + const appManifest = path.join(discovery.path, XBOX_APP_X_MANIFEST); + await fs.statAsync(appManifest); + const data = await fs.readFileAsync(appManifest, { encoding: 'utf8' }); + const parsed = await parseStringPromise(data); + return Promise.resolve(parsed?.Package?.Identity?.[0]?.$?.Version); + } catch (err) { + return Promise.reject(new Error('failed to parse appxmanifest.xml')); + } + } else { + const execPath = path.isAbsolute(discovery.executable) ? discovery.executable : path.join(discovery.path, discovery.executable); + try { + const version = await getFileVersion(execPath); + return Promise.resolve(version); + } catch (err) { + return Promise.reject(new util.NotFound(execPath)); + } + } +} + export async function getExtensionVersion() { const infoFile = JSON.parse(await fs.readFileAsync(path.join(__dirname, 'info.json'))); return infoFile.version; @@ -244,7 +291,7 @@ export function dismissNotifications(api: types.IExtensionApi) { // TODO: Find a better way to control the update notifications!!! const notificationIds = ['starfield-junction-activity', MY_GAMES_DATA_WARNING, JUNCTION_NOTIFICATION_ID, MISSING_PLUGINS_NOTIFICATION_ID, - 'starfield-update-notif-0.5.0', 'starfield-update-notif-0.6.0']; + 'starfield-update-notif-0.5.0', 'starfield-update-notif-0.6.0', 'starfield-update-notif-0.7.0']; for (const id of notificationIds) { // Can't batch these. api.dismissNotification(id); diff --git a/src/views/InfoPanel.tsx b/src/views/InfoPanel.tsx index c0a1147..7d7efcd 100644 --- a/src/views/InfoPanel.tsx +++ b/src/views/InfoPanel.tsx @@ -17,7 +17,7 @@ interface IConnectedProps { discovery: types.IDiscoveryResult; } -export default function InfoPanel(props: IBaseProps) { +export function InfoPanel(props: IBaseProps) { const { onInstallPluginsEnabler } = props; const { api } = React.useContext(MainContext); const [isXbox, setIsXbox] = React.useState(false); diff --git a/src/views/InfoPanelCK.tsx b/src/views/InfoPanelCK.tsx new file mode 100644 index 0000000..fd6ea55 --- /dev/null +++ b/src/views/InfoPanelCK.tsx @@ -0,0 +1,62 @@ +/* eslint-disable */ +import * as React from 'react'; +import { useSelector } from 'react-redux'; +import { Alert } from 'react-bootstrap'; +import { MainContext, selectors, tooltip, types, util } from 'vortex-api'; + +import { GAME_ID, NS } from '../common'; +import { forceRefresh } from '../util'; + +interface IConnectedProps { + loadOrder: types.ILoadOrderEntry[]; + discovery: types.IDiscoveryResult; +} + +export function InfoPanelCK() { + const { api } = React.useContext(MainContext); + const [isXbox, setIsXbox] = React.useState(false); + const t = (input: string) => api.translate(input, { ns: NS }); + const { loadOrder, discovery } = useSelector(mapStateToProps); + React.useEffect(() => { + if (loadOrder.length > 0) { + forceRefresh(api); + } + if (!isXbox && discovery?.store === 'xbox') { + setIsXbox(true); + } + }, [loadOrder, isXbox, setIsXbox, discovery]); + const renderXboxWarningEnabledSystem = () => { + return isXbox ? ( + +

{t('If you encounter the "This library isn\'t supported" error when launching the game - purge your mods and verify file integrity through the Xbox Game Pass App before deploying your mods again')}

+
+ ) : null; + }; + + return ( + <> + {renderXboxWarningEnabledSystem()} +

+ {t( + 'Drag and Drop the plugins to reorder how the game loads them. Please note, this screen will show all plugins available in the plugins directory, regardless of whether they were installed by Vortex or not.' + )} +

+

{t('Mod descriptions from mod authors may have information to determine the best order.')}

+

{t('Additional Information:')}

+
    +
  • {t('If installing a collection - wait for it to complete before re-visiting this page.')}
  • +
  • {t('Press the "Reset Plugins File" button if your plugins.txt file is in a corrupted state.')}
  • +
  • {t('Press the "Refresh List" button to refresh/sync changes.')}
  • +
  • {t('Press the "Sort with LOOT" button to sort your load order automatically')}
  • +
+ + ); +} + +function mapStateToProps(state: any): IConnectedProps { + const profile = selectors.activeProfile(state); + return { + loadOrder: util.getSafe(state, ['persistent', 'loadOrder', profile?.id], []), + discovery: selectors.discoveryByGame(state, GAME_ID), + }; +} diff --git a/src/views/Settings.tsx b/src/views/Settings.tsx index 8e23147..84dbdb6 100644 --- a/src/views/Settings.tsx +++ b/src/views/Settings.tsx @@ -1,71 +1,120 @@ import * as React from 'react'; -import { ControlLabel, FormGroup, Panel, HelpBlock } from 'react-bootstrap'; +import { ControlLabel, DropdownButton, FormGroup, Panel, MenuItem, HelpBlock } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { useSelector, useStore } from 'react-redux'; -import { MainContext, Toggle, util } from 'vortex-api'; +import { MainContext, Toggle, selectors, types, util } from 'vortex-api'; -import { setPluginsEnabler } from '../actions/settings'; +import { setLoadOrderManagementType, setPluginsEnabler } from '../actions/settings'; -import { NS, JUNCTION_TEXT, MISSING_PLUGINS_NOTIFICATION_ID } from '../common'; +import { NS, MISSING_PLUGINS_NOTIFICATION_ID } from '../common'; import { forceRefresh } from '../util'; +import { LoadOrderManagementType } from '../types'; interface IBaseProps { onSetDirectoryJunction: (enabled: boolean) => void; + needsEnabler: () => boolean; } interface IConnectedProps { enableDirectoryJunction: boolean; pluginEnabler: boolean; + activeProfile: types.IProfile; + loManagementType: LoadOrderManagementType; } type IProps = IBaseProps; -export default function Settings(props: IProps) { +function renderLOManagementType(props: IBaseProps & IConnectedProps): JSX.Element { const { t } = useTranslation(NS); - const { onSetDirectoryJunction } = props; + const { loManagementType, activeProfile } = props; + const context = React.useContext(MainContext); const store = useStore(); - const onToggle = React.useCallback((newVal) => { - onSetDirectoryJunction(newVal); - }, [onSetDirectoryJunction]); + const onSetManageType = React.useCallback((evt) => { + context.api.sendNotification({ + type: 'success', + message: t('Load order management type set to {{type}}', { type: evt }), + displayMS: 5000, + }); + store.dispatch(setLoadOrderManagementType(activeProfile.id, evt)); + context.api.dismissNotification(MISSING_PLUGINS_NOTIFICATION_ID); + forceRefresh(context.api); + }, [context, store]); + return ( + + {t('Drag and Drop')} + {t('Automated Sorting')} + + ); +} +function renderPluginEnablerToggle(props: IBaseProps & IConnectedProps): JSX.Element { + const { t } = useTranslation(NS); const context = React.useContext(MainContext); - + const store = useStore(); const onSetManageLO = React.useCallback(() => { store.dispatch(setPluginsEnabler(false)); context.api.dismissNotification(MISSING_PLUGINS_NOTIFICATION_ID); forceRefresh(context.api); }, [context, store]); - const { enableDirectoryJunction, pluginEnabler } = useSelector(mapStateToProps); - const loHelpBlockText = pluginEnabler + const { pluginEnabler, needsEnabler } = props; + const loHelpBlockText = props.pluginEnabler ? t('This will tell Vortex to disable plugin load order management.') : t('Please enable plugin load order management through the load order screen.'); + + return ( + <> + + {t('Manage Load Order')} + + + {loHelpBlockText} + + + ) +} + +export default function Settings(props: IProps) { + const { t } = useTranslation(NS); + const { onSetDirectoryJunction } = props; + const onToggle = React.useCallback((newVal) => { + onSetDirectoryJunction(newVal); + }, [onSetDirectoryJunction]); + + const connectedProps = useSelector(mapStateToProps); return (
{t('Starfield')} - - {t('Manage Load Order')} - - - {loHelpBlockText} - - - {t('Use Folder Junction')} - - - {t('This will allow you to enable/disable the use of folder junctions for the game.')} - + {props.needsEnabler() && renderPluginEnablerToggle({ ...props, ...connectedProps })} + <> + + {t('Use Folder Junction')} + + + {t('This will allow you to enable/disable the use of folder junctions for the game.')} + + + <> + {!props.needsEnabler() && renderLOManagementType({ ...props, ...connectedProps })} + @@ -74,8 +123,11 @@ export default function Settings(props: IProps) { } function mapStateToProps(state: any): IConnectedProps { + const profile = selectors.activeProfile(state); return { enableDirectoryJunction: util.getSafe(state, ['settings', 'starfield', 'enableDirectoryJunction'], false), pluginEnabler: util.getSafe(state, ['settings', 'starfield', 'pluginEnabler'], false), + activeProfile: profile, + loManagementType: util.getSafe(state, ['settings', 'loadOrderManagementType', profile.id], 'dnd'), }; } \ No newline at end of file From cebc099b49333d8a1bdc8cd2bd41be7038ecff26 Mon Sep 17 00:00:00 2001 From: IDCs Date: Wed, 5 Jun 2024 14:58:44 +0100 Subject: [PATCH 03/20] Added download SFSE/ASI Loader notification to setup --- src/setup.ts | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/setup.ts b/src/setup.ts index cfcf11e..d0d3018 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -1,6 +1,6 @@ /* eslint-disable */ import { fs, types, selectors, util } from 'vortex-api'; -import { isJunctionDir, createJunction, removeJunction } from './util'; +import { isJunctionDir, createJunction, removeJunction, requiresPluginEnabler } from './util'; import path from 'path'; import { setDirectoryJunctionEnabled } from './actions/settings'; @@ -8,6 +8,8 @@ import { GAME_ID, MY_GAMES_DATA_WARNING } from './common'; import { IJunctionProps } from './types'; import { migrateExtension } from './migrations/migrations'; +import { download } from './downloader'; +import { PLUGIN_REQUIREMENTS } from './loadOrder/StarFieldLoadOrder'; // This code executes when the user first manages Starfield AND each time they swap from another game to Starfield. export async function setup(api: types.IExtensionApi, @@ -21,10 +23,38 @@ export async function setup(api: types.IExtensionApi, if (myGamesData.toLowerCase() === gameDataFolder.toLowerCase()) { throw new Error('Starfield is not detected correctly, please update the location of the game in the "Games" tab. It must point to the folder where the game is installed and not the My Documents folder.'); } + const t = api.translate; // Make sure the folder exists await fs.ensureDirWritableAsync(myGamesFolder); await fs.ensureDirWritableAsync(path.join(discovery.path, 'Plugins')); await migrateExtension(api); + const requiresEnabler = await requiresPluginEnabler(api); + const requiredDownload = PLUGIN_REQUIREMENTS?.[discovery.store]?.[0]; + if (requiresEnabler || !requiredDownload) { + return; + } + + const existing = await requiredDownload.findMod(api); + if (existing) { + return; + } + + api.sendNotification({ + type: 'warning', + message: t('Would you like to download "{{requirement}}"', { replace: { requirement: requiredDownload.userFacingName } }), + actions: [{ + title: 'Close', + action: async (dismiss) => { + return dismiss(); + } + }, { + title: 'Download', + action: async (dismiss) => { + await download(api, [requiredDownload]); + return dismiss(); + } + }], + }); } export async function toggleJunction(api: types.IExtensionApi, enable: boolean): Promise { From e9e4a57d4c7590983932b70aeb28ff14690ac861 Mon Sep 17 00:00:00 2001 From: IDCs Date: Wed, 5 Jun 2024 15:00:38 +0100 Subject: [PATCH 04/20] updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f89c9a2..4182c60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - Added ability to change LO functionality based on game version - Added setting allowing to switch between dnd and auto-sort (classic gamebryo/loot) - Added new mid-master plugins +- SFSE/ASI Loader are now optional and can be downloaded via a notification at startup - Fixed game version resolution issues for XBox Game Pass - Setting controls will now hide/show depending on the game version. - Code cleanup From 7bf92791a7f8eb52913d906b0ed95cd9281c0843 Mon Sep 17 00:00:00 2001 From: IDCs Date: Wed, 5 Jun 2024 18:31:41 +0100 Subject: [PATCH 05/20] improved requirement download notification --- src/setup.ts | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/src/setup.ts b/src/setup.ts index d0d3018..a0e7ffd 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -4,7 +4,7 @@ import { isJunctionDir, createJunction, removeJunction, requiresPluginEnabler } import path from 'path'; import { setDirectoryJunctionEnabled } from './actions/settings'; -import { GAME_ID, MY_GAMES_DATA_WARNING } from './common'; +import { GAME_ID, MY_GAMES_DATA_WARNING, NS } from './common'; import { IJunctionProps } from './types'; import { migrateExtension } from './migrations/migrations'; @@ -41,10 +41,31 @@ export async function setup(api: types.IExtensionApi, api.sendNotification({ type: 'warning', - message: t('Would you like to download "{{requirement}}"', { replace: { requirement: requiredDownload.userFacingName } }), + allowSuppress: true, + id: 'sf-download-requirement-notification', + message: t('Would you like to download "{{requirement}}"', { + ns: NS, + replace: { requirement: requiredDownload.userFacingName, } + }), actions: [{ - title: 'Close', + title: 'More', action: async (dismiss) => { + await api.showDialog('question', 'Download and install "{{requirement}}"', { + bbcode: t('Some mods may require "{{requirement}}" to be installed in order to function correctly.[br][/br][br][/br]' + + 'Would you like to download it now?', { + ns: NS, + requirement: requiredDownload.userFacingName + }) + }, [ + { label: 'Close' }, + { + label: 'Download', + action: async () => { + await download(api, [requiredDownload]); + return dismiss(); + } + }, + ]) return dismiss(); } }, { From 0c7e24fcc3179faa730f1f7f6cac0ea3d188335f Mon Sep 17 00:00:00 2001 From: IDCs Date: Wed, 5 Jun 2024 19:20:00 +0100 Subject: [PATCH 06/20] improved dropdown button selection --- src/views/Settings.tsx | 54 +++++++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/src/views/Settings.tsx b/src/views/Settings.tsx index 84dbdb6..1ee5779 100644 --- a/src/views/Settings.tsx +++ b/src/views/Settings.tsx @@ -1,8 +1,9 @@ +/* eslint-disable */ import * as React from 'react'; -import { ControlLabel, DropdownButton, FormGroup, Panel, MenuItem, HelpBlock } from 'react-bootstrap'; +import { ControlLabel, Dropdown, DropdownButton, FormGroup, Panel, MenuItem, HelpBlock } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { useSelector, useStore } from 'react-redux'; -import { MainContext, Toggle, selectors, types, util } from 'vortex-api'; +import { useDispatch, useSelector, useStore } from 'react-redux'; +import { FlexLayout, MainContext, Toggle, selectors, types, util } from 'vortex-api'; import { setLoadOrderManagementType, setPluginsEnabler } from '../actions/settings'; @@ -25,32 +26,35 @@ interface IConnectedProps { type IProps = IBaseProps; +const dropDownItems: { [value: string]: string } = { + dnd: 'Drag and Drop', + gamebryo: 'Automated (LOOT)', +}; + function renderLOManagementType(props: IBaseProps & IConnectedProps): JSX.Element { const { t } = useTranslation(NS); const { loManagementType, activeProfile } = props; const context = React.useContext(MainContext); + const [selected, setSelected] = React.useState(dropDownItems[props.loManagementType]); + const dispatch = useDispatch(); const store = useStore(); const onSetManageType = React.useCallback((evt) => { - context.api.sendNotification({ - type: 'success', - message: t('Load order management type set to {{type}}', { type: evt }), - displayMS: 5000, - }); - store.dispatch(setLoadOrderManagementType(activeProfile.id, evt)); - context.api.dismissNotification(MISSING_PLUGINS_NOTIFICATION_ID); + dispatch(setLoadOrderManagementType(activeProfile.id, evt)); + setSelected(dropDownItems[evt]); forceRefresh(context.api); - }, [context, store]); + }, [context, store, setSelected, activeProfile, dispatch]); return ( - {t('Drag and Drop')} - {t('Automated Sorting')} + { + Object.entries(dropDownItems).map(([value, text]) => ( + {t(text)} + )) + } ); } @@ -72,6 +76,7 @@ function renderPluginEnablerToggle(props: IBaseProps & IConnectedProps): JSX.Ele return ( <> + {loHelpBlockText} {t('Manage Load Order')} - - {loHelpBlockText} - ) } @@ -94,26 +96,30 @@ export default function Settings(props: IProps) { }, [onSetDirectoryJunction]); const connectedProps = useSelector(mapStateToProps); + const combined = { ...props, ...connectedProps }; return ( {t('Starfield')} - {props.needsEnabler() && renderPluginEnablerToggle({ ...props, ...connectedProps })} + {props.needsEnabler() && renderPluginEnablerToggle(combined)} <> + + {t('This will allow you to enable/disable the use of folder junctions for the game.')} + {t('Use Folder Junction')} - - {t('This will allow you to enable/disable the use of folder junctions for the game.')} - <> - {!props.needsEnabler() && renderLOManagementType({ ...props, ...connectedProps })} + + {t('Allows you to switch between automated LOOT sorting or drag and drop')} + + {!props.needsEnabler() && renderLOManagementType(combined)} From fb87c4b9306daf8930e637e20a8e15a74a6235c3 Mon Sep 17 00:00:00 2001 From: IDCs Date: Mon, 10 Jun 2024 10:28:10 +0100 Subject: [PATCH 07/20] several fixes for CK release - fixed gameVersion check failing on non xbox variants - fixed old infopanel creating a cascade of refreshes - other small bits --- src/common.ts | 4 ++-- src/loadOrder/StarFieldLoadOrder.tsx | 3 --- src/util.ts | 9 +++++---- src/views/InfoPanel.tsx | 2 +- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/common.ts b/src/common.ts index 64741aa..0d9a58d 100644 --- a/src/common.ts +++ b/src/common.ts @@ -18,11 +18,11 @@ export const MISSING_PLUGINS_NOTIFICATION_ID = 'starfield-missing-plugins'; export const MY_GAMES_DATA_WARNING = 'starfield-my-games-data-warning'; // Below constraint is used for the GAME version. Not the extension. -export const PLUGIN_ENABLER_CONSTRAINT = '<1.10.0'; +export const PLUGIN_ENABLER_CONSTRAINT = '<1.12.0'; // This is the order we expect the native plugins to be arranged. export const NATIVE_PLUGINS = ['starfield.esm', 'blueprintships-starfield.esm', 'oldmars.esm', 'constellation.esm']; -export const NATIVE_MID_PLUGINS = ['sfbgs006.esm', 'sfbgs007.esm', 'sfbgs008.esm']; +export const NATIVE_MID_PLUGINS = ['sfbgs003.esm', 'sfbgs006.esm', 'sfbgs007.esm', 'sfbgs008.esm']; export const ALL_NATIVE_PLUGINS = [].concat(NATIVE_PLUGINS, NATIVE_MID_PLUGINS); export const DATA_SUBFOLDERS = [ diff --git a/src/loadOrder/StarFieldLoadOrder.tsx b/src/loadOrder/StarFieldLoadOrder.tsx index afc8146..fd40e39 100644 --- a/src/loadOrder/StarFieldLoadOrder.tsx +++ b/src/loadOrder/StarFieldLoadOrder.tsx @@ -216,9 +216,6 @@ class StarFieldLoadOrder implements types.ILoadOrderGameInfo { } const result = [].concat(loadOrder); - - // This is a bit hacky but necessary to keep the native plugins at the top of the load order. - await this.serializeLoadOrder(result, []); return Promise.resolve(result); } diff --git a/src/util.ts b/src/util.ts index 3d445cc..ccc9082 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,6 +1,6 @@ /* eslint-disable */ import { getFileVersion } from 'exe-version'; -import { fs, log, selectors, types, util } from 'vortex-api'; +import { actions, fs, log, selectors, types, util } from 'vortex-api'; import { PLUGINS_TXT, LOCAL_APP_DATA, GAME_ID, MY_GAMES_DATA_WARNING, MISSING_PLUGINS_NOTIFICATION_ID, JUNCTION_NOTIFICATION_ID, PLUGINS_BACKUP, XBOX_APP_X_MANIFEST, PLUGIN_ENABLER_CONSTRAINT } from './common'; import turbowalk, { IWalkOptions, IEntry } from 'turbowalk'; import { parseStringPromise } from 'xml2js'; @@ -239,12 +239,13 @@ export async function getGameVersionAsync(api: types.IExtensionApi): Promise { return isXbox ? ( From 444f01c03bf30756f89ebf2ae83023f8975ed9cd Mon Sep 17 00:00:00 2001 From: IDCs Date: Mon, 10 Jun 2024 11:14:51 +0100 Subject: [PATCH 08/20] fixed deserialization being called excessively - removed sTestFile migration for versions >=1.12 of the game - fixed long standing bug that could potentially wipe SFCustom.ini --- src/loadOrder/StarFieldLoadOrder.tsx | 39 +++++++++++++++------------- src/tests.ts | 2 +- src/views/InfoPanelCK.tsx | 5 +--- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/loadOrder/StarFieldLoadOrder.tsx b/src/loadOrder/StarFieldLoadOrder.tsx index fd40e39..3d532ba 100644 --- a/src/loadOrder/StarFieldLoadOrder.tsx +++ b/src/loadOrder/StarFieldLoadOrder.tsx @@ -121,25 +121,28 @@ class StarFieldLoadOrder implements types.ILoadOrderGameInfo { const profile = selectors.activeProfile(state); return util.getSafe(profile, ['modState', modId, 'enabled'], false); }; - const testFiles = await migrateTestFiles(this.mApi); - if (testFiles.length > 0) { - for (const file of testFiles) { - const mod = await findModByFile(this.mApi, MOD_TYPE_DATAPATH, file); - const invalid = !mod && !isInDataFolder(file); - const loEntry = { - enabled: !invalid, - id: file, - name: file, - modId: !!mod?.id ? isModEnabled(mod.id) ? mod.id : undefined : undefined, - locked: invalid, - data: { - isInvalid: invalid, + + if (await requiresPluginEnabler(this.mApi)) { + const testFiles = await migrateTestFiles(this.mApi); + if (testFiles.length > 0) { + for (const file of testFiles) { + const mod = await findModByFile(this.mApi, MOD_TYPE_DATAPATH, file); + const invalid = !mod && !isInDataFolder(file); + const loEntry = { + enabled: !invalid, + id: file, + name: file, + modId: !!mod?.id ? isModEnabled(mod.id) ? mod.id : undefined : undefined, + locked: invalid, + data: { + isInvalid: invalid, + } + } + if (invalid) { + invalidEntries.push(loEntry); + } else { + loadOrder.push(loEntry); } - } - if (invalid) { - invalidEntries.push(loEntry); - } else { - loadOrder.push(loEntry); } } } diff --git a/src/tests.ts b/src/tests.ts index 8aa74c3..ccc0af4 100644 --- a/src/tests.ts +++ b/src/tests.ts @@ -22,7 +22,7 @@ export async function testLooseFiles(api: types.IExtensionApi): Promise { try { // Ensure file is created if it does not exist. - await fs.ensureFileAsync(iniPath); + await fs.statAsync(iniPath).catch(err => fs.ensureFileAsync(iniPath)); let iniContent = (await fs.readFileAsync(iniPath, 'utf-8')) ?? ''; ini = parse(iniContent); return ini?.Archive?.bInvalidateOlderFiles === '1' diff --git a/src/views/InfoPanelCK.tsx b/src/views/InfoPanelCK.tsx index fd6ea55..fd0a24c 100644 --- a/src/views/InfoPanelCK.tsx +++ b/src/views/InfoPanelCK.tsx @@ -18,13 +18,10 @@ export function InfoPanelCK() { const t = (input: string) => api.translate(input, { ns: NS }); const { loadOrder, discovery } = useSelector(mapStateToProps); React.useEffect(() => { - if (loadOrder.length > 0) { - forceRefresh(api); - } if (!isXbox && discovery?.store === 'xbox') { setIsXbox(true); } - }, [loadOrder, isXbox, setIsXbox, discovery]); + }, [isXbox, setIsXbox, discovery]); const renderXboxWarningEnabledSystem = () => { return isXbox ? ( From e163eee1dea9b6057dc6791bccc85b7c53280a1a Mon Sep 17 00:00:00 2001 From: IDCs Date: Mon, 10 Jun 2024 11:19:10 +0100 Subject: [PATCH 09/20] some text change for 0.7.0 migration --- src/migrations/migrations.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/migrations/migrations.ts b/src/migrations/migrations.ts index 7830af2..01f7045 100644 --- a/src/migrations/migrations.ts +++ b/src/migrations/migrations.ts @@ -51,7 +51,8 @@ export async function migrate070(api: types.IExtensionApi, version: string) { newVersion: version }, bbcode: t('As of version "{{cutoff}}" of the game, the plugin enabler is no longer required as the game ' - + 'now fully supports plugins on its own.', { replace: { cutoff: PLUGIN_ENABLER_CONSTRAINT.slice(1) } }), + + 'now fully supports plugins on its own. If your game crashes, please disable "SFSE" or the "ASI Loader"' + + 'mods and wait for them to be updated.', { replace: { cutoff: PLUGIN_ENABLER_CONSTRAINT.slice(1) } }), }, [ { label: t('Close', { ns: NS }), From 5492c479c6f1a22b31851bc4e0af5fae161a6aee Mon Sep 17 00:00:00 2001 From: IDCs Date: Mon, 10 Jun 2024 11:31:30 +0100 Subject: [PATCH 10/20] disable the plugin enabler if game version >1.12 --- src/migrations/migrations.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/migrations/migrations.ts b/src/migrations/migrations.ts index 01f7045..a916289 100644 --- a/src/migrations/migrations.ts +++ b/src/migrations/migrations.ts @@ -2,7 +2,7 @@ import path from 'path'; import semver from 'semver'; import { actions, fs, selectors, types, util } from 'vortex-api'; -import { setMigrationVersion } from '../actions/settings'; +import { setMigrationVersion, setPluginsEnabler } from '../actions/settings'; import { DATA_SUBFOLDERS, GAME_ID, MOD_TYPE_DATAPATH, NS, PLUGIN_ENABLER_CONSTRAINT } from '../common'; import { deploy, nuclearPurge, getExtensionVersion, requiresPluginEnabler } from '../util'; @@ -31,8 +31,8 @@ export async function migrateExtension(api: types.IExtensionApi) { export async function migrate070(api: types.IExtensionApi, version: string) { const enablePlugins = await requiresPluginEnabler(api); - if (enablePlugins) { - return migrate060(api, version); + if (!enablePlugins) { + api.store.dispatch(setPluginsEnabler(false)); } const notificationId = 'starfield-update-notif-0.7.0'; From dcd21f103a8953e34261abc177898cde030488d8 Mon Sep 17 00:00:00 2001 From: IDCs Date: Mon, 10 Jun 2024 12:01:44 +0100 Subject: [PATCH 11/20] disabling management type button (for now) --- src/migrations/migrations.ts | 3 +-- src/views/Settings.tsx | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/migrations/migrations.ts b/src/migrations/migrations.ts index a916289..a10239d 100644 --- a/src/migrations/migrations.ts +++ b/src/migrations/migrations.ts @@ -15,8 +15,7 @@ export async function migrateExtension(api: types.IExtensionApi) { return Promise.resolve(); } - const currentVersion = '0.0.0'; - // const currentVersion = util.getSafe(state, ['settings', 'starfield', 'migrationVersion'], '0.0.0'); + const currentVersion = util.getSafe(state, ['settings', 'starfield', 'migrationVersion'], '0.0.0'); const newVersion = await getExtensionVersion(); if (semver.gt('0.7.0', currentVersion)) { await migrate070(api, newVersion); diff --git a/src/views/Settings.tsx b/src/views/Settings.tsx index 1ee5779..67f943b 100644 --- a/src/views/Settings.tsx +++ b/src/views/Settings.tsx @@ -49,6 +49,7 @@ function renderLOManagementType(props: IBaseProps & IConnectedProps): JSX.Elemen title={t(selected)} onSelect={onSetManageType} dropup + disabled={true} // Temporary > { Object.entries(dropDownItems).map(([value, text]) => ( From 11fcb1cdb12e3187c895653b91984883b7628a85 Mon Sep 17 00:00:00 2001 From: IDCs Date: Mon, 10 Jun 2024 12:48:53 +0100 Subject: [PATCH 12/20] modified readme --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c691457..e53077f 100644 --- a/README.md +++ b/README.md @@ -65,16 +65,16 @@ Similar to Fallout 4, Starfield requires certain INI tweaks to be set in order t ~~Additionally, Vortex will apply a tweak to re-route your Photo Mode captures to Data\Textures\Photos (unless you've already set it to something else) and there is now a button inside Vortex to quickly open this folder.~~ This was removed in version `0.6.7` of the extension due to a bug in Starfield that was introduced when the game updated to approximately version `1.10.31.0`. -# Plugin Load Ordering (0.6.x) +# Plugin Load Ordering (0.7.x) -The current implementation of the plugin management system in Starfield is temporary while we wait for the official creation kit from Bethesda. This means that we expect certain functionality to change in the future, yet we're confident enough to provide interim support. +~~The current implementation of the plugin management system in Starfield is temporary while we wait for the official creation kit from Bethesda. This means that we expect certain functionality to change in the future, yet we're confident enough to provide interim support.~~ The plugin management systen in Starfield has been enabled as of 9th of June 2024; plugin enablers are no longer required. A new "Load Order" page has been added to the extension to allow users to view their deployed mods and manage their load order. By default this system is disabled and can only be enabled through the Load Order page. The user can disable this feature at any time through the Settings -> Mods -> Starfield -> Manage Load Order Toggle. -Before enabling the plugin management system keep the following in mind: +~~Before enabling the plugin management system keep the following in mind:~~ -- Vortex will download any required mods/tools for load ordering to function as soon as you enable the plugin management system. -- Vortex will migrate any "sTestFileX=" entries it finds in the INI files to the plugins.txt file located inside "%APPLOCALDATA%/Starfield". Any INI entries will block the plugin management functionality from working. This is by Bethesda's design. +- Vortex will download any required mods/tools for load ordering to function ~~as soon as you enable the plugin management system.~~ at the time of writing, SFSE and the ASI Loader have yet to be updated to support the game - in the meantime if you experience game crashes - disable SFSE or the ASI Loader and any mods that require them. +~~- Vortex will migrate any "sTestFileX=" entries it finds in the INI files to the plugins.txt file located inside "%APPLOCALDATA%/Starfield".~~ Any sTestFileX INI entries will block the plugin management functionality from working. This is by Bethesda's design. Vortex now assumes that the "sTestFileX" INI edit pattern is no longer required as the creation kit has been released. - Game Pass version of the game will not be able to use SFSE or its plugins - the [Ultimate-ASI-Loader](https://github.com/ThirteenAG/Ultimate-ASI-Loader) is used to load plugins to the game instead. - Native plugins are not serialized to the "plugins.txt" file. From 54a762fa64184c1772d85d611220d0283fb44479 Mon Sep 17 00:00:00 2001 From: IDCs Date: Mon, 10 Jun 2024 12:49:57 +0100 Subject: [PATCH 13/20] minor readme change --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e53077f..98ba834 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ A new "Load Order" page has been added to the extension to allow users to view t ~~Before enabling the plugin management system keep the following in mind:~~ - Vortex will download any required mods/tools for load ordering to function ~~as soon as you enable the plugin management system.~~ at the time of writing, SFSE and the ASI Loader have yet to be updated to support the game - in the meantime if you experience game crashes - disable SFSE or the ASI Loader and any mods that require them. -~~- Vortex will migrate any "sTestFileX=" entries it finds in the INI files to the plugins.txt file located inside "%APPLOCALDATA%/Starfield".~~ Any sTestFileX INI entries will block the plugin management functionality from working. This is by Bethesda's design. Vortex now assumes that the "sTestFileX" INI edit pattern is no longer required as the creation kit has been released. +- ~~Vortex will migrate any "sTestFileX=" entries it finds in the INI files to the plugins.txt file located inside "%APPLOCALDATA%/Starfield".~~ Any sTestFileX INI entries will block the plugin management functionality from working. This is by Bethesda's design. Vortex now assumes that the "sTestFileX" INI edit pattern is no longer required as the creation kit has been released. - Game Pass version of the game will not be able to use SFSE or its plugins - the [Ultimate-ASI-Loader](https://github.com/ThirteenAG/Ultimate-ASI-Loader) is used to load plugins to the game instead. - Native plugins are not serialized to the "plugins.txt" file. From 40e1fd53e7e02fce374dc167987884fb0d974e1d Mon Sep 17 00:00:00 2001 From: IDCs Date: Mon, 10 Jun 2024 14:48:30 +0100 Subject: [PATCH 14/20] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 98ba834..26e1d18 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ Similar to Fallout 4, Starfield requires certain INI tweaks to be set in order t ~~The current implementation of the plugin management system in Starfield is temporary while we wait for the official creation kit from Bethesda. This means that we expect certain functionality to change in the future, yet we're confident enough to provide interim support.~~ The plugin management systen in Starfield has been enabled as of 9th of June 2024; plugin enablers are no longer required. -A new "Load Order" page has been added to the extension to allow users to view their deployed mods and manage their load order. By default this system is disabled and can only be enabled through the Load Order page. The user can disable this feature at any time through the Settings -> Mods -> Starfield -> Manage Load Order Toggle. +A new "Load Order" page has been added to the extension to allow users to view their deployed mods and manage their load order. ~~By default this system is disabled and can only be enabled through the Load Order page. The user can disable this feature at any time through the Settings -> Mods -> Starfield -> Manage Load Order Toggle.~~ ~~Before enabling the plugin management system keep the following in mind:~~ From f9dca5544722f81df95f5680646779c663322512 Mon Sep 17 00:00:00 2001 From: IDCs Date: Mon, 10 Jun 2024 14:49:58 +0100 Subject: [PATCH 15/20] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 26e1d18..a561f91 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,8 @@ A new "Load Order" page has been added to the extension to allow users to view t # Known Issues +- SFSE and ASI Loader are outdated - disable the relevant script loader/extender to ensure your game does not crash. + - This extension has been tested with all of the most popular mods, installers, script extenders, mod fixers etc. Please see this [Mod Compatibility List](https://forums.nexusmods.com/index.php?/topic/13262847-starfield-mod-compatibility-megathread/) forum post for details. - Symlink support has been disabled due inconsistencies with some files working and some not. Animation replacers don’t read symlinked, but textures seem ok. This does match the pattern of older Bethesda games which looked for a while like we could avoid. From 22e64239f57c9349567f45710df7c44f33aa47b9 Mon Sep 17 00:00:00 2001 From: IDCs Date: Tue, 11 Jun 2024 08:07:57 +0100 Subject: [PATCH 16/20] fixed SFSE not downloading for non-xbox/steam installations --- src/setup.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/setup.ts b/src/setup.ts index a0e7ffd..96386b1 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -29,7 +29,8 @@ export async function setup(api: types.IExtensionApi, await fs.ensureDirWritableAsync(path.join(discovery.path, 'Plugins')); await migrateExtension(api); const requiresEnabler = await requiresPluginEnabler(api); - const requiredDownload = PLUGIN_REQUIREMENTS?.[discovery.store]?.[0]; + const storeId = discovery?.store === 'xbox' ? 'xbox' : 'steam'; + const requiredDownload = PLUGIN_REQUIREMENTS?.[storeId]?.[0]; if (requiresEnabler || !requiredDownload) { return; } From 1d7c448303874f837289ce66ef3692b5e5cce4cf Mon Sep 17 00:00:00 2001 From: IDCs Date: Tue, 11 Jun 2024 08:12:43 +0100 Subject: [PATCH 17/20] commenting out loot button --- src/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index 1fb2a57..7f847f2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -145,9 +145,9 @@ function main(context: types.IExtensionContext) { context.registerAction('mod-icons', 500, 'open-ext', {}, 'Open Game Application Data Folder', openAppDataPath, (gameId?: string[]) => isStarfield(context, gameId)); context.registerAction('fb-load-order-icons', 150, 'open-ext', {}, 'View Plugins File', openAppDataPath, (gameId?: string[]) => isStarfield(context, gameId)); context.registerAction('fb-load-order-icons', 500, 'remove', {}, 'Reset Plugins File', () => removePluginsWrap(context.api), (gameId?: string[]) => isStarfield(context, gameId)); - context.registerAction('fb-load-order-icons', 600, 'loot-sort', {}, 'Sort via LOOT', () => { - context.api.showErrorNotification('Not Implemented Yet', 'Soon TM'); - }); + // context.registerAction('fb-load-order-icons', 600, 'loot-sort', {}, 'Sort via LOOT', () => { + // context.api.showErrorNotification('Not Implemented Yet', 'Soon TM'); + // }); context.registerLoadOrder(new StarFieldLoadOrder(context.api)); From b9e7a838f1fb98782ba993087ddc0f506338d3c6 Mon Sep 17 00:00:00 2001 From: IDCs Date: Tue, 11 Jun 2024 08:18:57 +0100 Subject: [PATCH 18/20] temporarily removing dnd/loot dropdown --- src/views/Settings.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/views/Settings.tsx b/src/views/Settings.tsx index 67f943b..2b03652 100644 --- a/src/views/Settings.tsx +++ b/src/views/Settings.tsx @@ -117,10 +117,12 @@ export default function Settings(props: IProps) { <> - - {t('Allows you to switch between automated LOOT sorting or drag and drop')} - - {!props.needsEnabler() && renderLOManagementType(combined)} + { + // + // {t('Allows you to switch between automated LOOT sorting or drag and drop')} + // + // {!props.needsEnabler() && renderLOManagementType(combined)} + } From dd4cc1fc0019df4c3462709891668a12395a4c71 Mon Sep 17 00:00:00 2001 From: insomnious Date: Tue, 11 Jun 2024 08:22:30 +0100 Subject: [PATCH 19/20] updated docs --- CHANGELOG.md | 13 +++++-------- README.md | 2 +- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4182c60..7f9cda0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,16 +4,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). -## [0.7.0] - 2024-xx-xx +## [0.7.0] - 2024-06-11 -- Added CK tool -- Added ability to change LO functionality based on game version -- Added setting allowing to switch between dnd and auto-sort (classic gamebryo/loot) -- Added new mid-master plugins +- Tweaks to support latest Starfield update (v1.12) +- Added Creation Kit as a tool +- Added new native plugins to locked list - SFSE/ASI Loader are now optional and can be downloaded via a notification at startup -- Fixed game version resolution issues for XBox Game Pass -- Setting controls will now hide/show depending on the game version. -- Code cleanup +- Fixed game version resolution issues for Xbox Game Pass ## [0.6.7] - 2024-03-19 diff --git a/README.md b/README.md index a561f91..30f08bb 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ It is also possible to manually set the game folder if the auto detection doesn' If your game lacks this file then it is likely that your installation has become corrupted somehow. -## Important note if managing the game through xbox game pass: +## Important note if managing the game through Xbox Game Pass: Currently the game discovery will resolve to the game's default `WindowsApps` location - Vortex's access to this directory is very limited due to the game store locking the files in a system owned virtual file system. As a workaround, please install the game into an external location, e.g. `C:/XboxGames/` and manually set the game folder inside Vortex to the `C:\XboxGames\Starfield\Content` folder. You should then be able to create the folder junction and mod your game. From aca0c32052912986b457e7846a81a8e0537b9c37 Mon Sep 17 00:00:00 2001 From: insomnious Date: Tue, 11 Jun 2024 08:32:51 +0100 Subject: [PATCH 20/20] removed loot instructions --- src/views/InfoPanelCK.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/views/InfoPanelCK.tsx b/src/views/InfoPanelCK.tsx index fd0a24c..1998c92 100644 --- a/src/views/InfoPanelCK.tsx +++ b/src/views/InfoPanelCK.tsx @@ -44,7 +44,6 @@ export function InfoPanelCK() {
  • {t('If installing a collection - wait for it to complete before re-visiting this page.')}
  • {t('Press the "Reset Plugins File" button if your plugins.txt file is in a corrupted state.')}
  • {t('Press the "Refresh List" button to refresh/sync changes.')}
  • -
  • {t('Press the "Sort with LOOT" button to sort your load order automatically')}
  • );