From 5f340bd314795c5cb841c946389e57f4ed87b49e Mon Sep 17 00:00:00 2001 From: Florian Scheuner Date: Tue, 3 Sep 2024 13:33:55 +0300 Subject: [PATCH] feat: improve MSFS base path detection (#423) * feat: rewrite MSFS base path detection * fix: openPath() does not focus Explorer window * chore: update changelog * refactor: dismiss promise (cherry picked from commit 0a3895c1ae19042f5c88863a58a30d98501bf4ea) --- .github/CHANGELOG.yaml | 3 + src/common/channels.ts | 2 + src/main/index.ts | 8 ++ src/renderer/actions/install-path.utils.tsx | 94 ++++++++-------- .../AddonSection/MyInstall/index.tsx | 26 +++-- src/renderer/components/ErrorModal/index.tsx | 106 ++++++++++++++---- .../components/SettingsSection/index.tsx | 3 + src/renderer/rendererSettings.ts | 41 +++++-- src/renderer/utils/Directories.ts | 26 ++--- src/renderer/utils/InstallManager.tsx | 3 +- 10 files changed, 201 insertions(+), 111 deletions(-) diff --git a/.github/CHANGELOG.yaml b/.github/CHANGELOG.yaml index 6f13ee1c..a006a602 100644 --- a/.github/CHANGELOG.yaml +++ b/.github/CHANGELOG.yaml @@ -17,6 +17,9 @@ releases: - title: Shortcut keys no longer override other applications shortcuts categories: [UI] authors: [alepouna] + - title: Improve detection of MSFS installation + categories: [Core] + authors: [FoxtrotSierra] - name: 3.4.0 changes: - title: Add a button to the error dialog to copy error messages to clipboard diff --git a/src/common/channels.ts b/src/common/channels.ts index d7fbab54..f052118b 100644 --- a/src/common/channels.ts +++ b/src/common/channels.ts @@ -4,6 +4,7 @@ export default { maximize: 'window/maximize', close: 'window/close', isMaximized: 'window/isMaximized', + reload: 'window/reload', }, update: { error: 'update/error', @@ -21,4 +22,5 @@ export default { requestSessionID: 'sentry/requestSessionID', provideSessionID: 'sentry/provideSessionID', }, + openPath: 'openPath', }; diff --git a/src/main/index.ts b/src/main/index.ts index a37f094b..ec043525 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -88,10 +88,18 @@ function initializeApp() { mainWindow.destroy(); }); + ipcMain.on(channels.window.reload, () => { + mainWindow.reload(); + }); + ipcMain.on(channels.window.isMaximized, (event) => { event.sender.send(channels.window.isMaximized, mainWindow.isMaximized()); }); + ipcMain.on(channels.openPath, (_, value: string) => { + void shell.openPath(value); + }); + ipcMain.on('request-startup-at-login-changed', (_, value: boolean) => { app.setLoginItemSettings({ openAtLogin: value, diff --git a/src/renderer/actions/install-path.utils.tsx b/src/renderer/actions/install-path.utils.tsx index e43e9cfd..f2e1b5b1 100644 --- a/src/renderer/actions/install-path.utils.tsx +++ b/src/renderer/actions/install-path.utils.tsx @@ -1,74 +1,74 @@ -import settings from 'renderer/rendererSettings'; +import settings, { msStoreBasePath, steamBasePath } from 'renderer/rendererSettings'; import { Directories } from 'renderer/utils/Directories'; import { dialog } from '@electron/remote'; +import fs from 'fs'; -export const setupMsfsCommunityPath = async (): Promise => { - const currentPath = Directories.installLocation(); - +const selectPath = async (currentPath: string, dialogTitle: string, setting: string): Promise => { const path = await dialog.showOpenDialog({ - title: 'Select your MSFS community directory', + title: dialogTitle, defaultPath: typeof currentPath === 'string' ? currentPath : '', properties: ['openDirectory'], }); if (path.filePaths[0]) { - settings.set('mainSettings.msfsCommunityPath', path.filePaths[0]); + settings.set(setting, path.filePaths[0]); return path.filePaths[0]; } else { return ''; } }; -export const setupInstallPath = async (): Promise => { - const currentPath = Directories.installLocation(); +export const setupMsfsBasePath = async (): Promise => { + const currentPath = Directories.msfsBasePath(); - const path = await dialog.showOpenDialog({ - title: 'Select your install directory', - defaultPath: typeof currentPath === 'string' ? currentPath : '', - properties: ['openDirectory'], - }); + const availablePaths: string[] = []; + if (fs.existsSync(msStoreBasePath)) { + availablePaths.push('Microsoft Store Edition'); + } + if (fs.existsSync(steamBasePath)) { + availablePaths.push('Steam Edition'); + } - if (path.filePaths[0]) { - settings.set('mainSettings.installPath', path.filePaths[0]); - if (!settings.get('mainSettings.separateLiveriesPath')) { - settings.set('mainSettings.liveriesPath', path.filePaths[0]); + if (availablePaths.length > 0) { + availablePaths.push('Custom Directory'); + + const { response } = await dialog.showMessageBox({ + title: 'FlyByWire Installer', + message: 'We found a possible MSFS installation.', + type: 'warning', + buttons: availablePaths, + }); + + const selection = availablePaths[response]; + switch (selection) { + case 'Microsoft Store Edition': + settings.set('mainSettings.msfsBasePath', msStoreBasePath); + return msStoreBasePath; + case 'Steam Edition': + settings.set('mainSettings.msfsBasePath', steamBasePath); + return steamBasePath; + case 'Custom Directory': + break; } - return path.filePaths[0]; - } else { - return ''; } -}; -export const setupTempLocation = async (): Promise => { - const currentPath = Directories.tempLocation(); + return await selectPath(currentPath, 'Select your MSFS base directory', 'mainSettings.msfsBasePath'); +}; - const path = await dialog.showOpenDialog({ - title: 'Select a location for temporary folders', - defaultPath: typeof currentPath === 'string' ? currentPath : '', - properties: ['openDirectory'], - }); +export const setupMsfsCommunityPath = async (): Promise => { + const currentPath = Directories.installLocation(); - if (path.filePaths[0]) { - settings.set('mainSettings.tempLocation', path.filePaths[0]); - return path.filePaths[0]; - } else { - return ''; - } + return await selectPath(currentPath, 'Select your MSFS community directory', 'mainSettings.msfsCommunityPath'); }; -export const setupLiveriesPath = async (): Promise => { - const currentPath = Directories.liveries(); +export const setupInstallPath = async (): Promise => { + const currentPath = Directories.installLocation(); - const path = await dialog.showOpenDialog({ - title: 'Select your liveries directory', - defaultPath: typeof currentPath === 'string' ? currentPath : '', - properties: ['openDirectory'], - }); + return await selectPath(currentPath, 'Select your install directory', 'mainSettings.installPath'); +}; - if (path.filePaths[0]) { - settings.set('mainSettings.liveriesPath', path.filePaths[0]); - return path.filePaths[0]; - } else { - return ''; - } +export const setupTempLocation = async (): Promise => { + const currentPath = Directories.tempLocation(); + + return await selectPath(currentPath, 'Select a location for temporary folders', 'mainSettings.tempLocation'); }; diff --git a/src/renderer/components/AddonSection/MyInstall/index.tsx b/src/renderer/components/AddonSection/MyInstall/index.tsx index 64f634f3..533725cb 100644 --- a/src/renderer/components/AddonSection/MyInstall/index.tsx +++ b/src/renderer/components/AddonSection/MyInstall/index.tsx @@ -6,10 +6,12 @@ import { NamedDirectoryDefinition, } from 'renderer/utils/InstallerConfiguration'; import { BoxArrowRight, Folder } from 'react-bootstrap-icons'; -import { shell } from 'electron'; +import { ipcRenderer, shell } from 'electron'; import { Directories } from 'renderer/utils/Directories'; import { useAppSelector } from 'renderer/redux/store'; import { InstallStatusCategories } from 'renderer/components/AddonSection/Enums'; +import fs from 'fs'; +import channels from 'common/channels'; export interface MyInstallProps { addon: Addon; @@ -39,21 +41,23 @@ export const MyInstall: FC = ({ addon }) => { } }; - const handleClickDirectory = (def: DirectoryDefinition) => { - let fullPath; + const fulldirectory = (def: DirectoryDefinition) => { switch (def.location.in) { case 'community': - fullPath = Directories.inInstallLocation(def.location.path); - break; + return Directories.inInstallLocation(def.location.path); case 'package': - fullPath = Directories.inInstallPackage(addon, def.location.path); - break; + return Directories.inInstallPackage(addon, def.location.path); case 'packageCache': - fullPath = Directories.inPackageCache(addon, def.location.path); - break; + return Directories.inPackageCache(addon, def.location.path); } + }; + + const handleClickDirectory = (def: DirectoryDefinition) => { + ipcRenderer.send(channels.openPath, fulldirectory(def)); + }; - shell.openPath(fullPath).then(); + const existsDirectory = (def: DirectoryDefinition) => { + return fs.existsSync(fulldirectory(def)); }; const directoriesDisabled = !InstallStatusCategories.installed.includes(installStates[addon.key]?.status); @@ -88,7 +92,7 @@ export const MyInstall: FC = ({ addon }) => { {directories.map((it) => ( + + + ); + } + if (installLocationError) { + return ( + <> + Seems like you're using Linux + + We're unable to autodetect your install location (community folder) currently. Please set the correct + location before we can continue. + + + + + ); + } + } + if (msfsBasePathError) { return ( <> - Seems like you're using Linux - We're unable to autodetect your install currently. Please set the correct location before we can - continue. + We couldn't determine the correct MSFS base path. Would you please help us?

+ It is usually located somewhere here:
+ "%LOCALAPPDATA%\Packages\Microsoft.FlightSimulator_8wekyb3d8bbwe\LocalCache"

or + here:
"%APPDATA%\Microsoft Flight Simulator\"
- + ); } - if (communityError && Directories.installLocation() !== 'linux') { + if (installLocationError) { return ( <> Your Community folder is set to @@ -53,7 +117,7 @@ export const ErrorModal = (): JSX.Element => { but we couldn't find it there. Please set the correct location before we can continue. - @@ -62,7 +126,7 @@ export const ErrorModal = (): JSX.Element => { return <>; }; - if (communityError || linuxError) { + if (installLocationError || msfsBasePathError) { return (
Something went wrong. diff --git a/src/renderer/components/SettingsSection/index.tsx b/src/renderer/components/SettingsSection/index.tsx index e333bcad..eb11d734 100644 --- a/src/renderer/components/SettingsSection/index.tsx +++ b/src/renderer/components/SettingsSection/index.tsx @@ -10,6 +10,8 @@ import settings from 'renderer/rendererSettings'; import * as packageInfo from '../../../../package.json'; import { Button, ButtonType } from '../Button'; import { PromptModal, useModals } from 'renderer/components/Modal'; +import { ipcRenderer } from 'electron'; +import channels from 'common/channels'; interface InstallButtonProps { type?: ButtonType; @@ -40,6 +42,7 @@ export const SettingsSection = (): JSX.Element => { // Workaround to flush the defaults settings.set('metaInfo.lastVersion', packageInfo.version); + ipcRenderer.send(channels.window.reload); }} />, ); diff --git a/src/renderer/rendererSettings.ts b/src/renderer/rendererSettings.ts index 21b8a8e8..6790acd7 100644 --- a/src/renderer/rendererSettings.ts +++ b/src/renderer/rendererSettings.ts @@ -7,7 +7,13 @@ import { Dispatch, SetStateAction, useEffect, useState } from 'react'; import * as packageInfo from '../../package.json'; import { Directories } from 'renderer/utils/Directories'; -const defaultCommunityDir = (): string => { +export const msStoreBasePath = path.join( + Directories.localAppData(), + '\\Packages\\Microsoft.FlightSimulator_8wekyb3d8bbwe\\LocalCache\\', +); +export const steamBasePath = path.join(Directories.appData(), '\\Microsoft Flight Simulator\\'); + +const msfsBasePath = (): string => { if (os.platform().toString() === 'linux') { return 'linux'; } @@ -15,12 +21,9 @@ const defaultCommunityDir = (): string => { // Ensure proper functionality in main- and renderer-process let msfsConfigPath = null; - const steamPath = path.join(Directories.appData(), '\\Microsoft Flight Simulator\\UserCfg.opt'); - const storePath = path.join( - Directories.localAppData(), - '\\Packages\\Microsoft.FlightSimulator_8wekyb3d8bbwe\\LocalCache\\UserCfg.opt', - ); - + const steamPath = path.join(steamBasePath, 'UserCfg.opt'); + const storePath = path.join(msStoreBasePath, 'UserCfg.opt'); + if (fs.existsSync(steamPath) && fs.existsSync(storePath)) return 'C:\\'; if (fs.existsSync(steamPath)) { msfsConfigPath = steamPath; } else if (fs.existsSync(storePath)) { @@ -37,6 +40,18 @@ const defaultCommunityDir = (): string => { return 'C:\\'; } + return path.dirname(msfsConfigPath); +}; + +export const defaultCommunityDir = (msfsBase: string): string => { + const msfsConfigPath = path.join(msfsBase, 'UserCfg.opt'); + if (!fs.existsSync(msfsConfigPath)) { + if (os.platform().toString() === 'linux') { + return 'linux'; + } + return 'C:\\'; + } + try { const msfsConfig = fs.readFileSync(msfsConfigPath).toString(); const msfsConfigLines = msfsConfig.split(/\r?\n/); @@ -88,7 +103,7 @@ interface RendererSettings { useLongDateFormat: boolean; useDarkTheme: boolean; allowSeasonalEffects: boolean; - msfsPackagePath: string; + msfsBasePath: string; configDownloadUrl: string; }; cache: { @@ -177,13 +192,17 @@ const schema: Schema = { type: 'boolean', default: true, }, + msfsBasePath: { + type: 'string', + default: msfsBasePath(), + }, msfsCommunityPath: { type: 'string', - default: defaultCommunityDir(), + default: defaultCommunityDir(msfsBasePath()), }, installPath: { type: 'string', - default: defaultCommunityDir(), + default: defaultCommunityDir(msfsBasePath()), }, separateTempLocation: { type: 'boolean', @@ -191,7 +210,7 @@ const schema: Schema = { }, tempLocation: { type: 'string', - default: defaultCommunityDir(), + default: defaultCommunityDir(msfsBasePath()), }, configDownloadUrl: { type: 'string', diff --git a/src/renderer/utils/Directories.ts b/src/renderer/utils/Directories.ts index a09ed73c..0feb7a2b 100644 --- a/src/renderer/utils/Directories.ts +++ b/src/renderer/utils/Directories.ts @@ -7,10 +7,6 @@ import { app } from '@electron/remote'; const TEMP_DIRECTORY_PREFIX = 'flybywire-current-install'; const TEMP_DIRECTORY_PREFIXES_FOR_CLEANUP = ['flybywire_current_install', TEMP_DIRECTORY_PREFIX]; - -const MSFS_APPDATA_PATH = 'Packages\\Microsoft.FlightSimulator_8wekyb3d8bbwe\\LocalState\\packages\\'; -const MSFS_STEAM_PATH = 'Microsoft Flight Simulator\\Packages'; - export class Directories { private static sanitize(suffix: string): string { return path.normalize(suffix).replace(/^(\.\.(\/|\\|$))+/, ''); @@ -24,6 +20,10 @@ export class Directories { return path.join(app.getPath('appData'), '..', 'Local'); } + static msfsBasePath(): string { + return settings.get('mainSettings.msfsBasePath') as string; + } + static communityLocation(): string { return settings.get('mainSettings.msfsCommunityPath') as string; } @@ -60,24 +60,12 @@ export class Directories { return path.join(Directories.tempLocation(), this.sanitize(targetDir)); } - static liveries(): string { - return settings.get('mainSettings.liveriesPath') as string; - } - - static inLiveries(targetDir: string): string { - return path.join(settings.get('mainSettings.liveriesPath') as string, this.sanitize(targetDir)); - } - - static inPackagesMicrosoftStore(targetDir: string): string { - return path.join(Directories.localAppData(), MSFS_APPDATA_PATH, this.sanitize(targetDir)); - } - - static inPackagesSteam(targetDir: string): string { - return path.join(Directories.appData(), MSFS_STEAM_PATH, this.sanitize(targetDir)); + static inPackages(targetDir: string): string { + return path.join(this.msfsBasePath(), 'packages', this.sanitize(targetDir)).replace('LocalCache', 'LocalState'); } static inPackageCache(addon: Addon, targetDir: string): string { - const baseDir = this.inPackagesSteam(this.sanitize(addon.targetDirectory)); + const baseDir = this.inPackages(this.sanitize(addon.targetDirectory)); return path.join(baseDir, this.sanitize(targetDir)); } diff --git a/src/renderer/utils/InstallManager.tsx b/src/renderer/utils/InstallManager.tsx index a2576405..bf2cd484 100644 --- a/src/renderer/utils/InstallManager.tsx +++ b/src/renderer/utils/InstallManager.tsx @@ -681,8 +681,7 @@ export class InstallManager { const installDir = Directories.inInstallLocation(addon.targetDirectory); await ipcRenderer.invoke(channels.installManager.uninstall, installDir, [ - Directories.inPackagesMicrosoftStore(addon.targetDirectory), - Directories.inPackagesSteam(addon.targetDirectory), + Directories.inPackages(addon.targetDirectory), ]); this.setCurrentInstallState(addon, { status: InstallStatus.NotInstalled });