Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion extensions/gamebryo-archive-invalidation
2 changes: 1 addition & 1 deletion extensions/games
2 changes: 1 addition & 1 deletion extensions/local-gamesettings
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
"test": "jest --no-cache",
"lint": "eslint . -c .eslintrc.js --ext .ts,.tsx --quiet",
"lint-to-file": "eslint . -c .eslintrc.js --ext .ts,.tsx --quiet --output-file ./eslint.log --no-color",
"build": "yarn run check_packages && yarn run build_rest && tsc -p .",
"build": "yarn run update_version && yarn run check_packages && yarn run build_rest && tsc -p .",
"buildwatch": "yarn run build_rest && tsc -p . --watch",
"buildext": "node ./tools/buildScripts/buildSingleExtension.js",
"generate_validation": "generate-validation .",
Expand All @@ -65,7 +65,8 @@
"test:e2e": "playwright test",
"test:e2e:headed": "playwright test --headed",
"test:e2e:debug": "playwright test --debug",
"test:e2e:report": "playwright show-report"
"test:e2e:report": "playwright show-report",
"update_version": "node ./scripts/update-version.js"
},
"jest": {
"testEnvironment": "jsdom",
Expand Down Expand Up @@ -235,7 +236,7 @@
"tslint-eslint-rules": "^5.4.0",
"tslint-react": "^4.1.0",
"typescript": "^5.0.0",
"vortex-api": "Nexus-Mods/vortex-api",
"vortex-api": "Nexus-Mods/vortex-api#18586-fallout-nv-user-journey",
"webpack": "^5.94.0",
"webpack-cli": "^5.1.4",
"webpack-node-externals": "^3.0.0"
Expand Down
15 changes: 15 additions & 0 deletions scripts/update-version.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const fs = require('fs');
const path = require('path');
const semver = require('semver');

const packageJsonPath = path.join(__dirname, '..', 'app', 'package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const constantsPath = path.join(__dirname, '..', 'src', 'constants.ts');
const currentVersion = packageJson.version;
const coercedVersion = semver.coerce(currentVersion);
const versionParts = coercedVersion.version.split('.');
let constantsContent = fs.readFileSync(constantsPath, 'utf8');
constantsContent = constantsContent.replace(/VORTEX_MAJOR: string = '.*?';/, `VORTEX_MAJOR: string = '${versionParts[0]}';`);
constantsContent = constantsContent.replace(/VORTEX_MINOR: string = '.*?';/, `VORTEX_MINOR: string = '${versionParts[1]}';`);
constantsContent = constantsContent.replace(/VORTEX_PATCH: string = '.*?';/, `VORTEX_PATCH: string = '${versionParts[2]}';`);
fs.writeFileSync(constantsPath, constantsContent, 'utf8');
6 changes: 6 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,9 @@ export const HTTP_HEADER_SIZE: number = 16384;
export const DEBUG_PORT: string = '9222';

export const VCREDIST_URL: string = 'https://learn.microsoft.com/en-US/cpp/windows/latest-supported-vc-redist';

const VORTEX_MAJOR: string = '1';
const VORTEX_MINOR: string = '16';
const VORTEX_PATCH: string = '0';
// Can be used by extensions to check compatibility
export const VORTEX_VERSION: string = `${VORTEX_MAJOR}.${VORTEX_MINOR}.${VORTEX_PATCH}`;
13 changes: 13 additions & 0 deletions src/extensions/download_management/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,16 @@ export const activeDownloads =
}
return prev;
}, {}));

export const getDownloadByIds = createSelector(
downloadFiles,
(state: IState, identifiers: { fileId: number, modId: number, gameId: string }) => identifiers,
(files: { [dlId: string]: IDownload }, identifiers: { fileId: number, modId: number, gameId: string }) => {
return Object.values(files).find(dl => {
if (dl.game.includes(identifiers.gameId) === false) {
return false;
}
return (dl.modInfo?.nexus?.ids?.fileId === identifiers.fileId)
&& (dl.modInfo?.nexus?.ids?.modId === identifiers.modId);
});
});
2 changes: 1 addition & 1 deletion src/extensions/mod_management/InstallContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ class InstallContext implements IInstallContext {
state: 'installing',
attributes: {
name: id,
installTime: new Date(),
installTime: new Date().toString(),
},
});
this.mAddedId = id;
Expand Down
24 changes: 24 additions & 0 deletions src/extensions/mod_management/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { IDiscoveryResult, IMod, IState } from '../../types/IState';
import { activeGameId } from '../../util/selectors';
import { getSafe } from '../../util/storeHelper';

import * as path from 'path';

import { getGame } from '../gamemode_management/util/getGame';

import getInstallPath from './util/getInstallPath';
Expand Down Expand Up @@ -103,4 +105,26 @@ export const modsForActiveGame = createSelector(
(activeGameId: string, state: IState) => {
return modsForGame(state, activeGameId);
},
);

