swift
fsdirectories.cpp
1 // SPDX-FileCopyrightText: Copyright (C) 2013 swift Project Community / Contributors
2 // SPDX-License-Identifier: GPL-3.0-or-later OR LicenseRef-swift-pilot-client-1
3 
5 
6 #include <QDir>
7 #include <QDomDocument>
8 #include <QDomNodeList>
9 #include <QFileInfo>
10 #include <QList>
11 #include <QPair>
12 #include <QSettings>
13 #include <QStandardPaths>
14 #include <QStringBuilder>
15 #include <QStringList>
16 #include <QTextStream>
17 #include <QVariant>
18 
19 #include "config/buildconfig.h"
20 #include "misc/directoryutils.h"
21 #include "misc/fileutils.h"
22 #include "misc/logmessage.h"
23 #include "misc/stringutils.h"
24 #include "misc/swiftdirectories.h"
25 
26 using namespace swift::config;
27 
28 namespace swift::misc::simulation::fscommon
29 {
30  using FsRegistryPathPair = QList<QPair<QString, QString>>;
31 
32  const QStringList &CFsDirectories::getLogCategories()
33  {
34  static const QStringList cats({ CLogCategories::validation(), CLogCategories::driver() });
35  return cats;
36  }
37 
38  QString fsxDirFromRegistryImpl()
39  {
40  QString fsxPath;
41  const FsRegistryPathPair fsxRegistryPathPairs = {
42  { QStringLiteral("HKEY_CURRENT_USER\\Software\\Microsoft\\Microsoft Games\\Flight Simulator\\10.0"),
43  QStringLiteral("AppPath") },
44  { QStringLiteral(
45  "HKEY_CURRENT_USER\\Software\\Microsoft\\Microsoft Games\\Flight Simulator - Steam Edition\\10.0"),
46  QStringLiteral("AppPath") },
47  { QStringLiteral("HKEY_LOCAL_MACHINE\\Software\\Microsoft\\Microsoft Games\\Flight Simulator\\10.0"),
48  QStringLiteral("SetupPath") },
49  { QStringLiteral(
50  "HKEY_LOCAL_MACHINE\\SOFTWARE\\Wow6432Node\\Microsoft\\Microsoft Games\\Flight Simulator\\10.0"),
51  QStringLiteral("SetupPath") }
52  };
53 
54  for (const auto &registryPair : fsxRegistryPathPairs)
55  {
56  const QSettings fsxRegistry(registryPair.first, QSettings::NativeFormat);
57  fsxPath = fsxRegistry.value(registryPair.second).toString().trimmed();
58 
59  if (fsxPath.isEmpty()) { continue; }
60  fsxPath = CFileUtils::normalizeFilePathToQtStandard(fsxPath);
61 
62  // if path does NOT exists we continue to search, maybe another one does
63  const QDir dir(fsxPath);
64  if (dir.exists()) { break; }
65  fsxPath.clear();
66  }
67  return CFileUtils::normalizeFilePathToQtStandard(fsxPath);
68  }
69 
70  const QString &CFsDirectories::fsxDirFromRegistry()
71  {
72  static const QString fsxPath(fsxDirFromRegistryImpl());
73  return fsxPath;
74  }
75 
76  QString fsxDirImpl()
77  {
78  const QString dir(CFsDirectories::fsxDirFromRegistry());
79  if (!dir.isEmpty()) { return dir; }
80  QStringList someDefaultDirs(
81  { "C:/Program Files (x86)/Microsoft Games/Microsoft Flight Simulator X", "C:/FSX" });
83  {
84  // developer directories
85  someDefaultDirs.push_back("P:/FSX (MSI)");
86  }
87  return CFileUtils::findFirstExisting(someDefaultDirs);
88  }
89 
90  const QString &CFsDirectories::fsxDir()
91  {
92  static const QString dir(fsxDirImpl());
93  return dir;
94  }
95 
96  static QString msfsDirImpl()
97  {
98  // first we look for a standard installation
99  const QStringList locations = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation);
100  for (const QString &path : locations)
101  {
102  const QString msfsPackage = CFileUtils::appendFilePaths(CFileUtils::appendFilePaths(path, "Packages"),
103  "Microsoft.FlightSimulator_8wekyb3d8bbwe");
104  const QDir d(msfsPackage);
105  if (!d.exists()) { continue; }
106  return msfsPackage;
107  }
108  // then we look for steam-edition
109  for (QString path : locations)
110  {
111  // there seems to be no constant for the roaming directory, so we have to do some magic
112  // https://doc.qt.io/qt-6/qstandardpaths.html
113  path.replace("Local", "Roaming");
114  const QString msfsPackage = CFileUtils::appendFilePaths(path, "Microsoft Flight Simulator");
115  const QString fileName = CFileUtils::appendFilePaths(msfsPackage, "UserCfg.opt");
116  const QFileInfo fi(fileName);
117  if (!fi.exists()) { continue; }
118  return msfsPackage;
119  }
120  return {};
121  }
122 
123  const QString &CFsDirectories::msfsDir()
124  {
125  static const QString dir(msfsDirImpl());
126  return dir;
127  }
128 
129  QString msfsPackagesDirImpl()
130  {
131  QString userCfg = "";
132 
133  QString msfsDirectory(CFsDirectories::msfsDir());
134 
135  // for Steam edition
136  if (msfsDirectory.contains("Roaming", Qt::CaseInsensitive))
137  {
138  userCfg = CFileUtils::appendFilePaths(msfsDirectory, "UserCfg.opt");
139  }
140  else
141  {
142  userCfg =
143  CFileUtils::appendFilePaths(CFileUtils::appendFilePaths(msfsDirectory, "LocalCache"), "UserCfg.opt");
144  }
145 
146  QFile file(CFileUtils::normalizeFilePathToQtStandard(userCfg));
147  if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { return {}; }
148 
149  QTextStream in(&file);
150  while (!in.atEnd())
151  {
152  QString line = in.readLine();
153  if (line.contains("InstalledPackagesPath"))
154  {
155  // change the split separator because of path names with multiple spaces in Steamedition
156  QStringList split = line.split("\"");
157  // we have 2 quotation marks in the line so 3 parts
158  if (split.size() != 3) { return {}; }
159  QString packagePath = split[1].remove("\"");
160  const QDir dir(packagePath);
161  if (dir.exists()) { return packagePath; }
162  }
163  }
164  return {};
165  }
166 
167  const QString &CFsDirectories::msfsPackagesDir()
168  {
169  static const QString dir(msfsPackagesDirImpl());
170  return dir;
171  }
172 
173  static QString msfs2024DirImpl()
174  {
175  const QStringList locations = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation);
176  for (const QString &path : locations)
177  {
178  const QString msfs2024Package = CFileUtils::appendFilePaths(CFileUtils::appendFilePaths(path, "Packages"),
179  "Microsoft.Limitless_8wekyb3d8bbwe");
180  const QDir d(msfs2024Package);
181  if (!d.exists()) { continue; }
182  return msfs2024Package;
183  }
184  // then we look for steam-edition
185  for (QString path : locations)
186  {
187  // there seems to be no constant for the roaming directory, so we have to do some magic
188  // https://doc.qt.io/qt-6/qstandardpaths.html
189  path.replace("Local", "Roaming");
190  const QString msfsPackage = CFileUtils::appendFilePaths(path, "Microsoft Flight Simulator 2024");
191  const QString fileName = CFileUtils::appendFilePaths(msfsPackage, "UserCfg.opt");
192  const QFileInfo fi(fileName);
193  if (!fi.exists()) { continue; }
194  return msfsPackage;
195  }
196  return {};
197  }
198 
199  const QString &CFsDirectories::msfs2024Dir()
200  {
201  static const QString dir(msfs2024DirImpl());
202  return dir;
203  }
204 
205  QString msfs2024PackagesDirImpl()
206  {
207  QString userCfg = "";
208 
209  QString msfs2024Directory(CFsDirectories::msfs2024Dir());
210  // for Steam edition
211  if (msfs2024Directory.contains("Roaming", Qt::CaseInsensitive))
212  {
213  userCfg = CFileUtils::appendFilePaths(msfs2024Directory, "UserCfg.opt");
214  }
215  else
216  {
217  userCfg = CFileUtils::appendFilePaths(CFileUtils::appendFilePaths(msfs2024Directory, "LocalCache"),
218  "UserCfg.opt");
219  }
220 
221  QFile file(userCfg);
222  if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { return {}; }
223 
224  QTextStream in(&file);
225  while (!in.atEnd())
226  {
227  QString line = in.readLine();
228  if (line.contains("InstalledPackagesPath"))
229  {
230  // change the split separator because of path names with multiple spaces in Steamedition
231  QStringList split = line.split("\"");
232  // we have 2 quotation marks in the line so 3 parts
233  if (split.size() != 3) { return {}; }
234  QString packagePath = split[1].remove("\"");
235  const QDir dir(packagePath);
236  if (dir.exists()) { return packagePath; }
237  }
238  }
239  return {};
240  }
241 
242  const QString &CFsDirectories::msfs2024PackagesDir()
243  {
244  static const QString dir(msfs2024PackagesDirImpl());
245  return dir;
246  }
247 
248  QString fsxSimObjectsDirFromRegistryImpl()
249  {
250  const QString fsxPath = CFileUtils::normalizeFilePathToQtStandard(CFsDirectories::fsxDirFromRegistry());
251  if (fsxPath.isEmpty()) { return {}; }
252  return CFsDirectories::fsxSimObjectsDirFromSimDir(fsxPath);
253  }
254 
255  const QString &CFsDirectories::fsxSimObjectsDirFromRegistry()
256  {
257  static const QString fsxPath(fsxSimObjectsDirFromRegistryImpl());
258  return fsxPath;
259  }
260 
261  QString fsxSimObjectsDirImpl()
262  {
263  QString dir(CFsDirectories::fsxDir());
264  if (dir.isEmpty()) { return {}; }
265  return CFsDirectories::fsxSimObjectsDirFromSimDir(dir);
266  }
267 
268  QString msfsSimObjectsDirImpl()
269  {
270  QString dir(CFsDirectories::msfsDir());
271  if (dir.isEmpty()) { return {}; }
272  return CFileUtils::normalizeFilePathToQtStandard(msfsPackagesDirImpl());
273  }
274 
275  QString msfs2024SimObjectsDirImpl()
276  {
277  QString dir(CFsDirectories::msfs2024Dir());
278  if (dir.isEmpty()) { return {}; }
279  return CFileUtils::normalizeFilePathToQtStandard(msfs2024PackagesDirImpl());
280  }
281 
282  const QString &CFsDirectories::fsxSimObjectsDir()
283  {
284  static const QString dir(fsxSimObjectsDirImpl());
285  return dir;
286  }
287 
288  const QString &CFsDirectories::msfsSimObjectsDir()
289  {
290  static const QString dir(msfsSimObjectsDirImpl());
291  return dir;
292  }
293 
294  const QString &CFsDirectories::msfs2024SimObjectsDir()
295  {
296  static const QString dir(msfs2024SimObjectsDirImpl());
297  return dir;
298  }
299  QString CFsDirectories::fsxSimObjectsDirFromSimDir(const QString &simDir)
300  {
301  if (simDir.isEmpty()) { return {}; }
302  return CFileUtils::appendFilePaths(CFileUtils::normalizeFilePathToQtStandard(simDir), "SimObjects");
303  }
304 
305  const QStringList &CFsDirectories::fsxSimObjectsExcludeDirectoryPatterns()
306  {
307  static const QStringList exclude { "SimObjects/Animals", "SimObjects/Misc", "SimObjects/GroundVehicles",
308  "SimObjects/Boats" };
309  return exclude;
310  }
311 
312  const QStringList &CFsDirectories::msfs20SimObjectsExcludeDirectoryPatterns()
313  {
314  static const QStringList exclude {
315  "OneStore/asobo-discovery",
316  "OneStore/asobo-flight",
317  "OneStore/asobo-landingchallenge",
318  "OneStore/asobo-mission",
319  "OneStore/asobo-tutorials",
320  "OneStore/asobo-vcockpits",
321  "OneStore/asobo-simobjects",
322  "OneStore/asobo-services",
323  "OneStore/asobo-vcockpits",
324  "OneStore/asobo-l",
325  "OneStore/asobo-m",
326  "OneStore/asobo-vfx",
327  "OneStore/fs",
328  "OneStore/esd",
329  "OneStore/microsoft-airport",
330  "OneStore/microsoft-bushtrip",
331  "OneStore/microsoft-discovery",
332  "landingchallenge",
333  "tutorials",
334 
335  };
336  return exclude;
337  }
338 
339  const QStringList &CFsDirectories::msfs2024SimObjectsExcludeDirectoryPatterns()
340  {
341  static const QStringList exclude {
342  "landingchallenge",
343  "tutorials",
344 
345  };
346  return exclude;
347  }
348 
349  QString p3dDirFromRegistryImpl()
350  {
351  QString p3dPath;
352  FsRegistryPathPair p3dRegistryPathPairs = {
353  // latest versions first
354  { QStringLiteral("HKEY_CURRENT_USER\\Software\\Lockheed Martin\\Prepar3d v6"), QStringLiteral("AppPath") },
355  { QStringLiteral("HKEY_CURRENT_USER\\Software\\Lockheed Martin\\Prepar3d v5"), QStringLiteral("AppPath") },
356  { QStringLiteral("HKEY_CURRENT_USER\\Software\\Lockheed Martin\\Prepar3d v4"), QStringLiteral("AppPath") },
357  { QStringLiteral("HKEY_CURRENT_USER\\Software\\Lockheed Martin\\Prepar3d v3"), QStringLiteral("AppPath") },
358  { QStringLiteral("HKEY_CURRENT_USER\\Software\\Lockheed Martin\\Prepar3d v2"), QStringLiteral("AppPath") },
359  { QStringLiteral("HKEY_CURRENT_USER\\Software\\LockheedMartin\\Prepar3d"), QStringLiteral("AppPath") }
360  };
361  for (const auto &registryPair : p3dRegistryPathPairs)
362  {
363  const QSettings p3dRegistry(registryPair.first, QSettings::NativeFormat);
364  p3dPath = p3dRegistry.value(registryPair.second).toString().trimmed();
365 
366  if (p3dPath.isEmpty()) { continue; }
367  p3dPath = CFileUtils::normalizeFilePathToQtStandard(p3dPath);
368 
369  // if path does NOT exists we continue to search, maybe another one does
370  const QDir dir(p3dPath);
371  if (dir.exists()) { break; }
372  p3dPath.clear();
373  }
374  return p3dPath;
375  }
376 
377  const QString &CFsDirectories::p3dDirFromRegistry()
378  {
379  static const QString p3dPath = CFileUtils::normalizeFilePathToQtStandard(p3dDirFromRegistryImpl());
380  return p3dPath;
381  }
382 
383  QString p3dDirImpl()
384  {
385  QString dir(CFsDirectories::p3dDirFromRegistry());
386  if (!dir.isEmpty()) { return dir; }
387  const QStringList someDefaultDirs({ "C:/Program Files (x86)/Lockheed Martin/Prepar3D v4",
388  "C:/Program Files (x86)/Lockheed Martin/Prepar3D v3",
389  "C:/Program Files (x86)/Lockheed Martin/Prepar3D v2",
390  "C:/Program Files (x86)/Lockheed Martin/Prepar3D" });
391  return CFileUtils::findFirstExisting(someDefaultDirs);
392  }
393 
394  const QString &CFsDirectories::p3dDir()
395  {
396  static const QString dir(p3dDirImpl());
397  return dir;
398  }
399 
400  QString p3dSimObjectsDirFromRegistryImpl()
401  {
402  const QString p3dPath = CFsDirectories::p3dDirFromRegistry();
403  if (p3dPath.isEmpty()) { return {}; }
404  return CFsDirectories::fsxSimObjectsDirFromSimDir(p3dPath);
405  }
406 
407  const QString &CFsDirectories::p3dSimObjectsDirFromRegistry()
408  {
409  static const QString p3dPath(p3dSimObjectsDirFromRegistryImpl());
410  return p3dPath;
411  }
412 
413  QString p3dSimObjectsDirImpl()
414  {
415  QString dir(CFsDirectories::p3dDir());
416  if (dir.isEmpty()) { return {}; }
417  return CFsDirectories::p3dSimObjectsDirFromSimDir(dir);
418  }
419 
420  const QString &CFsDirectories::p3dSimObjectsDir()
421  {
422  static const QString dir(p3dSimObjectsDirImpl());
423  return dir;
424  }
425 
426  QStringList CFsDirectories::fsxSimObjectsDirPlusAddOnXmlSimObjectsPaths(const QString &simObjectsDir)
427  {
428  // finding the user settings only works on P3D machine
429  QStringList allPaths = CFsDirectories::allFsxSimObjectPaths().values();
430  const QString sod = CFileUtils::normalizeFilePathToQtStandard(
431  simObjectsDir.isEmpty() ? CFsDirectories::fsxSimObjectsDir() : simObjectsDir);
432  if (!sod.isEmpty() && !allPaths.contains(sod, Qt::CaseInsensitive))
433  {
434  // case insensitive is important here
435  allPaths.push_front(sod);
436  }
437 
438  allPaths.removeAll({}); // remove all empty
439  allPaths.removeDuplicates();
440  allPaths.sort(Qt::CaseInsensitive);
441  return allPaths;
442  }
443 
444  QStringList CFsDirectories::msfsSimObjectsDirPath(const QString &simObjectsDir)
445  {
446  Q_UNUSED(simObjectsDir);
447  static const QStringList Path { CFsDirectories::msfsSimObjectsDir() };
448  return Path;
449  }
450 
451  QStringList CFsDirectories::msfs2024SimObjectsDirPath(const QString &simObjectsDir)
452  {
453  Q_UNUSED(simObjectsDir);
454  static const QStringList Path { CFsDirectories::msfs2024SimObjectsDir() };
455  return Path;
456  }
457  QStringList CFsDirectories::p3dSimObjectsDirPlusAddOnXmlSimObjectsPaths(const QString &simObjectsDir,
458  const QString &versionHint)
459  {
460  // finding the user settings only works on P3D machine
461  QStringList allPaths = CFsDirectories::allP3dAddOnXmlSimObjectPaths(versionHint).values();
462  const QString sod = CFileUtils::normalizeFilePathToQtStandard(
463  simObjectsDir.isEmpty() ? CFsDirectories::p3dSimObjectsDir() : simObjectsDir);
464  if (!sod.isEmpty() && !allPaths.contains(sod, Qt::CaseInsensitive))
465  {
466  // case insensitive is important here
467  allPaths.push_front(sod);
468  }
469 
470  allPaths.removeAll({}); // remove all empty
471  allPaths.removeDuplicates();
472  allPaths.sort(Qt::CaseInsensitive);
473  return allPaths;
474  }
475 
476  QString CFsDirectories::guessP3DVersion(const QString &candidate)
477  {
478  if (candidate.isEmpty()) { return "v5"; }
479  if (candidate.contains("v5", Qt::CaseInsensitive)) { return QStringLiteral("v5"); }
480  if (candidate.contains("v4", Qt::CaseInsensitive)) { return QStringLiteral("v4"); }
481 
482  if (candidate.contains("5", Qt::CaseInsensitive)) { return QStringLiteral("v5"); }
483  if (candidate.contains("4", Qt::CaseInsensitive)) { return QStringLiteral("v4"); }
484 
485  return "v5"; // that is the future (in 2020)
486  }
487 
488  QString CFsDirectories::p3dSimObjectsDirFromSimDir(const QString &simDir)
489  {
490  if (simDir.isEmpty()) { return {}; }
491  return CFileUtils::normalizeFilePathToQtStandard(CFileUtils::appendFilePaths(simDir, "SimObjects"));
492  }
493 
494  const QStringList &CFsDirectories::p3dSimObjectsExcludeDirectoryPatterns()
495  {
496  static const QStringList exclude {
497  // FSX
498  "SimObjects/Animals",
499  "SimObjects/Misc",
500  "SimObjects/GroundVehicles",
501  "SimObjects/Boats",
502 
503  // P3D new
504  "SimObjects/Avatars",
505  "SimObjects/Countermeasures",
506  "SimObjects/Submersible",
507  "SimObjects/Weapons",
508  };
509  return exclude;
510  }
511 
512  QString fs9DirFromRegistryImpl()
513  {
514  QString fs9Path;
515  FsRegistryPathPair fs9RegistryPathPairs = {
516  { QStringLiteral(
517  "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\DirectPlay\\Applications\\Microsoft Flight Simulator 2004"),
518  QStringLiteral("AppPath") },
519  { QStringLiteral(
520  "HKEY_LOCAL_MACHINE\\SOFTWARE\\Wow6432Node\\Microsoft\\DirectPlay\\Applications\\Microsoft Flight "
521  "Simulator 2004"),
522  QStringLiteral("AppPath") }
523  };
524 
525  for (const auto &registryPair : fs9RegistryPathPairs)
526  {
527  QSettings fs9Registry(registryPair.first, QSettings::NativeFormat);
528  fs9Path = fs9Registry.value(registryPair.second).toString().trimmed();
529 
530  if (fs9Path.isEmpty()) { continue; }
531  fs9Path = CFileUtils::normalizeFilePathToQtStandard(fs9Path);
532 
533  // if path does NOT exists we continue to search, maybe another one does
534  const QDir dir(fs9Path);
535  if (dir.exists()) { break; }
536  fs9Path.clear();
537  }
538  return fs9Path;
539  }
540 
541  const QString &CFsDirectories::fs9DirFromRegistry()
542  {
543  static const QString fs9Path(fs9DirFromRegistryImpl());
544  return fs9Path;
545  }
546 
547  QString fs9DirImpl()
548  {
549  QString dir(CFsDirectories::fs9DirFromRegistry());
550  if (!dir.isEmpty()) { return dir; }
551  const QStringList someDefaultDirs({ "C:/Flight Simulator 9", "C:/FS9" });
552  return CFileUtils::findFirstExisting(someDefaultDirs);
553  }
554 
555  const QString &CFsDirectories::fs9Dir()
556  {
557  static const QString v(fs9DirImpl());
558  return v;
559  }
560 
561  QString fs9AircraftDirFromRegistryImpl()
562  {
563  QString fs9Path = CFsDirectories::fs9DirFromRegistry();
564  if (fs9Path.isEmpty()) { return {}; }
565  return CFsDirectories::fs9AircraftDirFromSimDir(fs9Path);
566  }
567 
568  const QString &CFsDirectories::fs9AircraftDirFromRegistry()
569  {
570  static const QString dir(fs9AircraftDirFromRegistryImpl());
571  return dir;
572  }
573 
574  QString fs9AircraftDirImpl()
575  {
576  const QString dir(CFsDirectories::fs9Dir());
577  if (dir.isEmpty()) { return {}; }
578  return CFsDirectories::fs9AircraftDirFromSimDir(dir);
579  }
580 
581  const QString &CFsDirectories::fs9AircraftDir()
582  {
583  static const QString dir(fs9AircraftDirImpl());
584  return dir;
585  }
586 
587  QString CFsDirectories::fs9AircraftDirFromSimDir(const QString &simDir)
588  {
589  if (simDir.isEmpty()) { return {}; }
590  return CFileUtils::appendFilePaths(simDir, "Aircraft");
591  }
592 
593  const QStringList &CFsDirectories::fs9AircraftObjectsExcludeDirectoryPatterns()
594  {
595  static const QStringList exclude;
596  return exclude;
597  }
598 
599  QSet<QString> CFsDirectories::findP3dAddOnConfigFiles(const QString &versionHint)
600  {
601  static const QString cfgFile("add-ons.cfg");
602  return CFsDirectories::findP3dConfigFiles(cfgFile, versionHint);
603  }
604 
605  QSet<QString> CFsDirectories::findP3dSimObjectsConfigFiles(const QString &versionHint)
606  {
607  static const QString cfgFile("simobjects.cfg");
608  return CFsDirectories::findP3dConfigFiles(cfgFile, versionHint);
609  }
610 
611  QSet<QString> CFsDirectories::findP3dConfigFiles(const QString &configFile, const QString &versionHint)
612  {
613  // locations will be swift paths, I will go one level up and then search for Lockheed Martin
614  const QStringList locations = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation);
615  QSet<QString> files;
616  for (const QString &path : locations)
617  {
618  const QString pathUp = CFileUtils::appendFilePaths(CFileUtils::pathUp(path), "Lockheed Martin");
619  const QDir d(pathUp);
620  if (!d.exists()) { continue; }
621  if (logConfigPathReading())
622  {
623  CLogMessage(static_cast<CFsDirectories *>(nullptr)).info(u"P3D config dir: '%1'") << d.absolutePath();
624  }
625 
626  // all versions sub directories
627  // looking for "add-ons.cfg" or simobjects.cfg
628  const QStringList entries = d.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
629  for (const QString &entry : entries)
630  {
631  // right version or just one file
632  if (entry.contains(versionHint, Qt::CaseInsensitive))
633  {
634  const QString f = CFileUtils::appendFilePaths(d.absolutePath(), entry, configFile);
635  const QFileInfo fi(f);
636  if (fi.exists())
637  {
638  files.insert(f);
639  if (logConfigPathReading())
640  {
641  CLogMessage(static_cast<CFsDirectories *>(nullptr)).info(u"P3D config file: '%1'") << f;
642  }
643  }
644  } // contains
645  } // entries
646  }
647  return files;
648  }
649 
650  QSet<QString> CFsDirectories::allConfigFilesPathValues(const QStringList &configFiles, bool checked,
651  const QString &pathPrefix)
652  {
653  if (configFiles.isEmpty()) { return QSet<QString>(); }
654  QSet<QString> paths;
655  for (const QString &configFile : configFiles)
656  {
657  // manually parsing because QSettings did not work properly
658  const QString fileContent = CFileUtils::readFileToString(configFile);
659  if (fileContent.isEmpty()) { continue; }
660  const QList<QStringRef> lines = splitLinesRefs(fileContent);
661  static const QString p("Path=");
662  for (const QStringRef &line : lines)
663  {
664  const int i = line.lastIndexOf(p, -1, Qt::CaseInsensitive);
665  if (i < 0 || line.endsWith('=')) { continue; }
666  const QStringRef path = line.mid(i + p.length());
667  const QDir dir(QDir::fromNativeSeparators(
668  pathPrefix.isEmpty() ? path.toString() :
669  CFileUtils::appendFilePathsAndFixUnc(pathPrefix, path.toString())));
670  if (!checked || dir.exists()) { paths.insert(dir.absolutePath()); }
671  }
672  }
673  return paths;
674  }
675 
676  QSet<QString> CFsDirectories::allP3dAddOnXmlSimObjectPaths(const QStringList &addOnPaths, bool checked)
677  {
678  if (addOnPaths.isEmpty()) { return QSet<QString>(); }
679  QSet<QString> simObjectPaths;
680  for (const QString &addOnPath : addOnPaths)
681  {
682  const QString filename = CFileUtils::appendFilePaths(addOnPath, "add-on.xml");
683  QDomDocument doc;
684  QFile file(filename);
685  if (!file.open(QIODevice::ReadOnly) || !doc.setContent(&file)) { continue; }
686  if (CFsDirectories::logConfigPathReading())
687  {
688  CLogMessage(static_cast<CFsDirectories *>(nullptr)).info(u"Reading '%1' from addon path: '%2'")
689  << file.fileName() << addOnPath;
690  }
691 
692  const QDomNodeList components = doc.elementsByTagName("AddOn.Component");
693  for (int i = 0; i < components.size(); i++)
694  {
695  const QDomNode component = components.item(i);
696  const QDomElement category = component.firstChildElement("Category");
697  const QString categoryValue = category.text();
698  if (!caseInsensitiveStringCompare(categoryValue, QStringLiteral("SimObjects"))) { continue; }
699  const QDomElement path = component.firstChildElement("Path");
700  const QString pathValue = CFileUtils::normalizeFilePathToQtStandard(path.text());
701  const bool correctPath = pathValue.contains("Airplanes", Qt::CaseInsensitive) ||
702  pathValue.contains("Rotorcraft", Qt::CaseInsensitive);
703  if (!correctPath) { continue; }
704 
705  // absolute or relative path
706  const QString fp = QStringView { pathValue }.left(3).contains(':') ?
707  pathValue :
708  CFileUtils::appendFilePaths(addOnPath, pathValue);
709  if (CFsDirectories::logConfigPathReading())
710  {
711  CLogMessage(static_cast<CFsDirectories *>(nullptr)).info(u"Testing '%1' as addon path: '%2'")
712  << fp << addOnPath;
713  }
714  if (!checked || QDir(fp).exists())
715  {
716  simObjectPaths.insert(CFileUtils::normalizeFilePathToQtStandard(fp));
717  if (logConfigPathReading())
718  {
719  CLogMessage(static_cast<CFsDirectories *>(nullptr)).info(u"P3D SimObjects path: '%1'") << fp;
720  }
721  }
722  } // components
723  } // paths
724 
725  return simObjectPaths;
726  }
727 
728  QSet<QString> CFsDirectories::allP3dAddOnXmlSimObjectPaths(const QString &versionHint)
729  {
730  // all add-ons.cfg files
731  const QStringList addOnConfigFiles = CFsDirectories::findP3dAddOnConfigFiles(versionHint).values();
732 
733  // all PATH values in those files
734  const QStringList addOnPaths = CFsDirectories::allConfigFilesPathValues(addOnConfigFiles, true, {}).values();
735 
736  // based on all paths of all config files search the XML files
737  const QSet<QString> all = CFsDirectories::allP3dAddOnXmlSimObjectPaths(addOnPaths, true);
738  return all;
739  }
740 
741  QSet<QString> CFsDirectories::allFsxSimObjectPaths()
742  {
743  return CFsDirectories::fsxSimObjectsPaths(CFsDirectories::findFsxConfigFiles(), true);
744  }
745 
746  QStringList CFsDirectories::findFsxConfigFiles()
747  {
748  const QStringList locations = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation);
749  QStringList files;
750  for (const QString &path : locations)
751  {
752  const QString file = CFileUtils::appendFilePaths(CFileUtils::pathUp(path), "Microsoft/FSX/fsx.cfg");
753  const QFileInfo fi(file);
754  if (fi.exists())
755  {
756  files.push_back(fi.absoluteFilePath());
757  if (logConfigPathReading())
758  {
759  CLogMessage(static_cast<CFsDirectories *>(nullptr)).info(u"FSX config file: '%1'")
760  << fi.absoluteFilePath();
761  }
762  }
763  }
764  return files;
765  }
766 
767  QSet<QString> CFsDirectories::fsxSimObjectsPaths(const QStringList &fsxFiles, bool checked)
768  {
769  QSet<QString> paths;
770  for (const QString &fsxFile : fsxFiles) { paths.unite(CFsDirectories::fsxSimObjectsPaths(fsxFile, checked)); }
771  return paths;
772  }
773 
774  QSet<QString> CFsDirectories::msfsSimObjectsPaths(const QStringList &msfsFiles, bool checked)
775  {
776  QSet<QString> paths;
777  for (const QString &msfsFile : msfsFiles)
778  {
779  paths.unite(CFsDirectories::msfsSimObjectsPaths(msfsFile, checked));
780  }
781  return paths;
782  }
783 
784  QSet<QString> CFsDirectories::fsxSimObjectsPaths(const QString &fsxFile, bool checked)
785  {
786  const QString fileContent = CFileUtils::readFileToString(fsxFile);
787  if (fileContent.isEmpty()) { return QSet<QString>(); }
788  const QList<QStringRef> lines = splitLinesRefs(fileContent);
789  static const QString p("SimObjectPaths.");
790 
791  const QFileInfo fsxFileInfo(fsxFile);
792  const QString relPath = fsxFileInfo.absolutePath();
793 
794  QSet<QString> paths;
795  for (const QStringRef &line : lines)
796  {
797  const int i1 = line.lastIndexOf(p, -1, Qt::CaseInsensitive);
798  if (i1 < 0) { continue; }
799  const int i2 = line.lastIndexOf('=');
800  if (i2 < 0 || i1 >= i2 || line.endsWith('=')) { continue; }
801  const QStringRef path = line.mid(i2 + 1);
802  QString soPath = QDir::fromNativeSeparators(path.toString());
803  if (logConfigPathReading())
804  {
805  CLogMessage(static_cast<CFsDirectories *>(nullptr)).info(u"FSX SimObjects path checked: '%1' in '%2'")
806  << line << fsxFile;
807  }
808 
809  // ignore exclude patterns
810  if (containsAny(soPath, CFsDirectories::fsxSimObjectsExcludeDirectoryPatterns(), Qt::CaseInsensitive))
811  {
812  continue;
813  }
814 
815  // make absolute
816  if (!QStringView { soPath }.left(3).contains(':'))
817  {
818  soPath = CFileUtils::appendFilePaths(relPath, soPath);
819  }
820 
821  const QDir dir(soPath); // always absolute path now
822  if (checked && !dir.exists())
823  {
824  // skip, not existing
825  if (logConfigPathReading())
826  {
827  CLogMessage(static_cast<CFsDirectories *>(nullptr))
828  .info(u"FSX SimObjects path skipped, not existing: '%1' in '%2'")
829  << dir.absolutePath() << fsxFile;
830  }
831  continue;
832  }
833 
834  const QString afp = dir.absolutePath().toLower();
835  if (!CDirectoryUtils::containsFileInDir(afp, airFileFilter(), true))
836  {
837  if (logConfigPathReading())
838  {
839  CLogMessage(static_cast<CFsDirectories *>(nullptr))
840  .info(u"FSX SimObjects path: Skipping '%1' from '%2', no '%3' file")
841  << afp << fsxFile << airFileFilter();
842  }
843  continue;
844  }
845 
846  paths.insert(afp);
847  if (logConfigPathReading())
848  {
849  CLogMessage(static_cast<CFsDirectories *>(nullptr)).info(u"FSX SimObjects path: '%1' from '%2'")
850  << afp << fsxFile;
851  }
852  }
853  return paths;
854  }
855 
856  QSet<QString> CFsDirectories::msfsSimObjectsPaths(const QString &msfsFile, bool checked)
857  {
858  const QString fileContent = CFileUtils::readFileToString(msfsFile);
859  if (fileContent.isEmpty()) { return QSet<QString>(); }
860  const QList<QStringRef> lines = splitLinesRefs(fileContent);
861  static const QString p("SimObjectPaths.");
862 
863  const QFileInfo fsxFileInfo(msfsFile);
864  const QString relPath = fsxFileInfo.absolutePath();
865 
866  QSet<QString> paths;
867  for (const QStringRef &line : lines)
868  {
869  const int i1 = line.lastIndexOf(p, -1, Qt::CaseInsensitive);
870  if (i1 < 0) { continue; }
871  const int i2 = line.lastIndexOf('=');
872  if (i2 < 0 || i1 >= i2 || line.endsWith('=')) { continue; }
873  const QStringRef path = line.mid(i2 + 1);
874  QString soPath = QDir::fromNativeSeparators(path.toString());
875  if (logConfigPathReading())
876  {
877  CLogMessage(static_cast<CFsDirectories *>(nullptr)).info(u"MSFS SimObjects path checked: '%1' in '%2'")
878  << line << msfsFile;
879  }
880 
881  // ignore exclude patterns
882  if (containsAny(soPath, CFsDirectories::fsxSimObjectsExcludeDirectoryPatterns(), Qt::CaseInsensitive))
883  {
884  continue;
885  }
886 
887  // make absolute
888  if (!QStringView(soPath).left(3).contains(':')) { soPath = CFileUtils::appendFilePaths(relPath, soPath); }
889 
890  const QDir dir(soPath); // always absolute path now
891  if (checked && !dir.exists())
892  {
893  // skip, not existing
894  if (logConfigPathReading())
895  {
896  CLogMessage(static_cast<CFsDirectories *>(nullptr))
897  .info(u"FSX SimObjects path skipped, not existing: '%1' in '%2'")
898  << dir.absolutePath() << msfsFile;
899  }
900  continue;
901  }
902 
903  const QString afp = dir.absolutePath().toLower();
904  if (!CDirectoryUtils::containsFileInDir(afp, airFileFilter(), true))
905  {
906  if (logConfigPathReading())
907  {
908  CLogMessage(static_cast<CFsDirectories *>(nullptr))
909  .info(u"FSX SimObjects path: Skipping '%1' from '%2', no '%3' file")
910  << afp << msfsFile << airFileFilter();
911  }
912  continue;
913  }
914 
915  paths.insert(afp);
916  if (logConfigPathReading())
917  {
918  CLogMessage(static_cast<CFsDirectories *>(nullptr)).info(u"FSX SimObjects path: '%1' from '%2'")
919  << afp << msfsFile;
920  }
921  }
922  return paths;
923  }
924 
925  const QString &CFsDirectories::airFileFilter()
926  {
927  static const QString a("*.air");
928  return a;
929  }
930 
931  bool CFsDirectories::logConfigPathReading() { return true; }
932 } // namespace swift::misc::simulation::fscommon
static bool isLocalDeveloperDebugBuild()
Local build for developers.
Class for emitting a log message.
Definition: logmessage.h:27
Derived & info(const char16_t(&format)[N])
Set the severity to info, providing a format string.
SWIFT_MISC_EXPORT bool caseInsensitiveStringCompare(const QString &c1, const QString &c2)
Case insensitive string compare.
SWIFT_MISC_EXPORT bool containsAny(const QString &testString, const QStringList &any, Qt::CaseSensitivity cs)
Contains any string of the list?
SWIFT_MISC_EXPORT QList< QStringRef > splitLinesRefs(const QString &s)
Split a string into multiple lines. Blank lines are skipped.
std::vector< std::string > split(const std::string &str, size_t maxSplitCount=0, const std::string &delimiter=" ")
Split string by delimiter and maxSplitCount times.
Definition: qtfreeutils.h:55