Skip to content

Commit ff1a6f1

Browse files
Managing games functioning
1 parent 18a0e9e commit ff1a6f1

File tree

12 files changed

+409
-91
lines changed

12 files changed

+409
-91
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ npm-debug.log*
2727
.DS_Store
2828
yarn-error.log
2929

30+
# temporary files
31+
tmp.*
32+
3033
# temporary fonts directory used during font-svg generation
3134
/fonts/
3235

src/extensions/extension_manager/index.ts

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -129,10 +129,8 @@ function checkForUpdates(api: IExtensionApi) {
129129
displayMS: 5000,
130130
});
131131

132-
// Trigger dynamic update of game list if game extensions were updated
133-
if (gameUpdates.length > 0) {
134-
api.events.emit('refresh-game-list');
135-
}
132+
// Don't emit refresh-game-list here to prevent infinite loop
133+
// The refresh will be handled by the caller if needed
136134
}
137135
}
138136
});
@@ -313,10 +311,8 @@ function genUpdateInstalledExtensions(api: IExtensionApi) {
313311
displayMS: 5000,
314312
});
315313

316-
// Trigger dynamic update of game list if game extensions were installed
317-
if (newGameExtensions.length > 0) {
318-
api.events.emit('refresh-game-list');
319-
}
314+
// Don't emit refresh-game-list here to prevent infinite loop
315+
// The refresh will be handled by the caller if needed
320316
}
321317
}
322318
api.store.dispatch(setInstalledExtensions(ext));
@@ -388,6 +384,10 @@ function init(context: IExtensionContext) {
388384
});
389385