export const getMod = createSelector(
modsForGame,
(state: IState, gameId: string, modId: string | number) => modId,
(mods: { [modId: string]: IMod }, modId: string | number) => {
if (typeof modId === 'number') {
return Object.values(mods).find(mod => mod.attributes?.modId === modId);
}
return mods[modId];
},
);

export const getModInstallPath = createSelector(
getMod,
(state: IState, gameId: string) => installPathForGame(state, gameId),
(mod: IMod, gameInstallPath: string) => {
if (mod?.installationPath == null || gameInstallPath == null) {
return undefined;
}
return path.join(gameInstallPath, mod.installationPath);
},
);
131 changes: 125 additions & 6 deletions src/extensions/mod_management/types/IMod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,109 @@ export { IReference, IRule };
export type ModState =
'downloading' | 'downloaded' | 'installing' | 'installed';

/**
* Attributes specific to Nexus Mods Collections (when IMod.type === "collection")
*/
export interface ICollectionAttributes {
collectionId: number;
collectionSlug: string;
revisionId: number;
revisionNumber: number;
downloadGame: string;
customFileName?: string;
shortDescription?: string;
pictureUrl?: string;
uploader?: string;
uploaderId?: number;
uploaderAvatar?: string;
uploadedTimestamp?: number;
updatedTimestamp?: number;
rating?: {
average: number;
total: number;
};
recommendNewProfile?: boolean;
installInstructions?: string;
bugMessage?: string;
modSize?: number;
}

/**
* Common attributes shared by all mods
*/
export interface ICommonModAttributes {
// Basic mod information
author?: string;
version?: string;
modName?: string;
modVersion?: string;
name?: string;
description?: string;
shortDescription?: string;

// Source and download information
source?: string;
fileName?: string;
fileSize?: number;
fileMD5?: string;
logicalFileName?: string;
additionalLogicalFileNames?: string[];
customFileName?: string;
downloadGame?: string;
game?: string[];
fileType?: string;

// Nexus Mods specific
modId?: number;
fileId?: number;
category?: string | number;
homepage?: string;
pictureUrl?: string;
uploader?: string;
uploaderUrl?: string;
uploaderId?: number;
uploadedTimestamp?: number;
updatedTimestamp?: number;

// Installation tracking
installTime?: string;
installedAsDependency?: boolean;
referenceTag?: string;

// Installer and patching
installerChoices?: any;
patches?: any;
fileList?: IFileListItem[];

// Version and updates
newestVersion?: string;
newestFileId?: number;

// Ratings and endorsements
allowRating?: boolean;
endorsement?: string;
endorsed?: string;

// Special mod types and flags
scriptExtender?: boolean;
is4GBPatcher?: boolean;
isPrimary?: number | boolean;

// Size information
modSize?: number;

// Messages and warnings
bugMessage?: string;
}

/**
* Comprehensive type for mod attributes that can be either common mod attributes,
* collection-specific attributes, or any custom attributes
*/
export type IModAttributes = Partial<ICommonModAttributes & ICollectionAttributes> & {
[key: string]: any;
};

/**
* represents a mod in all states (being downloaded, downloaded, installed)
*
Expand All @@ -14,17 +117,33 @@ export interface IMod {
id: string;

state: ModState;
// mod type (empty string is the default)
// this type is primarily used to determine how and where to deploy the mod, it
// could be "enb" for example to tell vortex the mod needs to be installed to the game
// directory. Different games will have different types
/**
* mod type (empty string is the default)
* this type is primarily used to determine how and where to deploy the mod, it
* could be "enb" for example to tell vortex the mod needs to be installed to the game
* directory. Different games will have different types.
*
* Special types:
* - "" (empty string): Default mod type
* - "collection": Nexus Mods collection
* - "dinput": Direct input mod (e.g., 4GB patch)
* - "enb": ENB graphics mod
* - game-specific types defined by game extensions
*/
type: string;
// id of the corresponding download
archiveId?: string;
// path to the installed mod (will usually be the same as id)
installationPath: string;
// dictionary of extended information fields
attributes?: { [id: string]: any };
/**
* dictionary of extended information fields
*
* Type-safe access to common attributes and collection attributes:
* - Use ICommonModAttributes for standard mod properties (author, version, etc.)
* - Use ICollectionAttributes when type === "collection"
* - Index signature allows any custom attributes for game-specific extensions
*/
attributes?: IModAttributes;
// list of custom rules for this mod instance
rules?: IModRule[];
// list of enabled ini tweaks
Expand Down
2 changes: 1 addition & 1 deletion src/extensions/mod_management/util/refreshMods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ function refreshMods(api: IExtensionApi, gameId: string,
state: 'installed',
attributes: {
name: modName,
installTime: stat.ctime,
installTime: new Date(stat.ctime).toString(),
},
});
}
Expand Down
33 changes: 30 additions & 3 deletions src/extensions/mod_management/util/testModReference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { truthy } from '../../../util/util';

import { log } from '../../../util/log';

