From 4fc2db14ca6acd4acdf994e8ae07fce5cca3e5ff Mon Sep 17 00:00:00 2001 From: Roland Winklmeier Date: Thu, 8 Oct 2015 20:34:11 +0200 Subject: [PATCH] refs #487 Implement IAircraftModelLoader for XPlane --- .../simulation/aircraftmodelloader.cpp | 10 +- .../xplane/aircraftmodelloaderxplane.cpp | 499 ++++++++++++++++++ .../xplane/aircraftmodelloaderxplane.h | 122 +++++ 3 files changed, 629 insertions(+), 2 deletions(-) create mode 100644 src/blackmisc/simulation/xplane/aircraftmodelloaderxplane.cpp create mode 100644 src/blackmisc/simulation/xplane/aircraftmodelloaderxplane.h diff --git a/src/blackmisc/simulation/aircraftmodelloader.cpp b/src/blackmisc/simulation/aircraftmodelloader.cpp index a7371c004..c49a4894c 100644 --- a/src/blackmisc/simulation/aircraftmodelloader.cpp +++ b/src/blackmisc/simulation/aircraftmodelloader.cpp @@ -8,9 +8,14 @@ */ #include "aircraftmodelloader.h" +#include "blackmisc/blackmiscfreefunctions.h" #include "blackmisc/simulation/fscommon/aircraftcfgparser.h" +#include "blackmisc/simulation/xplane/aircraftmodelloaderxplane.h" +#include "blackmisc/simulation/xplane/xplaneutil.h" + using namespace BlackMisc::Simulation::FsCommon; +using namespace BlackMisc::Simulation::XPlane; namespace BlackMisc { @@ -54,8 +59,9 @@ namespace BlackMisc { if (simInfo.xplane()) { - Q_ASSERT_X(false, Q_FUNC_INFO, "Not yet implemented."); - return nullptr; + return make_unique( + CSimulatorInfo(CSimulatorInfo::XPLANE), + CXPlaneUtil::xbusLegacyDir()); } else { diff --git a/src/blackmisc/simulation/xplane/aircraftmodelloaderxplane.cpp b/src/blackmisc/simulation/xplane/aircraftmodelloaderxplane.cpp new file mode 100644 index 000000000..a1f50b754 --- /dev/null +++ b/src/blackmisc/simulation/xplane/aircraftmodelloaderxplane.cpp @@ -0,0 +1,499 @@ +/* Copyright (C) 2015 + * swift project Community / Contributors + * + * This file is part of swift project. It is subject to the license terms in the LICENSE file found in the top-level + * directory of this distribution and at http://www.swift-project.org/license.html. No part of swift project, + * including this file, may be copied, modified, propagated, or distributed except according to the terms + * contained in the LICENSE file. + */ + +#include "aircraftmodelloaderxplane.h" +#include "xplaneutil.h" +#include "blackmisc/predicates.h" +#include "blackmisc/logmessage.h" + +#include +#include +#include +#include + +#include + +using namespace BlackMisc; +using namespace BlackMisc::Aviation; +using namespace BlackMisc::Simulation; +using namespace BlackMisc::Network; + +namespace BlackMisc +{ + namespace Simulation + { + namespace XPlane + { + + static void normalizePath(QString &path) + { + for (auto &e : path) + { + if (e == '/' || e == ':' || e == '\\') + { + e = '/'; + } + } + } + + CAircraftModelLoaderXPlane::CAircraftModelLoaderXPlane() + { } + + CAircraftModelLoaderXPlane::CAircraftModelLoaderXPlane(const CSimulatorInfo &simInfo, const QString &rootDirectory, const QStringList &exludes) : + IAircraftModelLoader(simInfo), + m_rootDirectory(rootDirectory), + m_excludedDirectories(exludes) + { } + + CAircraftModelLoaderXPlane::~CAircraftModelLoaderXPlane() + { + // that should be safe as long as the worker uses deleteLater (which it does) + if (this->m_parserWorker) { this->m_parserWorker->waitForFinished(); } + } + + bool CAircraftModelLoaderXPlane::changeRootDirectory(const QString &directory) + { + if (m_rootDirectory == directory) { return false; } + if (directory.isEmpty() || !existsDir(directory)) { return false; } + + m_rootDirectory = directory; + return true; + } + + CPixmap CAircraftModelLoaderXPlane::iconForModel(const QString &modelString, CStatusMessage &statusMessage) const + { + // X-Plane does not have previews. Maybe we can just use the textures? + Q_UNUSED(modelString) + Q_UNUSED(statusMessage) + return {}; + } + + void CAircraftModelLoaderXPlane::startLoading(LoadMode mode) + { + m_installedModels.clear(); + if (m_rootDirectory.isEmpty()) + { + emit loadingFinished(false); + return; + } + + if (mode == ModeBackground) + { + if (m_parserWorker && !m_parserWorker->isFinished()) { return; } + auto rootDirectory = m_rootDirectory; + auto excludedDirectories = m_excludedDirectories; + m_parserWorker = BlackMisc::CWorker::fromTask(this, "CAircraftModelLoaderXPlane::performParsing", + [this, rootDirectory, excludedDirectories]() + { + auto models = performParsing(rootDirectory, excludedDirectories); + return models; + }); + m_parserWorker->thenWithResult(this, [this](const CAircraftModelList & models) + { + updateInstalledModels(models); + }); + } + else if (mode == ModeBlocking) + { + m_installedModels = performParsing(m_rootDirectory, m_excludedDirectories); + emit loadingFinished(true); + } + } + + bool CAircraftModelLoaderXPlane::isLoadingFinished() const + { + return !m_parserWorker || m_parserWorker->isFinished(); + } + + CAircraftModelList CAircraftModelLoaderXPlane::getAircraftModels() const + { + return m_installedModels; + } + + void CAircraftModelLoaderXPlane::updateInstalledModels(const CAircraftModelList &models) + { + m_installedModels = models; + emit loadingFinished(true); + } + + CAircraftModelList CAircraftModelLoaderXPlane::performParsing(const QString &rootDirectory, const QStringList &excludeDirectories) + { + Q_UNUSED(excludeDirectories); + QStringList packages; + QDirIterator it(rootDirectory, QDirIterator::Subdirectories); + while (it.hasNext()) + { + it.next(); + if (it.fileName() == "xsb_aircraft.txt") { packages << it.fileInfo().absolutePath(); } + } + + m_cslPackages.clear(); + for (const auto &packageFilePath : packages) + { + QString packageFile(packageFilePath); + packageFile += "/xsb_aircraft.txt"; + QFile file(packageFile); + file.open(QIODevice::ReadOnly); + QString content; + + QTextStream ts(&file); + content.append(ts.readAll()); + file.close(); + + auto package = parsePackageHeader(packageFilePath, content); + if (package.hasValidHeader()) m_cslPackages.push_back(package); + } + + CAircraftModelList installedModels; + // Now we do a full run + for (auto &package : m_cslPackages) + { + QString packageFile(package.path); + packageFile += "/xsb_aircraft.txt"; + QFile file(packageFile); + file.open(QIODevice::ReadOnly); + QString content; + + QTextStream ts(&file); + content.append(ts.readAll()); + file.close(); + parseFullPackage(content, package); + + for (const auto &plane : package.planes) + { + CAircraftModel model(plane.modelName, CAircraftModel::TypeOwnSimulatorModel); + model.setFileName(plane.filePath); + + CAircraftIcaoCode icao(plane.icao); + model.setAircraftIcaoCode(icao); + + CLivery livery; + livery.setCombinedCode(plane.livery); + CAirlineIcaoCode airline; + airline.setName(plane.airline); + livery.setAirlineIcaoCode(airline); + model.setLivery(livery); + + CDistributor distributor(package.name); + model.setDistributor(distributor); + + model.setSimulatorInfo(m_simulatorInfo); + installedModels.push_back(model); + } + } + return installedModels; + } + + bool CAircraftModelLoaderXPlane::existsDir(const QString &directory) const + { + if (directory.isEmpty()) { return false; } + QDir dir(directory); + //! \todo not available network dir can make this hang here + return dir.exists(); + } + + bool CAircraftModelLoaderXPlane::doPackageSub(QString &ioPath) + { + for (auto i = m_cslPackages.begin(); i != m_cslPackages.end(); ++i) + { + if (strncmp(qPrintable(i->name), qPrintable(ioPath), i->name.size()) == 0) + { + ioPath.remove(0, i->name.size()); + ioPath.insert(0, i->path); + return true; + } + } + return false; + } + + bool CAircraftModelLoaderXPlane::parseExportCommand(const QStringList &tokens, CSLPackage &package, const QString &path, int lineNum) + { + if (tokens.size() != 2) + { + CLogMessage(this).warning("%1 - %2: EXPORT_NAME command requires 1 argument.") << path << lineNum; + return false; + } + + auto p = std::find_if(m_cslPackages.begin(), m_cslPackages.end(), [&tokens](CSLPackage p) { return p.name == tokens[1]; }); + if (p == m_cslPackages.end()) + { + package.path = path; + package.name = tokens[1]; + return true; + } + else + { + CLogMessage(this).warning("WARNING: Package name %1 already in use by %2 reqested by use by %3") << tokens[1] << p->path << path; + return false; + } + } + + bool CAircraftModelLoaderXPlane::parseDependencyCommand(const QStringList &tokens, CSLPackage &package, const QString &path, int lineNum) + { + Q_UNUSED(package); + if (tokens.size() != 2) + { + CLogMessage(this).warning("%1 - %2: DEPENDENCY command requires 1 argument.") << path << lineNum; + return false; + } + + if (std::count_if(m_cslPackages.begin(), m_cslPackages.end(), [&tokens](CSLPackage p) { return p.name == tokens[1]; }) == 0) + { + CLogMessage(this).warning("WARNING: required package %1 not found. Aborting processing of this package.") << tokens[1]; + return false; + } + + return true; + } + + bool CAircraftModelLoaderXPlane::parseObjectCommand(const QStringList &tokens, CSLPackage &package, const QString &path, int lineNum) + { + if (tokens.size() != 2) + { + CLogMessage(this).warning("%1 - %2: OBJECT command requires 1 argument.") << path << lineNum; + return false; + } + QString relativePath(tokens[1]); + normalizePath(relativePath); + QString fullPath(relativePath); + if (!doPackageSub(fullPath)) + { + CLogMessage(this).warning("%1 - %2: package not found..") << path << lineNum; + return false; + } + + package.planes.push_back(CSLPlane()); + package.planes.back().modelName = relativePath; + package.planes.back().filePath = fullPath; + return true; + } + + bool CAircraftModelLoaderXPlane::parseTextureCommand(const QStringList &tokens, CSLPackage &package, const QString &path, int lineNum) + { + if (tokens.size() != 2) + { + CLogMessage(this).warning("%1 - %2: TEXTURE command requires 1 argument.") << path << lineNum; + return false; + } + + // Load regular texture + QString relativeTexPath = tokens[1]; + normalizePath(relativeTexPath); + QString absoluteTexPath(relativeTexPath); + + if (!doPackageSub(absoluteTexPath)) + { + CLogMessage(this).warning("%1 - %2: package not found..") << path << lineNum; + return false; + } + + package.planes.back().modelName += " "; + package.planes.back().modelName += relativeTexPath; + return true; + } + + bool CAircraftModelLoaderXPlane::parseAircraftCommand(const QStringList &tokens, CSLPackage &package, const QString &path, int lineNum) + { + // AIRCAFT + if (tokens.size() != 4) + { + CLogMessage(this).warning("%1 - %2: AIRCRAFT command requires 3 arguments.") << path << lineNum; + } + + QString relativePath = tokens[3]; + normalizePath(relativePath); + QString absolutePath(relativePath); + if (!doPackageSub(absolutePath)) + { + CLogMessage(this).warning("%1 - %2: package not found..") << path << lineNum; + return false; + } + package.planes.push_back(CSLPlane()); + package.planes.back().modelName = relativePath; + package.planes.back().filePath = absolutePath; + return true; + } + + bool CAircraftModelLoaderXPlane::parseObj8AircraftCommand(const QStringList &tokens, CSLPackage &package, const QString &path, int lineNum) + { + Q_UNUSED(package) + // OBJ8_AIRCRAFT + if (tokens.size() != 2) + { + CLogMessage(this).warning("%1 - %2: OBJ8_AIRCARFT command requires 1 argument.") << path << lineNum; + } + + // RW: I need an example of the file to properly implement and test it. + qFatal("Not implemented yet."); + return false; + } + + bool CAircraftModelLoaderXPlane::parseObj8Command(const QStringList &tokens, CSLPackage &package, const QString &path, int lineNum) + { + Q_UNUSED(package) + // OBJ8 + if (tokens.size() != 4) + { + CLogMessage(this).warning("%1 - %2: OBJ8_AIRCARFT command requires 3 arguments.") << path << lineNum; + } + + // RW: I need an example of the file to properly implement and test it. + qFatal("Not implemented yet."); + return false; + } + + bool CAircraftModelLoaderXPlane::parseHasGearCommand(const QStringList &tokens, CSLPackage &package, const QString &path, int lineNum) + { + Q_UNUSED(tokens) + Q_UNUSED(package) + Q_UNUSED(path) + Q_UNUSED(lineNum) + return true; + } + + bool CAircraftModelLoaderXPlane::parseIcaoCommand(const QStringList &tokens, CSLPackage &package, const QString &path, int lineNum) + { + // ICAO + if (tokens.size() != 2) + { + CLogMessage(this).warning("%1 - %2: ICAO command requires 1 argument.") << path << lineNum; + return false; + } + + QString icao = tokens[1]; + package.planes.back().icao = icao; + return true; + } + + bool CAircraftModelLoaderXPlane::parseAirlineCommand(const QStringList &tokens, CSLPackage &package, const QString &path, int lineNum) + { + // AIRLINE + if (tokens.size() != 3) + { + CLogMessage(this).warning("%1 - %2: AIRLINE command requires 2 arguments.") << path << lineNum; + return false; + } + + QString icao = tokens[1]; + package.planes.back().icao = icao; + QString airline = tokens[2]; + package.planes.back().airline = airline; + return true; + } + + bool CAircraftModelLoaderXPlane::parseLiveryCommand(const QStringList &tokens, CSLPackage &package, const QString &path, int lineNum) + { + // LIVERY + if (tokens.size() != 4) + { + CLogMessage(this).warning("%1 - %2: LIVERY command requires 3 arguments.") << path << lineNum; + return false; + } + + QString icao = tokens[1]; + package.planes.back().icao = icao; + QString airline = tokens[2]; + package.planes.back().airline = airline; + QString livery = tokens[3]; + package.planes.back().livery = livery; + return true; + } + + bool CAircraftModelLoaderXPlane::parseDummyCommand(const QStringList & /* tokens */, CSLPackage & /* package */, const QString & /* path */, int /*lineNum*/) + { + return true; + } + + CAircraftModelLoaderXPlane::CSLPackage CAircraftModelLoaderXPlane::parsePackageHeader(const QString &path, const QString &content) + { + using command = std::function; + using namespace std::placeholders; + + const QMap commands + { + { "EXPORT_NAME", std::bind(&CAircraftModelLoaderXPlane::parseExportCommand, this, _1, _2, _3, _4) } + }; + + CSLPackage package; + int lineNum = 0; + + QString localCopy(content); + QTextStream in(&localCopy); + while (!in.atEnd()) + { + ++lineNum; + QString line = in.readLine(); + auto tokens = line.split(QRegularExpression("\\s+")); + if (!tokens.empty()) + { + auto it = commands.find(tokens[0]); + if (it != commands.end()) + { + bool result = it.value()(tokens, package, path, lineNum); + // Stop loop once we found EXPORT command + if (result) break; + } + } + } + return package; + } + + void CAircraftModelLoaderXPlane::parseFullPackage(const QString &content, CSLPackage &package) + { + using command = std::function; + using namespace std::placeholders; + + const QMap commands + { + { "EXPORT_NAME", std::bind(&CAircraftModelLoaderXPlane::parseDummyCommand, this, _1, _2, _3, _4) }, + { "DEPENDENCY", std::bind(&CAircraftModelLoaderXPlane::parseDependencyCommand, this, _1, _2, _3, _4) }, + { "OBJECT", std::bind(&CAircraftModelLoaderXPlane::parseObjectCommand, this, _1, _2, _3, _4) }, + { "TEXTURE", std::bind(&CAircraftModelLoaderXPlane::parseTextureCommand, this, _1, _2, _3, _4) }, + { "AIRCRAFT", std::bind(&CAircraftModelLoaderXPlane::parseAircraftCommand, this, _1, _2, _3, _4) }, + { "OBJ8_AIRCRAFT", std::bind(&CAircraftModelLoaderXPlane::parseObj8AircraftCommand, this, _1, _2, _3, _4) }, + { "OBJ8", std::bind(&CAircraftModelLoaderXPlane::parseObj8Command, this, _1, _2, _3, _4) }, + { "HASGEAR", std::bind(&CAircraftModelLoaderXPlane::parseHasGearCommand, this, _1, _2, _3, _4) }, + { "ICAO", std::bind(&CAircraftModelLoaderXPlane::parseIcaoCommand, this, _1, _2, _3, _4) }, + { "AIRLINE", std::bind(&CAircraftModelLoaderXPlane::parseAirlineCommand, this, _1, _2, _3, _4) }, + { "LIVERY", std::bind(&CAircraftModelLoaderXPlane::parseLiveryCommand, this, _1, _2, _3, _4) }, + }; + + int lineNum = 0; + + QString localCopy(content); + QTextStream in(&localCopy); + while (!in.atEnd()) + { + ++lineNum; + QString line = in.readLine(); + auto tokens = line.split(QRegularExpression("\\s+"), QString::SkipEmptyParts); + if (!tokens.empty()) + { + auto it = commands.find(tokens[0]); + if (it != commands.end()) + { + bool result = it.value()(tokens, package, package.path, lineNum); + if (!result) + { + CLogMessage(this).warning("Ignoring CSL package %1") << package.name; + break; + } + } + else + { + CLogMessage(this).warning("Unrecognized command %1 in %2") << tokens[0] << package.name; + break; + } + } + } + } + + } // namespace + } // namespace +} // namespace diff --git a/src/blackmisc/simulation/xplane/aircraftmodelloaderxplane.h b/src/blackmisc/simulation/xplane/aircraftmodelloaderxplane.h new file mode 100644 index 000000000..03c7f0f44 --- /dev/null +++ b/src/blackmisc/simulation/xplane/aircraftmodelloaderxplane.h @@ -0,0 +1,122 @@ +/* Copyright (C) 2015 + * swift project Community / Contributors + * + * This file is part of swift project. It is subject to the license terms in the LICENSE file found in the top-level + * directory of this distribution and at http://www.swift-project.org/license.html. No part of swift project, + * including this file, may be copied, modified, propagated, or distributed except according to the terms + * contained in the LICENSE file. + */ + +//! \file + +#ifndef BLACKMISC_SIMULATION_XPLANE_AIRCRAFTMODELLOADERXPLANE_H +#define BLACKMISC_SIMULATION_XPLANE_AIRCRAFTMODELLOADERXPLANE_H + +#include "blackmisc/blackmiscexport.h" +#include "blackmisc/worker.h" +#include "blackmisc/pixmap.h" +#include "blackmisc/simulation/aircraftmodelloader.h" +#include "blackmisc/simulation/aircraftmodel.h" + +#include + +namespace BlackMisc +{ + namespace Simulation + { + namespace XPlane + { + /*! + * \brief XPlane aircraft model loader + * \todo Obj8Aircraft and Obj8 parsers are not yet implemented + */ + class BLACKMISC_EXPORT CAircraftModelLoaderXPlane : public BlackMisc::Simulation::IAircraftModelLoader + { + Q_OBJECT + + public: + //! Constructor + CAircraftModelLoaderXPlane(); + + //! Constructor + CAircraftModelLoaderXPlane(const BlackMisc::Simulation::CSimulatorInfo &simInfo, const QString &rootDirectory, const QStringList &exludes = {}); + + //! Virtual destructor + virtual ~CAircraftModelLoaderXPlane(); + + //! Change the directory + bool changeRootDirectory(const QString &directory); + + //! Current root directory + QString getRootDirectory() const { return this->m_rootDirectory; } + + //! \copydoc IAircraftModelLoader::getPixmapForModel + virtual BlackMisc::CPixmap iconForModel(const QString &modelName, BlackMisc::CStatusMessage &statusMessage) const override; + + //! \copydoc IAircraftModelLoader::startLoading + virtual void startLoading(LoadMode mode = ModeBackground) override; + + //! \copydoc IAircraftModelLoader::isLoadingFinished + virtual bool isLoadingFinished() const override; + + //! \copydoc IAircraftModelLoader::getAircraftModels + virtual BlackMisc::Simulation::CAircraftModelList getAircraftModels() const override; + + public slots: + //! Parsed or injected models + void updateInstalledModels(const BlackMisc::Simulation::CAircraftModelList &models); + + private: + struct CSLPlane + { + QString modelName; //!< Unique model name + QString filePath; //!< object filePath + QString icao; //!< Icao type of this model + QString airline; //!< Airline identifier. Can be empty. + QString livery; //!< Livery identifier. Can be empty. + }; + + struct CSLPackage + { + bool hasValidHeader() const + { + return !name.isEmpty() && !path.isEmpty(); + } + + QString name; + QString path; + QVector planes; + }; + + BlackMisc::Simulation::CAircraftModelList performParsing(const QString &rootDirectory, const QStringList &excludeDirectories); + //! Does the directory exist? + bool existsDir(const QString &directory = QString()) const; + bool doPackageSub(QString &ioPath); + + bool parseExportCommand(const QStringList &tokens, CSLPackage &package, const QString &path, int lineNum); + bool parseDependencyCommand(const QStringList &tokens, CSLPackage &package, const QString &path, int lineNum); + bool parseObjectCommand(const QStringList &tokens, CSLPackage &package, const QString &path, int lineNum); + bool parseTextureCommand(const QStringList &tokens, CSLPackage &package, const QString &path, int lineNum); + bool parseAircraftCommand(const QStringList &tokens, CSLPackage &package, const QString &path, int lineNum); + bool parseObj8AircraftCommand(const QStringList &tokens, CSLPackage &package, const QString &path, int lineNum); + bool parseObj8Command(const QStringList &tokens, CSLPackage &package, const QString &path, int lineNum); + bool parseHasGearCommand(const QStringList &tokens, CSLPackage &package, const QString &path, int lineNum); + bool parseIcaoCommand(const QStringList &tokens, CSLPackage &package, const QString &path, int lineNum); + bool parseAirlineCommand(const QStringList &tokens, CSLPackage &package, const QString &path, int lineNum); + bool parseLiveryCommand(const QStringList &tokens, CSLPackage &package, const QString &path, int lineNum); + bool parseDummyCommand(const QStringList &tokens, CSLPackage &package, const QString &path, int lineNum); + + CSLPackage parsePackageHeader(const QString &path, const QString &content); + void parseFullPackage(const QString &content, CSLPackage &package); + + QString m_rootDirectory; //!< root directory parsing aircraft.cfg files + QStringList m_excludedDirectories; //!< directories not to be parsed + QPointer m_parserWorker; //!< worker will destroy itself, so weak pointer + QVector m_cslPackages; //!< Parsed Packages. No lock required since accessed only from one thread + BlackMisc::Simulation::CAircraftModelList m_installedModels; + }; + } // namespace + } // namespace +} // namespace + +#endif // guard