390386
context.once(() => {
387+
// Expose the updateExtensions function so it can be called from other modules
388+
// This must be done in the once callback where the API is available
389+
context.api.ext['updateExtensions'] = updateExtensions;
390+
391391
let onDidFetch: () => void;
392392
const didFetchAvailableExtensions = new Promise((resolve => onDidFetch = resolve));
393393
updateExtensions(true)
@@ -404,7 +404,23 @@ function init(context: IExtensionContext) {
404404
.then(success => {
405405
if (success) {
406406
return updateExtensions(false)
407-
.then(() => success);
407+
.then(() => {
408+
// Check if any of the newly installed extensions are game extensions
409+
// If so, emit refresh-game-list to update the UI
410+
const state = context.api.getState();
411+
const previousExtensions = state.session.extensions.installed || {};
412+
const newExtensions = Object.keys(state.session.extensions.installed)
413+
.filter(extId => !previousExtensions[extId]);
414+
const newGameExtensions = newExtensions
415+
.filter(extId => state.session.extensions.installed[extId].type === 'game');
416+
417+
if (newGameExtensions.length > 0) {
418+
// Emit refresh-game-list with forceFullDiscovery=true to ensure game extensions are properly registered
419+
context.api.events.emit('refresh-game-list', true);
420+
}
421+
422+
return success;
423+
});
408424
} else {
409425
return Promise.resolve()
410426
.then(() => success);

src/extensions/extension_manager/util.ts

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,15 @@ export function downloadAndInstallExtension(api: IExtensionApi,
255255

256256
let dlPromise: Promise<string[]>;
257257

258+
// Show activity notification for better UX
259+
const notificationId = `installing-extension-${Date.now()}`;
260+
api.sendNotification({
261+
id: notificationId,
262+
type: 'activity',
263+
message: `Downloading ${ext.name}...`,
264+
displayMS: 10000,
265+
});
266+
258267
if (truthy(ext.modId)) {
259268
dlPromise = downloadFromNexus(api, ext);
260269
} else if (truthy(ext.githubRawPath)) {
@@ -263,6 +272,8 @@ export function downloadAndInstallExtension(api: IExtensionApi,
263272
dlPromise = downloadGithubRelease(api, ext);
264273
} else {
265274
// don't report an error if the extension list contains invalid data
275+
// Dismiss the activity notification
276+
api.dismissNotification(notificationId);
266277
return Promise.resolve(false);
267278
}
268279

@@ -272,14 +283,26 @@ export function downloadAndInstallExtension(api: IExtensionApi,
272283

273284
return dlPromise
274285
.then((dlIds: string[]) => {
286+
// Update notification to show installation phase
287+
api.sendNotification({
288+
id: notificationId,
289+
type: 'activity',
290+
message: `Installing ${ext.name}...`,
291+
displayMS: 10000,
292+
});
293+
275294
const state: IState = api.store.getState();
276295

277296
if ((dlIds === undefined) || (dlIds.length !== 1)) {
297+
// Dismiss the activity notification
298+
api.dismissNotification(notificationId);
278299
return Promise.reject(new ProcessCanceled('No download found'));
279300
}
280301
api.store.dispatch(setDownloadModInfo(dlIds[0], 'internal', true));
281302
download = getSafe(state, ['persistent', 'downloads', 'files', dlIds[0]], undefined);
282303
if (download === undefined) {
304+
// Dismiss the activity notification
305+
api.dismissNotification(notificationId);
283306
return Promise.reject(new Error('Download not found'));
284307
}
285308

@@ -304,9 +327,19 @@ export function downloadAndInstallExtension(api: IExtensionApi,
304327
const downloadPath = downloadPathForGame(state, SITE_ID);
305328
return installExtension(api, path.join(downloadPath, download.localPath), info);
306329
})
307-
.then(() => Promise.resolve(true))
308-
.catch(UserCanceled, () => null)
330+
.then(() => {
331+
// Dismiss the activity notification
332+
api.dismissNotification(notificationId);
333+
return Promise.resolve(true);
334+
})
335+
.catch(UserCanceled, () => {
336+
// Dismiss the activity notification
337+
api.dismissNotification(notificationId);
338+
return null;
339+
})
309340
.catch(ProcessCanceled, () => {
341+
// Dismiss the activity notification
342+
api.dismissNotification(notificationId);
310343
api.showDialog('error', 'Installation failed', {
311344
text: 'Failed to install the extension "{{extensionName}}" from "{{sourceName}}", '
312345
+ 'please check the notifications.',
@@ -323,10 +356,14 @@ export function downloadAndInstallExtension(api: IExtensionApi,
323356
return Promise.resolve(false);
324357
})
325358
.catch(ServiceTemporarilyUnavailable, err => {
359+
// Dismiss the activity notification
360+
api.dismissNotification(notificationId);
326361
log('warn', 'Failed to download from github', { message: err.message });
327362
return Promise.resolve(false);
328363
})
329364
.catch(err => {
365+
// Dismiss the activity notification
366+
api.dismissNotification(notificationId);
330367
api.showDialog('error', 'Installation failed', {
331368
text: 'Failed to install the extension "{{extensionName}}" from "{{sourceName}}"',
332369
parameters: {

src/extensions/gamemode_management/index.ts

Lines changed: 178 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ interface IProvider {
9292

9393
const gameInfoProviders: IProvider[] = [];
9494

95+
// Add this at the top of the file with the other module-level variables
96+
let isRefreshingGameList = false;
97+
9598
function refreshGameInfo(store: Redux.Store<IState>, gameId: string): Promise<void> {
9699
interface IKeyProvider {
97100
[key: string]: { priority: number, provider: string };
@@ -239,9 +242,19 @@ function browseGameLocation(api: IExtensionApi, gameId: string): Promise<void> {
239242
const state: IState = api.store.getState();
240243

241244
if (gameById(state, gameId) === undefined) {
242-
return api.showDialog('question', 'Game support not installed', {
243-
text: 'Support for this game is provided through an extension. '
244-
+ 'Please click "Manage" to install the extension and set it up.',
245+
// Find the extension to get the game name
246+
const extension = state.session.extensions.available.find(ext =>
247+
(ext?.gameId === gameId) || (ext.name === gameId));
248+
249+
// Get the game name for better user messaging
250+
const gameName = extension?.gameName || extension?.name?.replace(/^Game: /, '') || 'this game';
251+
252+
return api.showDialog('question', 'Game Support Not Installed', {
253+
text: 'Support for {{gameName}} is provided through a community extension that is not included with the main Vortex application. '
254+
+ 'To manage mods for {{gameName}}, you need to download and install this extension.',
255+
parameters: {
256+
gameName
257+
}
245258
}, [
246259
{ label: 'Close' },
247260
])
@@ -857,10 +870,169 @@ function init(context: IExtensionContext): boolean {
857870
.catch(err => callback(err));
858871
});
859872

860-
events.on('refresh-game-list', () => {
873+
events.on('refresh-game-list', (forceFullDiscovery?: boolean) => {
861874
// Refresh the known games list when game extensions are installed
862-
$.gameModeManager.refreshKnownGames();
863-
log('info', 'Game list refreshed after extension installation');
875+
if (forceFullDiscovery) {
876+
// Force a complete refresh including re-reading game extensions
877+
// Use the extension manager's exposed update function if available
878+
if (context.api.ext['updateExtensions']) {
879+
// Add a flag to prevent infinite loop
880+
if (isRefreshingGameList) {
881+
// Already refreshing, skip to prevent infinite loop
882+
return;
883+
}
884+
885+
// Set the flag to indicate we're refreshing
886+
isRefreshingGameList = true;
887+
888+
context.api.ext['updateExtensions'](false)
889+
.then(() => {
890+
// After updating extensions, we need to ensure game extensions are properly registered
891+
// Re-initialize the extension games list to capture any newly installed game extensions
892+
$.extensionGames = [];
893+
894+
// Get the current state and re-process all installed game extensions
895+
const state = context.api.getState();
896+
const installedExtensions = state.session.extensions.installed;
897+
898+
// Process each installed game extension to ensure it's registered
899+
return Promise.map(Object.keys(installedExtensions), extId => {
900+
const ext = installedExtensions[extId];
901+
if (ext.type === 'game' && ext.path) {
902+
try {
903+
// Try to load and register the game extension
904+
const indexPath = path.join(ext.path, 'index.js');
905+
return fs.statAsync(indexPath)
906+
.then(() => {
907+
const extensionModule = require(indexPath);
908+
if (typeof extensionModule.default === 'function') {
909+
// Create a minimal context for the extension to register its game
910+
const contextProxy = new Proxy({
911+
api: context.api,
912+
registerGame: (game: IGame) => {
913+
// Register the game in our extension games list
914+
game.extensionPath = ext.path;
915+
$.extensionGames.push(game);
916+
},
917+
registerGameStub: (game: IGame, extInfo: IExtensionDownloadInfo) => {
918+
// Handle game stubs if needed
919+
$.extensionStubs.push({ ext: extInfo, game });
920+
}
921+
}, {
922+
get: (target, prop) => {
923+
// Provide stub functions for other register methods
924+
if (prop === 'registerGame' || prop === 'registerGameStub') {
925+
return target[prop];
926+
} else if (typeof prop === 'string' && prop.startsWith('register')) {
927+
return () => {}; // Stub for other register methods
928+
}
929+
return target[prop];
930+
}
931+
});
932+
933+
// Call the extension's init function
934+
extensionModule.default(contextProxy);
935+
}
936+
return Promise.resolve();
937+
})
938+
.catch(() => {
939+
// File doesn't exist or other error, skip this extension
940+
return Promise.resolve();
941+
});
942+
} catch (err) {
943+
log('warn', 'Failed to load game extension', { extension: ext.name, error: err.message });
944+
return Promise.resolve();
945+
}
946+
}
947+
return Promise.resolve();
948+
})
949+
.then(() => {
950+
// Refresh the known games with the updated extension games list
951+
const gamesStored: IGameStored[] = $.extensionGames
952+
.map(game => ({
953+
name: game.name,
954+
shortName: game.shortName,
955+
id: game.id,
956+
logo: game.logo,
957+
extensionPath: game.extensionPath,
958+
contributed: game.contributed,
959+
final: game.final,
960+
version: game.version,
961+
executable: game.executable?.(),
962+
requiredFiles: game.requiredFiles,
963+
environment: game.environment,
964+
details: game.details,
965+
}))
966+
.filter(game =>
967+
(game.executable !== undefined) &&
968+
(game.requiredFiles !== undefined) &&
969+
(game.name !== undefined)
970+
);
971+
context.api.store.dispatch(setKnownGames(gamesStored));
972+
973+
// Also run quick discovery to find the game paths
974+
return $.gameModeManager.startQuickDiscovery(undefined, false);
975+
});
976+
})
977+
.then(() => {
978+
log('info', 'Game list fully refreshed after extension installation');
979+
// Clear the flag when done
980+
isRefreshingGameList = false;
981+
})
982+
.catch(err => {
983+
log('error', 'Failed to refresh game list', err.message);
984+
// Clear the flag on error
985+
isRefreshingGameList = false;
986+
});
987+
} else {
988+
// Fallback if the update function is not available
989+
const gamesStored: IGameStored[] = $.extensionGames
990+
.map(game => ({
991+
name: game.name,
992+
shortName: game.shortName,
993+
id: game.id,
994+
logo: game.logo,
995+
extensionPath: game.extensionPath,
996+
contributed: game.contributed,
997+
final: game.final,
998+
version: game.version,
999+
executable: game.executable?.(),
1000+
requiredFiles: game.requiredFiles,
1001+
environment: game.environment,
1002+
details: game.details,
1003+
}))
1004+
.filter(game =>
1005+
(game.executable !== undefined) &&
1006+
(game.requiredFiles !== undefined) &&
1007+
(game.name !== undefined)
1008+
);
1009+
context.api.store.dispatch(setKnownGames(gamesStored));
1010+
log('info', 'Game list refreshed after extension installation');
1011+
}
1012+
} else {
1013+
const gamesStored: IGameStored[] = $.extensionGames
1014+
.map(game => ({
1015+
name: game.name,
1016+
shortName: game.shortName,
1017+
id: game.id,
1018+
logo: game.logo,
1019+
extensionPath: game.extensionPath,
1020+
contributed: game.contributed,
1021+
final: game.final,
1022+
version: game.version,
1023+
executable: game.executable?.(),
1024+
requiredFiles: game.requiredFiles,
1025+
environment: game.environment,
1026+
details: game.details,
1027+
}))
1028+
.filter(game =>
1029+
(game.executable !== undefined) &&
1030+
(game.requiredFiles !== undefined) &&
1031+
(game.name !== undefined)
1032+
);
1033+
context.api.store.dispatch(setKnownGames(gamesStored));
1034+
log('info', 'Game list refreshed after extension installation');
1035+
}
8641036
});
8651037

8661038
const changeGameMode = (oldGameId: string, newGameId: string,

0 commit comments

Comments
 (0)