Skip to content

Commit

Permalink
Merge pull request #38 from Nexus-Mods/ck-update
Browse files Browse the repository at this point in the history
update to support creation kit
  • Loading branch information
insomnious authored Jun 11, 2024
2 parents df815d5 + aca0c32 commit 0661c20
Show file tree
Hide file tree
Showing 17 changed files with 408 additions and 92 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ 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-06-11

- 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

## [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)
Expand Down
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -65,21 +65,23 @@ 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.
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.

# 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.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "starfield",
"version": "0.6.7",
"version": "0.7.0",
"description": "Vortex Extension for Starfield",
"author": "Nexus Mods",
"private": true,
Expand Down
7 changes: 6 additions & 1 deletion src/actions/settings.ts
Original file line number Diff line number Diff line change
@@ -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);
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 }));
Binary file added src/assets/CK.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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.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 = ['sfbgs003.esm', 'sfbgs006.esm', 'sfbgs007.esm', 'sfbgs008.esm'];
export const ALL_NATIVE_PLUGINS = [].concat(NATIVE_PLUGINS, NATIVE_MID_PLUGINS);

export const DATA_SUBFOLDERS = [
'Meshes',
Expand Down
27 changes: 21 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';

Expand All @@ -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[] = [
{
Expand Down Expand Up @@ -58,6 +59,15 @@ const supportedTools: types.ITool[] = [
requiredFiles: [],
parameters: [],
},
{
id: 'creation-kit-sf',
name: 'Creation Kit',
executable: () => 'CreationKit.exe',
logo: 'CK.png',
requiredFiles: [
'CreationKit.exe',
],
},
];

const gameFinderQuery = {
Expand Down Expand Up @@ -93,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,
Expand All @@ -113,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
Expand All @@ -130,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));

Expand Down Expand Up @@ -170,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;
Expand Down
72 changes: 43 additions & 29 deletions src/loadOrder/StarFieldLoadOrder.tsx
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down Expand Up @@ -68,7 +71,16 @@ class StarFieldLoadOrder implements types.ILoadOrderGameInfo {
this.clearStateOnPurge = true;
this.toggleableEntries = true;
this.noCollectionGeneration = true;
this.usageInstructions = () => (<InfoPanel onInstallPluginsEnabler={this.mOnInstallPluginsEnabler} />);
this.usageInstructions = (() => {
const gameVersion = getGameVersionSync(api);
const needsEnabler = semver.satisfies(gameVersion, PLUGIN_ENABLER_CONSTRAINT);
return needsEnabler ? (
<InfoPanel onInstallPluginsEnabler={this.mOnInstallPluginsEnabler} />
) : (
<InfoPanelCK />
);
}) as any;

this.mApi = api;
this.deserializeLoadOrder = this.deserializeLoadOrder.bind(this);
this.serializeLoadOrder = this.serializeLoadOrder.bind(this);
Expand Down Expand Up @@ -109,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);
}
}
}
Expand Down Expand Up @@ -178,7 +193,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) {
Expand All @@ -204,9 +219,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);
}

Expand Down Expand Up @@ -259,7 +271,7 @@ class StarFieldLoadOrder implements types.ILoadOrderGameInfo {
private async serializePluginsFile(plugins: types.ILoadOrderEntry[]): Promise<void> {
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 ? '#' : '';
Expand All @@ -283,9 +295,11 @@ class StarFieldLoadOrder implements types.ILoadOrderGameInfo {
}
}

private isLOManagedByVortex(): boolean {
private async isLOManagedByVortex(): Promise<boolean> {
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;
}
}

Expand Down
Loading

0 comments on commit 0661c20

Please sign in to comment.