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
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  {
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  "point-of-interest",
335  "airport",
336 
337  };
338  return exclude;
339  }
340 
341  const QStringList &CFsDirectories::msfs2024SimObjectsExcludeDirectoryPatterns()
342  {
343  static const QStringList exclude { "PassiveAircraft", "STUB", "ZZZZ" };
344  return exclude;
345  }
346 
347  QString p3dDirFromRegistryImpl()
348  {
349  QString p3dPath;
350  FsRegistryPathPair p3dRegistryPathPairs = {
351  // latest versions first
352  { QStringLiteral("HKEY_CURRENT_USER\\Software\\Lockheed Martin\\Prepar3d v6"), QStringLiteral("AppPath") },
353  { QStringLiteral("HKEY_CURRENT_USER\\Software\\Lockheed Martin\\Prepar3d v5"), QStringLiteral("AppPath") },
354  { QStringLiteral("HKEY_CURRENT_USER\\Software\\Lockheed Martin\\Prepar3d v4"), QStringLiteral("AppPath") },
355  { QStringLiteral("HKEY_CURRENT_USER\\Software\\Lockheed Martin\\Prepar3d v3"), QStringLiteral("AppPath") },
356  { QStringLiteral("HKEY_CURRENT_USER\\Software\\Lockheed Martin\\Prepar3d v2"), QStringLiteral("AppPath") },
357  { QStringLiteral("HKEY_CURRENT_USER\\Software\\LockheedMartin\\Prepar3d"), QStringLiteral("AppPath") }
358  };
359  for (const auto &registryPair : p3dRegistryPathPairs)
360  {
361  const QSettings p3dRegistry(registryPair.first, QSettings::NativeFormat);
362  p3dPath = p3dRegistry.value(registryPair.second).toString().trimmed();
363 
364  if (p3dPath.isEmpty()) { continue; }
365  p3dPath = CFileUtils::normalizeFilePathToQtStandard(p3dPath);
366 
367  // if path does NOT exists we continue to search, maybe another one does
368  const QDir dir(p3dPath);
369  if (dir.exists()) { break; }
370  p3dPath.clear();
371  }
372  return p3dPath;
373  }
374 
375  const QString &CFsDirectories::p3dDirFromRegistry()
376  {
377  static const QString p3dPath = CFileUtils::normalizeFilePathToQtStandard(p3dDirFromRegistryImpl());
378  return p3dPath;
379  }
380 
381  QString p3dDirImpl()
382  {
383  QString dir(CFsDirectories::p3dDirFromRegistry());
384  if (!dir.isEmpty()) { return dir; }
385  const QStringList someDefaultDirs({ "C:/Program Files (x86)/Lockheed Martin/Prepar3D v4",
386  "C:/Program Files (x86)/Lockheed Martin/Prepar3D v3",
387  "C:/Program Files (x86)/Lockheed Martin/Prepar3D v2",
388  "C:/Program Files (x86)/Lockheed Martin/Prepar3D" });
389  return CFileUtils::findFirstExisting(someDefaultDirs);
390  }
391 
392  const QString &CFsDirectories::p3dDir()
393  {
394  static const QString dir(p3dDirImpl());
395  return dir;
396  }
397 
398  QString p3dSimObjectsDirFromRegistryImpl()
399  {
400  const QString p3dPath = CFsDirectories::p3dDirFromRegistry();
401  if (p3dPath.isEmpty()) { return {}; }
402  return CFsDirectories::fsxSimObjectsDirFromSimDir(p3dPath);
403  }
404 
405  const QString &CFsDirectories::p3dSimObjectsDirFromRegistry()
406  {
407  static const QString p3dPath(p3dSimObjectsDirFromRegistryImpl());
408  return p3dPath;
409  }
410 
411  QString p3dSimObjectsDirImpl()
412  {
413  QString dir(CFsDirectories::p3dDir());
414  if (dir.isEmpty()) { return {}; }
415  return CFsDirectories::p3dSimObjectsDirFromSimDir(dir);
416  }
417 
418  const QString &CFsDirectories::p3dSimObjectsDir()
419  {
420  static const QString dir(p3dSimObjectsDirImpl());
421  return dir;
422  }
423 
424  QStringList CFsDirectories::fsxSimObjectsDirPlusAddOnXmlSimObjectsPaths(const QString &simObjectsDir)
425  {
426  // finding the user settings only works on P3D machine
427  QStringList allPaths = CFsDirectories::allFsxSimObjectPaths().values();
428  const QString sod = CFileUtils::normalizeFilePathToQtStandard(
429  simObjectsDir.isEmpty() ? CFsDirectories::fsxSimObjectsDir() : simObjectsDir);
430  if (!sod.isEmpty() && !allPaths.contains(sod, Qt::CaseInsensitive))
431  {
432  // case insensitive is important here
433  allPaths.push_front(sod);
434  }
435 
436  allPaths.removeAll({}); // remove all empty
437  allPaths.removeDuplicates();
438  allPaths.sort(Qt::CaseInsensitive);
439  return allPaths;
440  }
441 
442  QStringList CFsDirectories::msfsSimObjectsDirPath(const QString &simObjectsDir)
443  {
444  Q_UNUSED(simObjectsDir);
445  static const QStringList Path { CFsDirectories::msfsSimObjectsDir() };
446  return Path;
447  }
448 
449  QStringList CFsDirectories::msfs2024SimObjectsDirPath(const QString &simObjectsDir)
450  {
451  Q_UNUSED(simObjectsDir);
452  static const QStringList Path { CFsDirectories::msfs2024SimObjectsDir() };
453  return Path;
454  }
455  QStringList CFsDirectories::p3dSimObjectsDirPlusAddOnXmlSimObjectsPaths(const QString &simObjectsDir,
456  const QString &versionHint)
457  {
458  // finding the user settings only works on P3D machine
459  QStringList allPaths = CFsDirectories::allP3dAddOnXmlSimObjectPaths(versionHint).values();
460  const QString sod = CFileUtils::normalizeFilePathToQtStandard(
461  simObjectsDir.isEmpty() ? CFsDirectories::p3dSimObjectsDir() : simObjectsDir);
462  if (!sod.isEmpty() && !allPaths.contains(sod, Qt::CaseInsensitive))
463  {
464  // case insensitive is important here
465  allPaths.push_front(sod);
466  }
467 
468  allPaths.removeAll({}); // remove all empty
469  allPaths.removeDuplicates();
470  allPaths.sort(Qt::CaseInsensitive);
471  return allPaths;
472  }
473 
474  QString CFsDirectories::guessP3DVersion(const QString &candidate)
475  {
476  if (candidate.isEmpty()) { return "v5"; }
477  if (candidate.contains("v5", Qt::CaseInsensitive)) { return QStringLiteral("v5"); }
478  if (candidate.contains("v4", Qt::CaseInsensitive)) { return QStringLiteral("v4"); }
479 
480  if (candidate.contains("5", Qt::CaseInsensitive)) { return QStringLiteral("v5"); }
481  if (candidate.contains("4", Qt::CaseInsensitive)) { return QStringLiteral("v4"); }
482 
483  return "v5"; // that is the future (in 2020)
484  }
485 
486  QString CFsDirectories::p3dSimObjectsDirFromSimDir(const QString &simDir)
487  {
488  if (simDir.isEmpty()) { return {}; }
489  return CFileUtils::normalizeFilePathToQtStandard(CFileUtils::appendFilePaths(simDir, "SimObjects"));
490  }
491 
492  const QStringList &CFsDirectories::p3dSimObjectsExcludeDirectoryPatterns()
493  {
494  static const QStringList exclude {
495  // FSX
496  "SimObjects/Animals",
497  "SimObjects/Misc",
498  "SimObjects/GroundVehicles",
499  "SimObjects/Boats",
500 
501  // P3D new
502  "SimObjects/Avatars",
503  "SimObjects/Countermeasures",
504  "SimObjects/Submersible",
505  "SimObjects/Weapons",
506  };
507  return exclude;
508  }
509 
510  QString fs9DirFromRegistryImpl()
511  {
512  QString fs9Path;
513  FsRegistryPathPair fs9RegistryPathPairs = {
514  { QStringLiteral(
515  "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\DirectPlay\\Applications\\Microsoft Flight Simulator 2004"),
516  QStringLiteral("AppPath") },
517  { QStringLiteral(
518  "HKEY_LOCAL_MACHINE\\SOFTWARE\\Wow6432Node\\Microsoft\\DirectPlay\\Applications\\Microsoft Flight "
519  "Simulator 2004"),
520  QStringLiteral("AppPath") }
521  };
522 
523  for (const auto &registryPair : fs9RegistryPathPairs)
524  {
525  QSettings fs9Registry(registryPair.first, QSettings::NativeFormat);
526  fs9Path = fs9Registry.value(registryPair.second).toString().trimmed();
527 
528  if (fs9Path.isEmpty()) { continue; }
529  fs9Path = CFileUtils::normalizeFilePathToQtStandard(fs9Path);
530 
531  // if path does NOT exists we continue to search, maybe another one does
532  const QDir dir(fs9Path);
533  if (dir.exists()) { break; }
534  fs9Path.clear();
535  }
536  return fs9Path;
537  }
538 
539  const QString &CFsDirectories::fs9DirFromRegistry()
540  {
541  static const QString fs9Path(fs9DirFromRegistryImpl());
542  return fs9Path;
543  }
544 
545  QString fs9DirImpl()
546  {
547  QString dir(CFsDirectories::fs9DirFromRegistry());
548  if (!dir.isEmpty()) { return dir; }
549  const QStringList someDefaultDirs({ "C:/Flight Simulator 9", "C:/FS9" });
550  return CFileUtils::findFirstExisting(someDefaultDirs);
551  }
552 
553  const QString &CFsDirectories::fs9Dir()
554  {
555  static const QString v(fs9DirImpl());
556  return v;
557  }
558 
559  QString fs9AircraftDirFromRegistryImpl()
560  {
561  QString fs9Path = CFsDirectories::fs9DirFromRegistry();
562  if (fs9Path.isEmpty()) { return {}; }
563  return CFsDirectories::fs9AircraftDirFromSimDir(fs9Path);
564  }
565 
566  const QString &CFsDirectories::fs9AircraftDirFromRegistry()
567  {
568  static const QString dir(fs9AircraftDirFromRegistryImpl());
569  return dir;
570  }
571 
572  QString fs9AircraftDirImpl()
573  {
574  const QString dir(CFsDirectories::fs9Dir());
575  if (dir.isEmpty()) { return {}; }
576  return CFsDirectories::fs9AircraftDirFromSimDir(dir);
577  }
578 
579  const QString &CFsDirectories::fs9AircraftDir()
580  {
581  static const QString dir(fs9AircraftDirImpl());
582  return dir;
583  }
584 
585  QString CFsDirectories::fs9AircraftDirFromSimDir(const QString &simDir)
586  {
587  if (simDir.isEmpty()) { return {}; }
588  return CFileUtils::appendFilePaths(simDir, "Aircraft");
589  }
590 
591  const QStringList &CFsDirectories::fs9AircraftObjectsExcludeDirectoryPatterns()
592  {
593  static const QStringList exclude;
594  return exclude;
595  }
596 
597  QSet<QString> CFsDirectories::findP3dAddOnConfigFiles(const QString &versionHint)
598  {
599  static const QString cfgFile("add-ons.cfg");
600  return CFsDirectories::findP3dConfigFiles(cfgFile, versionHint);
601  }
602 
603  QSet<QString> CFsDirectories::findP3dSimObjectsConfigFiles(const QString &versionHint)
604  {
605  static const QString cfgFile("simobjects.cfg");
606  return CFsDirectories::findP3dConfigFiles(cfgFile, versionHint);
607  }
608 
609  QSet<QString> CFsDirectories::findP3dConfigFiles(const QString &configFile, const QString &versionHint)
610  {
611  // locations will be swift paths, I will go one level up and then search for Lockheed Martin
613  QSet<QString> files;
614  for (const QString &path : locations)
615  {
616  const QString pathUp = CFileUtils::appendFilePaths(CFileUtils::pathUp(path), "Lockheed Martin");
617  const QDir d(pathUp);
618  if (!d.exists()) { continue; }
619  CLogMessage(static_cast<CFsDirectories *>(nullptr)).info(u"P3D config dir: '%1'") << d.absolutePath();
620 
621  // all versions sub directories
622  // looking for "add-ons.cfg" or simobjects.cfg
623  const QStringList entries = d.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
624  for (const QString &entry : entries)
625  {
626  // right version or just one file
627  if (entry.contains(versionHint, Qt::CaseInsensitive))
628  {
629  const QString f = CFileUtils::appendFilePaths(d.absolutePath(), entry, configFile);
630  const QFileInfo fi(f);
631  if (fi.exists())
632  {
633  files.insert(f);
634  CLogMessage(static_cast<CFsDirectories *>(nullptr)).info(u"P3D config file: '%1'") << f;
635  }
636  } // contains
637  } // entries
638  }
639  return files;
640  }
641 
642  QSet<QString> CFsDirectories::allConfigFilesPathValues(const QStringList &configFiles, bool checked,
643  const QString &pathPrefix)
644  {
645  if (configFiles.isEmpty()) { return {}; }
646  QSet<QString> paths;
647  for (const QString &configFile : configFiles)
648  {
649  // manually parsing because QSettings did not work properly
650  const QString fileContent = CFileUtils::readFileToString(configFile);
651  if (fileContent.isEmpty()) { continue; }
652  const QList<QStringView> lines = splitLinesRefs(fileContent);
653  static const QString p("Path=");
654  for (const QStringView line : lines)
655  {
656  const qsizetype i = line.lastIndexOf(p, -1, Qt::CaseInsensitive);
657  if (i < 0 || line.endsWith('=')) { continue; }
658  const QStringView path = line.mid(i + p.length());
660  pathPrefix.isEmpty() ? path.toString() :
661  CFileUtils::appendFilePathsAndFixUnc(pathPrefix, path.toString())));
662  if (!checked || dir.exists()) { paths.insert(dir.absolutePath()); }
663  }
664  }
665  return paths;
666  }
667 
668  QSet<QString> CFsDirectories::allP3dAddOnXmlSimObjectPaths(const QStringList &addOnPaths, bool checked)
669  {
670  if (addOnPaths.isEmpty()) { return {}; }
671  QSet<QString> simObjectPaths;
672  for (const QString &addOnPath : addOnPaths)
673  {
674  const QString filename = CFileUtils::appendFilePaths(addOnPath, "add-on.xml");
675  QDomDocument doc;
676  QFile file(filename);
677  if (!file.open(QIODevice::ReadOnly) || !doc.setContent(&file)) { continue; }
678 
679  CLogMessage(static_cast<CFsDirectories *>(nullptr)).info(u"Reading '%1' from addon path: '%2'")
680  << file.fileName() << addOnPath;
681 
682  const QDomNodeList components = doc.elementsByTagName("AddOn.Component");
683  for (int i = 0; i < components.size(); i++)
684  {
685  const QDomNode component = components.item(i);
686  const QDomElement category = component.firstChildElement("Category");
687  const QString categoryValue = category.text();
688  if (!caseInsensitiveStringCompare(categoryValue, QStringLiteral("SimObjects"))) { continue; }
689  const QDomElement path = component.firstChildElement("Path");
690  const QString pathValue = CFileUtils::normalizeFilePathToQtStandard(path.text());
691  const bool correctPath = pathValue.contains("Airplanes", Qt::CaseInsensitive) ||
692  pathValue.contains("Rotorcraft", Qt::CaseInsensitive);
693  if (!correctPath) { continue; }
694 
695  // absolute or relative path
696  const QString fp = QStringView { pathValue }.left(3).contains(':') ?
697  pathValue :
698  CFileUtils::appendFilePaths(addOnPath, pathValue);
699 
700  CLogMessage(static_cast<CFsDirectories *>(nullptr)).info(u"Testing '%1' as addon path: '%2'")
701  << fp << addOnPath;
702 
703  if (!checked || QDir(fp).exists())
704  {
705  simObjectPaths.insert(CFileUtils::normalizeFilePathToQtStandard(fp));
706 
707  CLogMessage(static_cast<CFsDirectories *>(nullptr)).info(u"P3D SimObjects path: '%1'") << fp;
708  }
709  } // components
710  } // paths
711 
712  return simObjectPaths;
713  }
714 
715  QSet<QString> CFsDirectories::allP3dAddOnXmlSimObjectPaths(const QString &versionHint)
716  {
717  // all add-ons.cfg files
718  const QStringList addOnConfigFiles = CFsDirectories::findP3dAddOnConfigFiles(versionHint).values();
719 
720  // all PATH values in those files
721  const QStringList addOnPaths = CFsDirectories::allConfigFilesPathValues(addOnConfigFiles, true, {}).values();
722 
723  // based on all paths of all config files search the XML files
724  const QSet<QString> all = CFsDirectories::allP3dAddOnXmlSimObjectPaths(addOnPaths, true);
725  return all;
726  }
727 
728  QSet<QString> CFsDirectories::allFsxSimObjectPaths()
729  {
730  return CFsDirectories::fsxSimObjectsPaths(CFsDirectories::findFsxConfigFiles(), true);
731  }
732 
733  QStringList CFsDirectories::findFsxConfigFiles()
734  {
736  QStringList files;
737  for (const QString &path : locations)
738  {
739  const QString file = CFileUtils::appendFilePaths(CFileUtils::pathUp(path), "Microsoft/FSX/fsx.cfg");
740  const QFileInfo fi(file);
741  if (fi.exists())
742  {
743  files.push_back(fi.absoluteFilePath());
744 
745  CLogMessage(static_cast<CFsDirectories *>(nullptr)).info(u"FSX config file: '%1'")
746  << fi.absoluteFilePath();
747  }
748  }
749  return files;
750  }
751 
752  QSet<QString> CFsDirectories::fsxSimObjectsPaths(const QStringList &fsxFiles, bool checked)
753  {
754  QSet<QString> paths;
755  for (const QString &fsxFile : fsxFiles) { paths.unite(CFsDirectories::fsxSimObjectsPaths(fsxFile, checked)); }
756  return paths;
757  }
758 
759  QSet<QString> CFsDirectories::msfsSimObjectsPaths(const QStringList &msfsFiles, bool checked)
760  {
761  QSet<QString> paths;
762  for (const QString &msfsFile : msfsFiles)
763  {
764  paths.unite(CFsDirectories::msfsSimObjectsPaths(msfsFile, checked));
765  }
766  return paths;
767  }
768 
769  QSet<QString> CFsDirectories::fsxSimObjectsPaths(const QString &fsxFile, bool checked)
770  {
771  const QString fileContent = CFileUtils::readFileToString(fsxFile);
772  if (fileContent.isEmpty()) { return {}; }
773  const QList<QStringView> lines = splitLinesRefs(fileContent);
774  static const QString p("SimObjectPaths.");
775 
776  const QFileInfo fsxFileInfo(fsxFile);
777  const QString relPath = fsxFileInfo.absolutePath();
778 
779  QSet<QString> paths;
780  for (const QStringView line : lines)
781  {
782  const qsizetype i1 = line.lastIndexOf(p, -1, Qt::CaseInsensitive);
783  if (i1 < 0) { continue; }
784  const qsizetype i2 = line.lastIndexOf('=');
785  if (i2 < 0 || i1 >= i2 || line.endsWith('=')) { continue; }
786  const QStringView path = line.mid(i2 + 1);
788 
789  CLogMessage(static_cast<CFsDirectories *>(nullptr)).info(u"FSX SimObjects path checked: '%1' in '%2'")
790  << line << fsxFile;
791 
792  // ignore exclude patterns
793  if (containsAny(soPath, CFsDirectories::fsxSimObjectsExcludeDirectoryPatterns(), Qt::CaseInsensitive))
794  {
795  continue;
796  }
797 
798  // make absolute
799  if (!QStringView { soPath }.left(3).contains(':'))
800  {
801  soPath = CFileUtils::appendFilePaths(relPath, soPath);
802  }
803 
804  const QDir dir(soPath); // always absolute path now
805  if (checked && !dir.exists())
806  {
807  // skip, not existing
808 
809  CLogMessage(static_cast<CFsDirectories *>(nullptr))
810  .info(u"FSX SimObjects path skipped, not existing: '%1' in '%2'")
811  << dir.absolutePath() << fsxFile;
812 
813  continue;
814  }
815 
816  const QString afp = dir.absolutePath().toLower();
817  if (!CDirectoryUtils::containsFileInDir(afp, airFileFilter(), true))
818  {
819 
820  CLogMessage(static_cast<CFsDirectories *>(nullptr))
821  .info(u"FSX SimObjects path: Skipping '%1' from '%2', no '%3' file")
822  << afp << fsxFile << airFileFilter();
823 
824  continue;
825  }
826 
827  paths.insert(afp);
828 
829  CLogMessage(static_cast<CFsDirectories *>(nullptr)).info(u"FSX SimObjects path: '%1' from '%2'")
830  << afp << fsxFile;
831  }
832  return paths;
833  }
834 
835  QSet<QString> CFsDirectories::msfsSimObjectsPaths(const QString &msfsFile, bool checked)
836  {
837  const QString fileContent = CFileUtils::readFileToString(msfsFile);
838  if (fileContent.isEmpty()) { return {}; }
839  const QList<QStringView> lines = splitLinesRefs(fileContent);
840  static const QString p("SimObjectPaths.");
841 
842  const QFileInfo fsxFileInfo(msfsFile);
843  const QString relPath = fsxFileInfo.absolutePath();
844 
845  QSet<QString> paths;
846  for (const QStringView line : lines)
847  {
848  const qsizetype i1 = line.lastIndexOf(p, -1, Qt::CaseInsensitive);
849  if (i1 < 0) { continue; }
850  const qsizetype i2 = line.lastIndexOf('=');
851  if (i2 < 0 || i1 >= i2 || line.endsWith('=')) { continue; }
852  const QStringView path = line.mid(i2 + 1);
854 
855  CLogMessage(static_cast<CFsDirectories *>(nullptr)).info(u"MSFS SimObjects path checked: '%1' in '%2'")
856  << line << msfsFile;
857 
858  // ignore exclude patterns
859  if (containsAny(soPath, CFsDirectories::fsxSimObjectsExcludeDirectoryPatterns(), Qt::CaseInsensitive))
860  {
861  continue;
862  }
863 
864  // make absolute
865  if (!QStringView(soPath).left(3).contains(':')) { soPath = CFileUtils::appendFilePaths(relPath, soPath); }
866 
867  const QDir dir(soPath); // always absolute path now
868  if (checked && !dir.exists())
869  {
870  // skip, not existing
871  CLogMessage(static_cast<CFsDirectories *>(nullptr))
872  .info(u"FSX SimObjects path skipped, not existing: '%1' in '%2'")
873  << dir.absolutePath() << msfsFile;
874 
875  continue;
876  }
877 
878  const QString afp = dir.absolutePath().toLower();
879  if (!CDirectoryUtils::containsFileInDir(afp, airFileFilter(), true))
880  {
881 
882  CLogMessage(static_cast<CFsDirectories *>(nullptr))
883  .info(u"FSX SimObjects path: Skipping '%1' from '%2', no '%3' file")
884  << afp << msfsFile << airFileFilter();
885 
886  continue;
887  }
888 
889  paths.insert(afp);
890 
891  CLogMessage(static_cast<CFsDirectories *>(nullptr)).info(u"FSX SimObjects path: '%1' from '%2'")
892  << afp << msfsFile;
893  }
894  return paths;
895  }
896 
897  const QString &CFsDirectories::airFileFilter()
898  {
899  static const QString a("*.air");
900  return a;
901  }
902 
903 } // 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 QList< QStringView > splitLinesRefs(const QString &s)
Split a string into multiple lines. Blank lines are skipped.
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?
QString absolutePath() const const
bool exists() const const
QString fromNativeSeparators(const QString &pathName)
QDomNodeList elementsByTagName(const QString &tagname) const const
QDomDocument::ParseResult setContent(QAnyStringView text, QDomDocument::ParseOptions options)
QString text() const const
QDomElement firstChildElement(const QString &tagName, const QString &namespaceURI) const const
QDomNode item(int index) const const
int size() const const
virtual QString fileName() const const override
bool open(FILE *fh, QIODeviceBase::OpenMode mode, QFileDevice::FileHandleFlags handleFlags)
QString absoluteFilePath() const const
QString absolutePath() const const
bool exists(const QString &path)
bool isEmpty() const const
void push_back(QList< T >::parameter_type value)
void push_front(QList< T >::parameter_type value)
qsizetype removeAll(const AT &t)
QSet< T >::iterator insert(QSet< T >::const_iterator it, const T &value)
QSet< T > & unite(QSet< T > &&other)
QStringList standardLocations(QStandardPaths::StandardLocation type)
void clear()
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
bool endsWith(QChar c, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
qsizetype lastIndexOf(QChar ch, Qt::CaseSensitivity cs) const const
QString left(qsizetype n) &&
qsizetype length() const const
QString mid(qsizetype position, qsizetype n) &&
QString & replace(QChar before, QChar after, Qt::CaseSensitivity cs)
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
QString toLower() const const
QString trimmed() const const
bool contains(QLatin1StringView str, Qt::CaseSensitivity cs) const const
qsizetype removeDuplicates()
void sort(Qt::CaseSensitivity cs)
QString toString() const const
CaseInsensitive
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