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