#include "datadragon.h" #include #include #include #include #include #include #include #include #include // std::max, champ matching #include "json.h" static const QString BASEURL = "https://ddragon.leagueoflegends.com/"; const DataDragon::ChampData DataDragon::EMPTYCHAMP; DataDragon::DataDragon(const QString& locale) : RestClient(BASEURL), locale(locale), cache({{"square", ".png"}, {"loading", "_0.jpg"}, {"splash", "_0.jpg"}}) { this->setObjectName("DataDragon"); } DataDragon::~DataDragon() { stopAndJoinThread(); } DataDragon::ChampData::ChampData() : key(-1) {} DataDragon::ChampData::ChampData(const QJsonObject& source) { name = getValue(source, "name", ""); id = getValue(source, "id", ""); key = getValue(source, "key", -1); partype = getValue(source, "partype", ""); title = getValue(source, "title", ""); } const QString& DataDragon::getVersion() { std::unique_lock lock(cachedatamutex); while(version.isEmpty() && shouldrun) { cachedatacv.wait(lock); } return version; } const std::vector& DataDragon::getChamps() { std::unique_lock lock(cachedatamutex); while(champs.empty() && shouldrun) { cachedatacv.wait(lock); } return champs; } QPixmap DataDragon::getImage(const QString& champid, ImageType imgtype, bool writeMemcache) { if(champid.isEmpty()) return {}; // query mem cache QPixmap img = memcache.getImage(champid, (int) imgtype); if(!img.isNull()) return img; // query HDD cache img = cache[(int) imgtype].getImage(champid); if(!img.isNull()) { // update mem cache memcache.addImage(img, champid, (int) imgtype); return img; } const QString url = getImageUrl(champid, imgtype); if(url.isEmpty()) return {}; QByteArray arr; try { arr = requestRaw(url); } catch(RestClient::WebException& e) {} if(arr.isEmpty()) { qCritical() << "image could not be loaded"; return {}; } // propably an error if(arr.size() < 1000) { qInfo() << "small icon url: " << url; qInfo() << "content: " << QString::fromLocal8Bit(arr); return {}; } // store HDD cache cache[(int) imgtype].addImageRaw(arr, champid); // remove from notDownloadedList notDownloadedImages.erase(champid); QPixmap decodedImage; decodedImage.loadFromData(arr); // store mem cache if(writeMemcache) { memcache.addImage(decodedImage, champid, (int) imgtype); } return decodedImage; } void DataDragon::getImageAsnyc(const QString& champid, notifyImgfunc_t func, ImageType imgtype) { if(!func) return; { std::lock_guard lock(tasksmutex); tasks.push_back({champid, func, imgtype}); } tasksnotemptycv.notify_one(); } static int startinglength(const QString& original, const QString& prefix) { int i = 0; for(; i < original.size() && i < prefix.size(); ++i) { if(original.left(i+1) != prefix.left(i+1)) { return i; } } return i; } static size_t matchChamp(const DataDragon::ChampData& champ, const QString& lower) { QString lowerid = champ.id.toLower(); QString lowername = champ.name.toLower(); if(lowerid == lower || lowername == lower) { return lower.size(); } int lengtha = startinglength(lowerid, lower); int lengthb = startinglength(lowername, lower); return qMax(lengtha, lengthb); } const DataDragon::ChampData& DataDragon::getBestMatchingChamp(const QString& name, int* count) { getChamps(); // for now: just check for a perfect hit QString lower = name.toLower(); int bestmatchingoffset = -1; int bestmatchinglength = 0; uint32_t bestmatchingcount = 0; for(size_t offset = 0; offset < champs.size(); ++offset) { const ChampData& it = champs.at(offset); int match = matchChamp(it, lower); if(match > bestmatchinglength) { bestmatchinglength = match; bestmatchingcount = 1; bestmatchingoffset = offset; } else if(match == bestmatchinglength) { bestmatchingcount ++; } } if(bestmatchinglength < lower.size()) { if(count) *count = 0; return EMPTYCHAMP; } if(count) *count = bestmatchingcount; if(bestmatchingoffset >= 0) return champs.at(bestmatchingoffset); return EMPTYCHAMP; } std::vector DataDragon::getMatchingChamp(const QString& name, uint32_t limit) { if(limit == 0) return {}; if(name.isEmpty()) return {}; getChamps(); QString lower = name.toLower(); std::vector matches; matches.resize(champs.size()); std::transform(champs.begin(), champs.end(), std::insert_iterator(matches, matches.begin()), std::bind(&matchChamp, std::placeholders::_1, lower)); // find smallest element std::vector matches_sort = matches; std::nth_element(matches_sort.begin(), matches_sort.begin(), matches_sort.end(), std::greater{}); size_t border = matches_sort.at(0); Log::trace << "border: " << border; std::vector out; out.reserve(limit); // take all with a match higher than that for(uint32_t i = 0; i < matches.size() && out.size() < limit; ++i) { if(matches[i] >= border) { out.push_back(&champs[i]); } } return out; } const DataDragon::ChampData* DataDragon::getChampByID(uint32_t id) { getChamps(); auto it = std::find_if(champs.begin(), champs.end(), [id](const ChampData& cd) { return cd.key == (int) id; }); // nothing found if(it == champs.end()) return nullptr; return &*it; } std::vector DataDragon::resolveChampIDs(const std::vector& champnames) { std::vector out; out.reserve(champnames.size()); std::transform(champnames.begin(), champnames.end(), std::insert_iterator(out, out.begin()), [this](const QString& champname) { auto cd = getBestMatchingChamp(champname); return cd.key; // might be 0 (invalid) }); return out; } void DataDragon::startThread() { shouldrun = true; bgthread = QThread::create(&DataDragon::threadLoop, this); bgthread->setObjectName("DataDragonThread"); bgthread->start(); this->moveToThread(bgthread); } void DataDragon::stop() { qDebug() << "stop DataDragon"; shouldrun = false; std::unique_lock lock(tasksmutex); tasks.clear(); notDownloadedImages.clear(); // this is a possible race condition! } QString DataDragon::getImageUrl(const QString& champid, ImageType type) { switch(type) { case ImageType::SQUARE: { if(getVersion().isEmpty()) { return {}; } // with version return getCDNString() + "img/champion/" + champid + ".png"; } case ImageType::LOADING: // no version return "cdn/img/champion/loading/" + champid + "_0.jpg"; case ImageType::SPLASH: // no version return "cdn/img/champion/splash/" + champid + "_0.jpg"; } return {}; } QString DataDragon::getCDNString() const { return "cdn/" + version + "/"; } void DataDragon::prefetchChampImage(const QString& champid, ImageType imgtype) { if(!cache[(int) imgtype].hasImage(champid)) { qDebug() << "prefetch " << champid << " type: " << (int) imgtype; getImage(champid, imgtype, false); } } void DataDragon::getVersionInternal() { std::unique_lock lock(cachedatamutex); if(!version.isEmpty()) return; version = champCache.getVersion(); if(!version.isEmpty()) return; try { QJsonDocument jversions = request("api/versions.json"); if(jversions.isArray()) { QJsonArray jverarr = jversions.array(); if(!jverarr.empty()) { version = jverarr.at(0).toString(); qInfo() << "got League version: " << version; lock.unlock(); cachedatacv.notify_all(); return; } } qCritical() << "error parsing version object"; } catch(RestClient::WebException& e) {} } void DataDragon::getChampsInternal() { std::unique_lock lock(cachedatamutex); if(!champs.empty() && version.isEmpty()) { return; } QJsonDocument jchamps = champCache.getChamps(); bool cacheSuccessfull = !jchamps.isEmpty(); if(!cacheSuccessfull) { try { jchamps = request(getCDNString() + "data/" + locale + "/champion.json"); if(jchamps.isEmpty()) { // try again with default locale locale = "en_US"; jchamps = request(getCDNString() + "data/" + locale + "/champion.json"); } } catch(RestClient::WebException& e) {} } if(jchamps.isObject()) { // save to cache if(!cacheSuccessfull) { champCache.saveChamps(jchamps, version); } QJsonObject obj = jchamps.object(); auto it = obj.constFind("data"); if(it != obj.constEnd() && it.value().isObject()) { QJsonObject jchampsdata = it.value().toObject(); for(auto champit = jchampsdata.constBegin(); champit != jchampsdata.constEnd(); champit++) { if(champit.value().isObject()) { champs.emplace_back(champit.value().toObject()); notDownloadedImages.insert(champs.back().id); } } } } qInfo() << "loaded " << champs.size() << " champs from cache: " << cacheSuccessfull; lock.unlock(); cachedatacv.notify_all(); } void DataDragon::stopThread() { shouldrun = false; tasksnotemptycv.notify_all(); } void DataDragon::stopAndJoinThread() { stopThread(); bgthread->wait(); } void DataDragon::threadLoop() { QEventLoop loop; // init version and champ list getVersionInternal(); getChampsInternal(); while(shouldrun) { // wait for a task in the list Task t; { std::unique_lock lock(tasksmutex); if(tasks.empty()) { if(notDownloadedImages.empty()) { tasksnotemptycv.wait(lock); } else { Log::note << "DataDragon background thread is idleing - prefetching champion images TODO: " << notDownloadedImages.size(); while(shouldrun && !notDownloadedImages.empty() && tasks.empty()) { lock.unlock(); auto it = notDownloadedImages.begin(); QString champid = *it; notDownloadedImages.erase(it); prefetchChampImage(champid, ImageType::SQUARE); emit this->loading(1.0 - (notDownloadedImages.size() / (float) champs.size())); emit this->fetchingChamp(champid); lock.lock(); } if(notDownloadedImages.empty() && tasks.empty()) { // everything prefetched, but nothing more to do static bool once = false; if(!once) { once = true; Log::note << "all champs are prefetched now"; emit this->loading( 1.0 ); } tasksnotemptycv.wait(lock); } } } if(tasks.empty()) continue; t = tasks.front(); tasks.pop_front(); } loop.processEvents(); QPixmap img = getImage(t.champid, t.type); t.func(img); } qDebug() << "DataDragon Thread terminated"; } std::ostream& operator<<(std::ostream& str, const DataDragon::ChampData& cd) { return str << "[n: " << cd.name.toStdString() << " " << " k: " << cd.key << " id: " << cd.id.toStdString() << "]"; }