swift
fsdclient.cpp
1 // SPDX-FileCopyrightText: Copyright (C) 2019 swift Project Community / Contributors
2 // SPDX-License-Identifier: GPL-3.0-or-later OR LicenseRef-swift-pilot-client-1
3 
4 #include "core/fsd/fsdclient.h"
5 
6 #include <QHostAddress>
7 #include <QMetaEnum>
8 #include <QNetworkReply>
9 #include <QStringView>
10 
11 #include "config/buildconfig.h"
12 #include "core/application.h"
13 #include "core/fsd/addatc.h"
14 #include "core/fsd/addpilot.h"
15 #include "core/fsd/atcdataupdate.h"
16 #include "core/fsd/authchallenge.h"
17 #include "core/fsd/authresponse.h"
19 #include "core/fsd/clientquery.h"
21 #include "core/fsd/deleteatc.h"
22 #include "core/fsd/deletepilot.h"
24 #include "core/fsd/flightplan.h"
27 #include "core/fsd/killrequest.h"
28 #include "core/fsd/mute.h"
30 #include "core/fsd/ping.h"
35 #include "core/fsd/pong.h"
36 #include "core/fsd/rehost.h"
37 #include "core/fsd/serializer.h"
38 #include "core/fsd/servererror.h"
39 #include "core/fsd/textmessage.h"
45 #include "misc/logmessage.h"
47 #include "misc/range.h"
48 #include "misc/swiftdirectories.h"
49 #include "misc/threadutils.h"
50 #include "misc/verify.h"
51 
52 using namespace swift::config;
53 using namespace swift::core::vatsim;
54 using namespace swift::misc;
55 using namespace swift::misc::aviation;
56 using namespace swift::misc::geo;
57 using namespace swift::misc::json;
58 using namespace swift::misc::network;
59 using namespace swift::misc::physical_quantities;
60 using namespace swift::misc::simulation;
61 
62 namespace swift::core::fsd
63 {
64  QString convertToUnicodeEscaped(const QString &str)
65  {
66  QString escaped;
67  for (const auto &ch : str)
68  {
69  const ushort code = ch.unicode();
70  if (code < 0x80) { escaped += ch; }
71  else { escaped += "\\u" % QString::number(code, 16).rightJustified(4, '0'); }
72  }
73  return escaped;
74  }
75 
76  const QStringList &CFSDClient::getLogCategories()
77  {
78  static const QStringList cats = [] {
79  QStringList cl = CContinuousWorker::getLogCategories();
80  cl.push_back(CLogCategories::network());
81  cl.push_back(CLogCategories::fsd());
82  return cl;
83  }();
84  return cats;
85  }
86 
87  // NOLINTBEGIN(cppcoreguidelines-pro-type-member-init)
88  CFSDClient::CFSDClient(IClientProvider *clientProvider, IOwnAircraftProvider *ownAircraftProvider,
89  IRemoteAircraftProvider *remoteAircraftProvider, QObject *owner)
90  : CContinuousWorker(owner, "FSDClient"), CClientAware(clientProvider), COwnAircraftAware(ownAircraftProvider),
91  CRemoteAircraftAware(remoteAircraftProvider), m_tokenBucket(10, 5000, 1)
92  {
93  // NOLINTEND(cppcoreguidelines-pro-type-member-init)
94 
95  initializeMessageTypes();
96  connectSocketSignals();
97 
98  m_positionUpdateTimer.setObjectName(this->objectName().append(":m_positionUpdateTimer"));
99  connect(&m_positionUpdateTimer, &QTimer::timeout, this, &CFSDClient::sendPilotDataUpdate);
100 
101  m_interimPositionUpdateTimer.setObjectName(this->objectName().append(":m_interimPositionUpdateTimer"));
102  connect(&m_interimPositionUpdateTimer, &QTimer::timeout, this, &CFSDClient::sendInterimPilotDataUpdate);
103 
104  m_visualPositionUpdateTimer.setObjectName(this->objectName().append(":m_visualPositionUpdateTimer"));
105  connect(&m_visualPositionUpdateTimer, &QTimer::timeout, this, [this] { sendVisualPilotDataUpdate(); });
106 
107  m_scheduledConfigUpdate.setObjectName(this->objectName().append(":m_scheduledConfigUpdate"));
108  connect(&m_scheduledConfigUpdate, &QTimer::timeout, this, &CFSDClient::sendIncrementalAircraftConfig);
109 
110  m_fsdSendMessageTimer.setObjectName(this->objectName().append(":m_fsdSendMessageTimer"));
111  connect(&m_fsdSendMessageTimer, &QTimer::timeout, this, [this]() { this->sendQueuedMessage(); });
112 
113  fsdMessageSettingsChanged();
114 
115  if (!m_statistics &&
116  (CBuildConfig::isLocalDeveloperDebugBuild() || (sApp && sApp->getOwnDistribution().isRestricted())))
117  {
118  CLogMessage(this).info(u"Enabled network statistics");
119  m_statistics = true;
120  }
121  }
122 
123  void CFSDClient::connectSocketSignals()
124  {
125  connect(m_socket.get(), &QTcpSocket::readyRead, this, &CFSDClient::readDataFromSocket, Qt::QueuedConnection);
126  connect(m_socket.get(), &QTcpSocket::connected, this, &CFSDClient::handleSocketConnected);
127  connect(m_socket.get(), &QTcpSocket::errorOccurred, this, &CFSDClient::printSocketError, Qt::QueuedConnection);
128  connect(m_socket.get(), &QTcpSocket::errorOccurred, this, &CFSDClient::handleSocketError, Qt::QueuedConnection);
129  }
130 
131 #ifdef SWIFT_VATSIM_SUPPORT
132  void CFSDClient::setClientIdAndKey(quint16 id, const QByteArray &key)
133  {
134  QWriteLocker l(&m_lockUserClientBuffered);
135  m_clientAuth = vatsim_auth_create(id, qPrintable(key));
136  m_serverAuth = vatsim_auth_create(id, qPrintable(key));
137  }
138 #endif
139 
140  void CFSDClient::setServer(const CServer &server)
141  {
142  Q_ASSERT_X(this->getConnectionStatus().isDisconnected(), Q_FUNC_INFO,
143  "Can't change server details while still connected");
144 
145  const QString codecName(server.getFsdSetup().getTextCodec());
146  auto codec = QStringDecoder::encodingForName(codecName);
147  if (!codec.has_value()) { codec = QStringConverter::Utf8; }
148  const int protocolRev = (server.getServerType() == CServer::FSDServerVatsim) ?
151 
152  QWriteLocker l(&m_lockUserClientBuffered);
153  m_server = server;
154  m_protocolRevision = protocolRev;
155  m_encoder = QStringEncoder(codec.value_or(QStringConverter::Utf8));
156  m_decoder = QStringDecoder(codec.value_or(QStringConverter::Utf8));
157  }
158 
159  void CFSDClient::setCallsign(const CCallsign &callsign)
160  {
161  Q_ASSERT_X(this->getConnectionStatus().isDisconnected(), Q_FUNC_INFO,
162  "Can't change callsign while still connected");
163  updateOwnCallsign(callsign);
164 
165  QWriteLocker l(&m_lockUserClientBuffered);
166  m_ownCallsign = callsign;
167  }
168 
170  {
171  Q_ASSERT_X(this->getConnectionStatus().isDisconnected(), Q_FUNC_INFO,
172  "Can't change ICAO codes while still connected");
173  updateOwnIcaoCodes(ownAircraft.getAircraftIcaoCode(), ownAircraft.getAirlineIcaoCode());
174 
175  QWriteLocker l(&m_lockUserClientBuffered);
176  m_ownAircraftIcaoCode = ownAircraft.getAircraftIcaoCode();
177  m_ownAirlineIcaoCode = ownAircraft.getAirlineIcaoCode();
178 
179  /* use setLiveryAndModelString
180  No longer do it here, use setLiveryAndModelString
181  m_ownLivery = ownAircraft.getModel().getSwiftLiveryString(m_simTypeInfo);
182  m_ownModelString = ownAircraft.getModelString();
183  m_sendLiveryString = true;
184  m_sendModelString = true;
185  */
186  }
187 
188  void CFSDClient::setLiveryAndModelString(const QString &livery, bool sendLiveryString, const QString &modelString,
189  bool sendModelString)
190  {
191  QWriteLocker l(&m_lockUserClientBuffered);
192  m_ownLivery = livery;
193  m_ownModelString = modelString;
194  m_sendLiveryString = sendLiveryString;
195  m_sendModelString = sendModelString;
196  }
197 
198  void CFSDClient::setSimType(const CSimulatorInfo &simInfo) { this->setSimType(simInfo.getSimulator()); }
199 
200  void CFSDClient::setSimType(swift::misc::simulation::CSimulatorInfo::Simulator simulator)
201  {
202  QWriteLocker l(&m_lockUserClientBuffered);
203  switch (simulator)
204  {
205  case CSimulatorInfo::FSX: m_simType = SimType::MSFSX; break;
206  case CSimulatorInfo::P3D: m_simType = SimType::P3Dv4; break;
207  case CSimulatorInfo::FS9: m_simType = SimType::MSFS2004; break;
208  case CSimulatorInfo::FG: m_simType = SimType::FlightGear; break;
209  case CSimulatorInfo::XPLANE: m_simType = SimType::XPLANE11; break;
210  case CSimulatorInfo::MSFS: m_simType = SimType::MSFS; break;
211  case CSimulatorInfo::MSFS2024: m_simType = SimType::MSFS2024; break;
212  default: m_simType = SimType::Unknown; break;
213  }
214  m_simTypeInfo = CSimulatorInfo(simulator);
215  }
216 
218  {
219  QReadLocker l(&m_lockUserClientBuffered);
220  const QStringList v = { m_ownModelString,
221  m_ownLivery,
222  m_ownAircraftIcaoCode.getDesignator(),
223  m_ownAirlineIcaoCode.getVDesignator(),
224  m_ownCallsign.asString(),
225  m_partnerCallsign.asString() };
226  return v;
227  }
228 
230  {
231  if (!CThreadUtils::isInThisThread(this))
232  {
233  QMetaObject::invokeMethod(this, [=] {
234  if (sApp && !sApp->isShuttingDown()) { connectToServer(); }
235  });
236  return;
237  }
238 
239  if (m_socket->isOpen()) { return; }
240  Q_ASSERT(!m_clientName.isEmpty());
241  Q_ASSERT((m_versionMajor + m_versionMinor) > 0);
242  Q_ASSERT(m_capabilities != Capabilities::None);
243 
244  if (m_hostApplication.isEmpty()) { m_hostApplication = this->getSimulatorNameAndVersion().replace(':', ' '); }
245 
246  this->clearState();
247  m_filterPasswordFromLogin = true;
248 
249  m_loginSince = QDateTime::currentMSecsSinceEpoch();
250  const qint64 timerMs = qRound(PendingConnectionTimeoutMs * 1.25);
251 
252  const QPointer<CFSDClient> myself(this);
253  QTimer::singleShot(timerMs, this, [=] {
254  if (!myself || !sApp || sApp->isShuttingDown()) { return; }
255  this->pendingTimeoutCheck();
256  });
257 
258  this->updateConnectionStatus(CConnectionStatus::Connecting);
259 
260  initiateConnection();
261  }
262 
264  {
265  if (!CThreadUtils::isInThisThread(this))
266  {
267  QMetaObject::invokeMethod(this, [=] {
268  if (sApp && !sApp->isShuttingDown()) { disconnectFromServer(); }
269  });
270  return;
271  }
272 
273  this->stopPositionTimers();
274  this->updateConnectionStatus(CConnectionStatus::Disconnecting);
275 
276  // allow also to close if broken
277  CLoginMode mode = this->getLoginMode();
278  if (m_socket->isOpen())
279  {
280  if (mode.isPilot()) { this->sendDeletePilot(); }
281  else if (mode.isObserver()) { this->sendDeleteAtc(); }
282  }
283  m_socket->close();
284 
285  this->updateConnectionStatus(CConnectionStatus::Disconnected);
286  this->clearState();
287  }
288 
289  void CFSDClient::sendLogin(const QString &token)
290  {
291  const CServer s = this->getServer();
292  const QString cid = s.getUser().getId();
293  const QString password = token.isEmpty() ? s.getUser().getPassword() : token;
294  const QString name = s.getUser().getRealNameAndHomeBase(); // m_server.getUser().getRealName();
295  const QString callsign = m_ownCallsign.asString();
296 
297  const CLoginMode m = this->getLoginMode();
298  if (m.isPilot())
299  {
300  const AddPilot pilotLogin(callsign, cid, password, m_pilotRating, m_protocolRevision, m_simType, name);
301  sendQueuedMessage(pilotLogin);
302  CStatusMessage(this).info(u"Sending login as '%1' '%2' '%3' '%4' '%5' '%6'")
303  << callsign << cid << toQString(m_pilotRating) << m_protocolRevision << toQString(m_simType) << name;
304  }
305  else if (m.isObserver())
306  {
307  const AddAtc addAtc(callsign, name, cid, password, m_atcRating, m_protocolRevision);
308  sendQueuedMessage(addAtc);
309  CStatusMessage(this).info(u"Sending OBS login as '%1' '%2' '%3' '%4' '%5'")
310  << callsign << cid << toQString(m_atcRating) << m_protocolRevision << name;
311  }
312 
313  if (m_server.getFsdSetup().receiveEuroscopeSimData())
314  {
315  this->sendClientQuery(ClientQueryType::EuroscopeSimData, {}, {});
316  }
317  }
318 
319  void CFSDClient::sendDeletePilot()
320  {
321  const QString cid = this->getServer().getUser().getId();
322  const DeletePilot deletePilot(m_ownCallsign.getFsdCallsignString(), cid);
323  sendQueuedMessage(deletePilot);
324  }
325 
326  void CFSDClient::sendDeleteAtc()
327  {
328  const QString cid = this->getServer().getUser().getId();
329  const DeleteAtc deleteAtc(getOwnCallsignAsString(), cid);
330  sendQueuedMessage(deleteAtc);
331  }
332 
333  void CFSDClient::sendPilotDataUpdate()
334  {
335  if (this->getConnectionStatus().isDisconnected() && !m_unitTestMode) { return; }
336  const CSimulatedAircraft myAircraft(getOwnAircraft());
337  if (m_loginMode == CLoginMode::Observer)
338  {
339  sendAtcDataUpdate(myAircraft.latitude().value(CAngleUnit::deg()),
340  myAircraft.longitude().value(CAngleUnit::deg()));
341  }
342  else
343  {
344  if (this->isVisualPositionSendingEnabledForServer())
345  {
346  // Slowfast must be sent before the ordinary update.
347  // Sending after causes a server performance issue.
348  // See https://discord.com/channels/775552633918062643/775736423319732256/960746661259390996
349  sendVisualPilotDataUpdate(true);
350  }
351 
352  PilotRating r = this->getPilotRating();
353  PilotDataUpdate pilotDataUpdate(
354  myAircraft.getTransponderMode(), getOwnCallsignAsString(),
355  static_cast<qint16>(myAircraft.getTransponderCode()), r, myAircraft.latitude().value(CAngleUnit::deg()),
356  myAircraft.longitude().value(CAngleUnit::deg()),
357  myAircraft.getAltitude().valueInteger(CLengthUnit::ft()),
358  myAircraft.getPressureAltitude().valueInteger(CLengthUnit::ft()),
359  myAircraft.getGroundSpeed().valueInteger(CSpeedUnit::kts()),
360  myAircraft.getPitch().value(CAngleUnit::deg()), myAircraft.getBank().value(CAngleUnit::deg()),
361  myAircraft.getHeading().normalizedTo360Degrees().value(CAngleUnit::deg()),
362  myAircraft.getParts().isOnGround());
363  sendQueuedMessage(pilotDataUpdate);
364  }
365  }
366 
367  void CFSDClient::sendInterimPilotDataUpdate()
368  {
369  if (this->getConnectionStatus().isDisconnected()) { return; }
370  const CSimulatedAircraft myAircraft(getOwnAircraft());
371  InterimPilotDataUpdate interimPilotDataUpdate(
372  getOwnCallsignAsString(), QString(), myAircraft.latitude().value(CAngleUnit::deg()),
373  myAircraft.longitude().value(CAngleUnit::deg()), myAircraft.getAltitude().valueInteger(CLengthUnit::ft()),
374  myAircraft.getGroundSpeed().valueInteger(CSpeedUnit::kts()), myAircraft.getPitch().value(CAngleUnit::deg()),
375  myAircraft.getBank().value(CAngleUnit::deg()),
376  myAircraft.getHeading().normalizedTo360Degrees().value(CAngleUnit::deg()),
377  myAircraft.getParts().isOnGround());
378 
379  for (const auto &receiver : std::as_const(m_interimPositionReceivers))
380  {
381  interimPilotDataUpdate.setReceiver(receiver.asString());
382  sendQueuedMessage(interimPilotDataUpdate);
383  // statistics
384  }
385  }
386 
387  void CFSDClient::sendVisualPilotDataUpdate(bool slowUpdate)
388  {
389  if (this->getConnectionStatus().isDisconnected() && !m_unitTestMode) { return; }
390  if (m_loginMode == CLoginMode::Observer || !isVisualPositionSendingEnabledForServer()) { return; }
391  const CSimulatedAircraft myAircraft(getOwnAircraft());
392 
393  if (!slowUpdate)
394  {
395  static constexpr double minVelocity = 0.00005;
396  if (std::abs(myAircraft.getVelocity().getVelocityX(CSpeedUnit::m_s())) < minVelocity &&
397  std::abs(myAircraft.getVelocity().getVelocityY(CSpeedUnit::m_s())) < minVelocity &&
398  std::abs(myAircraft.getVelocity().getVelocityZ(CSpeedUnit::m_s())) < minVelocity &&
399  std::abs(myAircraft.getVelocity().getPitchVelocity(CAngleUnit::rad(), CTimeUnit::s())) < minVelocity &&
400  std::abs(myAircraft.getVelocity().getRollVelocity(CAngleUnit::rad(), CTimeUnit::s())) < minVelocity &&
401  std::abs(myAircraft.getVelocity().getHeadingVelocity(CAngleUnit::rad(), CTimeUnit::s())) < minVelocity)
402  {
403  if (m_stoppedSendingVisualPositions) { return; }
404  m_stoppedSendingVisualPositions = true;
405  m_visualPositionUpdateSentCount = 0;
406  }
407  else { m_stoppedSendingVisualPositions = false; }
408 
409  if (!m_serverWantsVisualPositions) { return; }
410  }
411  VisualPilotDataUpdate visualPilotDataUpdate(
412  getOwnCallsignAsString(), myAircraft.latitude().value(CAngleUnit::deg()),
413  myAircraft.longitude().value(CAngleUnit::deg()), myAircraft.getAltitude().value(CLengthUnit::ft()),
414  myAircraft.getAltitude().value(CLengthUnit::ft()) -
415  myAircraft.getGroundElevation().value(CLengthUnit::ft()),
416  myAircraft.getPitch().value(CAngleUnit::deg()), myAircraft.getBank().value(CAngleUnit::deg()),
417  myAircraft.getHeading().normalizedTo360Degrees().value(CAngleUnit::deg()),
418  myAircraft.getVelocity().getVelocityX(CSpeedUnit::m_s()),
419  myAircraft.getVelocity().getVelocityY(CSpeedUnit::m_s()),
420  myAircraft.getVelocity().getVelocityZ(CSpeedUnit::m_s()),
421  myAircraft.getVelocity().getPitchVelocity(CAngleUnit::rad(), CTimeUnit::s()),
422  myAircraft.getVelocity().getRollVelocity(CAngleUnit::rad(), CTimeUnit::s()),
423  myAircraft.getVelocity().getHeadingVelocity(CAngleUnit::rad(), CTimeUnit::s()));
424 
425  if (m_stoppedSendingVisualPositions) { sendQueuedMessage(visualPilotDataUpdate.toStopped()); }
426  else if (m_visualPositionUpdateSentCount++ % 25 == 0) { sendQueuedMessage(visualPilotDataUpdate.toPeriodic()); }
427  else { sendQueuedMessage(visualPilotDataUpdate); }
428  }
429 
430  void CFSDClient::sendAtcDataUpdate(double latitude, double longitude)
431  {
432  const AtcDataUpdate atcDataUpdate(getOwnCallsignAsString(), 199998, CFacilityType::OBS, 300,
433  AtcRating::Observer, latitude, longitude, 0);
434  sendQueuedMessage(atcDataUpdate);
435  }
436 
437  void CFSDClient::sendPing(const QString &receiver)
438  {
439  const qint64 msecSinceEpoch = QDateTime::currentMSecsSinceEpoch();
440  const QString timeString = QString::number(msecSinceEpoch);
441 
442  const Ping ping(getOwnCallsignAsString(), receiver, timeString);
443  sendQueuedMessage(ping);
444 
445  // statistics
446  increaseStatisticsValue(QStringLiteral("sendPing"));
447  }
448 
449  void CFSDClient::sendClientQueryIsValidAtc(const CCallsign &callsign)
450  {
451  sendClientQuery(ClientQueryType::IsValidATC, {}, { callsign.asString() });
452  }
453 
454  void CFSDClient::sendClientQueryCapabilities(const CCallsign &callsign)
455  {
456  sendClientQuery(ClientQueryType::Capabilities, callsign);
457  }
458 
460  {
461  sendClientQuery(ClientQueryType::Com1Freq, callsign);
462  }
463 
465  {
466  sendClientQuery(ClientQueryType::RealName, callsign);
467  }
468 
470  {
471  sendClientQuery(ClientQueryType::Server, callsign);
472  }
473 
475  {
476  sendClientQuery(ClientQueryType::ATIS, callsign);
477  }
478 
480  {
481  sendClientQuery(ClientQueryType::FP, {}, { callsign.toQString() });
482  }
483 
485  {
486  QString data = QJsonDocument(JsonPackets::aircraftConfigRequest()).toJson(QJsonDocument::Compact);
487  data = convertToUnicodeEscaped(data);
488  sendClientQuery(ClientQueryType::AircraftConfig, callsign, { data });
489  }
490 
491  void CFSDClient::sendClientQuery(ClientQueryType queryType, const CCallsign &receiver, const QStringList &queryData)
492  {
493  if (queryType == ClientQueryType::Unknown) { return; }
494  if (!CThreadUtils::isInThisThread(this))
495  {
496  QMetaObject::invokeMethod(this, [=] {
497  if (sApp && !sApp->isShuttingDown()) { sendClientQuery(queryType, receiver, queryData); }
498  });
499  return;
500  }
501 
502  const QString receiverCallsign = receiver.getFsdCallsignString();
503  if (queryType == ClientQueryType::IsValidATC)
504  {
505  const ClientQuery clientQuery(getOwnCallsignAsString(), "SERVER", ClientQueryType::IsValidATC, queryData);
506  sendQueuedMessage(clientQuery);
507  }
508  else if (queryType == ClientQueryType::Capabilities)
509  {
510  const ClientQuery clientQuery(getOwnCallsignAsString(), receiverCallsign, ClientQueryType::Capabilities);
511  sendQueuedMessage(clientQuery);
512  }
513  else if (queryType == ClientQueryType::Com1Freq)
514  {
515  const ClientQuery clientQuery(getOwnCallsignAsString(), receiverCallsign, ClientQueryType::Com1Freq);
516  sendQueuedMessage(clientQuery);
517  }
518  else if (queryType == ClientQueryType::RealName)
519  {
520  const ClientQuery clientQuery(getOwnCallsignAsString(), receiverCallsign, ClientQueryType::RealName);
521  sendQueuedMessage(clientQuery);
522  }
523  else if (queryType == ClientQueryType::Server)
524  {
525  ClientQuery clientQuery(getOwnCallsignAsString(), receiverCallsign, ClientQueryType::Server);
526  sendQueuedMessage(clientQuery);
527  }
528  else if (queryType == ClientQueryType::ATIS)
529  {
530  const ClientQuery clientQuery(getOwnCallsignAsString(), receiverCallsign, ClientQueryType::ATIS);
531  sendQueuedMessage(clientQuery);
532  if (m_serverType != ServerType::Vatsim) { m_pendingAtisQueries.insert(receiver, {}); }
533  }
534  else if (queryType == ClientQueryType::PublicIP)
535  {
536  const ClientQuery clientQuery(getOwnCallsignAsString(), receiverCallsign, ClientQueryType::PublicIP);
537  sendQueuedMessage(clientQuery);
538  }
539  else if (queryType == ClientQueryType::INF)
540  {
541  const ClientQuery clientQuery(getOwnCallsignAsString(), receiverCallsign, ClientQueryType::INF);
542  sendQueuedMessage(clientQuery);
543  }
544  else if (queryType == ClientQueryType::FP)
545  {
546  if (queryData.isEmpty()) { return; }
547  const ClientQuery clientQuery(getOwnCallsignAsString(), "SERVER", ClientQueryType::FP, queryData);
548  sendQueuedMessage(clientQuery);
549  }
550  else if (queryType == ClientQueryType::AircraftConfig)
551  {
552  if (queryData.isEmpty()) { return; }
553  const ClientQuery clientQuery(getOwnCallsignAsString(), receiverCallsign, ClientQueryType::AircraftConfig,
554  queryData);
555  sendQueuedMessage(clientQuery);
556  }
557  else if (queryType == ClientQueryType::EuroscopeSimData)
558  {
559  const ClientQuery clientQuery(getOwnCallsignAsString(), "@94835", ClientQueryType::EuroscopeSimData,
560  { "1" });
561  sendQueuedMessage(clientQuery);
562  }
563 
564  increaseStatisticsValue(QStringLiteral("sendClientQuery"), toQString(queryType));
565  }
566 
568  {
569  if (messages.isEmpty()) { return; }
570  if (!CThreadUtils::isInThisThread(this))
571  {
572  QMetaObject::invokeMethod(this, [=] {
573  if (sApp && !sApp->isShuttingDown()) { sendTextMessages(messages); }
574  });
575  return;
576  }
577 
578  const CTextMessageList privateMessages = messages.getPrivateMessages().markedAsSent();
579  const QString ownCallsign = getOwnCallsignAsString();
580 
581  for (const auto &message : privateMessages)
582  {
583  if (message.getRecipientCallsign().isEmpty()) { continue; }
584  const TextMessage textMessage(ownCallsign, message.getRecipientCallsign().getFsdCallsignString(),
585  message.getMessage());
586  sendQueuedMessage(textMessage);
587  increaseStatisticsValue(QStringLiteral("sendTextMessages.PM"));
588  emit textMessageSent(message);
589  }
590 
591  const CTextMessageList radioMessages = messages.getRadioMessages().markedAsSent();
592  QVector<int> frequencies;
593  for (const auto &message : radioMessages)
594  {
595  // Adjust to nearest frequency, in case of 5kHz difference
596  const CAtcStationList stations = m_atcStations.findIfFrequencyIsWithinSpacing(message.getFrequency());
597  const CFrequency freq = stations.isEmpty() ?
598  message.getFrequency() :
599  stations.findClosest(1, getOwnAircraftPosition()).front().getFrequency();
600 
601  // I could send the same message to n frequencies in one step
602  // if this is really required, I need to group by message
603  // currently I send individual messages
604  frequencies.clear();
605  int freqkHz = freq.valueInteger(CFrequencyUnit::kHz());
606  frequencies.push_back(freqkHz);
607  sendRadioMessage(frequencies, message.getMessage());
608  increaseStatisticsValue(QStringLiteral("sendTextMessages.FREQ"));
609  emit textMessageSent(message);
610  }
611  }
612 
614  {
615  if (message.isEmpty()) { return; }
616  sendTextMessages({ message });
617  }
618 
619  void CFSDClient::sendTextMessage(TextMessageGroups receiverGroup, const QString &message)
620  {
621  if (message.isEmpty()) { return; }
622  if (!CThreadUtils::isInThisThread(this))
623  {
624  QMetaObject::invokeMethod(this, [=] {
625  if (sApp && !sApp->isShuttingDown()) { sendTextMessage(receiverGroup, message); }
626  });
627  return;
628  }
629 
630  QString receiver;
631  if (receiverGroup == TextMessageGroups::AllClients) { receiver = '*'; }
632  else if (receiverGroup == TextMessageGroups::AllAtcClients) { receiver = QStringLiteral("*A"); }
633  else if (receiverGroup == TextMessageGroups::AllPilotClients) { receiver = QStringLiteral("*P"); }
634  else if (receiverGroup == TextMessageGroups::AllSups) { receiver = QStringLiteral("*S"); }
635  else { return; }
636  const TextMessage textMessage(getOwnCallsignAsString(), receiver, message);
637  sendQueuedMessage(textMessage);
638  if (receiver == QStringLiteral("*S"))
639  {
640  const CCallsign sender(getOwnCallsignAsString());
641  const CCallsign recipient(receiver);
642  CTextMessage t(message, sender, recipient);
643  t.markAsSent();
644  emit textMessageSent(t);
645  }
646  increaseStatisticsValue(QStringLiteral("sendTextMessages"));
647  }
648 
649  void CFSDClient::sendTextMessage(const QString &receiver, const QString &message)
650  {
651  const CTextMessage msg(message, getOwnCallsign(), { receiver });
652  sendTextMessage(msg);
653  }
654 
655  void CFSDClient::sendRadioMessage(const QVector<int> &frequencieskHz, const QString &message)
656  {
658  for (const int &frequency : frequencieskHz)
659  {
660  receivers.push_back(QStringLiteral("@%1").arg(frequency - 100000));
661  }
662 
663  const TextMessage radioMessage(getOwnCallsignAsString(), receivers.join('&'), message);
664  sendQueuedMessage(radioMessage);
665  increaseStatisticsValue(QStringLiteral("sendTextMessages"));
666  }
667 
668  void CFSDClient::sendFlightPlan(const CFlightPlan &flightPlan)
669  {
670  if (!CThreadUtils::isInThisThread(this))
671  {
672  QMetaObject::invokeMethod(this, [=] {
673  if (sApp && !sApp->isShuttingDown()) { sendFlightPlan(flightPlan); }
674  });
675  return;
676  }
677 
678  // Removed with T353 although it is standard
679  // const QString route = QString(flightPlan.getRoute()).replace(" ", ".");
680 
681  QString route = flightPlan.getRoute();
682  QString remarks = flightPlan.getRemarks();
683  route.remove(':');
684  remarks.remove(':');
685 
687  const QString alt = flightPlan.getCruiseAltitude().asFpVatsimAltitudeString();
688  // const QString alt = flightPlan.getCruiseAltitude().asFpAltitudeString();
689 
690  FlightType flightType = getFlightType(flightPlan.getFlightRules());
691 
692  QString act;
693 
695  {
696  act = flightPlan.getAircraftInfo().asIcaoString();
697  }
698  else { act = flightPlan.getAircraftInfo().asFaaString(); }
699 
700  Q_ASSERT_X(!act.isEmpty(), Q_FUNC_INFO, "Aircraft type must not be empty");
701 
702  const QList<int> timePartsEnroute = flightPlan.getEnrouteTime().getHrsMinSecParts();
703  const QList<int> timePartsFuel = flightPlan.getFuelTime().getHrsMinSecParts();
704  const FlightPlan fp(getOwnCallsignAsString(), "SERVER", flightType, act,
705  flightPlan.getCruiseTrueAirspeed().valueInteger(CSpeedUnit::kts()),
706  flightPlan.getOriginAirportIcao().asString(),
707  flightPlan.getTakeoffTimePlanned().toUTC().toString("hhmm").toInt(),
708  flightPlan.getTakeoffTimeActual().toUTC().toString("hhmm").toInt(), alt,
709  flightPlan.getDestinationAirportIcao().asString(), timePartsEnroute[CTime::Hours],
710  timePartsEnroute[CTime::Minutes], timePartsFuel[CTime::Hours],
711  timePartsFuel[CTime::Minutes], flightPlan.getAlternateAirportIcao().asString(), remarks,
712  route);
713 
714  sendQueuedMessage(fp);
715  increaseStatisticsValue(QStringLiteral("sendFlightPlan"));
716  }
717 
719  {
720  if (!CThreadUtils::isInThisThread(this))
721  {
722  QMetaObject::invokeMethod(this, [=] {
723  if (sApp && !sApp->isShuttingDown()) { sendPlaneInfoRequest(receiver); }
724  });
725  return;
726  }
727 
728  const PlaneInfoRequest planeInfoRequest(getOwnCallsignAsString(), receiver.toQString());
729  sendQueuedMessage(planeInfoRequest);
730  increaseStatisticsValue(QStringLiteral("sendPlaneInfoRequest"));
731  }
732 
734  {
735  if (!CThreadUtils::isInThisThread(this))
736  {
737  QMetaObject::invokeMethod(this, [=] {
738  if (sApp && !sApp->isShuttingDown()) { sendPlaneInfoRequestFsinn(callsign); }
739  });
740  return;
741  }
742 
743  const bool connected = isConnected();
744  SWIFT_VERIFY_X(connected, Q_FUNC_INFO, "Can't send to server when disconnected");
745  if (!connected) { return; }
746 
747  const CSimulatedAircraft myAircraft(getOwnAircraft());
748  const QString modelString = this->getConfiguredModelString(myAircraft);
749  const PlaneInfoRequestFsinn planeInfoRequestFsinn(
750  getOwnCallsignAsString(), callsign.toQString(), myAircraft.getAirlineIcaoCodeDesignator(),
751  myAircraft.getAircraftIcaoCodeDesignator(), myAircraft.getAircraftIcaoCombinedType(), modelString);
752  sendQueuedMessage(planeInfoRequestFsinn);
753  increaseStatisticsValue(QStringLiteral("sendPlaneInfoRequestFsinn"));
754  }
755 
756  void CFSDClient::sendPlaneInformation(const QString &receiver, const QString &aircraft, const QString &airline,
757  const QString &livery)
758  {
759  const PlaneInformation planeInformation(getOwnCallsignAsString(), receiver, aircraft, airline, livery);
760  sendQueuedMessage(planeInformation);
761  increaseStatisticsValue(QStringLiteral("sendPlaneInformation"));
762  }
763 
764  void CFSDClient::sendPlaneInformationFsinn(const CCallsign &callsign)
765  {
766  if (this->getConnectionStatus().isDisconnected() && !m_unitTestMode) { return; }
767  const CSimulatedAircraft myAircraft(getOwnAircraft());
768  const QString modelString = this->getConfiguredModelString(myAircraft);
769  const PlaneInformationFsinn planeInformationFsinn(
770  getOwnCallsignAsString(), callsign.toQString(), myAircraft.getAirlineIcaoCodeDesignator(),
771  myAircraft.getAircraftIcaoCodeDesignator(), myAircraft.getAircraftIcaoCombinedType(), modelString);
772  sendQueuedMessage(planeInformationFsinn);
773  increaseStatisticsValue(QStringLiteral("sendPlaneInformationFsinn"));
774  }
775 
776  void CFSDClient::sendAircraftConfiguration(const QString &receiver, const QString &aircraftConfigJson)
777  {
778  if (aircraftConfigJson.size() == 0) { return; }
779  const ClientQuery clientQuery(getOwnCallsignAsString(), receiver, ClientQueryType::AircraftConfig,
780  { aircraftConfigJson });
781  sendQueuedMessage(clientQuery);
782  }
783 
784  void CFSDClient::sendMessageString(const QString &message)
785  {
786  if (message.isEmpty()) { return; }
787  const QByteArray bufferEncoded = m_encoder(message);
788  if (m_printToConsole) { qDebug() << "FSD Sent=>" << bufferEncoded; }
789  if (!m_unitTestMode) { m_socket->write(bufferEncoded); }
790 
791  // remove CR/LF and emit
792  emitRawFsdMessage(message.trimmed(), true);
793  }
794 
795  void CFSDClient::sendQueuedMessage()
796  {
797  if (m_queuedFsdMessages.isEmpty()) { return; }
798  const qsizetype s = m_queuedFsdMessages.size(); // NOLINT(cppcoreguidelines-init-variables)
799  this->sendMessageString(m_queuedFsdMessages.dequeue());
800 
801  // send up to 6 at once
802  if (s > 5) { this->sendMessageString(m_queuedFsdMessages.dequeue()); }
803  if (s > 10) { this->sendMessageString(m_queuedFsdMessages.dequeue()); }
804  if (s > 20) { this->sendMessageString(m_queuedFsdMessages.dequeue()); }
805  if (s > 30) { this->sendMessageString(m_queuedFsdMessages.dequeue()); }
806 
807  // overload
808  // no idea, if we ever get here
809  if (s > 50)
810  {
811  const StatusSeverity severity = s > 75 ? SeverityWarning : SeverityInfo;
812  CLogMessage(this).log(severity, u"Too many queued messages (%1), bulk send!") << s;
813  int sendNo = 10;
814  if (s > 75) { sendNo = 20; }
815  if (s > 100) { sendNo = 30; }
816 
817  for (int i = 0; i < sendNo; i++) { this->sendMessageString(m_queuedFsdMessages.dequeue()); }
818  }
819  }
820 
821  void CFSDClient::sendFsdMessage(const QString &message)
822  {
823  // UNIT tests
824  parseMessage(message);
825  }
826 
827  QString CFSDClient::getConfiguredModelString(const CSimulatedAircraft &myAircraft) const
828  {
829  if (!m_sendModelString) { return noModelString(); }
830  QReadLocker l(&m_lockUserClientBuffered);
831  const QString ms = m_ownModelString.isEmpty() ? myAircraft.getModelString() : m_ownModelString;
832  return ms.isEmpty() ? noModelString() : ms;
833  }
834 
835  QString CFSDClient::getConfiguredLiveryString(const CSimulatedAircraft &myAircraft) const
836  {
837  if (!m_sendLiveryString) { return {}; }
838  QReadLocker l(&m_lockUserClientBuffered);
839  const QString livery = m_ownLivery.isEmpty() ? myAircraft.getModel().getSwiftLiveryString() : m_ownLivery;
840  return livery;
841  }
842 
843  void CFSDClient::sendAuthChallenge(const QString &challenge)
844  {
845  const AuthChallenge pduAuthChallenge(getOwnCallsignAsString(), "SERVER", challenge);
846  sendDirectMessage(pduAuthChallenge); // avoid timeouts
847  increaseStatisticsValue(QStringLiteral("sendAuthChallenge"));
848  }
849 
850  void CFSDClient::sendAuthResponse(const QString &response)
851  {
852  const AuthResponse pduAuthResponse(getOwnCallsignAsString(), "SERVER", response);
853  sendDirectMessage(pduAuthResponse); // avoid timeouts
854  increaseStatisticsValue(QStringLiteral("sendAuthResponse"));
855  }
856 
857  void CFSDClient::sendPong(const QString &receiver, const QString &timestamp)
858  {
859  const Pong pong(getOwnCallsignAsString(), receiver, timestamp);
860  sendQueuedMessage(pong);
861  increaseStatisticsValue(QStringLiteral("sendPong"));
862  }
863 
864  void CFSDClient::sendClientResponse(ClientQueryType queryType, const QString &receiver)
865  {
866  if (queryType == ClientQueryType::Unknown) { return; }
867  if (queryType == ClientQueryType::IsValidATC)
868  {
869  this->handleIllegalFsdState("Never use sendClientResponse with IsValidATC from the client");
870  return;
871  }
872 
873  increaseStatisticsValue(QStringLiteral("sendClientResponse"), toQString(queryType));
874 
875  QStringList responseData;
876  const QString ownCallsign = getOwnCallsignAsString();
877 
878  if (queryType == ClientQueryType::Capabilities)
879  {
880  responseData.clear();
881  if (m_capabilities & Capabilities::AtcInfo) responseData.push_back(toQString(Capabilities::AtcInfo) % "=1");
882  if (m_capabilities & Capabilities::SecondaryPos)
883  responseData.push_back(toQString(Capabilities::SecondaryPos) % "=1");
884  if (m_capabilities & Capabilities::AircraftInfo)
885  responseData.push_back(toQString(Capabilities::AircraftInfo) % "=1");
886  if (m_capabilities & Capabilities::OngoingCoord)
887  responseData.push_back(toQString(Capabilities::OngoingCoord) % "=1");
888  if (m_capabilities & Capabilities::InterminPos)
889  responseData.push_back(toQString(Capabilities::InterminPos) % "=1");
890  if (m_capabilities & Capabilities::FastPos) responseData.push_back(toQString(Capabilities::FastPos) % "=1");
891  if (m_capabilities & Capabilities::VisPos) responseData.push_back(toQString(Capabilities::VisPos) % "=1");
892  if (m_capabilities & Capabilities::Stealth) responseData.push_back(toQString(Capabilities::Stealth) % "=1");
893  if (m_capabilities & Capabilities::AircraftConfig)
894  responseData.push_back(toQString(Capabilities::AircraftConfig) % "=1");
895  if (m_capabilities & Capabilities::IcaoEquipment)
896  responseData.push_back(toQString(Capabilities::IcaoEquipment) % "=1");
897  const ClientResponse clientResponse(ownCallsign, receiver, ClientQueryType::Capabilities, responseData);
898  sendQueuedMessage(clientResponse);
899  }
900  else if (queryType == ClientQueryType::Com1Freq)
901  {
902  const QString com1Frequency = QString::number(
903  getOwnAircraft().getCom1System().getFrequencyActive().value(CFrequencyUnit::MHz()), 'f', 3);
904  responseData.push_back(com1Frequency);
905  const ClientResponse pduClientResponse(ownCallsign, receiver, ClientQueryType::Com1Freq, responseData);
906  sendQueuedMessage(pduClientResponse);
907  }
908  else if (queryType == ClientQueryType::RealName)
909  {
910  // real name
911  // responseData.push_back(m_server.getUser().getRealName());
912  responseData.push_back(m_server.getUser().getRealNameAndHomeBase()); // getHomeBase
913  // sector file in use (blank if pilot)
914  responseData.push_back({});
915  // current user rating
916  if (m_loginMode.isObserver()) { responseData.push_back(toQString(m_atcRating)); }
917  else { responseData.push_back(toQString(m_pilotRating)); }
918 
919  const ClientResponse pduClientQueryResponse(ownCallsign, receiver, ClientQueryType::RealName, responseData);
920  sendQueuedMessage(pduClientQueryResponse);
921  }
922  else if (queryType == ClientQueryType::Server)
923  {
924  responseData.push_back(m_server.getAddress());
925  const ClientResponse pduClientQueryResponse(ownCallsign, receiver, ClientQueryType::Server, responseData);
926  sendQueuedMessage(pduClientQueryResponse);
927  }
928  else if (queryType == ClientQueryType::ATIS)
929  {
930  this->handleIllegalFsdState(
931  QStringLiteral("Dont send '%1' as pilot client!").arg(toQString(ClientQueryType::ATIS)));
932  }
933  else if (queryType == ClientQueryType::PublicIP)
934  {
935  this->handleIllegalFsdState(
936  QStringLiteral("Dont send '%1' as pilot client!").arg(toQString(ClientQueryType::PublicIP)));
937  }
938  else if (queryType == ClientQueryType::INF)
939  {
940  const QString cid = m_server.getUser().getId();
941  const QString realName = m_server.getUser().getRealName();
942 
943  const CAircraftSituation situation = this->getOwnAircraftPosition();
944  const double latitude = situation.latitude().value(CAngleUnit::deg());
945  const double longitude = situation.longitude().value(CAngleUnit::deg());
946  const int altitude = situation.getAltitude().valueInteger(CLengthUnit::ft());
947 
948  std::array<char, 50> sysuid = {};
949 #ifdef SWIFT_VATSIM_SUPPORT
950  vatsim_get_system_unique_id(sysuid.data());
951 #endif
952 
953  const QString userInfo = QStringLiteral("CID=") % cid % " " % m_clientName % " IP=" %
954  m_socket->localAddress().toString() % " SYS_UID=" % sysuid.data() % " FSVER=" %
955  m_hostApplication % " LT=" % QString::number(latitude) % " LO=" %
956  QString::number(longitude) % " AL=" % QString::number(altitude) % " " % realName;
957 
958  const TextMessage textMessage(ownCallsign, receiver, userInfo);
959  sendQueuedMessage(textMessage);
960  }
961  else if (queryType == ClientQueryType::FP)
962  {
963  this->handleIllegalFsdState(
964  QStringLiteral("Dont send '%1' as pilot client!").arg(toQString(ClientQueryType::FP)));
965  }
966  else if (queryType == ClientQueryType::AircraftConfig)
967  {
968  this->handleIllegalFsdState(
969  QStringLiteral("Dont send '%1' as pilot client!").arg(toQString(ClientQueryType::AircraftConfig)));
970  }
971  }
972 
973 #ifdef SWIFT_VATSIM_SUPPORT
974  void CFSDClient::sendClientIdentification(const QString &fsdChallenge)
975  {
976  std::array<char, 50> sysuid = {};
977  vatsim_get_system_unique_id(sysuid.data());
978  const QString cid = m_server.getUser().getId();
979  const ClientIdentification clientIdentification(
980  getOwnCallsignAsString(), vatsim_auth_get_client_id(m_clientAuth), m_clientName, m_versionMajor,
981  m_versionMinor, cid, sysuid.data(), fsdChallenge);
982  this->sendQueuedMessage(clientIdentification);
983 
984  if (getServer().getEcosystem().isSystem(CEcosystem::VATSIM))
985  {
986  this->getVatsimAuthToken(cid, m_server.getUser().getPassword(),
987  { this, [this](const QString &token) {
988  this->sendLogin(token);
989  this->updateConnectionStatus(CConnectionStatus::Connected);
990  } });
991  }
992  else
993  {
994  this->sendLogin();
995  this->updateConnectionStatus(CConnectionStatus::Connected);
996  }
997  increaseStatisticsValue(QStringLiteral("sendClientIdentification"));
998  }
999 #endif
1000 
1001  void CFSDClient::getVatsimAuthToken(const QString &cid, const QString &password,
1002  const swift::misc::CSlot<void(const QString &)> &callback)
1003  {
1004  Q_ASSERT_X(sApp, Q_FUNC_INFO, "Need app");
1006  nwRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
1007  const QJsonObject jsonRequest { { "cid", cid }, { "password", password } };
1008 
1009  sApp->postToNetwork(nwRequest, CApplication::NoLogRequestId, QJsonDocument(jsonRequest).toJson(),
1010  { this, [=](QNetworkReply *nwReply) {
1011  const QByteArray data = nwReply->readAll();
1012  const QJsonObject json = QJsonDocument::fromJson(data).object();
1013 
1014  if (json.value("success").toBool()) { callback(json.value("token").toString()); }
1015  else
1016  {
1017  const QString error = json.value("error_msg").isString() ?
1018  json.value("error_msg").toString() :
1019  nwReply->errorString();
1020  CLogMessage(this).error(u"VATSIM auth token endpoint: %1") << error;
1021  disconnectFromServer();
1022  }
1023  nwReply->deleteLater();
1024  } });
1025  }
1026 
1027  void CFSDClient::sendIncrementalAircraftConfig()
1028  {
1029  if (!m_unitTestMode && (!this->isConnected() || !this->getSetupForServer().sendAircraftParts())) { return; }
1030  const CAircraftParts currentParts(this->getOwnAircraftParts());
1031 
1032  // If it hasn't changed, return
1033  if (m_sentAircraftConfig == currentParts) { return; }
1034 
1035  if (!m_tokenBucket.tryConsume()) { return; }
1036 
1037  const QJsonObject previousConfig = m_sentAircraftConfig.toJson();
1038  const QJsonObject currentConfig = currentParts.toJson();
1039  const QJsonObject incrementalConfig = getIncrementalObject(previousConfig, currentConfig);
1040 
1041  const QString dataStr = convertToUnicodeEscaped(
1042  QJsonDocument(QJsonObject { { "config", incrementalConfig } }).toJson(QJsonDocument::Compact));
1043 
1044  sendAircraftConfiguration("@94836", dataStr);
1045  m_sentAircraftConfig = currentParts;
1046  }
1047 
1048  void CFSDClient::initializeMessageTypes()
1049  {
1050  m_messageTypeMapping["#AA"] = MessageType::AddAtc;
1051  m_messageTypeMapping["#AP"] = MessageType::AddPilot;
1052  m_messageTypeMapping["%"] = MessageType::AtcDataUpdate;
1053  m_messageTypeMapping["$ZC"] = MessageType::AuthChallenge;
1054  m_messageTypeMapping["$ZR"] = MessageType::AuthResponse;
1055  m_messageTypeMapping["$ID"] = MessageType::ClientIdentification;
1056  m_messageTypeMapping["$CQ"] = MessageType::ClientQuery;
1057  m_messageTypeMapping["$CR"] = MessageType::ClientResponse;
1058  m_messageTypeMapping["#DA"] = MessageType::DeleteATC;
1059  m_messageTypeMapping["#DP"] = MessageType::DeletePilot;
1060  m_messageTypeMapping["$FP"] = MessageType::FlightPlan;
1061  m_messageTypeMapping["#PC"] = MessageType::ProController;
1062  m_messageTypeMapping["$DI"] = MessageType::FsdIdentification;
1063  m_messageTypeMapping["$!!"] = MessageType::KillRequest;
1064  m_messageTypeMapping["@"] = MessageType::PilotDataUpdate;
1065  m_messageTypeMapping["^"] = MessageType::VisualPilotDataUpdate;
1066  m_messageTypeMapping["#SL"] = MessageType::VisualPilotDataPeriodic;
1067  m_messageTypeMapping["#ST"] = MessageType::VisualPilotDataStopped;
1068  m_messageTypeMapping["$SF"] = MessageType::VisualPilotDataToggle;
1069  m_messageTypeMapping["$PI"] = MessageType::Ping;
1070  m_messageTypeMapping["$PO"] = MessageType::Pong;
1071  m_messageTypeMapping["$ER"] = MessageType::ServerError;
1072  m_messageTypeMapping["#DL"] = MessageType::ServerHeartbeat;
1073  m_messageTypeMapping["#TM"] = MessageType::TextMessage;
1074  m_messageTypeMapping["#SB"] = MessageType::PilotClientCom;
1075  m_messageTypeMapping["$XX"] = MessageType::Rehost;
1076  m_messageTypeMapping["#MU"] = MessageType::Mute;
1077 
1078  // Euroscope
1079  m_messageTypeMapping["SIMDATA"] = MessageType::EuroscopeSimData;
1080  }
1081 
1082  void CFSDClient::handleAtcDataUpdate(const QStringList &tokens)
1083  {
1084  const AtcDataUpdate atcDataUpdate = AtcDataUpdate::fromTokens(tokens);
1085  const QString senderCs = atcDataUpdate.sender();
1086  const CCallsign cs(senderCs, CCallsign::Atc);
1087 
1088  // Filter non-ATC like OBS stations, like pilots logging in as shared cockpit co-pilots.
1089  if (atcDataUpdate.m_facility == CFacilityType::Unknown && !cs.isObserverCallsign())
1090  {
1091  return;
1092  } // like in old version
1093  if (atcDataUpdate.m_facility == CFacilityType::OBS && !cs.hasSuffix()) { return; }
1094 
1095  CFrequency freq(atcDataUpdate.m_frequencykHz, CFrequencyUnit::kHz());
1096  freq.switchUnit(CFrequencyUnit::MHz()); // we would not need to bother, but this makes it easier to identify
1097 
1098  // Here we could round to channel spacing, based on
1099  // https://discordapp.com/channels/539048679160676382/539486489977946112/651514202405601291
1100  // CComSystem::roundToChannelSpacing(freq, CComSystem::ChannelSpacing25KHz);
1101 
1102  const CLength networkRange(atcDataUpdate.m_visibleRange, CLengthUnit::NM());
1103  const CLength range = fixAtcRange(networkRange, cs);
1104  const CCoordinateGeodetic position(atcDataUpdate.m_latitude, atcDataUpdate.m_longitude, 0);
1105 
1106  emit this->atcDataUpdateReceived(cs, freq, position, range);
1107 
1108  m_atcStations.replaceOrAddObjectByCallsign({ cs, {}, freq, position, range });
1109  }
1110 
1111 #ifdef SWIFT_VATSIM_SUPPORT
1112  void CFSDClient::handleAuthChallenge(const QStringList &tokens)
1113  {
1114  const AuthChallenge authChallenge = AuthChallenge::fromTokens(tokens);
1115  char response[33];
1116  vatsim_auth_generate_response(m_clientAuth, qPrintable(authChallenge.m_challengeKey), response);
1117  sendAuthResponse(QString(response));
1118 
1119  char challenge[33];
1120  vatsim_auth_generate_challenge(m_serverAuth, challenge);
1121  m_lastServerAuthChallenge = QString(challenge);
1122  sendAuthChallenge(m_lastServerAuthChallenge);
1123  }
1124 #endif
1125 
1126 #ifdef SWIFT_VATSIM_SUPPORT
1127  void CFSDClient::handleAuthResponse(const QStringList &tokens)
1128  {
1129  const AuthResponse authResponse = AuthResponse::fromTokens(tokens);
1130 
1131  char expectedResponse[33];
1132  vatsim_auth_generate_response(m_serverAuth, qPrintable(m_lastServerAuthChallenge), expectedResponse);
1133  if (authResponse.m_response != QString(expectedResponse))
1134  {
1135  CLogMessage().error(u"The server you are connected to is not a VATSIM server. Disconnecting!");
1136  disconnectFromServer();
1137  }
1138  }
1139 #endif
1140 
1141  void CFSDClient::handleDeleteATC(const QStringList &tokens)
1142  {
1143  const DeleteAtc deleteAtc = DeleteAtc::fromTokens(tokens);
1144  emit deleteAtcReceived(deleteAtc.m_cid);
1145 
1146  m_atcStations.removeByCallsign(deleteAtc.m_cid);
1147  }
1148 
1149  void CFSDClient::handleDeletePilot(const QStringList &tokens)
1150  {
1151  const DeletePilot deletePilot = DeletePilot::fromTokens(tokens);
1152  const CCallsign cs(deletePilot.sender(), CCallsign::Aircraft);
1153  clearState(cs);
1154  emit deletePilotReceived(deletePilot.m_cid);
1155  }
1156 
1157  void CFSDClient::handleTextMessage(const QStringList &tokens)
1158  {
1159  const TextMessage textMessage = TextMessage::fromTokens(tokens);
1160 
1161  const CCallsign sender(textMessage.sender());
1162  const CCallsign receiver(textMessage.receiver());
1163 
1164  if (textMessage.m_type == TextMessage::PrivateMessage)
1165  {
1166  // Other FSD servers send the controller ATIS as text message. The following conditions need to be met:
1167  // * non-VATSIM server. VATSIM has a specific ATIS message
1168  // * Receiver callsign must be owner callsign and not any type of broadcast.
1169  // * We have requested the ATIS of this controller before.
1170  if (m_server.getServerType() != CServer::FSDServerVatsim &&
1171  m_ownCallsign.asString() == textMessage.receiver() && m_pendingAtisQueries.contains(sender))
1172  {
1173  maybeHandleAtisReply(sender, receiver, textMessage.m_message);
1174  return;
1175  }
1176 
1177  CTextMessage tm(textMessage.m_message, sender, receiver);
1178  tm.setCurrentUtcTime();
1179  this->consolidateTextMessage(tm); // emit textMessagesReceived({ tm });
1180  }
1181  else if (textMessage.m_type == TextMessage::RadioMessage)
1182  {
1183  const CFrequency com1 = getOwnAircraft().getCom1System().getFrequencyActive();
1184  const CFrequency com2 = getOwnAircraft().getCom2System().getFrequencyActive();
1185  QList<CFrequency> frequencies;
1186 
1187  for (int freqKhz : textMessage.m_frequencies)
1188  {
1189  CFrequency f(freqKhz, CFrequencyUnit::kHz());
1190  CComSystem::roundToChannelSpacing(f, CComSystem::ChannelSpacing8_33KHz);
1191  if (f == com1 || f == com2) { frequencies.push_back(f); }
1192  }
1193  if (frequencies.isEmpty()) { return; }
1194  CTextMessageList messages(textMessage.m_message, frequencies, CCallsign(textMessage.sender()));
1195  messages.setCurrentUtcTime();
1196  emit textMessagesReceived(messages);
1197  }
1198  }
1199 
1200  void CFSDClient::handlePilotDataUpdate(const QStringList &tokens)
1201  {
1202  const PilotDataUpdate dataUpdate = PilotDataUpdate::fromTokens(tokens);
1203  const CCallsign callsign(dataUpdate.sender(), CCallsign::Aircraft);
1204 
1205  CAircraftSituation situation(
1206  callsign, CCoordinateGeodetic(dataUpdate.m_latitude, dataUpdate.m_longitude, dataUpdate.m_altitudeTrue),
1207  CHeading(dataUpdate.m_heading, CHeading::True, CAngleUnit::deg()),
1208  CAngle(dataUpdate.m_pitch, CAngleUnit::deg()), CAngle(dataUpdate.m_bank, CAngleUnit::deg()),
1209  CSpeed(dataUpdate.m_groundSpeed, CSpeedUnit::kts()));
1210  situation.setPressureAltitude(CAltitude(dataUpdate.m_altitudePressure, CAltitude::MeanSeaLevel,
1211  CAltitude::PressureAltitude, CLengthUnit::ft()));
1212  // NotSetGroundDetails because here we do not know if this FSD protocol actually utilizes this flag
1213  const COnGroundInfo og(dataUpdate.m_onGround ? COnGroundInfo::OnGround : COnGroundInfo::NotOnGround,
1214  COnGroundInfo::NotSetGroundDetails);
1215  situation.setOnGroundInfo(og);
1216 
1217  // Ref T297, default offset time
1218  situation.setCurrentUtcTime();
1219  const qint64 offsetTimeMs =
1220  receivedPositionFixTsAndGetOffsetTime(situation.getCallsign(), situation.getMSecsSinceEpoch());
1221  situation.setTimeOffsetMs(offsetTimeMs);
1222 
1223  // I did have a situation where I got wrong transponder codes (KB)
1224  // So I now check for a valid code in order to detect such codes
1225  CTransponder transponder;
1226  if (CTransponder::isValidTransponderCode(dataUpdate.m_transponderCode))
1227  {
1228  transponder = CTransponder(dataUpdate.m_transponderCode, dataUpdate.m_transponderMode);
1229  }
1230  else
1231  {
1232  if (CBuildConfig::isLocalDeveloperDebugBuild())
1233  {
1234  CLogMessage(this).debug(u"Wrong transponder code '%1' for '%2'")
1235  << dataUpdate.m_transponderCode << callsign;
1236  }
1237 
1238  // I set a default: IFR standby is a reasonable default
1239  transponder = CTransponder(2000, CTransponder::StateStandby);
1240  }
1241  emit pilotDataUpdateReceived(situation, transponder);
1242  }
1243 
1244  void CFSDClient::handleEuroscopeSimData(const QStringList &tokens)
1245  {
1246  const EuroscopeSimData data = EuroscopeSimData::fromTokens(tokens);
1247 
1248  CAircraftSituation situation(CCallsign(data.sender(), CCallsign::Aircraft),
1249  CCoordinateGeodetic(data.m_latitude, data.m_longitude, data.m_altitude),
1250  CHeading(data.m_heading, CAngleUnit::deg()),
1251  CAngle(-data.m_pitch, CAngleUnit::deg()), CAngle(-data.m_bank, CAngleUnit::deg()),
1252  CSpeed(data.m_groundSpeed, CSpeedUnit::kts()));
1253  // NotSetGroundDetails because here we do not know if this FSD protocol actually utilizes this flag
1254  const COnGroundInfo og(data.m_onGround ? COnGroundInfo::OnGround : COnGroundInfo::NotOnGround,
1255  COnGroundInfo::NotSetGroundDetails);
1256  situation.setOnGroundInfo(og);
1257 
1258  // Ref T297, default offset time
1259  situation.setCurrentUtcTime();
1260  const qint64 offsetTimeMs =
1261  receivedPositionFixTsAndGetOffsetTime(situation.getCallsign(), situation.getMSecsSinceEpoch());
1262  situation.setTimeOffsetMs(offsetTimeMs);
1263 
1264  CAircraftParts parts;
1265  parts.setLights(data.m_lights);
1266  parts.setGearDown(data.m_gearPercent);
1267  parts.setOnGround(data.m_onGround);
1268 
1269  emit euroscopeSimDataUpdatedReceived(situation, parts, currentOffsetTime(data.sender()), data.m_model,
1270  data.m_livery);
1271  }
1272 
1273  void CFSDClient::handleVisualPilotDataUpdate(const QStringList & /*tokens*/, MessageType /*messageType*/)
1274  {
1275 #if 0
1276  VisualPilotDataUpdate dataUpdate;
1277  switch (messageType)
1278  {
1279  case MessageType::VisualPilotDataUpdate: dataUpdate = VisualPilotDataUpdate::fromTokens(tokens); break;
1280  case MessageType::VisualPilotDataPeriodic: dataUpdate = VisualPilotDataPeriodic::fromTokens(tokens).toUpdate(); break;
1281  case MessageType::VisualPilotDataStopped: dataUpdate = VisualPilotDataStopped::fromTokens(tokens).toUpdate(); break;
1282  default: qFatal("Precondition violated"); break;
1283  }
1284  const CCallsign callsign(dataUpdate.sender(), CCallsign::Aircraft);
1285 
1286  CAircraftSituation situation(
1287  callsign,
1288  CCoordinateGeodetic(dataUpdate.m_latitude, dataUpdate.m_longitude, dataUpdate.m_altitudeTrue),
1289  CHeading(dataUpdate.m_heading, CHeading::True, CAngleUnit::deg()),
1290  CAngle(dataUpdate.m_pitch, CAngleUnit::deg()),
1291  CAngle(dataUpdate.m_bank, CAngleUnit::deg()));
1292 
1293  // not used
1294  // situation.setVelocity(CAircraftVelocity(
1295  // dataUpdate.m_xVelocity, dataUpdate.m_yVelocity, dataUpdate.m_zVelocity, CSpeedUnit::m_s(),
1296  // dataUpdate.m_pitchRadPerSec, dataUpdate.m_bankRadPerSec, dataUpdate.m_headingRadPerSec, CAngleUnit::rad(), CTimeUnit::s()));
1297 
1298  // Ref T297, default offset time
1299  situation.setCurrentUtcTime();
1300  const qint64 offsetTimeMs = receivedPositionFixTsAndGetOffsetTime(situation.getCallsign(), situation.getMSecsSinceEpoch());
1301  situation.setTimeOffsetMs(offsetTimeMs);
1302 
1303  emit visualPilotDataUpdateReceived(situation);
1304 #endif
1305  }
1306 
1307  void CFSDClient::handleVisualPilotDataToggle(const QStringList &tokens)
1308  {
1309  const VisualPilotDataToggle toggle = VisualPilotDataToggle::fromTokens(tokens);
1310  m_serverWantsVisualPositions = toggle.m_active;
1311  }
1312 
1313  void CFSDClient::handlePing(const QStringList &tokens)
1314  {
1315  const Ping ping = Ping::fromTokens(tokens);
1316  sendPong(ping.sender(), ping.m_timestamp);
1317  }
1318 
1319  void CFSDClient::handlePong(const QStringList &tokens)
1320  {
1321  const Pong pong = Pong::fromTokens(tokens);
1322  const qint64 msecSinceEpoch = QDateTime::currentMSecsSinceEpoch();
1323  const qint64 elapsedTime = msecSinceEpoch - pong.m_timestamp.toLongLong();
1324  emit pongReceived(pong.sender(), static_cast<double>(elapsedTime));
1325  }
1326 
1327  void CFSDClient::handleKillRequest(const QStringList &tokens)
1328  {
1329  KillRequest killRequest = KillRequest::fromTokens(tokens);
1330  emit killRequestReceived(killRequest.m_reason);
1331  disconnectFromServer();
1332  }
1333 
1334  void CFSDClient::handleFlightPlan(const QStringList &tokens)
1335  {
1336  FlightPlan fp = FlightPlan::fromTokens(tokens);
1337  CFlightPlan::FlightRules rules = CFlightPlan::VFR;
1338 
1339  switch (fp.m_flightType)
1340  {
1341  case FlightType::VFR: rules = CFlightPlan::VFR; break;
1342  case FlightType::IFR: rules = CFlightPlan::IFR; break;
1343  case FlightType::DVFR: rules = CFlightPlan::DVFR; break;
1344  case FlightType::SVFR: rules = CFlightPlan::SVFR; break;
1345  }
1346 
1347  QString cruiseAltString = fp.m_cruiseAlt.trimmed();
1348  if (!cruiseAltString.isEmpty() && is09OnlyString(cruiseAltString))
1349  {
1350  int ca = cruiseAltString.toInt();
1351  // we have a 0-9 only string
1352  // we assume values like 24000 as FL
1353  // RefT323, also major tool such as PFPX and Simbrief do so
1354  if (rules == CFlightPlan::IFR)
1355  {
1356  if (ca >= 1000) { cruiseAltString = u"FL" % QString::number(ca / 100); }
1357  else { cruiseAltString = u"FL" % cruiseAltString; }
1358  }
1359  else // VFR
1360  {
1361  if (ca >= 5000) { cruiseAltString = u"FL" % QString::number(ca / 100); }
1362  else { cruiseAltString = cruiseAltString % u"ft"; }
1363  }
1364  }
1365  CAltitude cruiseAlt;
1366  cruiseAlt.parseFromString(cruiseAltString, CPqString::SeparatorBestGuess);
1367 
1368  const QString depTimePlanned = QStringLiteral("0000").append(QString::number(fp.m_estimatedDepTime)).right(4);
1369  const QString depTimeActual = QStringLiteral("0000").append(QString::number(fp.m_actualDepTime)).right(4);
1370 
1371  const CCallsign callsign(fp.sender(), CCallsign::Aircraft);
1372  const CFlightPlan flightPlan(
1373  callsign, CFlightPlanAircraftInfo(fp.m_aircraftIcaoType), fp.m_depAirport, fp.m_destAirport,
1374  fp.m_altAirport, fromStringUtc(depTimePlanned, "hhmm"), fromStringUtc(depTimeActual, "hhmm"),
1375  CTime(fp.m_hoursEnroute * 60 + fp.m_minutesEnroute, CTimeUnit::min()),
1376  CTime(fp.m_fuelAvailHours * 60 + fp.m_fuelAvailMinutes, CTimeUnit::min()), cruiseAlt,
1377  CSpeed(fp.m_trueCruisingSpeed, CSpeedUnit::kts()), rules, fp.m_route, fp.m_remarks);
1378 
1379  emit flightPlanReceived(callsign, flightPlan);
1380  }
1381 
1382  void CFSDClient::handleClientQuery(const QStringList &tokens)
1383  {
1384  const ClientQuery clientQuery = ClientQuery::fromTokens(tokens);
1385  // NOLINTBEGIN(bugprone-branch-clone)
1386  if (clientQuery.m_queryType == ClientQueryType::Unknown) { return; }
1387  if (clientQuery.m_queryType == ClientQueryType::IsValidATC)
1388  {
1389  // This is usually sent to the server only. If it ever arrives here, just ignore it.
1390  }
1391  else if (clientQuery.m_queryType == ClientQueryType::Capabilities)
1392  {
1393  sendClientResponse(ClientQueryType::Capabilities, clientQuery.sender());
1394  }
1395  else if (clientQuery.m_queryType == ClientQueryType::Com1Freq)
1396  {
1397  sendClientResponse(ClientQueryType::Com1Freq, clientQuery.sender());
1398  }
1399  else if (clientQuery.m_queryType == ClientQueryType::RealName)
1400  {
1401  sendClientResponse(ClientQueryType::RealName, clientQuery.sender());
1402  }
1403  else if (clientQuery.m_queryType == ClientQueryType::Server)
1404  {
1405  sendClientResponse(ClientQueryType::Server, clientQuery.sender());
1406  }
1407  else if (clientQuery.m_queryType == ClientQueryType::ATIS)
1408  {
1409  // This is answered by ATC clients only. If we get such a request, ignore it.
1410  }
1411  else if (clientQuery.m_queryType == ClientQueryType::PublicIP)
1412  {
1413  // This is usually sent to the server only. If it ever arrives here, just ignore it.
1414  }
1415  else if (clientQuery.m_queryType == ClientQueryType::INF)
1416  {
1417  sendClientResponse(ClientQueryType::INF, clientQuery.sender());
1418  }
1419  else if (clientQuery.m_queryType == ClientQueryType::FP)
1420  {
1421  // This is usually sent to the server only. If it ever arrives here, just ignore it.
1422  }
1423  else if (clientQuery.m_queryType == ClientQueryType::AircraftConfig)
1424  {
1425  QStringList aircraftConfigTokens = tokens.mid(3);
1426  QString aircraftConfigJson = aircraftConfigTokens.join(":");
1427 
1428  const CCallsign callsign(clientQuery.sender(), CCallsign::Aircraft);
1429 
1430  QJsonParseError parserError;
1431  const QByteArray json = aircraftConfigJson.toUtf8();
1432  const QJsonDocument doc = QJsonDocument::fromJson(json, &parserError);
1433 
1434  if (parserError.error != QJsonParseError::NoError)
1435  {
1436  CLogMessage(this).warning(u"Failed to parse aircraft config packet: '%1' packet: '%2'")
1437  << parserError.errorString() << QString(json);
1438  return; // we cannot parse the packet, so we give up here
1439  }
1440 
1441  const QJsonObject packet = doc.object();
1442  if (packet == JsonPackets::aircraftConfigRequest())
1443  {
1444  // this MUST work for NOT IN RANGE aircraft as well
1445  // Here we send our OWN parts
1446  QJsonObject config = this->getOwnAircraftParts().toJson();
1447  config.insert(CAircraftParts::attributeNameIsFullJson(), true);
1448  QString data = QJsonDocument(QJsonObject { { "config", config } }).toJson(QJsonDocument::Compact);
1449  data = convertToUnicodeEscaped(data);
1450  sendAircraftConfiguration(clientQuery.sender(), data);
1451  return;
1452  }
1453 
1454  const bool inRange = isAircraftInRange(callsign);
1455  if (!inRange) { return; } // sort out all broadcasts we DO NOT NEED
1456  if (!getSetupForServer().receiveAircraftParts()) { return; }
1457  const QJsonObject config = doc.object().value("config").toObject();
1458  if (config.isEmpty()) { return; }
1459 
1460  const qint64 offsetTimeMs = currentOffsetTime(callsign);
1461  emit aircraftConfigReceived(clientQuery.sender(), config, offsetTimeMs);
1462  }
1463  // NOLINTEND(bugprone-branch-clone)
1464  }
1465 
1466  void CFSDClient::handleClientResponse(const QStringList &tokens)
1467  {
1468  const ClientResponse clientResponse = ClientResponse::fromTokens(tokens);
1469  if (clientResponse.isUnknownQuery()) { return; }
1470  const QString sender = clientResponse.sender();
1471 
1472  QString responseData1;
1473  QString responseData2;
1474  // NOLINTBEGIN(bugprone-branch-clone)
1475  if (!clientResponse.m_responseData.empty()) { responseData1 = clientResponse.m_responseData.at(0); }
1476 
1477  if (clientResponse.m_responseData.size() > 1) { responseData2 = clientResponse.m_responseData.at(1); }
1478 
1479  if (clientResponse.m_queryType == ClientQueryType::IsValidATC)
1480  {
1481  emit validAtcResponseReceived(responseData2, responseData1 == u"Y");
1482  }
1483  else if (clientResponse.m_queryType == ClientQueryType::Capabilities)
1484  {
1485  Capabilities capabilities = Capabilities::None;
1486  for (auto keyValuePair : clientResponse.m_responseData)
1487  {
1488  if (keyValuePair.count('=') != 1) { continue; }
1489 
1490  const QStringList split = keyValuePair.split('=');
1491  if (split.size() < 2) { continue; }
1492  const QString key = split.at(0);
1493  const QString value = split.at(1);
1494 
1495  if (value == "1") { capabilities |= fromQString<Capabilities>(key); }
1496  }
1497 
1498  CClient::Capabilities caps = CClient::None;
1499  if (capabilities & Capabilities::AtcInfo) { caps |= CClient::FsdAtisCanBeReceived; }
1500  if (capabilities & Capabilities::FastPos) { caps |= CClient::FsdWithInterimPositions; }
1501  if (capabilities & Capabilities::VisPos) { caps |= CClient::FsdWithVisualPositions; }
1502  if (capabilities & Capabilities::AircraftInfo) { caps |= CClient::FsdWithIcaoCodes; }
1503  if (capabilities & Capabilities::AircraftConfig) { caps |= CClient::FsdWithAircraftConfig; }
1504 
1505  emit capabilityResponseReceived(clientResponse.sender(), caps);
1506  }
1507  else if (clientResponse.m_queryType == ClientQueryType::Com1Freq)
1508  {
1509  if (responseData1.isEmpty()) { return; }
1510  bool ok {};
1511  const double freqMHz = responseData1.toDouble(&ok);
1512  if (!ok) { return; }
1513  emit com1FrequencyResponseReceived(clientResponse.sender(), CFrequency(freqMHz, CFrequencyUnit::MHz()));
1514  }
1515  else if (clientResponse.m_queryType == ClientQueryType::RealName)
1516  {
1517  // The response also includes sector name and pilot rating, but we ignore them here.
1518  emit realNameResponseReceived(clientResponse.sender(), responseData1);
1519  }
1520  else if (clientResponse.m_queryType == ClientQueryType::Server)
1521  {
1522  emit serverResponseReceived(clientResponse.sender(), responseData1);
1523  }
1524  else if (clientResponse.m_queryType == ClientQueryType::ATIS)
1525  {
1526  if (responseData1.isEmpty())
1527  {
1528  // networkLog(vatSeverityDebug, "VatFsdClient::handleClientQueryResponse", "ATIS line type cannot be
1529  // empty!");
1530  }
1531  updateAtisMap(clientResponse.sender(), fromQString<AtisLineType>(responseData1), responseData2);
1532  }
1533  else if (clientResponse.m_queryType == ClientQueryType::PublicIP)
1534  {
1535  // To be implemented if needed
1536  }
1537  else if (clientResponse.m_queryType == ClientQueryType::INF)
1538  {
1539  // To be implemented if needed
1540  }
1541  else if (clientResponse.m_queryType == ClientQueryType::FP)
1542  {
1543  // FP is sent back as a $FP answer from the server and never as part of a client response.
1544  }
1545  else if (clientResponse.m_queryType == ClientQueryType::AircraftConfig)
1546  {
1547  // Currently not existing.
1548  }
1549  // NOLINTEND(bugprone-branch-clone)
1550  }
1551 
1552  void CFSDClient::handleServerError(const QStringList &tokens)
1553  {
1554  const ServerError serverError = ServerError::fromTokens(tokens);
1555  switch (serverError.m_errorNumber)
1556  {
1557  case ServerErrorCode::CallsignInUse: CLogMessage(this).error(u"The requested callsign is already taken"); break;
1558  case ServerErrorCode::InvalidCallsign: CLogMessage(this).error(u"The requested callsign is not valid"); break;
1559  case ServerErrorCode::InvalidCidPassword:
1560  CLogMessage(this).error(u"Wrong user ID or password, inactive account");
1561  break;
1562  case ServerErrorCode::InvalidRevision:
1563  CLogMessage(this).error(u"This server does not support our protocol version");
1564  break;
1565  case ServerErrorCode::ServerFull: CLogMessage(this).error(u"The server is full"); break;
1566  case ServerErrorCode::CidSuspended: CLogMessage(this).error(u"Your user account is suspended"); break;
1567  case ServerErrorCode::RatingTooLow:
1568  CLogMessage(this).error(u"You are not authorized to use the requested rating");
1569  break;
1570  case ServerErrorCode::InvalidClient:
1571  CLogMessage(this).error(u"This software is not authorized for use on this network");
1572  break;
1573  case ServerErrorCode::RequestedLevelTooHigh:
1574  CLogMessage(this).error(u"You are not authorized to use the requested pilot rating");
1575  break;
1576 
1577  case ServerErrorCode::NoError: CLogMessage(this).info(u"OK"); break;
1578  case ServerErrorCode::SyntaxError:
1579  CLogMessage(this).error(
1580  u"Malformed packet, syntax error: '%1'. This can also occur if an OBS sends frequency text messages.")
1581  << serverError.getCausingParameter();
1582  break;
1583  case ServerErrorCode::InvalidSrcCallsign:
1584  CLogMessage(this).info(u"FSD message was using an invalid callsign: %1 (%2)")
1585  << serverError.getCausingParameter() << serverError.getDescription();
1586  break;
1587  case ServerErrorCode::NoSuchCallsign:
1588  CLogMessage(this).info(u"FSD Server: no such callsign: %1 %2")
1589  << serverError.getCausingParameter() << serverError.getDescription();
1590  break;
1591  case ServerErrorCode::NoFlightPlan: CLogMessage(this).info(u"FSD Server: no flight plan"); break;
1592  case ServerErrorCode::NoWeatherProfile:
1593  CLogMessage(this).info(u"FSD Server: requested weather profile does not exist");
1594  break;
1595 
1596  // we have no idea what these mean
1597  case ServerErrorCode::AlreadyRegistered:
1598  CLogMessage(this).warning(u"Server says already registered: %1") << serverError.getDescription();
1599  break;
1600  case ServerErrorCode::InvalidCtrl:
1601  CLogMessage(this).warning(u"Server invalid control: %1") << serverError.getDescription();
1602  break;
1603  case ServerErrorCode::Unknown:
1604  CLogMessage(this).warning(u"Server sent unknown error code: %1 (%2)")
1605  << serverError.getCausingParameter() << serverError.getDescription();
1606  break;
1607  case ServerErrorCode::AuthTimeout: CLogMessage(this).warning(u"Client did not authenticate in time"); break;
1608  }
1609  if (serverError.isFatalError()) { disconnectFromServer(); }
1610  }
1611 
1612  void CFSDClient::handleRehost(const QStringList &tokens)
1613  {
1614  const Rehost rehost = Rehost::fromTokens(tokens);
1615 
1616  CLogMessage(this).info(u"Server requested we switch server to %1") << rehost.m_hostname;
1617 
1618  SWIFT_AUDIT_X(!m_rehosting, Q_FUNC_INFO, "Rehosting already in progress");
1619 
1620  m_rehosting = true;
1621  auto rehostingSocket = std::make_shared<QTcpSocket>();
1622  connect(rehostingSocket.get(), &QTcpSocket::connected, this, [this, rehostingSocket] {
1623  readDataFromSocket();
1624  CLogMessage(this).debug(u"Successfully switched server");
1625  m_socket = rehostingSocket;
1626  m_rehosting = false;
1627  rehostingSocket->disconnect(this);
1628  connectSocketSignals();
1629  readDataFromSocket();
1630  });
1631  connect(rehostingSocket.get(), &QTcpSocket::errorOccurred, this, [this, rehostingSocket] {
1632  CLogMessage(this).warning(u"Failed to switch server: %1") << rehostingSocket->errorString();
1633  m_rehosting = false;
1634  rehostingSocket->disconnect(this);
1635  if (m_socket->state() != QAbstractSocket::ConnectedState)
1636  {
1637  updateConnectionStatus(CConnectionStatus::Disconnected);
1638  }
1639  });
1640 
1641  initiateConnection(rehostingSocket, rehost.m_hostname);
1642  }
1643 
1644  void CFSDClient::handleMute(const QStringList &tokens)
1645  {
1646  const Mute mute = Mute::fromTokens(tokens);
1647  if (mute.receiver() != m_ownCallsign.asString()) { return; }
1648  emit muteRequestReceived(mute.m_mute);
1649  }
1650 
1651  void CFSDClient::initiateConnection(std::shared_ptr<QTcpSocket> rehostingSocket, const QString &rehostingHost)
1652  {
1653  const CServer server = this->getServer();
1654  const auto socket = rehostingSocket ? rehostingSocket : m_socket;
1655 
1656  // NOLINTBEGIN(cppcoreguidelines-init-variables)
1657  const QString host = rehostingSocket ? rehostingHost : server.getAddress();
1658  const quint16 port = rehostingSocket ? m_socket->peerPort() : static_cast<quint16>(getServer().getPort());
1659  // NOLINTEND(cppcoreguidelines-init-variables)
1660 
1661  resolveLoadBalancing(host, [=](const QString &host) {
1662  socket->connectToHost(host, port);
1663  if (!rehostingSocket) { this->startPositionTimers(); }
1664  });
1665  }
1666 
1667  void CFSDClient::resolveLoadBalancing(const QString &host, std::function<void(const QString &)> callback)
1668  {
1669  if (QHostAddress(host).isNull() && (getServer().getName() == "AUTOMATIC" || m_rehosting) &&
1670  getServer().getEcosystem() == CEcosystem::VATSIM)
1671  {
1672  // Not an IP -> Get IP for load balancing via HTTP
1673  Q_ASSERT_X(sApp, Q_FUNC_INFO, "Need app");
1674  CUrl url = sApp->getVatsimFsdHttpUrl();
1675  sApp->getFromNetwork(url, { this, [=](QNetworkReply *nwReplyPtr) {
1677 
1678  if (nwReply->error() == QNetworkReply::NoError)
1679  {
1680  QHostAddress addr(static_cast<QString>(nwReply->readAll()));
1681  if (!addr.isNull())
1682  {
1683  callback(addr.toString());
1684  return;
1685  }
1686  }
1687  callback(host);
1688  } });
1689  }
1690  else { callback(host); }
1691  }
1692 
1693  void CFSDClient::handleCustomPilotPacket(const QStringList &tokens)
1694  {
1695  const QString subType = tokens.at(2);
1696 
1697  if (subType == u"PIR")
1698  {
1699  PlaneInfoRequest planeInfoRequest = PlaneInfoRequest::fromTokens(tokens);
1700 
1701  const CSimulatedAircraft myAircraft = this->getOwnAircraft();
1702  const QString airlineIcao = m_server.getFsdSetup().force3LetterAirlineCodes() ?
1703  myAircraft.getAirlineIcaoCode().getDesignator() :
1704  myAircraft.getAirlineIcaoCode().getVDesignator();
1705  const QString acTypeICAO = myAircraft.getAircraftIcaoCode().getDesignator();
1706  const QString livery = this->getConfiguredLiveryString(myAircraft);
1707 
1708  sendPlaneInformation(planeInfoRequest.sender(), acTypeICAO, airlineIcao, livery);
1709  }
1710  else if (subType == "PI")
1711  {
1712  if (tokens.size() > 6 && tokens.at(3) == "X")
1713  {
1714  // This is the old version of a plane info request and no active client should ever send it.
1715  }
1716  else if (tokens.size() > 4 && tokens.at(3) == "GEN")
1717  {
1718  const PlaneInformation planeInformation = PlaneInformation::fromTokens(tokens);
1719  emit planeInformationReceived(planeInformation.sender(), planeInformation.m_aircraft,
1720  planeInformation.m_airline, planeInformation.m_livery);
1721  }
1722  }
1723  else if (subType == "I")
1724  {
1725  // SquawkBox interim pilot position. This one is producing too many precision errors. Therefore ignore it.
1726  }
1727  else if (subType == "VI")
1728  {
1729  // swift's updated interim pilot update.
1730  if (!isInterimPositionReceivingEnabledForServer()) { return; }
1731 
1732  const InterimPilotDataUpdate interimPilotDataUpdate = InterimPilotDataUpdate::fromTokens(tokens);
1733  const CCallsign callsign(interimPilotDataUpdate.sender(), CCallsign::Aircraft);
1734 
1735  CAircraftSituation situation(callsign,
1736  CCoordinateGeodetic(interimPilotDataUpdate.m_latitude,
1737  interimPilotDataUpdate.m_longitude,
1738  interimPilotDataUpdate.m_altitudeTrue),
1739  CHeading(interimPilotDataUpdate.m_heading, CHeading::True, CAngleUnit::deg()),
1740  CAngle(interimPilotDataUpdate.m_pitch, CAngleUnit::deg()),
1741  CAngle(interimPilotDataUpdate.m_bank, CAngleUnit::deg()),
1742  CSpeed(interimPilotDataUpdate.m_groundSpeed, CSpeedUnit::kts()));
1743  // NotSetGroundDetails because here we do not know if this FSD protocol actually utilizes this flag
1744  const COnGroundInfo og(interimPilotDataUpdate.m_onGround ? COnGroundInfo::OnGround :
1745  COnGroundInfo::NotOnGround,
1746  COnGroundInfo::NotSetGroundDetails);
1747  situation.setOnGroundInfo(og);
1748 
1749  // Ref T297, default offset time
1750  situation.setCurrentUtcTime();
1751  const qint64 offsetTimeMs =
1752  receivedPositionFixTsAndGetOffsetTime(situation.getCallsign(), situation.getMSecsSinceEpoch());
1753  situation.setTimeOffsetMs(offsetTimeMs);
1754 
1755  emit interimPilotDataUpdatedReceived(situation);
1756  }
1757  else if (subType == "FSIPI")
1758  {
1759  const PlaneInformationFsinn planeInformationFsinn = PlaneInformationFsinn::fromTokens(tokens);
1760  emit planeInformationFsinnReceived(planeInformationFsinn.sender(), planeInformationFsinn.m_airlineIcao,
1761  planeInformationFsinn.m_aircraftIcao,
1762  planeInformationFsinn.m_aircraftIcaoCombinedType,
1763  planeInformationFsinn.m_sendMModelString);
1764  }
1765  else if (subType == "FSIPIR")
1766  {
1767  const PlaneInfoRequestFsinn planeInfoRequestFsinn = PlaneInfoRequestFsinn::fromTokens(tokens);
1768  sendPlaneInformationFsinn(planeInfoRequestFsinn.sender());
1769  emit planeInformationFsinnReceived(planeInfoRequestFsinn.sender(), planeInfoRequestFsinn.m_airlineIcao,
1770  planeInfoRequestFsinn.m_aircraftIcao,
1771  planeInfoRequestFsinn.m_aircraftIcaoCombinedType,
1772  planeInfoRequestFsinn.m_sendMModelString);
1773  }
1774  else
1775  {
1776  // Unknown #SB opcode, just pass it on.
1777  const QString sender = tokens.at(0);
1778  const QStringList data = tokens.mid(3);
1779  emit customPilotPacketReceived(sender, data);
1780  }
1781  }
1782 
1783 #ifdef SWIFT_VATSIM_SUPPORT
1784  void CFSDClient::handleFsdIdentification(const QStringList &tokens)
1785  {
1786  if (m_protocolRevision >= PROTOCOL_REVISION_VATSIM_AUTH)
1787  {
1788  const FSDIdentification fsdIdentification = FSDIdentification::fromTokens(tokens);
1789  vatsim_auth_set_initial_challenge(m_clientAuth, qPrintable(fsdIdentification.m_initialChallenge));
1790 
1791  char fsdChallenge[33];
1792  vatsim_auth_generate_challenge(m_serverAuth, fsdChallenge);
1793  vatsim_auth_set_initial_challenge(m_serverAuth, fsdChallenge);
1794  sendClientIdentification(QString(fsdChallenge));
1795  }
1796  else
1797  {
1798  CLogMessage(this).error(
1799  u"You tried to connect to a VATSIM server without using VATSIM protocol, disconnecting!");
1800  disconnectFromServer();
1801  }
1802  }
1803 #endif
1804 
1805  void CFSDClient::handleUnknownPacket(const QString &line)
1806  {
1807  CLogMessage(this).warning(u"FSD unknown packet: '%1'") << line;
1808  }
1809 
1810  void CFSDClient::handleUnknownPacket(const QStringList &tokens) { this->handleUnknownPacket(tokens.join(", ")); }
1811 
1812  void CFSDClient::printSocketError(QAbstractSocket::SocketError socketError)
1813  {
1814  if (m_rehosting) { return; }
1815 
1816  CLogMessage(this).error(u"FSD socket error: %1") << this->socketErrorString(socketError);
1817  }
1818 
1819  void CFSDClient::handleSocketError(QAbstractSocket::SocketError socketError)
1820  {
1821  if (m_rehosting) { return; }
1822 
1823  const QString error = this->socketErrorString(socketError);
1824  switch (socketError)
1825  {
1826  // all named here need a logoff
1828  emit this->severeNetworkError(error);
1829  this->disconnectFromServer();
1830  break;
1831  default: break;
1832  }
1833  }
1834 
1835  void CFSDClient::handleSocketConnected()
1836  {
1837  if (m_protocolRevision == PROTOCOL_REVISION_CLASSIC)
1838  {
1839  this->sendLogin();
1840  this->updateConnectionStatus(CConnectionStatus::Connected);
1841  }
1842  }
1843 
1844  void CFSDClient::updateConnectionStatus(CConnectionStatus newStatus)
1845  {
1846  if (this->getConnectionStatus() == newStatus) { return; }
1847  if (newStatus.isConnected())
1848  {
1849  CEcosystem ecoSystem;
1850  {
1851  QWriteLocker l(&m_lockUserClientBuffered);
1852  m_server.setConnectedSinceNow();
1853  ecoSystem = m_server.getEcosystem();
1854  }
1855  this->setCurrentEcosystem(ecoSystem);
1856  }
1857  else
1858  {
1859  QWriteLocker l(&m_lockUserClientBuffered);
1860  m_server.markAsDisconnected();
1861  }
1862 
1863  if (newStatus.isDisconnected())
1864  {
1865  this->stopPositionTimers();
1866  this->clearState();
1867  this->setLastEcosystem(m_server.getEcosystem());
1868  this->setCurrentEcosystem(CEcosystem::NoSystem);
1869  this->saveNetworkStatistics(m_server.getName());
1870  }
1871 
1872  CConnectionStatus oldStatus;
1873  {
1874  QWriteLocker l(&m_lockConnectionStatus);
1875  oldStatus = m_connectionStatus;
1876  m_connectionStatus = newStatus;
1877  }
1878 
1879  emit this->connectionStatusChanged(oldStatus, newStatus);
1880  }
1881 
1882  void CFSDClient::consolidateTextMessage(const CTextMessage &textMessage)
1883  {
1884  if (textMessage.isSupervisorMessage()) { emit this->textMessagesReceived(textMessage); }
1885  else
1886  {
1887  m_textMessagesToConsolidate.addConsolidatedTextMessage(textMessage);
1888  m_dsSendTextMessage.inputSignal(); // trigger
1889  }
1890  }
1891 
1892  void CFSDClient::emitConsolidatedTextMessages()
1893  {
1894  emit this->textMessagesReceived(m_textMessagesToConsolidate);
1895  m_textMessagesToConsolidate.clear();
1896  }
1897 
1898  qint64 CFSDClient::receivedPositionFixTsAndGetOffsetTime(const CCallsign &callsign, qint64 markerTs)
1899  {
1900  Q_ASSERT_X(!callsign.isEmpty(), Q_FUNC_INFO, "Need callsign");
1901 
1902  if (markerTs < 0) { markerTs = QDateTime::currentMSecsSinceEpoch(); }
1903  if (!m_lastPositionUpdate.contains(callsign))
1904  {
1905  m_lastPositionUpdate.insert(callsign, markerTs);
1906  return CFsdSetup::c_positionTimeOffsetMsec;
1907  }
1908  const qint64 oldTs = m_lastPositionUpdate.value(callsign); // NOLINT(cppcoreguidelines-init-variables)
1909  m_lastPositionUpdate[callsign] = markerTs;
1910 
1911  // Ref T297, dynamic offsets
1912  const qint64 diff = qAbs(markerTs - oldTs);
1913  this->insertLatestOffsetTime(callsign, diff);
1914 
1915  int count = 0;
1916  const qint64 avgTimeMs = this->averageOffsetTimeMs(callsign, count, 3); // latest average
1917  qint64 offsetTime = CFsdSetup::c_positionTimeOffsetMsec;
1918 
1919  if (avgTimeMs < CFsdSetup::c_interimPositionTimeOffsetMsec && count >= 3)
1920  {
1921  offsetTime = CFsdSetup::c_interimPositionTimeOffsetMsec;
1922  }
1923 
1924  return m_additionalOffsetTime + offsetTime;
1925  }
1926 
1927  qint64 CFSDClient::currentOffsetTime(const CCallsign &callsign) const
1928  {
1929  Q_ASSERT_X(!callsign.isEmpty(), Q_FUNC_INFO, "Need callsign");
1930 
1931  if (!m_lastOffsetTimes.contains(callsign) || m_lastOffsetTimes[callsign].isEmpty())
1932  {
1933  return CFsdSetup::c_positionTimeOffsetMsec;
1934  }
1935  return m_lastOffsetTimes[callsign].front();
1936  }
1937 
1938  void CFSDClient::clearState()
1939  {
1940  m_rehosting = false;
1941  m_stoppedSendingVisualPositions = false;
1942  m_serverWantsVisualPositions = false;
1943  m_visualPositionUpdateSentCount = 0;
1944  m_textMessagesToConsolidate.clear();
1945  m_pendingAtisQueries.clear();
1946  m_lastPositionUpdate.clear();
1947  m_lastOffsetTimes.clear();
1948  m_atcStations.clear();
1949  m_queuedFsdMessages.clear();
1950  m_sentAircraftConfig = CAircraftParts::null();
1951  m_loginSince = -1;
1952  }
1953 
1954  void CFSDClient::clearState(const CCallsign &callsign)
1955  {
1956  if (callsign.isEmpty()) { return; }
1957  m_pendingAtisQueries.remove(callsign);
1958  m_lastPositionUpdate.remove(callsign);
1959  m_interimPositionReceivers.remove(callsign);
1960  m_lastOffsetTimes.remove(callsign);
1961  }
1962 
1963  void CFSDClient::insertLatestOffsetTime(const CCallsign &callsign, qint64 offsetMs)
1964  {
1965  QList<qint64> &offsets = m_lastOffsetTimes[callsign];
1966  offsets.push_front(offsetMs);
1967  if (offsets.size() > c_maxOffsetTimes) { offsets.removeLast(); }
1968  }
1969 
1970  qint64 CFSDClient::averageOffsetTimeMs(const CCallsign &callsign, int &count, int maxLastValues) const
1971  {
1972  const QList<qint64> &offsets = m_lastOffsetTimes[callsign];
1973  if (offsets.size() < 1) { return -1; }
1974  qint64 sum = 0;
1975  count = 0;
1976  for (qint64 v : offsets)
1977  {
1978  count++;
1979  sum += v;
1980  if (count > maxLastValues) { break; }
1981  }
1982  return qRound(static_cast<double>(sum) / count);
1983  }
1984 
1985  bool CFSDClient::isInterimPositionSendingEnabledForServer() const
1986  {
1987  const CFsdSetup::SendReceiveDetails d = this->getSetupForServer().getSendReceiveDetails();
1988  return (d & CFsdSetup::SendInterimPositions);
1989  }
1990 
1991  bool CFSDClient::isInterimPositionReceivingEnabledForServer() const
1992  {
1993  const CFsdSetup::SendReceiveDetails d = this->getSetupForServer().getSendReceiveDetails();
1994  return (d & CFsdSetup::ReceiveInterimPositions);
1995  }
1996 
1997  bool CFSDClient::isVisualPositionSendingEnabledForServer() const
1998  {
1999  const CFsdSetup::SendReceiveDetails d = this->getSetupForServer().getSendReceiveDetails();
2000  return (d & CFsdSetup::SendVisualPositions);
2001  }
2002 
2003  const CFsdSetup &CFSDClient::getSetupForServer() const { return m_server.getFsdSetup(); }
2004 
2005  void CFSDClient::maybeHandleAtisReply(const CCallsign &sender, const CCallsign &receiver, const QString &message)
2006  {
2007  Q_ASSERT(m_pendingAtisQueries.contains(sender));
2008  PendingAtisQuery &pendingQuery = m_pendingAtisQueries[sender];
2009  pendingQuery.m_atisMessage.push_back(message);
2010 
2011  // Wait maximum 5 seconds for the reply and release as text message after
2012  if (pendingQuery.m_queryTime.secsTo(QDateTime::currentDateTimeUtc()) > 5)
2013  {
2014  const QString atisMessage(pendingQuery.m_atisMessage.join(QChar::LineFeed));
2015  CTextMessage tm(atisMessage, sender, receiver);
2016  tm.setCurrentUtcTime();
2017  this->consolidateTextMessage(tm); // emit textMessagesReceived(tm);
2018  m_pendingAtisQueries.remove(sender);
2019  return;
2020  }
2021 
2022  // 4 digits followed by z (e.g. 0200z) is always the last atis line.
2023  // Some controllers leave the logoff time empty. Hence we accept anything
2024  // between 0-4 digits.
2025  thread_local const QRegularExpression reLogoff("^\\d{0,4}z$");
2026  if (reLogoff.match(message).hasMatch())
2027  {
2028  emit atisLogoffTimeReplyReceived(sender, message);
2029  CInformationMessage atisMessage(CInformationMessage::ATIS);
2030  for (const auto &line : std::as_const(pendingQuery.m_atisMessage))
2031  {
2032  if (!atisMessage.isEmpty()) atisMessage.appendMessage("\n");
2033  atisMessage.appendMessage(line);
2034  }
2035  emit atisReplyReceived(CCallsign(sender.toQString(), CCallsign::Atc), atisMessage);
2036  m_pendingAtisQueries.remove(sender);
2037  return;
2038  }
2039  }
2040 
2041  void CFSDClient::fsdMessageSettingsChanged()
2042  {
2043  if (m_rawFsdMessageLogFile.isOpen()) { m_rawFsdMessageLogFile.close(); }
2044  const CRawFsdMessageSettings setting = m_fsdMessageSetting.get();
2045  m_rawFsdMessagesEnabled = setting.areRawFsdMessagesEnabled();
2046 
2047  if (setting.getFileWriteMode() == CRawFsdMessageSettings::None || setting.getFileDir().isEmpty()) { return; }
2048  if (setting.getFileWriteMode() == CRawFsdMessageSettings::Truncate)
2049  {
2050  const QString filePath = CFileUtils::appendFilePaths(setting.getFileDir(), "rawfsdmessages.log");
2051  m_rawFsdMessageLogFile.setFileName(filePath);
2052  const bool res = m_rawFsdMessageLogFile.open(QIODevice::Text | QIODevice::WriteOnly);
2053  SWIFT_VERIFY_X(res, Q_FUNC_INFO, "Could not open log file");
2054  }
2055  else if (setting.getFileWriteMode() == CRawFsdMessageSettings::Append)
2056  {
2057  const QString filePath = CFileUtils::appendFilePaths(setting.getFileDir(), "rawfsdmessages.log");
2058  m_rawFsdMessageLogFile.setFileName(filePath);
2059  const bool res = m_rawFsdMessageLogFile.open(QIODevice::Text | QIODevice::WriteOnly | QIODevice::Append);
2060  SWIFT_VERIFY_X(res, Q_FUNC_INFO, "Could not open log file");
2061  }
2062  else if (setting.getFileWriteMode() == CRawFsdMessageSettings::Timestamped)
2063  {
2064  QString filename("rawfsdmessages");
2065  filename += QLatin1String("_");
2066  filename += QDateTime::currentDateTime().toString(QStringLiteral("yyMMddhhmmss"));
2067  filename += QLatin1String(".log");
2068  const QString filePath = CFileUtils::appendFilePaths(setting.getFileDir(), filename);
2069  m_rawFsdMessageLogFile.setFileName(filePath);
2070  const bool res = m_rawFsdMessageLogFile.open(QIODevice::Text | QIODevice::WriteOnly);
2071  SWIFT_VERIFY_X(res, Q_FUNC_INFO, "Could not open log file");
2072  }
2073  }
2074 
2075  swift::misc::aviation::CCallsignSet CFSDClient::getInterimPositionReceivers() const
2076  {
2077  return m_interimPositionReceivers;
2078  }
2079 
2080  void CFSDClient::setInterimPositionReceivers(const swift::misc::aviation::CCallsignSet &interimPositionReceivers)
2081  {
2082  m_interimPositionReceivers = interimPositionReceivers;
2083  }
2084 
2085  bool CFSDClient::isPendingConnection() const
2086  {
2087  return m_connectionStatus.isConnecting() || m_connectionStatus.isDisconnecting();
2088  }
2089 
2090  int CFSDClient::increaseStatisticsValue(const QString &identifier, const QString &appendix)
2091  {
2092  if (identifier.isEmpty() || !m_statistics) { return -1; }
2093 
2094  QWriteLocker l(&m_lockStatistics);
2095  const QString i = appendix.isEmpty() ? identifier : identifier % u"." % appendix;
2096  int &v = m_callStatistics[i];
2097  v++;
2098 
2099  constexpr int MaxTimeValues = 50;
2100  m_callByTime.push_front(QPair<qint64, QString>(QDateTime::currentMSecsSinceEpoch(), i));
2101  if (m_callByTime.size() > MaxTimeValues) { m_callByTime.removeLast(); }
2102  return v;
2103  }
2104 
2105  int CFSDClient::increaseStatisticsValue(const QString &identifier, int value)
2106  {
2107  return increaseStatisticsValue(identifier, QString::number(value));
2108  }
2109 
2110  void CFSDClient::clearStatistics()
2111  {
2112  QWriteLocker l(&m_lockStatistics);
2113  m_callStatistics.clear();
2114  m_callByTime.clear();
2115  }
2116 
2117  QString CFSDClient::getNetworkStatisticsAsText(bool reset, const QString &separator)
2118  {
2119  QVector<std::pair<int, QString>> transformed;
2120  QMap<QString, int> callStatistics;
2121  QVector<QPair<qint64, QString>> callByTime;
2122 
2123  {
2124  QReadLocker l(&m_lockStatistics);
2125  callStatistics = m_callStatistics;
2126  callByTime = m_callByTime;
2127  }
2128 
2129  if (callStatistics.isEmpty()) { return {}; }
2130  for (const auto [key, value] : makePairsRange(std::as_const(callStatistics)))
2131  {
2132  // key is pair.first, value is pair.second
2133  transformed.push_back({ value, key });
2134  }
2135 
2136  // sorted by value
2137  std::sort(transformed.begin(), transformed.end(), std::greater<>());
2138  QString stats;
2139  for (const auto &pair : transformed)
2140  {
2141  stats += (stats.isEmpty() ? QString() : separator) % pair.second % u": " % QString::number(pair.first);
2142  }
2143 
2144  for (const auto &pair : transformed)
2145  {
2146  stats += (stats.isEmpty() ? QString() : separator) % pair.second % u": " % QString::number(pair.first);
2147  }
2148 
2149  if (!callByTime.isEmpty())
2150  {
2151  const qint64 lastTs = callByTime.front().first;
2152  for (const auto &pair : std::as_const(callByTime))
2153  {
2154  const qint64 deltaTs = lastTs - pair.first;
2155  stats += separator % QStringLiteral("%1").arg(deltaTs, 5, 10, QChar('0')) % u": " % pair.second;
2156  }
2157  }
2158 
2159  if (reset) { this->clearStatistics(); }
2160  return stats;
2161  }
2162 
2163  void CFSDClient::gracefulShutdown()
2164  {
2165  disconnectFromServer(); // async, runs in background thread
2166  quitAndWait();
2167  }
2168 
2169  void CFSDClient::readDataFromSocketMaxLines(int maxLines)
2170  {
2171  if (m_socket->bytesAvailable() < 1) { return; }
2172 
2173  int lines = 0;
2174 
2175  // reads at least one line if available
2176  while (m_socket->canReadLine())
2177  {
2178  const QByteArray dataEncoded = m_socket->readLine();
2179  if (dataEncoded.isEmpty()) { continue; }
2180  const QString data = m_decoder(dataEncoded);
2181  this->parseMessage(data);
2182  lines++;
2183 
2184  static constexpr int MaxLines = 75 - 1;
2185  if (maxLines < 0) { maxLines = MaxLines; }
2186 
2187  if (lines > maxLines)
2188  {
2189  static constexpr int DelayMs = 10;
2190  const int newMax = qRound(1.2 * lines); // 20% more
2191 
2192  CLogMessage(this).debug(u"ReadDataFromSocket has too many lines (>%1), will read again in %2ms")
2193  << MaxLines << DelayMs;
2194  QPointer<CFSDClient> myself(this);
2195  QTimer::singleShot(DelayMs, this, [=] {
2196  if (!sApp || sApp->isShuttingDown()) { return; }
2197  if (myself) { myself->readDataFromSocketMaxLines(newMax); }
2198  });
2199  break;
2200  }
2201  }
2202  }
2203 
2204  QString CFSDClient::socketErrorString(QAbstractSocket::SocketError error) const
2205  {
2206  QString e = CFSDClient::socketErrorToQString(error);
2207  if (!m_socket->errorString().isEmpty()) { e += QStringLiteral(": ") % m_socket->errorString(); }
2208  return e;
2209  }
2210 
2211  QString CFSDClient::socketErrorToQString(QAbstractSocket::SocketError error)
2212  {
2213  static const QMetaEnum metaEnum = QMetaEnum::fromType<QAbstractSocket::SocketError>();
2214  return metaEnum.valueToKey(error);
2215  }
2216 
2217  void CFSDClient::parseMessage(const QString &lineRaw)
2218  {
2219  MessageType messageType = MessageType::Unknown;
2220  QString cmd;
2221  const QString line = lineRaw.trimmed();
2222 
2223  if (m_printToConsole) { qDebug() << "FSD Recv=>" << line; }
2224  emitRawFsdMessage(line, false);
2225 
2226  for (const QString &str : makeKeysRange(std::as_const(m_messageTypeMapping)))
2227  {
2228  if (line.startsWith(str))
2229  {
2230  cmd = str;
2231  messageType = m_messageTypeMapping.value(str, MessageType::Unknown);
2232  break;
2233  }
2234  }
2235 
2236  // statistics
2237  if (m_statistics)
2238  {
2239  increaseStatisticsValue(QStringLiteral("parseMessage"), this->messageTypeToString(messageType));
2240  }
2241 
2242  if (messageType != MessageType::Unknown)
2243  {
2244  // Cutoff the cmd from the beginning
2245  const QString payload = line.mid(cmd.size()).trimmed();
2246 
2247  // We expected a payload, but there is nothing
2248  if (payload.length() == 0) { return; }
2249 
2250  const QStringList tokens = payload.split(':');
2251  switch (messageType)
2252  {
2253  // ignored ones
2254  case MessageType::AddAtc:
2255  case MessageType::AddPilot:
2256  case MessageType::ServerHeartbeat:
2257  case MessageType::ProController:
2258  case MessageType::ClientIdentification: break;
2259 
2260  // handled ones
2261  case MessageType::AtcDataUpdate: handleAtcDataUpdate(tokens); break;
2262 #ifdef SWIFT_VATSIM_SUPPORT
2263  case MessageType::AuthChallenge: handleAuthChallenge(tokens); break;
2264  case MessageType::AuthResponse: handleAuthResponse(tokens); break;
2265 #endif
2266  case MessageType::ClientQuery: handleClientQuery(tokens); break;
2267  case MessageType::ClientResponse: handleClientResponse(tokens); break;
2268  case MessageType::DeleteATC: handleDeleteATC(tokens); break;
2269  case MessageType::DeletePilot: handleDeletePilot(tokens); break;
2270  case MessageType::FlightPlan: handleFlightPlan(tokens); break;
2271 #ifdef SWIFT_VATSIM_SUPPORT
2272  case MessageType::FsdIdentification: handleFsdIdentification(tokens); break;
2273 #endif
2274  case MessageType::KillRequest: handleKillRequest(tokens); break;
2275  case MessageType::PilotDataUpdate: handlePilotDataUpdate(tokens); break;
2276  case MessageType::Ping: handlePing(tokens); break;
2277  case MessageType::Pong: handlePong(tokens); break;
2278  case MessageType::ServerError: handleServerError(tokens); break;
2279  case MessageType::TextMessage: handleTextMessage(tokens); break;
2280  case MessageType::PilotClientCom: handleCustomPilotPacket(tokens); break;
2281  case MessageType::VisualPilotDataUpdate:
2282  case MessageType::VisualPilotDataPeriodic:
2283  case MessageType::VisualPilotDataStopped: handleVisualPilotDataUpdate(tokens, messageType); break;
2284  case MessageType::VisualPilotDataToggle: handleVisualPilotDataToggle(tokens); break;
2285  case MessageType::EuroscopeSimData: handleEuroscopeSimData(tokens); break;
2286  case MessageType::Rehost: handleRehost(tokens); break;
2287  case MessageType::Mute: handleMute(tokens); break;
2288 
2289  // normally we should not get here
2290  default:
2291  case MessageType::Unknown: handleUnknownPacket(tokens); break;
2292  }
2293  }
2294  else { handleUnknownPacket(line); }
2295  }
2296 
2297  void CFSDClient::emitRawFsdMessage(const QString &fsdMessage, bool isSent)
2298  {
2299  if (!m_unitTestMode && !m_rawFsdMessagesEnabled) { return; }
2300  QString fsdMessageFiltered(fsdMessage);
2301  if (m_filterPasswordFromLogin)
2302  {
2303  if (fsdMessageFiltered.startsWith("#AP"))
2304  {
2305  thread_local const QRegularExpression re(R"(^(#AP\w+:SERVER:\d+:)[^:]+(:\d+:\d+:\d+:.+)$)");
2306  fsdMessageFiltered.replace(re, "\\1<password>\\2");
2307  m_filterPasswordFromLogin = false;
2308  }
2309  }
2310 
2311  const QString prefix = isSent ? "FSD Sent=>" : "FSD Recv=>";
2312  CRawFsdMessage rawMessage(prefix + fsdMessageFiltered);
2313  rawMessage.setCurrentUtcTime();
2314  if (m_rawFsdMessageLogFile.isOpen())
2315  {
2316  QTextStream stream(&m_rawFsdMessageLogFile);
2317  stream << rawMessage.toQString().trimmed() << Qt::endl;
2318  }
2319  emit rawFsdMessage(rawMessage);
2320  }
2321 
2322  bool CFSDClient::saveNetworkStatistics(const QString &server)
2323  {
2324  if (m_callStatistics.isEmpty()) { return false; }
2325 
2326  const QString s = this->getNetworkStatisticsAsText(false, "\n");
2327  if (s.isEmpty()) { return false; }
2328  const QString fn = QStringLiteral("networkstatistics_%1_%2.log")
2329  .arg(QDateTime::currentDateTimeUtc().toString("yyMMddhhmmss"), server);
2330  const QString fp = CFileUtils::appendFilePaths(CSwiftDirectories::logDirectory(), fn);
2331  return CFileUtils::writeStringToFile(s, fp);
2332  }
2333 
2334  void CFSDClient::startPositionTimers()
2335  {
2336  m_positionUpdateTimer.start(c_updatePositionIntervalMsec);
2337  m_scheduledConfigUpdate.start(c_processingIntervalMsec);
2338  m_fsdSendMessageTimer.start(c_sendFsdMsgIntervalMsec);
2339  m_queuedFsdMessages.clear(); // clear everything before the timer is started
2340 
2341  // interim positions
2342  // NOLINTBEGIN(bugprone-branch-clone)
2343  if (this->isInterimPositionSendingEnabledForServer())
2344  {
2345  m_interimPositionUpdateTimer.start(c_updateInterimPositionIntervalMsec);
2346  }
2347  else { m_interimPositionUpdateTimer.stop(); }
2348  if (this->isVisualPositionSendingEnabledForServer())
2349  {
2350  m_visualPositionUpdateTimer.start(c_updateVisualPositionIntervalMsec);
2351  }
2352  else { m_visualPositionUpdateTimer.stop(); }
2353  // NOLINTEND(bugprone-branch-clone)
2354  }
2355 
2356  void CFSDClient::stopPositionTimers()
2357  {
2358  m_positionUpdateTimer.stop();
2359  m_interimPositionUpdateTimer.stop();
2360  m_visualPositionUpdateTimer.stop();
2361  m_scheduledConfigUpdate.stop();
2362  m_fsdSendMessageTimer.stop();
2363  }
2364 
2365  void CFSDClient::updateAtisMap(const QString &callsign, AtisLineType type, const QString &line)
2366  {
2367  // NOLINTBEGIN(bugprone-branch-clone)
2368  if (type == AtisLineType::VoiceRoom)
2369  {
2370  m_mapAtisMessages[callsign].m_voiceRoom = line;
2371  m_mapAtisMessages[callsign].m_lineCount++;
2372  return;
2373  }
2374  if (type == AtisLineType::TextMessage)
2375  {
2376  m_mapAtisMessages[callsign].m_textLines.push_back(line);
2377  m_mapAtisMessages[callsign].m_lineCount++;
2378  return;
2379  }
2380  if (type == AtisLineType::ZuluLogoff)
2381  {
2382  m_mapAtisMessages[callsign].m_zuluLogoff = line;
2383  m_mapAtisMessages[callsign].m_lineCount++;
2384  return;
2385  }
2386  if (!m_mapAtisMessages.contains(callsign)) { return; }
2387 
2388  // Ignore the check for line count.
2389  m_mapAtisMessages[callsign].m_lineCount++;
2390 
2391  const CCallsign cs(callsign, CCallsign::Atc);
2392  // emit atisVoiceRoomReplyReceived(cs, m_mapAtisMessages[callsign].voiceRoom);
2393  emit atisLogoffTimeReplyReceived(cs, m_mapAtisMessages[callsign].m_zuluLogoff);
2394 
2395  CInformationMessage atisMessage(CInformationMessage::ATIS);
2396  for (const QString &tm : std::as_const(m_mapAtisMessages[callsign].m_textLines))
2397  {
2398  const QString fixed = tm.trimmed();
2399  if (!fixed.isEmpty())
2400  {
2401  // detect the stupid z1, z2, z3 placeholders
2403  thread_local const QRegularExpression RegExp(R"([\n\t\r])");
2404  const QString test = fixed.toLower().remove(RegExp);
2405  if (test == "z") return;
2406  if (test.startsWith("z") && test.length() == 2) return; // z1, z2, ..
2407  if (test.length() == 1) return; // sometimes just z
2408 
2409  // append
2410  if (!atisMessage.isEmpty()) atisMessage.appendMessage("\n");
2411  atisMessage.appendMessage(fixed);
2412  }
2413  }
2414 
2415  emit this->atisReplyReceived(cs, atisMessage);
2416 
2417  m_mapAtisMessages.remove(callsign);
2418  // NOLINTEND(bugprone-branch-clone)
2419  }
2420 
2421  void CFSDClient::pendingTimeoutCheck()
2422  {
2423  if (!this->isPendingConnection()) { return; }
2424 
2425  const qint64 age = QDateTime::currentMSecsSinceEpoch() - m_loginSince;
2426  if (age < PendingConnectionTimeoutMs) { return; }
2427 
2428  // time out
2429  CLogMessage(this).warning(u"Timeout on pending connection to '%1'") << this->getServer().getName();
2430  this->disconnectFromServer();
2431  }
2432 
2433  CLength CFSDClient::fixAtcRange(const CLength &networkRange, const CCallsign &cs)
2434  {
2444  // ATIS often have a range of 0 nm. Correct this to a proper value.
2445  const QString suffix = cs.getSuffix();
2446  if (suffix.contains(QStringLiteral("ATIS"), Qt::CaseInsensitive))
2447  {
2448  static const CLength l_Atis(150.0, CLengthUnit::NM());
2449  return maxOrNotNull(networkRange, l_Atis);
2450  }
2451  if (suffix.contains(QStringLiteral("GND"), Qt::CaseInsensitive))
2452  {
2453  static const CLength l_Gnd(10.0, CLengthUnit::NM());
2454  return maxOrNotNull(networkRange, l_Gnd);
2455  }
2456  if (suffix.contains(QStringLiteral("TWR"), Qt::CaseInsensitive))
2457  {
2458  static const CLength l_Twr(25.0, CLengthUnit::NM());
2459  return maxOrNotNull(networkRange, l_Twr);
2460  }
2461  if (suffix.contains(QStringLiteral("DEP"), Qt::CaseInsensitive))
2462  {
2463  static const CLength l_Dep(150.0, CLengthUnit::NM());
2464  return maxOrNotNull(networkRange, l_Dep);
2465  }
2466  if (suffix.contains(QStringLiteral("APP"), Qt::CaseInsensitive))
2467  {
2468  static const CLength l_App(150.0, CLengthUnit::NM());
2469  return maxOrNotNull(networkRange, l_App);
2470  }
2471  if (suffix.contains(QStringLiteral("CTR"), Qt::CaseInsensitive))
2472  {
2473  static const CLength l_Ctr(300.0, CLengthUnit::NM());
2474  return maxOrNotNull(networkRange, l_Ctr);
2475  }
2476  if (suffix.contains(QStringLiteral("FSS"), Qt::CaseInsensitive))
2477  {
2478  static const CLength l_Fss(1500.0, CLengthUnit::NM());
2479  return maxOrNotNull(networkRange, l_Fss);
2480  }
2481 
2482  return networkRange;
2483  }
2484 
2485  CLength CFSDClient::maxOrNotNull(const CLength &l1, const CLength &l2)
2486  {
2487  if (l1.isNull()) { return l2; }
2488  if (l2.isNull()) { return l1; }
2489  return (l2 > l1) ? l2 : l1;
2490  }
2491 
2492  QString CFSDClient::noColons(const QString &input)
2493  {
2494  if (!input.contains(':')) { return input; }
2495  QString copy(input);
2496  return copy.remove(':');
2497  }
2498 
2499  FlightType CFSDClient::getFlightType(CFlightPlan::FlightRules flightRules)
2500  {
2501  switch (flightRules)
2502  {
2503  case CFlightPlan::IFR: return FlightType::IFR;
2504  case CFlightPlan::VFR: return FlightType::VFR;
2505  case CFlightPlan::SVFR: return FlightType::SVFR;
2506  case CFlightPlan::DVFR: return FlightType::DVFR;
2507  default: return FlightType::IFR;
2508  }
2509  }
2510 
2511  const QString &CFSDClient::messageTypeToString(MessageType mt) const
2512  {
2513  QHash<QString, MessageType>::const_iterator i = m_messageTypeMapping.constBegin();
2514  while (i != m_messageTypeMapping.constEnd())
2515  {
2516  if (i.value() == mt) { return i.key(); }
2517  ++i;
2518  }
2519 
2520  static const QString empty;
2521  return empty;
2522  }
2523 
2524  void CFSDClient::handleIllegalFsdState(const QString &message)
2525  {
2526  if (CBuildConfig::isLocalDeveloperDebugBuild()) { SWIFT_VERIFY_X(false, Q_FUNC_INFO, "Illegal FSD state"); }
2527  CLogMessage(this).warning(message);
2528  }
2529 
2530  const QJsonObject &CFSDClient::JsonPackets::aircraftConfigRequest()
2531  {
2532  static const QJsonObject jsonObject { { "request", "full" } };
2533  return jsonObject;
2534  }
2535 
2536 } // namespace swift::core::fsd
SWIFT_CORE_EXPORT swift::core::CApplication * sApp
Single instance of application object.
Definition: application.cpp:71
QNetworkReply * getFromNetwork(const swift::misc::network::CUrl &url, const CallbackSlot &callback, int maxRedirects=DefaultMaxRedirects)
Request to get network reply.
data::CGlobalSetup getGlobalSetup() const
Global setup.
static constexpr int NoLogRequestId
network request without logging
Definition: application.h:413
swift::misc::db::CDistribution getOwnDistribution() const
Own distribution.
QNetworkReply * postToNetwork(const QNetworkRequest &request, int logId, const QByteArray &data, const CallbackSlot &callback)
Post to network.
swift::misc::network::CUrl getVatsimFsdHttpUrl() const
Get VATSIM FSD HTTP URL.
bool isShuttingDown() const
Is application shutting down?
swift::misc::network::CUrl getVatsimAuthUrl() const
VATSIM auth URL.
Definition: globalsetup.h:143
FSD Message: Add Pilot.
Definition: addpilot.h:19
void setCallsign(const swift::misc::aviation::CCallsign &callsign)
Preset functions.
Definition: fsdclient.cpp:159
void setIcaoCodes(const swift::misc::simulation::CSimulatedAircraft &ownAircraft)
Preset functions.
Definition: fsdclient.cpp:169
void sendClientQueryCom1Freq(const swift::misc::aviation::CCallsign &callsign)
Definition: fsdclient.cpp:459
void sendFlightPlan(const swift::misc::aviation::CFlightPlan &flightPlan)
Definition: fsdclient.cpp:668
bool isConnected() const
Connection status.
Definition: fsdclient.h:254
void sendTextMessages(const swift::misc::network::CTextMessageList &messages)
Definition: fsdclient.cpp:567
void sendClientQueryAtis(const swift::misc::aviation::CCallsign &callsign)
Definition: fsdclient.cpp:474
void setServer(const swift::misc::network::CServer &server)
Preset functions.
Definition: fsdclient.cpp:140
void setLiveryAndModelString(const QString &livery, bool sendLiveryString, const QString &modelString, bool sendModelString)
Preset functions.
Definition: fsdclient.cpp:188
const swift::misc::network::CServer & getServer() const
Get the server.
Definition: fsdclient.h:161
swift::misc::network::CLoginMode getLoginMode() const
Mode.
Definition: fsdclient.h:189
void disconnectFromServer()
Connect/disconnect.
Definition: fsdclient.cpp:263
void sendPlaneInfoRequest(const swift::misc::aviation::CCallsign &receiver)
Definition: fsdclient.cpp:718
void sendClientQueryRealName(const swift::misc::aviation::CCallsign &callsign)
Definition: fsdclient.cpp:464
PilotRating getPilotRating() const
Rating.
Definition: fsdclient.h:197
QStringList getPresetValues() const
List of all preset values.
Definition: fsdclient.cpp:217
void sendClientQueryServer(const swift::misc::aviation::CCallsign &callsign)
Definition: fsdclient.cpp:469
void setSimType(const swift::misc::simulation::CSimulatorInfo &simInfo)
Preset functions.
Definition: fsdclient.cpp:198
void sendClientQueryAircraftConfig(const swift::misc::aviation::CCallsign &callsign)
Definition: fsdclient.cpp:484
void textMessageSent(const swift::misc::network::CTextMessage &sentMessage)
We have sent a text message.
void sendTextMessage(const swift::misc::network::CTextMessage &message)
Definition: fsdclient.cpp:613
void sendClientQueryFlightPlan(const swift::misc::aviation::CCallsign &callsign)
Definition: fsdclient.cpp:479
void sendRadioMessage(const QVector< int > &frequencieskHz, const QString &message)
Definition: fsdclient.cpp:655
void connectToServer()
Connect/disconnect.
Definition: fsdclient.cpp:229
void sendPlaneInfoRequestFsinn(const swift::misc::aviation::CCallsign &callsign)
Definition: fsdclient.cpp:733
swift::misc::network::CConnectionStatus getConnectionStatus() const
Connection status.
Definition: fsdclient.h:249
bool isDisconnected() const
Connection status.
Definition: fsdclient.h:255
FSD Message: flightplan.
Definition: flightplan.h:16
FSinn specific version of plane information request.
Request to send plane information. Shall be answered by a PlaneInformation message.
This packet is sent in reply to a PIR request to inform the client which multiplayer model to use....
Text, radio or private message.
Definition: textmessage.h:19
QString getFileDir() const
Get file directory.
FileWriteMode getFileWriteMode() const
Get file write mode.
bool areRawFsdMessagesEnabled() const
Are raw FSD messages enabled?
Base class for a long-lived worker object which lives in its own thread.
Definition: worker.h:300
Class for emitting a log message.
Definition: logmessage.h:27
Derived & warning(const char16_t(&format)[N])
Set the severity to warning, providing a format string.
Derived & log(StatusSeverity s, const char16_t(&m)[N])
Set the severity and format string.
Derived & error(const char16_t(&format)[N])
Set the severity to error, providing a format string.
Derived & debug()
Set the severity to debug.
Derived & info(const char16_t(&format)[N])
Set the severity to info, providing a format string.
bool isEmpty() const
Synonym for empty.
Definition: sequence.h:285
Callable wrapper for a member function with function signature F.
Definition: slot.h:62
Streamable status message, e.g.
static bool isInThisThread(const QObject *toBeTested)
Is the current thread the object's thread?
Definition: threadutils.cpp:16
qint64 getMSecsSinceEpoch() const
Timestamp as ms value.
void setCurrentUtcTime()
Set the current time as timestamp.
void setTimeOffsetMs(qint64 offset)
Milliseconds to add to timestamp for interpolation.
const QString & getDesignator() const
Get ICAO designator, e.g. "B737".
Value object encapsulating information of aircraft's parts.
Definition: aircraftparts.h:26
void setOnGround(bool onGround)
Set aircraft on ground.
void setGearDown(bool down)
Set gear down.
Definition: aircraftparts.h:97
void setLights(const CAircraftLights &lights)
Set aircraft lights.
Definition: aircraftparts.h:81
Value object encapsulating information of an aircraft's situation.
void setPressureAltitude(const CAltitude &altitude)
Set pressure altitude.
geo::CLongitude longitude() const
Longitude.
geo::CLatitude latitude() const
Latitude.
const CCallsign & getCallsign() const
Corresponding callsign.
void setOnGroundInfo(const aviation::COnGroundInfo &info)
Set the on ground info.
const CAltitude & getAltitude() const
Get altitude.
const QString & getDesignator() const
Get airline, e.g. "DLH".
QString getVDesignator() const
Get airline, e.g. "DLH", but "VMVA" for virtual airlines.
const QString & asString() const
Get code.
Altitude as used in aviation, can be AGL or MSL altitude.
Definition: altitude.h:52
QString asFpVatsimAltitudeString() const
As simple VATSIM string, only FLxxx or altitude as ft.
Definition: altitude.cpp:363
void parseFromString(const QString &value)
Parse value from string.
Definition: altitude.cpp:140
Value object for a list of ATC stations.
CAtcStationList findIfFrequencyIsWithinSpacing(const physical_quantities::CFrequency &frequency)
Find 0..n stations with frequency (with 5 kHz spacing for .x20/.x25 and .x70/.x75)
Value object encapsulating information of a callsign.
Definition: callsign.h:30
const QString & asString() const
Get callsign (normalized)
Definition: callsign.h:96
QString getSuffix() const
Get the callsign suffix ("TWR", "ATIS" ...) if any ("_" is removed)
Definition: callsign.cpp:212
bool isEmpty() const
Is empty?
Definition: callsign.h:63
QString getFsdCallsignString() const
The callsign string used with FSD.
Definition: callsign.cpp:176
Value object for a set of callsigns.
Definition: callsignset.h:26
Flightplan-related information about an aircraft (aircraft ICAO, equipment and WTC)
QString asIcaoString() const
Full string in ICAO format: "AIRCRAFT_ICAO/WTC-EQUIPMENT/SSR".
QString asFaaString() const
Full string in FAA format: "H/J (if heavy/super)/AIRCRAFT_ICAO/EQUIPMENT-CODE".
Value object for a flight plan.
Definition: flightplan.h:148
const physical_quantities::CTime & getFuelTime() const
Get amount of fuel load in time.
Definition: flightplan.h:339
const physical_quantities::CSpeed & getCruiseTrueAirspeed() const
Get planned cruise TAS.
Definition: flightplan.h:354
const QString & getRoute() const
Get route string.
Definition: flightplan.h:363
FlightRules
Flight rules (VFR or IFR)
Definition: flightplan.h:155
const QDateTime & getTakeoffTimePlanned() const
Get planned takeoff time (planned)
Definition: flightplan.h:318
FlightRules getFlightRules() const
Get flight rules as in FlightRules.
Definition: flightplan.h:357
const QString & getRemarks() const
Get remarks string.
Definition: flightplan.h:375
const CAirportIcaoCode & getAlternateAirportIcao() const
Get alternate destination airport ICAO code.
Definition: flightplan.h:315
const CAltitude & getCruiseAltitude() const
Cruising altitudes.
Definition: flightplan.h:348
const physical_quantities::CTime & getEnrouteTime() const
Get planned enroute flight time.
Definition: flightplan.h:330
const CAirportIcaoCode & getOriginAirportIcao() const
Get origin airport ICAO code.
Definition: flightplan.h:309
const CAirportIcaoCode & getDestinationAirportIcao() const
Get destination airport ICAO code.
Definition: flightplan.h:312
const QDateTime & getTakeoffTimeActual() const
Get actual takeoff time (actual)
Definition: flightplan.h:324
CFlightPlanAircraftInfo getAircraftInfo() const
Get ICAO aircraft NAV/COM equipment.
Definition: flightplan.h:384
Heading as used in aviation, can be true or magnetic heading.
Definition: heading.h:41
Value object encapsulating information message (ATIS, METAR, TAF)
Information about the ground status.
Definition: ongroundinfo.h:19
bool isRestricted() const
Restricted channel?
Definition: distribution.h:66
CONTAINER findClosest(int number, const ICoordinateGeodetic &coordinate) const
Find 0..n objects closest to the given coordinate.
QString toQString(bool i18n=false) const
Cast as QString.
Definition: mixinstring.h:74
Class which can be directly used to access an.
Value object encapsulating information about a connection status.
bool isConnected() const
Query status.
bool isDisconnected() const
Query status.
Ecosystem of server belonging together.
Definition: ecosystem.h:21
Value object for a FSD setup.
Definition: fsdsetup.h:24
bool shouldSendFlightPlanEquipmentInIcaoFormat() const
FSD setup flags.
Definition: fsdsetup.h:110
const QString & getTextCodec() const
Get codec.
Definition: fsdsetup.h:77
bool receiveEuroscopeSimData() const
FSD setup flags.
Definition: fsdsetup.h:109
Value object encapsulating information about login mode.
Definition: loginmode.h:18
bool isObserver() const
Is login as observer?
Definition: loginmode.h:37
bool isPilot() const
Is login as pilot?
Definition: loginmode.h:34
Value object for a raw FSD message.
Definition: rawfsdmessage.h:25
Value object encapsulating information of a server.
Definition: server.h:28
const CFsdSetup & getFsdSetup() const
Get FSD setup.
Definition: server.h:140
const QString & getAddress() const
Get address.
Definition: server.h:80
const CUser & getUser() const
Get user.
Definition: server.h:86
ServerType getServerType() const
Get server type.
Definition: server.h:164
Value object encapsulating information of a text message.
Definition: textmessage.h:31
bool isEmpty() const
Empty message.
Definition: textmessage.h:90
bool isSupervisorMessage() const
Supervisor message?
Definition: textmessage.cpp:58
Value object encapsulating a list of text messages.
CTextMessageList getPrivateMessages() const
Private messages.
CTextMessageList getRadioMessages() const
Public messages.
CTextMessageList markedAsSent()
Marked as sent.
Value object encapsulating information of a location, kind of simplified CValueObject compliant versi...
Definition: url.h:27
const QString & getPassword() const
Get password.
Definition: user.h:65
const QString & getRealName() const
Get full name.
Definition: user.h:59
const QString & getId() const
Get id.
Definition: user.h:119
QString getRealNameAndHomeBase(const QString &separator=QString(" ")) const
Real name + homebase.
Definition: user.cpp:45
Direct in memory access to client (network client) data.
Physical unit angle (radians, degrees)
Definition: angle.h:23
Physical unit length (length)
Definition: length.h:18
int valueInteger(MU unit) const
As integer value.
double value(MU unit) const
Value in given unit.
QList< int > getHrsMinSecParts() const
Parts hh, mm, ss.
Definition: time.cpp:89
QString getSwiftLiveryString(bool aircraftIcao=true, bool livery=true, bool model=true) const
swift livery string (to be sent via network)
Delegating class which can be directly used to access an.
bool updateOwnCallsign(const aviation::CCallsign &callsign)
Update aircraft's callsign.
aviation::CCallsign getOwnCallsign() const
Own aircraft's callsign.
bool updateOwnIcaoCodes(const aviation::CAircraftIcaoCode &aircraftIcaoData, const aviation::CAirlineIcaoCode &airlineIcaoCode)
Update ICAO data.
swift::misc::geo::CCoordinateGeodetic getOwnAircraftPosition() const
Own aircraft's position.
CSimulatedAircraft getOwnAircraft() const
Own aircraft.
Class which can be directly used to access an.
Comprehensive information of an aircraft.
const QString & getAirlineIcaoCodeDesignator() const
Airline ICAO code designator.
const aviation::CAircraftIcaoCode & getAircraftIcaoCode() const
Get aircraft ICAO info.
const QString & getAircraftIcaoCombinedType() const
Aircraft ICAO combined code.
const QString & getAircraftIcaoCodeDesignator() const
Aircraft ICAO code designator.
const simulation::CAircraftModel & getModel() const
Get model (model used for mapping)
const aviation::CAirlineIcaoCode & getAirlineIcaoCode() const
Airline ICAO code if any.
const QString & getModelString() const
Get model string.
QString getSimulatorNameAndVersion() const
Version and simulator details info.
Simple hardcoded info about the corresponding simulator.
Definition: simulatorinfo.h:41
Simulator getSimulator() const
Simulator.
Direct threadsafe in memory access to own aircraft.
Direct thread safe in memory access to remote aircraft.
FlightType
Flight types.
Definition: enums.h:90
PilotRating
Pilot ratings.
Definition: enums.h:33
ClientQueryType
Client query types.
Definition: enums.h:72
@ EuroscopeSimData
Broadcast to announce we request SIMDATA packets.
Capabilities
Client capability flags *‍/.
Definition: enums.h:130
TextMessageGroups
Message groups.
Definition: fsdclient.h:66
constexpr int PROTOCOL_REVISION_VATSIM_AUTH
Protocol version.
Definition: fsdclient.h:52
constexpr int PROTOCOL_REVISION_VATSIM_VELOCITY
Protocol version.
Definition: fsdclient.h:53
constexpr int PROTOCOL_REVISION_CLASSIC
Protocol version.
Definition: fsdclient.h:50
MessageType
Message type.
Definition: messagebase.h:19
Free functions in swift::misc.
auto makeKeysRange(const T &container)
Returns a const CRange for iterating over the keys of a Qt associative container.
Definition: range.h:353
bool is09OnlyString(const QString &testString)
String with 0-9 only.
Definition: stringutils.h:173
StatusSeverity
Status severities.
Definition: statusmessage.h:35
auto makePairsRange(const T &container)
Returns a const CRange for iterating over the keys and values of a Qt associative container.
Definition: range.h:374
SWIFT_MISC_EXPORT QDateTime fromStringUtc(const QString &dateTimeString, const QString &format)
Same as QDateTime::fromString but QDateTime will be set to UTC.
void errorOccurred(QAbstractSocket::SocketError socketError)
bool isEmpty() const const
QDateTime currentDateTime()
QDateTime currentDateTimeUtc()
qint64 currentMSecsSinceEpoch()
QString toString(QStringView format) const const
QDateTime toUTC() const const
QHash< Key, T >::iterator insert(const Key &key, const T &value)
Key key(const T &value) const const
T value(const Key &key) const const
void readyRead()
QJsonDocument fromJson(const QByteArray &json, QJsonParseError *error)
QJsonObject object() const const
QByteArray toJson(QJsonDocument::JsonFormat format) const const
QJsonObject::iterator insert(QLatin1StringView key, const QJsonValue &value)
bool isEmpty() const const
QJsonValue value(QLatin1StringView key) const const
QString errorString() const const
bool isString() const const
bool toBool(bool defaultValue) const const
QJsonObject toObject() const const
QString toString() const const
QList< T >::const_reference at(qsizetype i) const const
QList< T >::iterator begin()
void clear()
QList< T >::iterator end()
T & first()
QList< T >::reference front()
bool isEmpty() const const
QList< T > mid(qsizetype pos, qsizetype length) const const
void push_back(QList< T >::parameter_type value)
void push_front(QList< T >::parameter_type value)
void removeLast()
qsizetype size() const const
bool isEmpty() const const
const char * valueToKey(quint64 value) const const
bool invokeMethod(QObject *context, Functor &&function, Args &&... arguments)
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
int receivers(const char *signal) const const
QObject * sender() const const
void setObjectName(QAnyStringView name)
T dequeue()
QString & append(QChar ch)
QString arg(Args &&... args) const const
const QChar at(qsizetype position) const const
QString::const_iterator constBegin() const const
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
QChar * data()
bool isEmpty() const const
qsizetype length() const const
QString mid(qsizetype position, qsizetype n) &&
QString number(double n, char format, int precision)
void push_back(QChar ch)
void push_front(QChar ch)
QString & remove(QChar ch, Qt::CaseSensitivity cs)
QString & replace(QChar before, QChar after, Qt::CaseSensitivity cs)
QString right(qsizetype n) &&
QString rightJustified(qsizetype width, QChar fill, bool truncate) const const
qsizetype size() const const
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
double toDouble(bool *ok) const const
int toInt(bool *ok, int base) const const
QByteArray toUtf8() const const
QString trimmed() const const
const QChar * unicode() const const
std::optional< QStringConverter::Encoding > encodingForName(QAnyStringView name)
QString join(QChar separator) const const
CaseInsensitive
QueuedConnection
QTextStream & endl(QTextStream &stream)
QTextStream & fixed(QTextStream &stream)
char * toString(QSizePolicy sp)
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
QFuture< QtFuture::ArgsType< Signal >> connect(Sender *sender, Signal signal)
void timeout()
#define SWIFT_AUDIT_X(COND, WHERE, WHAT)
A weaker kind of verify.
Definition: verify.h:38
#define SWIFT_VERIFY_X(COND, WHERE, WHAT)
A weaker kind of assert.
Definition: verify.h:26