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