swift
fscommonutil.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 <QVariant>
17 
18 #include "config/buildconfig.h"
19 #include "misc/directoryutils.h"
20 #include "misc/fileutils.h"
21 #include "misc/logmessage.h"
24 #include "misc/stringutils.h"
25 #include "misc/swiftdirectories.h"
26 
27 using namespace swift::config;
28 
29 namespace swift::misc::simulation::fscommon
30 {
31  const QStringList &CFsCommonUtil::getLogCategories()
32  {
33  static const QStringList cats({ CLogCategories::validation(), CLogCategories::driver() });
34  return cats;
35  }
36 
37  bool CFsCommonUtil::adjustFileDirectory(CAircraftModel &model, const QString &simObjectsDirectory)
38  {
39  if (model.hasExistingCorrespondingFile()) { return true; }
40  if (simObjectsDirectory.isEmpty()) { return false; }
41  if (!model.hasFileName()) { return false; } // we can do nothing here
42 
43  const QString simObjectsDirectoryFix = CFileUtils::fixWindowsUncPath(simObjectsDirectory);
44  const QDir dir(simObjectsDirectoryFix);
45  if (!dir.exists()) { return false; }
46 
47  const QString lastSegment = u'/' % CFileUtils::lastPathSegment(simObjectsDirectoryFix) % u'/';
48  const int index = model.getFileName().lastIndexOf(lastSegment);
49  if (index < 0) { return false; }
50  const QString relPart = model.getFileName().mid(index + lastSegment.length());
51  if (relPart.isEmpty()) { return false; }
52  const QString newFile = CFileUtils::appendFilePathsAndFixUnc(simObjectsDirectory, relPart);
53  const QFileInfo nf(newFile);
54  if (!nf.exists()) { return false; }
55 
56  model.setFileName(newFile);
57  return true;
58  }
59 
60  bool CFsCommonUtil::adjustFileDirectory(CAircraftModel &model, const QStringList &simObjectsDirectories)
61  {
62  for (const QString &simObjectDir : simObjectsDirectories)
63  {
64  if (CFsCommonUtil::adjustFileDirectory(model, simObjectDir)) { return true; }
65  }
66  return false;
67  }
68 
69  int CFsCommonUtil::copyFsxTerrainProbeFiles(const QString &simObjectDir, CStatusMessageList &messages)
70  {
71  messages.clear();
72  if (!CDirectoryUtils::existsUnemptyDirectory(CSwiftDirectories::shareTerrainProbeDirectory()))
73  {
74  messages.push_back(CStatusMessage(static_cast<CFsCommonUtil *>(nullptr), CStatusMessage::SeverityError,
75  u"No terrain probe source files in '%1'")
76  << CSwiftDirectories::shareTerrainProbeDirectory());
77  return -1;
78  }
79 
80  if (simObjectDir.isEmpty())
81  {
82  messages.push_back(CStatusMessage(static_cast<CFsCommonUtil *>(nullptr), CStatusMessage::SeverityError,
83  u"No simObject directory"));
84  return -1;
85  }
86 
87  QString targetDir = CFileUtils::appendFilePathsAndFixUnc(simObjectDir, "Misc");
88  const QDir td(targetDir);
89  if (!td.exists())
90  {
91  messages.push_back(CStatusMessage(static_cast<CFsCommonUtil *>(nullptr), CStatusMessage::SeverityError,
92  u"Cannot access target directory '%1'")
93  << targetDir);
94  return -1;
95  }
96 
97  const QString lastSegment = CFileUtils::lastPathSegment(CSwiftDirectories::shareTerrainProbeDirectory());
98  targetDir = CFileUtils::appendFilePathsAndFixUnc(targetDir, lastSegment);
99  const bool hasDir = td.mkpath(targetDir);
100  if (!hasDir)
101  {
102  messages.push_back(CStatusMessage(static_cast<CFsCommonUtil *>(nullptr), CStatusMessage::SeverityError,
103  u"Cannot create target directory '%1'")
104  << targetDir);
105  return -1;
106  }
107 
108  const int copied =
109  CDirectoryUtils::copyDirectoryRecursively(CSwiftDirectories::shareTerrainProbeDirectory(), targetDir, true);
110  messages.push_back(CStatusMessage(static_cast<CFsCommonUtil *>(nullptr), CStatusMessage::SeverityInfo,
111  u"Copied %1 files from '%2' to '%3'")
112  << copied << CSwiftDirectories::shareTerrainProbeDirectory() << targetDir);
113  return copied;
114  }
115 
116  CStatusMessageList CFsCommonUtil::validateAircraftConfigFiles(const CAircraftModelList &models,
117  CAircraftModelList &validModels,
118  CAircraftModelList &invalidModels,
119  bool ignoreEmptyFileNames, int stopAtFailedFiles,
120  std::atomic_bool &wasStopped)
121  {
122  CStatusMessage m;
123  CAircraftModelList sorted(models);
124  sorted.sortByFileName();
125  wasStopped = false;
126  CStatusMessageList msgs = sorted.validateFiles(validModels, invalidModels, ignoreEmptyFileNames,
127  stopAtFailedFiles, wasStopped, QString(), true);
128  if (wasStopped || validModels.isEmpty()) { return msgs; }
129 
130  const CAircraftModelList nonFsModels = validModels.findNonFsFamilyModels();
131  if (!nonFsModels.isEmpty())
132  {
133  for (const CAircraftModel &model : nonFsModels)
134  {
135  m = CStatusMessage(
136  static_cast<CFsCommonUtil *>(nullptr), CStatusMessage::SeverityError,
137  QStringLiteral("Removed '%1' non FS family model").arg(model.getModelStringAndDbKey()), true);
138  msgs.push_back(m);
139  if (wasStopped) { break; } // allow to break from "outside"
140  }
141 
142  const int d = validModels.removeIfNotFsFamily();
143  m = CStatusMessage(static_cast<CFsCommonUtil *>(nullptr), CStatusMessage::SeverityError,
144  QStringLiteral("Removed %1 non FS family models").arg(d), true);
145  msgs.push_back(m);
146  }
147 
148  // all those files should work
149  int removedCfgEntries = 0;
150  const QSet<QString> fileNames = validModels.getAllFileNames();
151  for (const QString &fileName : fileNames)
152  {
153  bool ok = false;
154  if (wasStopped) { break; } // allow to break from "outside"
155  const CAircraftCfgEntriesList entries = CAircraftCfgParser::performParsingOfSingleFile(fileName, ok, msgs);
156  const QSet<QString> removeModelStrings = entries.getTitleSetUpperCase();
157  const CAircraftModelList removedModels = validModels.removeIfFileButNotInSet(fileName, removeModelStrings);
158  for (const CAircraftModel &removedModel : removedModels)
159  {
160  removedCfgEntries++;
161  m = CStatusMessage(static_cast<CFsCommonUtil *>(nullptr), CStatusMessage::SeverityError,
162  QStringLiteral("'%1' removed because no longer in '%2'")
163  .arg(removedModel.getModelStringAndDbKey(), removedModel.getFileName()),
164  true);
165  msgs.push_back(m);
166  CAircraftModelList::addAsValidOrInvalidModel(removedModel, false, validModels, invalidModels);
167  }
168  }
169 
170  if (removedCfgEntries < 1)
171  {
172  m = CStatusMessage(static_cast<CFsCommonUtil *>(nullptr), CStatusMessage::SeverityInfo,
173  QStringLiteral("Not removed any models, all OK!"), true);
174  msgs.push_back(m);
175  }
176 
177  if (!validModels.isEmpty())
178  {
179  m = CStatusMessage(static_cast<CFsCommonUtil *>(nullptr), CStatusMessage::SeverityInfo,
180  QStringLiteral("cfg validation, valid models: %1").arg(validModels.size()), true);
181  msgs.push_back(m);
182  }
183  if (!invalidModels.isEmpty())
184  {
185  m = CStatusMessage(static_cast<CFsCommonUtil *>(nullptr), CStatusMessage::SeverityWarning,
186  QStringLiteral("cfg validation, invalid models: %1").arg(invalidModels.size()), true);
187  msgs.push_back(m);
188  }
189 
190  // finished
191  return msgs;
192  }
193 
194  CStatusMessageList CFsCommonUtil::validateP3DSimObjectsPath(
195  const CAircraftModelList &models, CAircraftModelList &validModels, CAircraftModelList &invalidModels,
196  bool ignoreEmptyFileNames, int stopAtFailedFiles, std::atomic_bool &wasStopped, const QString &simulatorDir)
197  {
198  const QString simObjectsDir = simulatorDir.isEmpty() ? CFsDirectories::p3dSimObjectsDir() :
199  CFsDirectories::p3dSimObjectsDirFromSimDir(simulatorDir);
200  const QStringList simObjectPaths = CFsDirectories::p3dSimObjectsDirPlusAddOnXmlSimObjectsPaths(
201  simObjectsDir, CFsDirectories::guessP3DVersion(simObjectsDir));
202  return CFsCommonUtil::validateSimObjectsPath(QSet<QString>(simObjectPaths.begin(), simObjectPaths.end()),
203  models, validModels, invalidModels, ignoreEmptyFileNames,
204  stopAtFailedFiles, wasStopped);
205  }
206 
207  CStatusMessageList CFsCommonUtil::validateFSXSimObjectsPath(const CAircraftModelList &models,
208  CAircraftModelList &validModels,
209  CAircraftModelList &invalidModels,
210  bool ignoreEmptyFileNames, int stopAtFailedFiles,
211  std::atomic_bool &stopped, const QString &simulatorDir)
212  {
213  Q_UNUSED(simulatorDir)
214  const QStringList simObjectPaths = CFsDirectories::fsxSimObjectsDirPlusAddOnXmlSimObjectsPaths();
215  return CFsCommonUtil::validateSimObjectsPath(QSet<QString>(simObjectPaths.begin(), simObjectPaths.end()),
216  models, validModels, invalidModels, ignoreEmptyFileNames,
217  stopAtFailedFiles, stopped);
218  }
219 
220  CStatusMessageList CFsCommonUtil::validateMSFSSimObjectsPath(const CAircraftModelList &models,
221  CAircraftModelList &validModels,
222  CAircraftModelList &invalidModels,
223  bool ignoreEmptyFileNames, int stopAtFailedFiles,
224  std::atomic_bool &stopped, const QString &simulatorDir)
225  {
226  Q_UNUSED(simulatorDir)
227  const QStringList simObjectPaths = CFsDirectories::msfsSimObjectsDirPath();
228  return CFsCommonUtil::validateSimObjectsPath(QSet<QString>(simObjectPaths.begin(), simObjectPaths.end()),
229  models, validModels, invalidModels, ignoreEmptyFileNames,
230  stopAtFailedFiles, stopped);
231  }
232 
233  CStatusMessageList CFsCommonUtil::validateMSFS2024SimObjectsPath(
234  const CAircraftModelList &models, CAircraftModelList &validModels, CAircraftModelList &invalidModels,
235  bool ignoreEmptyFileNames, int stopAtFailedFiles, std::atomic_bool &stopped, const QString &simulatorDir)
236  {
237  Q_UNUSED(simulatorDir)
238  const QStringList simObjectPaths = CFsDirectories::msfs2024SimObjectsDirPath();
239  return CFsCommonUtil::validateSimObjectsPath(QSet<QString>(simObjectPaths.begin(), simObjectPaths.end()),
240  models, validModels, invalidModels, ignoreEmptyFileNames,
241  stopAtFailedFiles, stopped);
242  }
243 
245  CFsCommonUtil::validateSimObjectsPath(const QSet<QString> &simObjectDirs, const CAircraftModelList &models,
246  CAircraftModelList &validModels, CAircraftModelList &invalidModels,
247  bool ignoreEmptyFileNames, int stopAtFailedFiles, std::atomic_bool &stopped)
248  {
249  CStatusMessageList msgs;
250  if (simObjectDirs.isEmpty())
251  {
252  msgs.push_back(CStatusMessage(static_cast<CFsCommonUtil *>(nullptr))
253  .validationInfo(u"No SimObject directories from cfg files, skipping validation"));
254  return msgs;
255  }
256 
257  CAircraftModelList sortedModels(models);
258  sortedModels.sortByFileName();
259  if (sortedModels.isEmpty())
260  {
261  msgs.push_back(
262  CStatusMessage(static_cast<CFsCommonUtil *>(nullptr)).validationInfo(u"No models to validate"));
263  return msgs;
264  }
265 
266  // info
267  const QString simObjDirs = joinStringSet(simObjectDirs, ", ");
268  msgs.push_back(CStatusMessage(static_cast<CFsCommonUtil *>(nullptr))
269  .validationInfo(u"Validating %1 models against %2 SimObjects path(s): '%3'")
270  << models.size() << simObjectDirs.size() << simObjDirs);
271 
272  // validate
273  int failed = 0;
274  for (const CAircraftModel &model : models)
275  {
276  if (!model.hasFileName())
277  {
278  if (ignoreEmptyFileNames) { continue; }
279  msgs.push_back(CStatusMessage(static_cast<CFsCommonUtil *>(nullptr))
280  .validationWarning(u"No file name for model '%1'")
281  << model.getModelString());
282  CAircraftModelList::addAsValidOrInvalidModel(model, false, validModels, invalidModels);
283  continue;
284  }
285 
286  bool ok = false;
287  for (const QString &path : simObjectDirs)
288  {
289  if (!model.isInPath(path, CFileUtils::osFileNameCaseSensitivity())) { continue; }
290  ok = true;
291  break;
292  }
293  CAircraftModelList::addAsValidOrInvalidModel(model, ok, validModels, invalidModels);
294  if (!ok)
295  {
296  msgs.push_back(CStatusMessage(static_cast<CFsCommonUtil *>(nullptr))
297  .validationWarning(u"Model '%1' '%2' in none of the %3 SimObjects path(s)")
298  << model.getModelString() << model.getFileName() << simObjectDirs.size());
299  failed++;
300  }
301 
302  if (stopAtFailedFiles > 0 && failed >= stopAtFailedFiles)
303  {
304  stopped = true;
305  msgs.push_back(CStatusMessage(static_cast<CFsCommonUtil *>(nullptr))
306  .validationWarning(u"Stopping after %1 failed models")
307  << failed);
308  break;
309  }
310  } // models
311 
312  return msgs;
313  }
314 } // namespace swift::misc::simulation::fscommon
size_type size() const
Returns number of elements in the sequence.
Definition: sequence.h:273
void push_back(const T &value)
Appends an element at the end of the sequence.
Definition: sequence.h:305
void clear()
Removes all elements in the sequence.
Definition: sequence.h:288
bool isEmpty() const
Synonym for empty.
Definition: sequence.h:285
Streamable status message, e.g.
Status messages, e.g. from Core -> GUI.
Aircraft model (used by another pilot, my models on disk)
Definition: aircraftmodel.h:71
bool hasExistingCorrespondingFile() const
Does the corresponding file exist?
void setFileName(const QString &fileName)
File name.
const QString & getFileName() const
File name (corresponding data for simulator, only available if representing simulator model.
Value object encapsulating a list of aircraft models.
CAircraftModelList removeIfFileButNotInSet(const QString &fileName, const QSet< QString > &modelStrings)
Remove those models of a particular file, but not in the given set.
QSet< QString > getAllFileNames() const
All file names.
CStatusMessageList validateFiles(CAircraftModelList &validModels, CAircraftModelList &invalidModels, bool ignoreEmptyFileNames, int stopAtFailedFiles, std::atomic_bool &wasStopped, const QString &simRootDirectory, bool alreadySortedByFn=false) const
Validate files (file exists etc.)
int removeIfNotFsFamily()
Remove if NOT FS family model, ie. FSX/P3D/FS9/MSFS.
CAircraftModelList findNonFsFamilyModels() const
All models NOT of the FS (FSX, P3D, FS9, MSFS) family.
QSet< QString > getTitleSetUpperCase() const
Titles as set in upper case.
SWIFT_MISC_EXPORT QString joinStringSet(const QSet< QString > &set, const QString &separator)
Convert string to bool.