/* * Copyright (C) 2006, 2007 Ronald Lamprecht * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "lev/RatingManager.hh" #include "main.hh" #include "ecl_util.hh" #include "errors.hh" #include "LocalToXML.hh" #include "Utf8ToXML.hh" #include "XMLtoUtf8.hh" #include #include #include #include #include #if _XERCES_VERSION < 30000 #include #endif using namespace std; using namespace enigma; XERCES_CPP_NAMESPACE_USE namespace enigma { namespace lev { std::time_t timeValue(const std::string &timeString) { std::tm time; time.tm_year = atoi(timeString.substr(0,4).c_str()) - 1900; time.tm_mon = atoi(timeString.substr(5,2).c_str()) - 1; time.tm_mday = atoi(timeString.substr(8,2).c_str()); time.tm_hour = atoi(timeString.substr(11,2).c_str()); time.tm_min = atoi(timeString.substr(14,2).c_str()); time.tm_sec = atoi(timeString.substr(17,2).c_str()); time.tm_isdst = false; return std::mktime(&time); } std::time_t timeValue(const XMLCh * const string) { std::string timeString = XMLtoUtf8(string).c_str(); return timeValue(timeString); } Rating::Rating() { intelligence = 0; dexterity = 0; patience = 0; knowledge = 0; speed = 0; bestScoreEasy = -1; //DEFAULT_TIME; bestScoreDifficult = -1; //DEFAULT_TIME; parScoreEasy = -1; parScoreDifficult = -1; numSolvedEasy = 0; numSolvedDifficult = 0; pdmSolvedEasy = 0; pdmSolvedDifficult = 0; averageRating = -1; } RatingManager *RatingManager::theSingleton = 0; RatingManager* RatingManager::instance() { if (theSingleton == 0) { theSingleton = new RatingManager(); } return theSingleton; } RatingManager::RatingManager() : didAddRatings (false), didEditRatings (false) { std::tm time; time.tm_year = 2006 - 1900; // the past - all ratings are newer as this time.tm_mon = 02; // is the date it was coded time.tm_mday = 30; time.tm_hour = 20; time.tm_min = 10; time.tm_sec = 00; time.tm_isdst = false; ratingVersion = std::mktime(&time); if(app.state->getString("RatingsUpdateTime").empty()) { // guarantee usable "RatingsUpdateTime" char str[22]; std::strftime(str, 22, "%Y-%m-%dT%H:%M:%SZ", std::gmtime(&ratingVersion)); std::string currentTimeString = str; app.state->setProperty("RatingsUpdateTime", currentTimeString); } std::string sysRatingPath; std::string userRatingPath; if (app.systemFS->findFile(RATINGSFILENAME, sysRatingPath)) { // preload cache with application distributed ratings loadURI(sysRatingPath); } if (app.resourceFS->findFile(RATINGSFILENAME, userRatingPath) && userRatingPath != sysRatingPath) { // update with user saved ratings - ignore cache changes as there // is no reason to store the cache. loadURI(userRatingPath); } if (app.prefs->getBool("RatingsAutoUpdate") == true) update(); } RatingManager::~RatingManager() { } RatingManager::loadResult RatingManager::loadURI(std::string path) { RatingManager::loadResult status = checked; try { app.domParserErrorHandler->resetErrors(); app.domParserErrorHandler->reportToErr(); app.domParserSchemaResolver->resetResolver(); app.domParserSchemaResolver->addSchemaId("ratings.xsd","ratings.xsd"); DOMDocument *doc = app.domParser->parseURI(path.c_str()); if (doc != NULL && !app.domParserErrorHandler->getSawErrors()) { DOMElement *updateElem = reinterpret_cast(doc->getElementsByTagName( Utf8ToXML("update").x_str())->item(0)); std::time_t newVersion = timeValue(updateElem->getAttribute(Utf8ToXML("date").x_str())); bool isUpdate = (std::difftime(newVersion, ratingVersion) > 0) ? true : false; if (isUpdate) { ratingVersion = newVersion; ratingVersionString = XMLtoUtf8(updateElem->getAttribute(Utf8ToXML("date").x_str())).c_str(); urlFullUpdate = XMLtoUtf8(updateElem->getAttribute(Utf8ToXML("urlFull").x_str())).c_str(); urlIncrementalUpdate = XMLtoUtf8(updateElem->getAttribute(Utf8ToXML("urlIncremental").x_str())).c_str(); updateMinDelay = XMLString::parseInt(updateElem->getAttribute(Utf8ToXML("delay").x_str())); } DOMNodeList *levelList = doc->getElementsByTagName( Utf8ToXML("level").x_str()); for (int i = 0, l = levelList-> getLength(); i < l; i++) { DOMElement *levelElem = reinterpret_cast(levelList->item(i)); const XMLCh * attr = levelElem->getAttribute(Utf8ToXML("id").x_str()); std::string id = XMLtoUtf8(attr).c_str(); attr = levelElem->getAttribute(Utf8ToXML("sv").x_str()); short sv = XMLString::stringLen(attr) > 0 ? XMLString::parseInt(attr) : 0; std::string cacheKey = id + "#" + ecl::strf("%d", sv); std::map::iterator it = cache.find(cacheKey); Rating * theRating; bool isNewRating = false; if (it != cache.end()) { if (isUpdate) // update the outdated entry theRating = it->second; else // keep the newer entry continue; } else { // add not existing entries in all cases theRating = new Rating(); isNewRating = true; } status = updated; attr = levelElem->getAttribute(Utf8ToXML("int").x_str()); theRating->intelligence = XMLString::stringLen(attr) > 0 ? XMLString::parseInt(attr) : 0; attr = levelElem->getAttribute(Utf8ToXML("dex").x_str()); theRating->dexterity = XMLString::stringLen(attr) > 0 ? XMLString::parseInt(attr) : 0; attr = levelElem->getAttribute(Utf8ToXML("pat").x_str()); theRating->patience = XMLString::stringLen(attr) > 0 ? XMLString::parseInt(attr) : 0; attr = levelElem->getAttribute(Utf8ToXML("kno").x_str()); theRating->knowledge = XMLString::stringLen(attr) > 0 ? XMLString::parseInt(attr) : 0; attr = levelElem->getAttribute(Utf8ToXML("spe").x_str()); theRating->speed = XMLString::stringLen(attr) > 0 ? XMLString::parseInt(attr) : 0; attr = levelElem->getAttribute(Utf8ToXML("bse").x_str()); theRating->bestScoreEasy = XMLString::stringLen(attr) > 0 ? XMLString::parseInt(attr) : -1; attr = levelElem->getAttribute(Utf8ToXML("bseh").x_str()); theRating->bestScoreEasyHolder = XMLtoUtf8(attr).c_str(); attr = levelElem->getAttribute(Utf8ToXML("bsd").x_str()); theRating->bestScoreDifficult = XMLString::stringLen(attr) > 0 ? XMLString::parseInt(attr) : -1; attr = levelElem->getAttribute(Utf8ToXML("bsdh").x_str()); theRating->bestScoreDifficultHolder = XMLtoUtf8(attr).c_str(); attr = levelElem->getAttribute(Utf8ToXML("pare").x_str()); theRating->parScoreEasy = XMLString::stringLen(attr) > 0 ? XMLString::parseInt(attr) : -1; attr = levelElem->getAttribute(Utf8ToXML("pard").x_str()); theRating->parScoreDifficult = XMLString::stringLen(attr) > 0 ? XMLString::parseInt(attr) : -1; attr = levelElem->getAttribute(Utf8ToXML("solvne").x_str()); theRating->numSolvedEasy = XMLString::stringLen(attr) > 0 ? XMLString::parseInt(attr) : 0; attr = levelElem->getAttribute(Utf8ToXML("solvpe").x_str()); theRating->pdmSolvedEasy = XMLString::stringLen(attr) > 0 ? XMLString::parseInt(attr) : 0; attr = levelElem->getAttribute(Utf8ToXML("solvnd").x_str()); theRating->numSolvedDifficult = XMLString::stringLen(attr) > 0 ? XMLString::parseInt(attr) : 0; attr = levelElem->getAttribute(Utf8ToXML("solvpd").x_str()); theRating->pdmSolvedDifficult = XMLString::stringLen(attr) > 0 ? XMLString::parseInt(attr) : 0; attr = levelElem->getAttribute(Utf8ToXML("avgur").x_str()); theRating->averageRating = XMLString::stringLen(attr) > 0 ? XMLString::parseInt(attr) : -1; if (isNewRating) cache.insert(std::make_pair(cacheKey, theRating)); } doc->release(); } else { status = error; } } catch (const XMLException& toCatch) { char* message = XMLString::transcode(toCatch.getMessage()); cerr << "Exception on load of preferences: " << message << "\n"; XMLString::release(&message); return error; } catch (const DOMException& toCatch) { char* message = XMLString::transcode(toCatch.msg); cerr << "Exception on load of preferences: " << message << "\n"; XMLString::release(&message); return error; } catch (...) { cerr << "Unexpected Exception on load of preferences\n" ; return error; } return status; } void RatingManager::update() { std::time_t currentTime = std::time(NULL); // check if an update is allowed std::time_t currentWeirdTime = std::mktime(gmtime(¤tTime)); double updateDelay = std::difftime(currentWeirdTime, timeValue(app.state->getString("RatingsUpdateTime"))); if (updateDelay/86400 < updateMinDelay) { Log << "Rating update not yet allowed: " < 3 * updateMinDelay) { result = loadURI(urlFullUpdate); } } if (result != error) { // save current time as rating update time char str[22]; std::strftime(str, 22, "%Y-%m-%dT%H:%M:%SZ", std::gmtime(¤tTime)); std::string currentTimeString = str; app.state->setProperty("RatingsUpdateTime", currentTimeString); } if (didUpdate) didAddRatings = true; // we need to save the cache else if (result == checked) ; // the update had no new ratings } // the following local vars should be ivars or arguments to doit - but // as is anti OO it does not allow to transfer a context. DOMDocument *saveDoc; DOMElement *levelsElem; void saveLevelRating(const std::map::value_type info) { DOMElement *level = saveDoc->createElement(Utf8ToXML("level").x_str()); std::string id = info.first.substr(0, info.first.rfind('#')); std::string sv = info.first.substr(info.first.rfind('#')+1); level->setAttribute(Utf8ToXML("id").x_str(), Utf8ToXML(id.c_str()).x_str()); level->setAttribute(Utf8ToXML("sv").x_str(), Utf8ToXML(sv.c_str()).x_str()); level->setAttribute(Utf8ToXML("int").x_str(), Utf8ToXML(ecl::strf("%d",info.second->intelligence).c_str()).x_str()); level->setAttribute(Utf8ToXML("dex").x_str(), Utf8ToXML(ecl::strf("%d",info.second->dexterity).c_str()).x_str()); level->setAttribute(Utf8ToXML("pat").x_str(), Utf8ToXML(ecl::strf("%d",info.second->patience).c_str()).x_str()); level->setAttribute(Utf8ToXML("kno").x_str(), Utf8ToXML(ecl::strf("%d",info.second->knowledge).c_str()).x_str()); level->setAttribute(Utf8ToXML("spe").x_str(), Utf8ToXML(ecl::strf("%d",info.second->speed).c_str()).x_str()); level->setAttribute(Utf8ToXML("bse").x_str(), Utf8ToXML(ecl::strf("%d",info.second->bestScoreEasy).c_str()).x_str()); level->setAttribute(Utf8ToXML("bsd").x_str(), Utf8ToXML(ecl::strf("%d",info.second->bestScoreDifficult).c_str()).x_str()); level->setAttribute(Utf8ToXML("bseh").x_str(), Utf8ToXML(info.second->bestScoreEasyHolder.c_str()).x_str()); level->setAttribute(Utf8ToXML("bsdh").x_str(), Utf8ToXML(info.second->bestScoreDifficultHolder.c_str()).x_str()); level->setAttribute(Utf8ToXML("pare").x_str(), Utf8ToXML(ecl::strf("%d",info.second->parScoreEasy).c_str()).x_str()); level->setAttribute(Utf8ToXML("pard").x_str(), Utf8ToXML(ecl::strf("%d",info.second->parScoreDifficult).c_str()).x_str()); level->setAttribute(Utf8ToXML("solvne").x_str(), Utf8ToXML(ecl::strf("%d",info.second->numSolvedEasy).c_str()).x_str()); level->setAttribute(Utf8ToXML("solvpe").x_str(), Utf8ToXML(ecl::strf("%d",info.second->pdmSolvedEasy).c_str()).x_str()); level->setAttribute(Utf8ToXML("solvnd").x_str(), Utf8ToXML(ecl::strf("%d",info.second->numSolvedDifficult).c_str()).x_str()); level->setAttribute(Utf8ToXML("solvpd").x_str(), Utf8ToXML(ecl::strf("%d",info.second->pdmSolvedDifficult).c_str()).x_str()); level->setAttribute(Utf8ToXML("avgur").x_str(), Utf8ToXML(ecl::strf("%d",info.second->averageRating).c_str()).x_str()); levelsElem->appendChild(level); } void RatingManager::save() { if (!(didAddRatings || didEditRatings)) return; if (didEditRatings) { // set the rating version to the current time char date[25]; std::time_t current = std::time(NULL); strftime(date, 25, "%Y-%m-%dT%H:%M:%SZ",gmtime(¤t)); ratingVersionString = date; } std::string ratTemplatePath; if (!app.systemFS->findFile("schemas/ratings.xml" , ratTemplatePath)) { cerr << "Ratings: no template found\n"; return; } try { app.domParserErrorHandler->resetErrors(); app.domParserErrorHandler->reportToErr(); app.domParserSchemaResolver->resetResolver(); app.domParserSchemaResolver->addSchemaId("ratings.xsd","ratings.xsd"); saveDoc = app.domParser->parseURI(ratTemplatePath.c_str()); if (saveDoc != NULL && !app.domParserErrorHandler->getSawErrors()) { DOMElement * updateElem = reinterpret_cast(saveDoc->getElementsByTagName( Utf8ToXML("update").x_str())->item(0)); updateElem->setAttribute(Utf8ToXML("date").x_str(), Utf8ToXML(ratingVersionString.c_str()).x_str()); updateElem->setAttribute(Utf8ToXML("urlFull").x_str(), Utf8ToXML(urlFullUpdate.c_str()).x_str()); updateElem->setAttribute(Utf8ToXML("urlIncremental").x_str(), Utf8ToXML(urlIncrementalUpdate.c_str()).x_str()); updateElem->setAttribute(Utf8ToXML("delay").x_str(), Utf8ToXML(ecl::strf("%d",updateMinDelay).c_str()).x_str()); levelsElem = reinterpret_cast(saveDoc->getElementsByTagName( Utf8ToXML("levels").x_str())->item(0)); std::for_each(cache.begin(), cache.end(), saveLevelRating); std::string writePath = app.userPath + "/" + RATINGSFILENAME; #if _XERCES_VERSION >= 30000 app.domSer->writeToURI(saveDoc, LocalToXML(writePath.c_str()).x_str()); #else XMLFormatTarget *myFormTarget = new LocalFileFormatTarget(writePath.c_str()); app.domSer->writeNode(myFormTarget, *saveDoc); #endif saveDoc->release(); } } catch (const XMLException& toCatch) { char* message = XMLString::transcode(toCatch.getMessage()); cerr << "Exception on load of preferences: " << message << "\n"; XMLString::release(&message); return; } catch (const DOMException& toCatch) { char* message = XMLString::transcode(toCatch.msg); cerr << "Exception on load of preferences: " << message << "\n"; XMLString::release(&message); return; } catch (...) { cerr << "Unexpected Exception on load of preferences\n" ; return; } } void RatingManager::registerRating(std::string levelId, short levelScoreVersion, short intelligence, short dexterity, short patience, short knowledge, short speed, short bestScoreEasy, std::string bestScoreEasyHolder, short bestScoreDifficult, std::string bestScoreDifficultHolder) { std::string cacheKey = levelId + "#" + ecl::strf("%d", levelScoreVersion); std::map::iterator i = cache.find(cacheKey); if (i != cache.end()) { // Log << "Rating cache hit for " << levelId <<"\n"; return; } Rating *theRating = new Rating(); theRating->intelligence = intelligence; theRating->dexterity = dexterity; theRating->patience = patience; theRating->knowledge = knowledge; theRating->speed = speed; theRating->bestScoreEasy = bestScoreEasy; theRating->bestScoreEasyHolder = bestScoreEasyHolder; theRating->bestScoreDifficult = bestScoreDifficult; theRating->bestScoreDifficultHolder = bestScoreDifficultHolder; cache.insert(std::make_pair(cacheKey, theRating)); didAddRatings = true; return; } Rating * RatingManager::registerNewRating(Proxy * levelProxy) { std::string cacheKey = levelProxy->getId() + "#" + ecl::strf("%d", levelProxy->getScoreVersion()); std::map::iterator it = cache.find(cacheKey); if (it != cache.end()) { return it->second; } Rating *theRating = new Rating(); cache.insert(std::make_pair(cacheKey, theRating)); didAddRatings = true; return theRating; } Rating * RatingManager::findRating(Proxy * levelProxy) { std::string cacheKey = levelProxy->getId() + "#" + ecl::strf("%d", levelProxy->getScoreVersion()); std::map::iterator it = cache.find(cacheKey); if (it != cache.end()) { Rating * theRating = it->second; return theRating; } else { return NULL; } } short RatingManager::getIntelligence(Proxy *levelProxy) { Rating * theRating = findRating(levelProxy); if (theRating != NULL) { return theRating->intelligence; } return 0; } void RatingManager::setIntelligence(Proxy *levelProxy, short intelligence) { Rating * theRating = registerNewRating(levelProxy); if (theRating != NULL) { theRating->intelligence = intelligence; didEditRatings = true; } } short RatingManager::getDexterity(Proxy *levelProxy) { Rating * theRating = findRating(levelProxy); if (theRating != NULL) { return theRating->dexterity; } return 0; } void RatingManager::setDexterity(Proxy *levelProxy, short dexterity) { Rating * theRating = registerNewRating(levelProxy); if (theRating != NULL) { theRating->dexterity = dexterity; didEditRatings = true; } } short RatingManager::getPatience(Proxy *levelProxy) { Rating * theRating = findRating(levelProxy); if (theRating != NULL) { return theRating->patience; } return 0; } void RatingManager::setPatience(Proxy *levelProxy, short patience) { Rating * theRating = registerNewRating(levelProxy); if (theRating != NULL) { theRating->patience = patience; didEditRatings = true; } } short RatingManager::getKnowledge(Proxy *levelProxy) { Rating * theRating = findRating(levelProxy); if (theRating != NULL) { return theRating->knowledge; } return 0; } void RatingManager::setKnowledge(Proxy *levelProxy, short knowledge) { Rating * theRating = registerNewRating(levelProxy); if (theRating != NULL) { theRating->knowledge = knowledge; didEditRatings = true; } } short RatingManager::getSpeed(Proxy *levelProxy) { Rating * theRating = findRating(levelProxy); if (theRating != NULL) { return theRating->speed; } return 0; } void RatingManager::setSpeed(Proxy *levelProxy, short speed) { Rating * theRating = registerNewRating(levelProxy); if (theRating != NULL) { theRating->speed = speed; didEditRatings = true; } } short RatingManager::getDifficulty(Proxy *levelProxy) { int difficulty = 7*getIntelligence(levelProxy) + 6*getDexterity(levelProxy) + 4*getPatience(levelProxy) + 3*getKnowledge(levelProxy) + 4*getSpeed(levelProxy) - 23; return difficulty > 0 ? difficulty : 0; } short RatingManager::getBestScore(Proxy *levelProxy, int difficulty) { if (difficulty == DIFFICULTY_EASY && !levelProxy->hasEasymode()) difficulty = DIFFICULTY_HARD; switch (difficulty) { case DIFFICULTY_EASY: return getBestScoreEasy(levelProxy); case DIFFICULTY_HARD: return getBestScoreDifficult(levelProxy); default: ecl::Assert (false, "RatingManager::getBestScore illegal difficulty"); } return -1; } std::string RatingManager::cutHolders(std::string org, int factor) { if (factor <= 0) return org; int others = 0; if (org.rfind(" others") == org.length() - 7) { // ratings string is already cut std::string::size_type lastPlus = org.rfind('+'); std::istringstream othersString(org.substr(lastPlus + 1)); othersString >> std::dec >> others; org = org.substr(0, lastPlus); } std::string::size_type cutPlus; for (int i=0; i< factor; i++) { cutPlus = org.rfind('+'); if (cutPlus == std::string::npos) return ""; org = org.substr(0, cutPlus); others++; } return ecl::strf("%s+%d others", org.c_str(), others); } std::string RatingManager::getBestScoreHolder(Proxy *levelProxy, int difficulty, int cut) { if (difficulty == DIFFICULTY_EASY && !levelProxy->hasEasymode()) difficulty = DIFFICULTY_HARD; switch (difficulty) { case DIFFICULTY_EASY: return getBestScoreEasyHolder(levelProxy, cut); case DIFFICULTY_HARD: return getBestScoreDifficultHolder(levelProxy, cut); default: ecl::Assert (false, "RatingManager::getBestScoreHolder illegal difficulty"); } return ""; } short RatingManager::getBestScoreEasy(Proxy *levelProxy) { Rating * theRating = findRating(levelProxy); if (theRating != NULL) { return theRating->bestScoreEasy; } return -1; } std::string RatingManager::getBestScoreEasyHolder(Proxy *levelProxy, int cut) { Rating * theRating = findRating(levelProxy); if (theRating != NULL) { return cutHolders(theRating->bestScoreEasyHolder, cut); } return ""; } short RatingManager::getBestScoreDifficult(Proxy *levelProxy) { Rating * theRating = findRating(levelProxy); if (theRating != NULL) { return theRating->bestScoreDifficult; } return -1; } std::string RatingManager::getBestScoreDifficultHolder(Proxy *levelProxy, int cut) { Rating * theRating = findRating(levelProxy); if (theRating != NULL) { return cutHolders(theRating->bestScoreDifficultHolder, cut); } return ""; } short RatingManager::getParScore(Proxy *levelProxy, int difficulty) { if (difficulty == DIFFICULTY_EASY && !levelProxy->hasEasymode()) difficulty = DIFFICULTY_HARD; switch (difficulty) { case DIFFICULTY_EASY: return getParScoreEasy(levelProxy); case DIFFICULTY_HARD: return getParScoreDifficult(levelProxy); default: ecl::Assert (false, "RatingManager::getParScore illegal difficulty"); } return -1; } short RatingManager::getParScoreEasy(Proxy *levelProxy) { Rating * theRating = findRating(levelProxy); if (theRating != NULL) { return theRating->parScoreEasy; } return -1; } short RatingManager::getParScoreDifficult(Proxy *levelProxy) { Rating * theRating = findRating(levelProxy); if (theRating != NULL) { return theRating->parScoreDifficult; } return -1; } short RatingManager::getNumSolvedEasy(Proxy *levelProxy) { Rating * theRating = findRating(levelProxy); if (theRating != NULL) { return theRating->numSolvedEasy; } return 0; } short RatingManager::getNumSolvedDifficult(Proxy *levelProxy) { Rating * theRating = findRating(levelProxy); if (theRating != NULL) { return theRating->numSolvedDifficult; } return 0; } short RatingManager::getPdmSolvedEasy(Proxy *levelProxy) { Rating * theRating = findRating(levelProxy); if (theRating != NULL) { return theRating->pdmSolvedEasy; } return 0; } std::string RatingManager::getPcSolvedEasy(Proxy *levelProxy) { std::string s = ecl::strf("%5.3d", getPdmSolvedEasy(levelProxy)); s.insert(3,1,'.'); return s; } short RatingManager::getPdmSolvedDifficult(Proxy *levelProxy) { Rating * theRating = findRating(levelProxy); if (theRating != NULL) { return theRating->pdmSolvedDifficult; } return 0; } std::string RatingManager::getPcSolvedDifficult(Proxy *levelProxy) { std::string s = ecl::strf("%5.3d", getPdmSolvedDifficult(levelProxy)); s.insert(3,1,'.'); return s; } short RatingManager::getDAverageRating(Proxy *levelProxy) { Rating * theRating = findRating(levelProxy); if (theRating != NULL) { return theRating->averageRating; } return -1; } std::string RatingManager::getAverageRating(Proxy *levelProxy) { short rat = getDAverageRating(levelProxy); std::string s; if ( rat >= 0 ) { s = ecl::strf("%3.2d", rat); s.insert(2,1,'.'); } else { s = "-"; } return s; } }} // namespace enigma::lev