import { IMod, IModReference, IFileListItem } from '../types/IMod';
import { IMod, IModReference, IFileListItem, IModAttributes } from '../types/IMod';
import { IDownload } from '../../download_management/types/IDownload';

import * as _ from 'lodash';
Expand Down Expand Up @@ -32,6 +32,33 @@ export interface IModLookupInfo {
fileList?: IFileListItem[];
}

export function modAttributesToLookupInfo(mod: IMod | IModAttributes | IModLookupInfo): IModLookupInfo {
const gameAttrib = (mod as IModAttributes).game;
if (gameAttrib && Array.isArray(gameAttrib) && gameAttrib.length > 0) {
return mod as IModLookupInfo;
}
const attrs: IModAttributes = (mod as IMod).attributes ?? (mod as IModAttributes);
return {
id: (mod as IMod).id,
fileMD5: attrs.fileMD5,
fileSizeBytes: attrs.fileSize ?? 0,
fileName: attrs.fileName ?? attrs.modName ?? attrs.name,
name: attrs.modName ?? attrs.name,
logicalFileName: attrs.logicalFileName,
additionalLogicalFileNames: attrs.additionalLogicalFileNames,
customFileName: attrs.customFileName,
version: attrs.version ?? attrs.modVersion ?? '',
game: attrs.game,
fileId: attrs.fileId?.toString(),
modId: attrs.modId?.toString(),
source: attrs.source,
referenceTag: attrs.referenceTag,
installerChoices: attrs.installerChoices,
patches: attrs.patches,
fileList: attrs.fileList,
};
}

// test if the reference is by id only, meaning it is only useful in the current setup
export function idOnlyRef(ref: IModReference) {
return (ref?.id !== undefined)
Expand Down Expand Up @@ -381,8 +408,8 @@ export function testModReference(mod: IMod | IModLookupInfo, reference: IModRefe
return false;
}

if ((mod as any).attributes) {
return testRef((mod as IMod).attributes as IModLookupInfo, mod.id,
if ((mod as IMod).attributes) {
return testRef(modAttributesToLookupInfo(mod), mod.id,
reference, source, fuzzyVersion);
} else {
const lookup = mod as IModLookupInfo;
Expand Down
6 changes: 3 additions & 3 deletions src/extensions/nexus_integration/attributes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ function NexusId(props: INexusIdProps) {
<NexusModIdDetail
t={t}
modId={mod.id}
nexusModId={mod.attributes?.modId}
nexusFileId={mod.attributes?.fileId}
nexusModId={mod.attributes?.modId?.toString()}
nexusFileId={mod.attributes?.fileId?.toString()}
fileHash={mod.attributes?.fileMD5}
archiveId={hasArchive ? mod.archiveId : undefined}
activeGameId={gameMode}
Expand All @@ -91,7 +91,7 @@ function createEndorsedIcon(store: Redux.Store<any>,
mod: IMod,
onEndorse: EndorseMod,
t: TFunction) {
const nexusModId: string = mod.attributes?.modId ?? mod.attributes?.collectionId;
const nexusModId: string = mod.attributes?.modId?.toString() ?? mod.attributes?.collectionId?.toString();
const version: string = mod.attributes?.version;
const state: string = getSafe(mod, ['state'], undefined);

Expand Down
3 changes: 2 additions & 1 deletion src/extensions/nexus_integration/eventHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -666,7 +666,8 @@ interface IDownloadResult {
export function onGetModFiles(api: IExtensionApi, nexus: Nexus)
: (...args: any[]) => Promise<IFileInfo[]> {
return (gameId: string, modId: number): Promise<IFileInfo[]> => {
return Promise.resolve(nexus.getModFiles(modId, gameId))
const domainGameId = nexusGameId(gameById(api.getState(), gameId), gameId);
return Promise.resolve(nexus.getModFiles(modId, domainGameId))
.then(result => result.files)
.catch(err => {
api.showErrorNotification('Failed to get list of mod files', err, {
Expand Down
4 changes: 2 additions & 2 deletions src/extensions/nexus_integration/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -998,7 +998,7 @@ function endorseCollectionImpl(api: IExtensionApi, nexus: Nexus, gameMode: strin

const gameId = mod.attributes?.downloadGame;

const nexusCollectionId: number = parseInt(mod.attributes.collectionId, 10);
const nexusCollectionId: number = mod.attributes.collectionId;

store.dispatch(setModAttribute(gameId, mod.id, 'endorsed', 'pending'));
const game = gameById(api.store.getState(), gameId);
Expand All @@ -1019,7 +1019,7 @@ function endorseModImpl(api: IExtensionApi, nexus: Nexus, gameMode: string,

const gameId = mod.attributes?.downloadGame;

const nexusModId: number = parseInt(mod.attributes.modId, 10);
const nexusModId: number = mod.attributes.modId;
const version: string = getSafe(mod.attributes, ['version'], undefined)
|| getSafe(mod.attributes, ['modVersion'], undefined);

Expand Down
Loading