From c9198d672da26fac0c567abcd6fac9e901ad38c8 Mon Sep 17 00:00:00 2001 From: Sude Date: Mon, 13 Mar 2023 22:25:33 +0200 Subject: [PATCH] Added option to list game details as JSON Removed --list-details option Added --list-format to select a format that --list uses "no_details" is the same as --list previously and the default value "details" is the same as --list-details was previously "json" is JSON formatted output --- include/config.h | 2 +- include/downloader.h | 3 +- include/globalconstants.h | 11 + main.cpp | 14 +- src/downloader.cpp | 409 +++++++++++++++++++------------------- 5 files changed, 236 insertions(+), 203 deletions(-) diff --git a/include/config.h b/include/config.h index 8924abf..248afde 100644 --- a/include/config.h +++ b/include/config.h @@ -240,7 +240,6 @@ class Config bool bRepair; bool bUpdated; bool bList; - bool bListDetails; bool bCheckStatus; bool bShowWishlist; bool bNotifications; @@ -322,6 +321,7 @@ class Config size_t iChunkSize; int iProgressInterval; int iMsgLevel; + unsigned int iListFormat; }; #endif // CONFIG_H__ diff --git a/include/downloader.h b/include/downloader.h index 18b14de..4d37283 100644 --- a/include/downloader.h +++ b/include/downloader.h @@ -101,7 +101,7 @@ class Downloader void clearUpdateNotifications(); void repair(); void download(); - + void downloadCloudSaves(const std::string& product_id, int build_index = -1); void downloadCloudSavesById(const std::string& product_id, int build_index = -1); void uploadCloudSaves(const std::string& product_id, int build_index = -1); @@ -156,6 +156,7 @@ class Downloader static int progressCallbackForThread(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow); template void printProgress(const ThreadSafeQueue& download_queue); static void getGameDetailsThread(Config config, const unsigned int& tid); + void printGameDetailsAsText(gameDetails& game); static int progressCallback(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow); static size_t writeData(void *ptr, size_t size, size_t nmemb, FILE *stream); diff --git a/include/globalconstants.h b/include/globalconstants.h index 3e1be99..6749c27 100644 --- a/include/globalconstants.h +++ b/include/globalconstants.h @@ -117,6 +117,17 @@ namespace GlobalConstants { CDN_LUMEN, "lumen", "Lumen", "lumen|lumen_cdn" }, { CDN_AKAMAI, "akamai_edgecast_proxy", "Akamai", "akamai|akamai_cdn|akamai_ec|akamai_edgecast_proxy" } }; + + const unsigned int LIST_FORMAT_NO_DETAILS = 1 << 0; + const unsigned int LIST_FORMAT_DETAILS_TEXT = 1 << 1; + const unsigned int LIST_FORMAT_DETAILS_JSON = 1 << 2; + + const std::vector LIST_FORMAT = + { + { LIST_FORMAT_NO_DETAILS, "no_details", "No details", "n|nd|no_details" }, + { LIST_FORMAT_DETAILS_TEXT, "details", "Details", "d|details" }, + { LIST_FORMAT_DETAILS_JSON, "json", "JSON", "j|json" } + }; } #endif // GLOBALCONSTANTS_H_INCLUDED diff --git a/main.cpp b/main.cpp index 9a9008d..fe75ab3 100644 --- a/main.cpp +++ b/main.cpp @@ -161,6 +161,13 @@ int main(int argc, char *argv[]) } include_options_text += "Separate with \",\" to use multiple values"; + // Create help text for --list-format option + std::string list_format_text = "Select output format\n"; + for (unsigned int i = 0; i < GlobalConstants::LIST_FORMAT.size(); ++i) + { + list_format_text += GlobalConstants::LIST_FORMAT[i].str + " = " + GlobalConstants::LIST_FORMAT[i].regexp + "\n"; + } + std::string galaxy_product_id_install; std::string galaxy_product_id_show_builds; std::string galaxy_product_id_show_cloud_paths; @@ -202,6 +209,7 @@ int main(int argc, char *argv[]) std::string sGalaxyLanguage; std::string sGalaxyArch; std::string sGalaxyCDN; + std::string sListFormat; Globals::globalConfig.bReport = false; // Commandline options (no config file) options_cli_no_cfg.add_options() @@ -209,7 +217,7 @@ int main(int argc, char *argv[]) ("version", "Print version information") ("login", bpo::value(&Globals::globalConfig.bLogin)->zero_tokens()->default_value(false), "Login") ("list", bpo::value(&Globals::globalConfig.bList)->zero_tokens()->default_value(false), "List games") - ("list-details", bpo::value(&Globals::globalConfig.bListDetails)->zero_tokens()->default_value(false), "List games with detailed info") + ("list-format", bpo::value(&sListFormat)->default_value("no_details"), list_format_text.c_str()) ("download", bpo::value(&Globals::globalConfig.bDownload)->zero_tokens()->default_value(false), "Download") ("repair", bpo::value(&Globals::globalConfig.bRepair)->zero_tokens()->default_value(false), "Repair downloaded files\nUse --repair --download to redownload files when filesizes don't match (possibly different version). Redownload will rename the old file (appends .old to filename)") ("game", bpo::value(&Globals::globalConfig.sGameRegex)->default_value(""), "Set regular expression filter\nfor download/list/repair (Perl syntax)") @@ -572,6 +580,8 @@ int main(int argc, char *argv[]) Globals::globalConfig.dlConf.bPatches = (Globals::globalConfig.dlConf.iInclude & OPTION_PATCHES); Globals::globalConfig.dlConf.bLanguagePacks = (Globals::globalConfig.dlConf.iInclude & OPTION_LANGPACKS); Globals::globalConfig.dlConf.bDLC = (Globals::globalConfig.dlConf.iInclude & OPTION_DLCS); + + Globals::globalConfig.iListFormat = Util::getOptionValue(sListFormat, GlobalConstants::LIST_FORMAT, false); } catch (std::exception& e) { @@ -780,7 +790,7 @@ int main(int argc, char *argv[]) downloader.repair(); else if (Globals::globalConfig.bDownload) // Download games downloader.download(); - else if (Globals::globalConfig.bListDetails || Globals::globalConfig.bList) // Detailed list of games/extras + else if (Globals::globalConfig.bList) // List games/extras res = downloader.listGames(); else if (Globals::globalConfig.bListTags) // List tags res = downloader.listTags(); diff --git a/src/downloader.cpp b/src/downloader.cpp index 428ab68..f6e0117 100644 --- a/src/downloader.cpp +++ b/src/downloader.cpp @@ -525,206 +525,8 @@ int Downloader::getGameDetails() int Downloader::listGames() { - if (Globals::globalConfig.bListDetails) // Detailed list + if (Globals::globalConfig.iListFormat == GlobalConstants::LIST_FORMAT_NO_DETAILS) { - if (this->games.empty()) { - int res = this->getGameDetails(); - if (res > 0) - return res; - } - - for (unsigned int i = 0; i < games.size(); ++i) - { - std::cout << "gamename: " << games[i].gamename << std::endl - << "product id: " << games[i].product_id << std::endl - << "title: " << games[i].title << std::endl - << "icon: " << games[i].icon << std::endl; - if (!games[i].serials.empty()) - std::cout << "serials:" << std::endl << games[i].serials << std::endl; - - // List installers - if (Globals::globalConfig.dlConf.bInstallers && !games[i].installers.empty()) - { - std::cout << "installers: " << std::endl; - for (unsigned int j = 0; j < games[i].installers.size(); ++j) - { - std::string filepath = games[i].installers[j].getFilepath(); - if (Globals::globalConfig.blacklist.isBlacklisted(filepath)) - { - if (Globals::globalConfig.iMsgLevel >= MSGLEVEL_VERBOSE) - std::cerr << "skipped blacklisted file " << filepath << std::endl; - continue; - } - - std::string languages = Util::getOptionNameString(games[i].installers[j].language, GlobalConstants::LANGUAGES); - - std::cout << "\tid: " << games[i].installers[j].id << std::endl - << "\tname: " << games[i].installers[j].name << std::endl - << "\tpath: " << games[i].installers[j].path << std::endl - << "\tsize: " << games[i].installers[j].size << std::endl - << "\tupdated: " << (games[i].installers[j].updated ? "True" : "False") << std::endl - << "\tlanguage: " << languages << std::endl - << "\tversion: " << games[i].installers[j].version << std::endl - << std::endl; - } - } - // List extras - if (Globals::globalConfig.dlConf.bExtras && !games[i].extras.empty()) - { - std::cout << "extras: " << std::endl; - for (unsigned int j = 0; j < games[i].extras.size(); ++j) - { - std::string filepath = games[i].extras[j].getFilepath(); - if (Globals::globalConfig.blacklist.isBlacklisted(filepath)) - { - if (Globals::globalConfig.iMsgLevel >= MSGLEVEL_VERBOSE) - std::cerr << "skipped blacklisted file " << filepath << std::endl; - continue; - } - - std::cout << "\tid: " << games[i].extras[j].id << std::endl - << "\tname: " << games[i].extras[j].name << std::endl - << "\tpath: " << games[i].extras[j].path << std::endl - << "\tsize: " << games[i].extras[j].size << std::endl - << std::endl; - } - } - // List patches - if (Globals::globalConfig.dlConf.bPatches && !games[i].patches.empty()) - { - std::cout << "patches: " << std::endl; - for (unsigned int j = 0; j < games[i].patches.size(); ++j) - { - std::string filepath = games[i].patches[j].getFilepath(); - if (Globals::globalConfig.blacklist.isBlacklisted(filepath)) - { - if (Globals::globalConfig.iMsgLevel >= MSGLEVEL_VERBOSE) - std::cerr << "skipped blacklisted file " << filepath << std::endl; - continue; - } - - std::string languages = Util::getOptionNameString(games[i].patches[j].language, GlobalConstants::LANGUAGES); - - std::cout << "\tid: " << games[i].patches[j].id << std::endl - << "\tname: " << games[i].patches[j].name << std::endl - << "\tpath: " << games[i].patches[j].path << std::endl - << "\tsize: " << games[i].patches[j].size << std::endl - << "\tupdated: " << (games[i].patches[j].updated ? "True" : "False") << std::endl - << "\tlanguage: " << languages << std::endl - << "\tversion: " << games[i].patches[j].version << std::endl - << std::endl; - } - } - // List language packs - if (Globals::globalConfig.dlConf.bLanguagePacks && !games[i].languagepacks.empty()) - { - std::cout << "language packs: " << std::endl; - for (unsigned int j = 0; j < games[i].languagepacks.size(); ++j) - { - std::string filepath = games[i].languagepacks[j].getFilepath(); - if (Globals::globalConfig.blacklist.isBlacklisted(filepath)) - { - if (Globals::globalConfig.iMsgLevel >= MSGLEVEL_VERBOSE) - std::cerr << "skipped blacklisted file " << filepath << std::endl; - continue; - } - - std::cout << "\tid: " << games[i].languagepacks[j].id << std::endl - << "\tname: " << games[i].languagepacks[j].name << std::endl - << "\tpath: " << games[i].languagepacks[j].path << std::endl - << "\tsize: " << games[i].languagepacks[j].size << std::endl - << std::endl; - } - } - if (Globals::globalConfig.dlConf.bDLC && !games[i].dlcs.empty()) - { - std::cout << "DLCs: " << std::endl; - for (unsigned int j = 0; j < games[i].dlcs.size(); ++j) - { - if (!games[i].dlcs[j].serials.empty()) - { - std::cout << "\tDLC gamename: " << games[i].dlcs[j].gamename << std::endl - << "\tserials:" << games[i].dlcs[j].serials << std::endl; - } - - for (unsigned int k = 0; k < games[i].dlcs[j].installers.size(); ++k) - { - std::string filepath = games[i].dlcs[j].installers[k].getFilepath(); - if (Globals::globalConfig.blacklist.isBlacklisted(filepath)) - { - if (Globals::globalConfig.iMsgLevel >= MSGLEVEL_VERBOSE) - std::cerr << "skipped blacklisted file " << filepath << std::endl; - continue; - } - - std::cout << "\tgamename: " << games[i].dlcs[j].gamename << std::endl - << "\tproduct id: " << games[i].dlcs[j].product_id << std::endl - << "\tid: " << games[i].dlcs[j].installers[k].id << std::endl - << "\tname: " << games[i].dlcs[j].installers[k].name << std::endl - << "\tpath: " << games[i].dlcs[j].installers[k].path << std::endl - << "\tsize: " << games[i].dlcs[j].installers[k].size << std::endl - << "\tupdated: " << (games[i].dlcs[j].installers[k].updated ? "True" : "False") << std::endl - << "\tversion: " << games[i].dlcs[j].installers[k].version << std::endl - << std::endl; - } - for (unsigned int k = 0; k < games[i].dlcs[j].patches.size(); ++k) - { - std::string filepath = games[i].dlcs[j].patches[k].getFilepath(); - if (Globals::globalConfig.blacklist.isBlacklisted(filepath)) { - if (Globals::globalConfig.iMsgLevel >= MSGLEVEL_VERBOSE) - std::cerr << "skipped blacklisted file " << filepath << std::endl; - continue; - } - - std::cout << "\tgamename: " << games[i].dlcs[j].gamename << std::endl - << "\tproduct id: " << games[i].dlcs[j].product_id << std::endl - << "\tid: " << games[i].dlcs[j].patches[k].id << std::endl - << "\tname: " << games[i].dlcs[j].patches[k].name << std::endl - << "\tpath: " << games[i].dlcs[j].patches[k].path << std::endl - << "\tsize: " << games[i].dlcs[j].patches[k].size << std::endl - << "\tversion: " << games[i].dlcs[j].patches[k].version << std::endl - << std::endl; - } - for (unsigned int k = 0; k < games[i].dlcs[j].extras.size(); ++k) - { - std::string filepath = games[i].dlcs[j].extras[k].getFilepath(); - if (Globals::globalConfig.blacklist.isBlacklisted(filepath)) { - if (Globals::globalConfig.iMsgLevel >= MSGLEVEL_VERBOSE) - std::cerr << "skipped blacklisted file " << filepath << std::endl; - continue; - } - - std::cout << "\tgamename: " << games[i].dlcs[j].gamename << std::endl - << "\tproduct id: " << games[i].dlcs[j].product_id << std::endl - << "\tid: " << games[i].dlcs[j].extras[k].id << std::endl - << "\tname: " << games[i].dlcs[j].extras[k].name << std::endl - << "\tpath: " << games[i].dlcs[j].extras[k].path << std::endl - << "\tsize: " << games[i].dlcs[j].extras[k].size << std::endl - << std::endl; - } - for (unsigned int k = 0; k < games[i].dlcs[j].languagepacks.size(); ++k) - { - std::string filepath = games[i].dlcs[j].languagepacks[k].getFilepath(); - if (Globals::globalConfig.blacklist.isBlacklisted(filepath)) { - if (Globals::globalConfig.iMsgLevel >= MSGLEVEL_VERBOSE) - std::cerr << "skipped blacklisted file " << filepath << std::endl; - continue; - } - - std::cout << "\tgamename: " << games[i].dlcs[j].gamename << std::endl - << "\tproduct id: " << games[i].dlcs[j].product_id << std::endl - << "\tid: " << games[i].dlcs[j].languagepacks[k].id << std::endl - << "\tname: " << games[i].dlcs[j].languagepacks[k].name << std::endl - << "\tpath: " << games[i].dlcs[j].languagepacks[k].path << std::endl - << "\tsize: " << games[i].dlcs[j].languagepacks[k].size << std::endl - << std::endl; - } - } - } - } - } - else - { // List game names if (gameItems.empty()) this->getGameList(); @@ -742,6 +544,25 @@ int Downloader::listGames() std::cout << "+> " << gameItems[i].dlcnames[j] << std::endl; } } + else + { + if (this->games.empty()) { + int res = this->getGameDetails(); + if (res > 0) + return res; + } + + if (Globals::globalConfig.iListFormat == GlobalConstants::LIST_FORMAT_DETAILS_JSON) + { + for (auto game : this->games) + std::cout << game.getDetailsAsJson() << std::endl; + } + else if (Globals::globalConfig.iListFormat == GlobalConstants::LIST_FORMAT_DETAILS_TEXT) + { + for (auto game : this->games) + printGameDetailsAsText(game); + } + } return 0; } @@ -6295,3 +6116,193 @@ std::string Downloader::getGalaxyInstallDirectory(galaxyAPI *galaxyHandle, const return install_directory; } + +void Downloader::printGameDetailsAsText(gameDetails& game) +{ + std::cout << "gamename: " << game.gamename << std::endl + << "product id: " << game.product_id << std::endl + << "title: " << game.title << std::endl + << "icon: " << game.icon << std::endl; + if (!game.serials.empty()) + std::cout << "serials:" << std::endl << game.serials << std::endl; + + // List installers + if (Globals::globalConfig.dlConf.bInstallers && !game.installers.empty()) + { + std::cout << "installers: " << std::endl; + for (unsigned int j = 0; j < game.installers.size(); ++j) + { + std::string filepath = game.installers[j].getFilepath(); + if (Globals::globalConfig.blacklist.isBlacklisted(filepath)) + { + if (Globals::globalConfig.iMsgLevel >= MSGLEVEL_VERBOSE) + std::cerr << "skipped blacklisted file " << filepath << std::endl; + continue; + } + + std::string languages = Util::getOptionNameString(game.installers[j].language, GlobalConstants::LANGUAGES); + + std::cout << "\tid: " << game.installers[j].id << std::endl + << "\tname: " << game.installers[j].name << std::endl + << "\tpath: " << game.installers[j].path << std::endl + << "\tsize: " << game.installers[j].size << std::endl + << "\tupdated: " << (game.installers[j].updated ? "True" : "False") << std::endl + << "\tlanguage: " << languages << std::endl + << "\tversion: " << game.installers[j].version << std::endl + << std::endl; + } + } + // List extras + if (Globals::globalConfig.dlConf.bExtras && !game.extras.empty()) + { + std::cout << "extras: " << std::endl; + for (unsigned int j = 0; j < game.extras.size(); ++j) + { + std::string filepath = game.extras[j].getFilepath(); + if (Globals::globalConfig.blacklist.isBlacklisted(filepath)) + { + if (Globals::globalConfig.iMsgLevel >= MSGLEVEL_VERBOSE) + std::cerr << "skipped blacklisted file " << filepath << std::endl; + continue; + } + + std::cout << "\tid: " << game.extras[j].id << std::endl + << "\tname: " << game.extras[j].name << std::endl + << "\tpath: " << game.extras[j].path << std::endl + << "\tsize: " << game.extras[j].size << std::endl + << std::endl; + } + } + // List patches + if (Globals::globalConfig.dlConf.bPatches && !game.patches.empty()) + { + std::cout << "patches: " << std::endl; + for (unsigned int j = 0; j < game.patches.size(); ++j) + { + std::string filepath = game.patches[j].getFilepath(); + if (Globals::globalConfig.blacklist.isBlacklisted(filepath)) + { + if (Globals::globalConfig.iMsgLevel >= MSGLEVEL_VERBOSE) + std::cerr << "skipped blacklisted file " << filepath << std::endl; + continue; + } + + std::string languages = Util::getOptionNameString(game.patches[j].language, GlobalConstants::LANGUAGES); + + std::cout << "\tid: " << game.patches[j].id << std::endl + << "\tname: " << game.patches[j].name << std::endl + << "\tpath: " << game.patches[j].path << std::endl + << "\tsize: " << game.patches[j].size << std::endl + << "\tupdated: " << (game.patches[j].updated ? "True" : "False") << std::endl + << "\tlanguage: " << languages << std::endl + << "\tversion: " << game.patches[j].version << std::endl + << std::endl; + } + } + // List language packs + if (Globals::globalConfig.dlConf.bLanguagePacks && !game.languagepacks.empty()) + { + std::cout << "language packs: " << std::endl; + for (unsigned int j = 0; j < game.languagepacks.size(); ++j) + { + std::string filepath = game.languagepacks[j].getFilepath(); + if (Globals::globalConfig.blacklist.isBlacklisted(filepath)) + { + if (Globals::globalConfig.iMsgLevel >= MSGLEVEL_VERBOSE) + std::cerr << "skipped blacklisted file " << filepath << std::endl; + continue; + } + + std::cout << "\tid: " << game.languagepacks[j].id << std::endl + << "\tname: " << game.languagepacks[j].name << std::endl + << "\tpath: " << game.languagepacks[j].path << std::endl + << "\tsize: " << game.languagepacks[j].size << std::endl + << std::endl; + } + } + if (Globals::globalConfig.dlConf.bDLC && !game.dlcs.empty()) + { + std::cout << "DLCs: " << std::endl; + for (unsigned int j = 0; j < game.dlcs.size(); ++j) + { + if (!game.dlcs[j].serials.empty()) + { + std::cout << "\tDLC gamename: " << game.dlcs[j].gamename << std::endl + << "\tserials:" << game.dlcs[j].serials << std::endl; + } + + for (unsigned int k = 0; k < game.dlcs[j].installers.size(); ++k) + { + std::string filepath = game.dlcs[j].installers[k].getFilepath(); + if (Globals::globalConfig.blacklist.isBlacklisted(filepath)) + { + if (Globals::globalConfig.iMsgLevel >= MSGLEVEL_VERBOSE) + std::cerr << "skipped blacklisted file " << filepath << std::endl; + continue; + } + + std::cout << "\tgamename: " << game.dlcs[j].gamename << std::endl + << "\tproduct id: " << game.dlcs[j].product_id << std::endl + << "\tid: " << game.dlcs[j].installers[k].id << std::endl + << "\tname: " << game.dlcs[j].installers[k].name << std::endl + << "\tpath: " << game.dlcs[j].installers[k].path << std::endl + << "\tsize: " << game.dlcs[j].installers[k].size << std::endl + << "\tupdated: " << (game.dlcs[j].installers[k].updated ? "True" : "False") << std::endl + << "\tversion: " << game.dlcs[j].installers[k].version << std::endl + << std::endl; + } + for (unsigned int k = 0; k < game.dlcs[j].patches.size(); ++k) + { + std::string filepath = game.dlcs[j].patches[k].getFilepath(); + if (Globals::globalConfig.blacklist.isBlacklisted(filepath)) { + if (Globals::globalConfig.iMsgLevel >= MSGLEVEL_VERBOSE) + std::cerr << "skipped blacklisted file " << filepath << std::endl; + continue; + } + + std::cout << "\tgamename: " << game.dlcs[j].gamename << std::endl + << "\tproduct id: " << game.dlcs[j].product_id << std::endl + << "\tid: " << game.dlcs[j].patches[k].id << std::endl + << "\tname: " << game.dlcs[j].patches[k].name << std::endl + << "\tpath: " << game.dlcs[j].patches[k].path << std::endl + << "\tsize: " << game.dlcs[j].patches[k].size << std::endl + << "\tversion: " << game.dlcs[j].patches[k].version << std::endl + << std::endl; + } + for (unsigned int k = 0; k < game.dlcs[j].extras.size(); ++k) + { + std::string filepath = game.dlcs[j].extras[k].getFilepath(); + if (Globals::globalConfig.blacklist.isBlacklisted(filepath)) { + if (Globals::globalConfig.iMsgLevel >= MSGLEVEL_VERBOSE) + std::cerr << "skipped blacklisted file " << filepath << std::endl; + continue; + } + + std::cout << "\tgamename: " << game.dlcs[j].gamename << std::endl + << "\tproduct id: " << game.dlcs[j].product_id << std::endl + << "\tid: " << game.dlcs[j].extras[k].id << std::endl + << "\tname: " << game.dlcs[j].extras[k].name << std::endl + << "\tpath: " << game.dlcs[j].extras[k].path << std::endl + << "\tsize: " << game.dlcs[j].extras[k].size << std::endl + << std::endl; + } + for (unsigned int k = 0; k < game.dlcs[j].languagepacks.size(); ++k) + { + std::string filepath = game.dlcs[j].languagepacks[k].getFilepath(); + if (Globals::globalConfig.blacklist.isBlacklisted(filepath)) { + if (Globals::globalConfig.iMsgLevel >= MSGLEVEL_VERBOSE) + std::cerr << "skipped blacklisted file " << filepath << std::endl; + continue; + } + + std::cout << "\tgamename: " << game.dlcs[j].gamename << std::endl + << "\tproduct id: " << game.dlcs[j].product_id << std::endl + << "\tid: " << game.dlcs[j].languagepacks[k].id << std::endl + << "\tname: " << game.dlcs[j].languagepacks[k].name << std::endl + << "\tpath: " << game.dlcs[j].languagepacks[k].path << std::endl + << "\tsize: " << game.dlcs[j].languagepacks[k].size << std::endl + << std::endl; + } + } + } +}