Skip to content

Commit 9a0f2de

Browse files
feat: multi-sim support (#512)
* feat: add new per-sim setting variables * feat: msfs2024 base path detection * feat: wip managed sim selector * feat: managedSimSelector switches variable * feat: some fancy styling * refactor: make prettier happy * feat: only switch between enabled sims * refactor: some code optimization * refactor: more type safety * refactor: even safer * feat: restrict enabling and disabling simulators & autoswitch * refactor: shorten function name * feat: filter addons by simulator * fix: select first available addon when switching sim * feat: wip directory handling * fix: error handling * fix: error handling * refactor: move initial msfs path setup functions to install path utils * fix: temp handling * refactor: simplify base path handling * refactor: remove unused function * refactor: safer code * feat: indent temp path setting * fix: remove unexpected temp location behavior * fix: error handling of uninstalled simulators * feat: handling of no installed simulator * feat: use OS Temp as default temp directory * fix: mistakes * feat: temp location error handling * fix: missing arguments * refactor: remove unnecessary check * refactor: remove import * fix: missing arguments * fix: handle no previously selected simulator * fix: standardize error modal buttons * fix: MSFS2024 SU2 changes to UserCfg.opt * refactor: use cn to combine twMerge with clsx * refactor: explicitly type possibleBasePaths * refactor: acceptance of raw strings for Simulator type no longer required * Revert "refactor: acceptance of raw strings for Simulator type no longer required" This reverts commit 22e07e1. * refactor: double import * feat: use null instead of C:\ for nonexitent path * fix: unintentional import * refactor: ErrorModal * refactor: ErrorModal pt.2 * fix: regex match keys that include a hyphen * feat(local config): add A380X and SimBridge for MSFS2024 * docs: add changelog
1 parent 60e9f65 commit 9a0f2de

29 files changed

Lines changed: 1295 additions & 379 deletions

.github/CHANGELOG.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@
99

1010

1111
releases:
12-
- name: 3.4.4
12+
- name: 3.5.0
1313
changes:
14+
- title: Add multi-simulator support (MSFS2020 and MSFS2024)
15+
categories: [Core]
16+
authors: [FoxtotSierra]
1417
- title: Added reload-button to developer configuration
1518
categories: [Core]
1619
authors: [Revyn112]
@@ -23,9 +26,6 @@ releases:
2326
- title: Reset-to-default button for developer configuration
2427
categories: [Core]
2528
authors: [Gaudv]
26-
- title: fix move people to new installer config url
27-
categories: [Core]
28-
authors: [FoxtrotSierra]
2929
- name: 3.4.3
3030
changes:
3131
- title: Change installer configuration to Cloudflare

package-lock.json

Lines changed: 12 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "fbw-installer",
33
"productName": "FlyByWire Installer",
4-
"version": "3.4.3-dev.1",
4+
"version": "3.5.0-dev.1",
55
"description": "Desktop application to install and customize FlyByWire addons",
66
"configUrls": {
77
"production": "https://flybywirecdn.com/installer/config/production.json",
@@ -114,6 +114,7 @@
114114
"@types/winreg": "^1.2.31",
115115
"@types/ws": "^8.5.3",
116116
"check-disk-space": "^3.3.1",
117+
"clsx": "^2.1.1",
117118
"config-ini-parser": "~1.5.9",
118119
"dateformat": "^5.0.1",
119120
"electron-store": "^8.0.1",
Lines changed: 100 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,76 @@
1-
import settings, { msStoreBasePath, steamBasePath } from 'renderer/rendererSettings';
1+
import * as fs from 'fs';
2+
import walk from 'walkdir';
3+
import * as path from 'path';
4+
import * as os from 'os';
5+
import settings from 'renderer/rendererSettings';
26
import { Directories } from 'renderer/utils/Directories';
37
import { dialog } from '@electron/remote';
4-
import fs from 'fs';
8+
import { managedSim, TypeOfSimulator } from 'renderer/utils/SimManager';
9+
10+
const possibleBasePaths: Record<TypeOfSimulator, { store: string; steam: string }> = {
11+
msfs2020: {
12+
store: path.join(Directories.localAppData(), '\\Packages\\Microsoft.FlightSimulator_8wekyb3d8bbwe\\LocalCache\\'),
13+
steam: path.join(Directories.appData(), '\\Microsoft Flight Simulator\\'),
14+
},
15+
msfs2024: {
16+
store: path.join(Directories.localAppData(), '\\Packages\\Microsoft.Limitless_8wekyb3d8bbwe\\LocalCache\\'),
17+
steam: path.join(Directories.appData(), '\\Microsoft Flight Simulator 2024\\'),
18+
},
19+
};
20+
21+
export const msfsBasePath = (sim: TypeOfSimulator): string | null => {
22+
if (os.platform().toString() === 'linux') {
23+
return null;
24+
}
25+
26+
// Ensure proper functionality in main- and renderer-process
27+
let msfsConfigPath = null;
28+
29+
const steamPath = path.join(possibleBasePaths[sim].steam, 'UserCfg.opt');
30+
const storePath = path.join(possibleBasePaths[sim].store, 'UserCfg.opt');
31+
if (fs.existsSync(steamPath) && fs.existsSync(storePath)) return null;
32+
if (fs.existsSync(steamPath)) {
33+
msfsConfigPath = steamPath;
34+
} else if (fs.existsSync(storePath)) {
35+
msfsConfigPath = storePath;
36+
} else {
37+
walk(Directories.localAppData(), (path) => {
38+
if (path.includes('Flight') && path.includes('UserCfg.opt')) {
39+
msfsConfigPath = path;
40+
}
41+
});
42+
}
43+
44+
if (!msfsConfigPath) {
45+
return null;
46+
}
47+
48+
return path.dirname(msfsConfigPath);
49+
};
50+
51+
export const defaultCommunityDir = (msfsBase: string | null): string | null => {
52+
if (!msfsBase) {
53+
return null;
54+
}
55+
const msfsConfigPath = path.join(msfsBase, 'UserCfg.opt');
56+
if (!fs.existsSync(msfsConfigPath)) {
57+
return null;
58+
}
59+
60+
try {
61+
const msfsConfig = fs.readFileSync(msfsConfigPath).toString();
62+
const msfsConfigLines = msfsConfig.split(/\r?\n/);
63+
// Intentional space after InstalledPackagesPath to ensure not matching the InstalledPackagesPathNextBoot property added in MSFS2024 SU2.
64+
const packagesPathLine = msfsConfigLines.find((line) => line.includes('InstalledPackagesPath '));
65+
const communityDir = path.join(packagesPathLine.split(' ').slice(1).join(' ').replaceAll('"', ''), '\\Community');
66+
67+
return fs.existsSync(communityDir) ? communityDir : null;
68+
} catch (e) {
69+
console.warn('Could not parse community dir from file', msfsConfigPath);
70+
console.error(e);
71+
return null;
72+
}
73+
};
574

675
const selectPath = async (currentPath: string, dialogTitle: string, setting: string): Promise<string> => {
776
const path = await dialog.showOpenDialog({
@@ -18,14 +87,14 @@ const selectPath = async (currentPath: string, dialogTitle: string, setting: str
1887
}
1988
};
2089

21-
export const setupMsfsBasePath = async (): Promise<string> => {
22-
const currentPath = Directories.msfsBasePath();
90+
export const setupSimulatorBasePath = async (sim: TypeOfSimulator): Promise<string> => {
91+
const currentPath = Directories.simulatorBasePath(sim);
2392

2493
const availablePaths: string[] = [];
25-
if (fs.existsSync(msStoreBasePath)) {
94+
if (fs.existsSync(possibleBasePaths[sim].store)) {
2695
availablePaths.push('Microsoft Store Edition');
2796
}
28-
if (fs.existsSync(steamBasePath)) {
97+
if (fs.existsSync(possibleBasePaths[sim].steam)) {
2998
availablePaths.push('Steam Edition');
3099
}
31100

@@ -34,41 +103,53 @@ export const setupMsfsBasePath = async (): Promise<string> => {
34103

35104
const { response } = await dialog.showMessageBox({
36105
title: 'FlyByWire Installer',
37-
message: 'We found a possible MSFS installation.',
106+
message: `We found a possible MSFS ${sim.slice(-4)} installation.`,
38107
type: 'warning',
39108
buttons: availablePaths,
40109
});
41110

42111
const selection = availablePaths[response];
43112
switch (selection) {
44113
case 'Microsoft Store Edition':
45-
settings.set('mainSettings.msfsBasePath', msStoreBasePath);
46-
return msStoreBasePath;
114+
settings.set(`mainSettings.simulator.${sim}.basePath`, possibleBasePaths[sim].store);
115+
return possibleBasePaths[sim].store;
47116
case 'Steam Edition':
48-
settings.set('mainSettings.msfsBasePath', steamBasePath);
49-
return steamBasePath;
117+
settings.set(`mainSettings.simulator.${sim}.basePath`, possibleBasePaths[sim].steam);
118+
return possibleBasePaths[sim].steam;
50119
case 'Custom Directory':
51120
break;
52121
}
53122
}
54123

55-
return await selectPath(currentPath, 'Select your MSFS base directory', 'mainSettings.msfsBasePath');
124+
return await selectPath(
125+
currentPath,
126+
`Select your MSFS ${sim.slice(-4)} base directory`,
127+
`mainSettings.simulator.${sim}.basePath`,
128+
);
56129
};
57130

58-
export const setupMsfsCommunityPath = async (): Promise<string> => {
59-
const currentPath = Directories.installLocation();
131+
export const setupMsfsCommunityPath = async (sim: TypeOfSimulator): Promise<string> => {
132+
const currentPath = Directories.installLocation(sim);
60133

61-
return await selectPath(currentPath, 'Select your MSFS community directory', 'mainSettings.msfsCommunityPath');
134+
return await selectPath(
135+
currentPath,
136+
`Select your MSFS ${sim.slice(-4)} community directory`,
137+
`mainSettings.simulator.${sim}.communityPath`,
138+
);
62139
};
63140

64-
export const setupInstallPath = async (): Promise<string> => {
65-
const currentPath = Directories.installLocation();
141+
export const setupInstallPath = async (sim: TypeOfSimulator): Promise<string> => {
142+
const currentPath = Directories.installLocation(sim);
66143

67-
return await selectPath(currentPath, 'Select your install directory', 'mainSettings.installPath');
144+
return await selectPath(
145+
currentPath,
146+
`Select your MSFS ${sim.slice(-4)} install directory`,
147+
`mainSettings.simulator.${sim}.installPath`,
148+
);
68149
};
69150

70151
export const setupTempLocation = async (): Promise<string> => {
71-
const currentPath = Directories.tempLocation();
152+
const currentPath = Directories.tempLocation(managedSim());
72153

73154
return await selectPath(currentPath, 'Select a location for temporary folders', 'mainSettings.tempLocation');
74155
};

src/renderer/assets/msfs2020.png

10.9 KB
Loading

src/renderer/assets/msfs2024.png

12.5 KB
Loading

src/renderer/components/AddonSection/Configure/TrackSelector.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@ import { useSelector } from 'react-redux';
33
import { InstallerStore } from 'renderer/redux/store';
44
import { Check } from 'tabler-icons-react';
55
import { Addon, AddonTrack } from 'renderer/utils/InstallerConfiguration';
6-
6+
import cn from 'renderer/utils/cn';
77
import '../index.css';
8-
import { twMerge } from 'tailwind-merge';
98

109
export const Tracks: React.FC = ({ children }) => (
1110
<div className="flex flex-row items-stretch justify-start gap-3">{children}</div>
@@ -27,8 +26,8 @@ export const Track: React.FC<TrackProps> = ({ isSelected, isInstalled, handleSel
2726

2827
return (
2928
<div
30-
className={twMerge(
31-
`flex w-60 h-24 cursor-pointer flex-col rounded-sm-md border-2 border-transparent bg-navy-dark text-white transition-all duration-200 hover:border-navy-lightest hover:text-gray-300`,
29+
className={cn(
30+
'flex w-60 h-24 cursor-pointer flex-col rounded-sm-md border-2 border-transparent bg-navy-dark text-white transition-all duration-200 hover:border-navy-lightest hover:text-gray-300',
3231
isSelected && 'border-2 border-cyan text-cyan',
3332
)}
3433
onClick={() => handleSelected(track)}

src/renderer/components/AddonSection/MyInstall/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export const MyInstall: FC<MyInstallProps> = ({ addon }) => {
4444
const fulldirectory = (def: DirectoryDefinition) => {
4545
switch (def.location.in) {
4646
case 'community':
47-
return Directories.inInstallLocation(def.location.path);
47+
return Directories.inInstallLocation(addon.simulator, def.location.path);
4848
case 'package':
4949
return Directories.inInstallPackage(addon, def.location.path);
5050
case 'packageCache':

src/renderer/components/AddonSection/index.tsx

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import React, { FC, useCallback, useEffect, useState } from 'react';
2-
import { setupInstallPath } from 'renderer/actions/install-path.utils';
32
import { DownloadItem } from 'renderer/redux/types';
43
import { useSelector } from 'react-redux';
54
import { InstallerStore, useAppDispatch, useAppSelector } from '../../redux/store';
@@ -25,6 +24,7 @@ import { StateSection } from 'renderer/components/AddonSection/StateSection';
2524
import { ExternalApps } from 'renderer/utils/ExternalApps';
2625
import { MyInstall } from 'renderer/components/AddonSection/MyInstall';
2726
import rehypeRaw from 'rehype-raw';
27+
import { Simulators } from 'renderer/utils/SimManager';
2828

2929
const abortControllers = new Array<AbortController>(20);
3030
abortControllers.fill(new AbortController());
@@ -70,6 +70,7 @@ export interface AircraftSectionURLParams {
7070
export const AddonSection = (): JSX.Element => {
7171
const dispatch = useAppDispatch();
7272
const history = useHistory();
73+
const [managedSim] = useSetting<Simulators>('cache.main.managedSim');
7374

7475
const { publisherName } = useParams<AircraftSectionURLParams>();
7576
const publisherData = useAppSelector((state) =>
@@ -108,7 +109,9 @@ export const AddonSection = (): JSX.Element => {
108109
}, [history, publisherData.addons, publisherName, selectedAddon]);
109110

110111
useEffect(() => {
111-
const firstAvailableAddon = publisherData.addons.find((addon) => addon.enabled);
112+
const firstAvailableAddon = publisherData.addons
113+
.filter((addon) => addon.simulator === managedSim)
114+
.find((addon) => addon.enabled);
112115

113116
if (!firstAvailableAddon) {
114117
history.push(`/addon-section/${publisherName}/no-available-addons`);
@@ -117,11 +120,13 @@ export const AddonSection = (): JSX.Element => {
117120

118121
const lastSeenAddonKey = settings.get('cache.main.lastShownAddonKey');
119122
const addonToSelect =
120-
publisherData.addons.find((addon) => addon.key === lastSeenAddonKey) ||
123+
publisherData.addons
124+
.filter((addon) => addon.simulator === managedSim)
125+
.find((addon) => addon.key === lastSeenAddonKey) ||
121126
publisherData.addons.find((addon) => addon.key === firstAvailableAddon.key);
122127

123128
setSelectedAddon(addonToSelect);
124-
}, [history, publisherData.addons, publisherName]);
129+
}, [history, publisherData.addons, publisherName, managedSim]);
125130

126131
const installedTrack = (installedTracks[selectedAddon.key] as AddonTrack) ?? null;
127132

@@ -227,11 +232,7 @@ export const AddonSection = (): JSX.Element => {
227232
};
228233

229234
const handleInstall = async () => {
230-
if (settings.has('mainSettings.installPath')) {
231-
await InstallManager.installAddon(selectedAddon, publisherData, showModalAsync);
232-
} else {
233-
await setupInstallPath();
234-
}
235+
await InstallManager.installAddon(selectedAddon, publisherData, showModalAsync);
235236
};
236237

237238
const handleCancel = useCallback(() => {
@@ -276,6 +277,7 @@ export const AddonSection = (): JSX.Element => {
276277
<AddonBar>
277278
<div className="flex flex-col gap-y-4">
278279
{publisherData.addons
280+
.filter((it) => it.simulator === managedSim)
279281
.filter((it) => !it.category)
280282
.map((addon) => (
281283
<AddonBarItem
@@ -296,9 +298,9 @@ export const AddonSection = (): JSX.Element => {
296298
{publisherData.defs
297299
?.filter((it) => it.kind === 'addonCategory')
298300
.map((category: AddonCategoryDefinition) => {
299-
const categoryAddons = publisherData.addons.filter(
300-
(it) => it.category?.substring(1) === category.key,
301-
);
301+
const categoryAddons = publisherData.addons
302+
.filter((it) => it.simulator === managedSim)
303+
.filter((it) => it.category?.substring(1) === category.key);
302304

303305
if (categoryAddons.length === 0) {
304306
return null;
@@ -315,6 +317,7 @@ export const AddonSection = (): JSX.Element => {
315317

316318
<div className="flex flex-col gap-y-4">
317319
{publisherData.addons
320+
.filter((it) => it.simulator === managedSim)
318321
.filter((it) => it.category?.substring(1) === category.key)
319322
.map((addon) => (
320323
<AddonBarItem

0 commit comments

Comments
 (0)