stk-code_catmod/src/addons/news_manager.cpp
2015-03-30 11:42:50 +11:00

486 lines
18 KiB
C++

// SuperTuxKart - a fun racing game with go-kart
// Copyright (C) 2011-2015 Joerg Henrichs
//
// 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 3
// 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#include "addons/news_manager.hpp"
#include "config/user_config.hpp"
#include "io/file_manager.hpp"
#include "online/http_request.hpp"
#include "online/request_manager.hpp"
#include "states_screens/addons_screen.hpp"
#include "states_screens/main_menu_screen.hpp"
#include "utils/string_utils.hpp"
#include "utils/time.hpp"
#include "utils/translation.hpp"
#include <iostream>
using namespace Online;
NewsManager *NewsManager::m_news_manager=NULL;
// ----------------------------------------------------------------------------
NewsManager::NewsManager() : m_news(std::vector<NewsMessage>())
{
m_current_news_message = -1;
m_error_message.setAtomic("");
m_force_refresh = false;
init(false);
} // NewsManage
// ---------------------------------------------------------------------------
NewsManager::~NewsManager()
{
} // ~NewsManager
// ---------------------------------------------------------------------------
/** This function initialises the data for the news manager. It starts a
* separate thread to execute downloadNews() - which (if necessary) downloads
* the news.xml file and updates the list of news messages. It also
* initialises the addons manager (which can trigger another download of
* news.xml).
* \param force_refresh Re-download news.xml, even if
*/
void NewsManager::init(bool force_refresh)
{
m_force_refresh = force_refresh;
// The rest (which potentially involves downloading news.xml) is handled
// in a separate thread, so that the GUI remains responsive. It is only
// started if internet access is enabled, else nothing is done in the
// thread anyway (and the addons menu is disabled as a result).
if(UserConfigParams::m_internet_status==RequestManager::IPERM_ALLOWED)
{
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
// Should be the default, but just in case:
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
//pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL);
pthread_t thread_id;
int error = pthread_create(&thread_id, &attr,
&NewsManager::downloadNews, this);
if (error)
{
Log::warn("news", "Could not create thread, error=%d", error);
// In this case just execute the downloading code with this thread
downloadNews(this);
}
pthread_attr_destroy(&attr);
}
} //init
// ---------------------------------------------------------------------------
/** This function submits request which will download the news.xml file
* if necessary. It is running in its own thread, so we can use blocking
* download calls without blocking the GUI.
* \param obj This is 'this' object, passed on during pthread creation.
*/
void* NewsManager::downloadNews(void *obj)
{
NewsManager *me = (NewsManager*)obj;
me->clearErrorMessage();
std::string xml_file = file_manager->getAddonsFile("news.xml");
bool news_exists = file_manager->fileExists(xml_file);
// The news message must be updated if either it has never been updated,
// or if the time of the last update was more than news_frequency ago,
// or because a 'refresh' was explicitly requested by the user, or no
// news.xml file exists.
bool download = ( UserConfigParams::m_news_last_updated==0 ||
UserConfigParams::m_news_last_updated
+UserConfigParams::m_news_frequency
< StkTime::getTimeSinceEpoch() ||
me->m_force_refresh ||
!news_exists )
&& UserConfigParams::m_internet_status==RequestManager::IPERM_ALLOWED;
const XMLNode *xml = NULL;
if(!download && news_exists)
{
// If (so far) we don't need to download, there should be an existing
// file. Try to read this, and do some basic checks
xml = new XMLNode(xml_file);
// A proper news file has at least a version number, mtime, frequency
// and an include node (which contains addon data) defined. If this is
// not the case, assume that it is an invalid download, or a corrupt
// local file. Try downloading again after resetting the news server
// back to the default.
int version=-1;
if( !xml->get("version", &version) || version!=1 ||
!xml->get("mtime", &version) ||
!xml->getNode("include") ||
!xml->get("frequency", &version) )
{
delete xml;
xml = NULL;
download = true;
} // if xml not consistemt
} // if !download
if(download)
{
core::stringw error_message("");
HTTPRequest *download_req = new HTTPRequest("news.xml");
download_req->setAddonsURL("news.xml");
// Initialise the online portion of the addons manager.
if(UserConfigParams::logAddons())
Log::info("addons", "Downloading news.");
download_req->executeNow();
if(download_req->hadDownloadError())
{
// Assume that the server address is wrong. And retry
// with the default server address again (just in case
// that a redirect went wrong, or a wrong/incorrect
// address somehow made its way into the config file.
delete download_req;
// We need a new object, since the state of the old
// download request is now done.
download_req = new HTTPRequest("news.xml");
UserConfigParams::m_server_addons.revertToDefaults();
// make sure the new server address is actually used
download_req->setAddonsURL("news.xml");
download_req->executeNow();
if(download_req->hadDownloadError())
{
// This message must be translated dynamically in the main menu.
// If it would be translated here, it wouldn't be translated
// if the language is changed in the menu!
error_message = N_("Error downloading news: '%s'.");
const char *const curl_error = download_req->getDownloadErrorMessage();
error_message = StringUtils::insertValues(error_message, curl_error);
addons_manager->setErrorState();
me->setErrorMessage(error_message);
Log::error("news", core::stringc(error_message).c_str());
} // hadDownloadError
} // hadDownloadError
if(!download_req->hadDownloadError())
UserConfigParams::m_news_last_updated = StkTime::getTimeSinceEpoch();
delete download_req;
// No download error, update the last_updated time value, and delete
// the potentially loaded xml file
delete xml;
xml = NULL;
} // hadDownloadError
if(xml) delete xml;
xml = NULL;
// Process new.xml now.
if(file_manager->fileExists(xml_file))
{
xml = new XMLNode(xml_file);
me->checkRedirect(xml);
me->updateNews(xml, xml_file);
addons_manager->init(xml, me->m_force_refresh);
delete xml;
}
// We can't finish stk (esp. delete the file manager) before
// this part of the code is reached (since otherwise the file
// manager might be access after it was deleted).
me->setCanBeDeleted();
pthread_exit(NULL);
return 0; // prevent warning
} // downloadNews
// ---------------------------------------------------------------------------
/** Checks if a redirect is received, causing a new server to be used for
* downloading addons.
* \param xml XML data structure containing the redirect information.
*/
void NewsManager::checkRedirect(const XMLNode *xml)
{
std::string new_server;
int result = xml->get("redirect", &new_server);
if(result==1 && new_server!="")
{
if(UserConfigParams::logAddons())
{
Log::info("[Addons]", "Current server: '%s'\n [Addons] New server: '%s'",
UserConfigParams::m_server_addons.c_str(), new_server.c_str());
}
UserConfigParams::m_server_addons = new_server;
}
std::string hw_report_server;
if(xml->get("hw-report-server", &hw_report_server)==1 && hw_report_server.size()>0)
{
Log::info("hw report", "New server at '%s'.", hw_report_server.c_str());
UserConfigParams::m_server_hw_report = hw_report_server;
}
float polling;
if(xml->get("menu-polling-interval", &polling))
{
RequestManager::get()->setMenuPollingInterval(polling);
}
if(xml->get("game-polling-interval", &polling))
{
RequestManager::get()->setGamePollingInterval(polling);
}
} // checkRedirect
// ----------------------------------------------------------------------------
/** Updates the 'news' string to be displayed in the main menu.
* \param xml The XML data from the news file.
* \param filename The filename of the news xml file. Only needed
* in case of an error (e.g. the file might be corrupted)
* - the file will be deleted so that on next start of stk it
* will be updated again.
*/
void NewsManager::updateNews(const XMLNode *xml, const std::string &filename)
{
m_all_news_messages = "";
const core::stringw message_divider=" +++ ";
// This function is also called in case of a reinit, so
// we have to delete existing news messages here first.
m_news.lock();
m_news.getData().clear();
m_news.unlock();
bool error = true;
int frequency=0;
if(xml->get("frequency", &frequency))
UserConfigParams::m_news_frequency = frequency;
for(unsigned int i=0; i<xml->getNumNodes(); i++)
{
const XMLNode *node = xml->getNode(i);
if(node->getName()!="message") continue;
core::stringw news;
node->get("content", &news);
int id=-1;
node->get("id", &id);
bool important=false;
node->get("important", &important);
std::string cond;
node->get("condition", &cond);
if(!conditionFulfilled(cond))
continue;
m_news.lock();
{
if(!important)
m_all_news_messages += m_all_news_messages.size()>0
? message_divider + news
: news;
else
// Define this if news messages should be removed
// after being shown a certain number of times.
{
NewsMessage n(news, id, important);
m_news.getData().push_back(n);
}
} // m_news.lock()
m_news.unlock();
error = false;
}
if(error)
{
// In case of an error (e.g. the file only contains
// an error message from the server), delete the file
// so that it is not read again (and this will force
// a new read on the next start, instead of waiting
// for some time).
file_manager->removeFile(filename);
NewsMessage n(_("Can't access stkaddons server..."), -1);
m_news.lock();
m_news.getData().push_back(n);
m_all_news_messages="";
m_news.unlock();
}
} // updateNews
// ----------------------------------------------------------------------------
/** Add a news message. This is used to add error messages, e.g. for problems
* when downloading addons.
* \param s The news message to add.
*/
void NewsManager::addNewsMessage(const core::stringw &s)
{
NewsMessage n(s, -1);
m_news.lock();
m_news.getData().push_back(n);
m_news.unlock();
} // addNewsMessage
// ----------------------------------------------------------------------------
/** Returns the important message with the smallest id that has not been
* shown, or NULL if no important (not shown before) message exists atm. The
* user config is updated to store the last important message id shown.
*/
const core::stringw NewsManager::getImportantMessage()
{
int index = -1;
m_news.lock();
for(unsigned int i=0; i<m_news.getData().size(); i++)
{
const NewsMessage &m = m_news.getData()[i];
//
if(m.isImportant() &&
m.getMessageId()>UserConfigParams::m_last_important_message_id &&
(index == -1 ||
m.getMessageId() < m_news.getData()[index].getMessageId() ) )
{
index = i;
} // if new unshown important message with smaller message id
}
core::stringw message("");
if(index>=0)
{
const NewsMessage &m = m_news.getData()[index];
message = m.getNews();
UserConfigParams::m_last_important_message_id = m.getMessageId();
}
m_news.unlock();
return message;
} // getImportantMessage
// ----------------------------------------------------------------------------
/** Returns the next loaded news message. It will 'wrap around', i.e.
* if there is only one message it will be returned over and over again.
* To be used by the the main menu to get the next news message after
* one message was scrolled off screen.
*/
const core::stringw NewsManager::getNextNewsMessage()
{
// Only display error message in case of a problem.
if(m_error_message.getAtomic().size()>0)
return _(m_error_message.getAtomic().c_str());
m_news.lock();
if(m_all_news_messages.size()>0)
{
// Copy the news message while it is locked.
core::stringw anm = m_all_news_messages;
m_news.unlock();
return anm;
}
if(m_news.getData().size()==0)
{
// Lock
m_news.unlock();
return "";
}
core::stringw m("");
{
m_current_news_message++;
if(m_current_news_message >= (int)m_news.getData().size())
m_current_news_message = 0;
m = m_news.getData()[m_current_news_message].getNews();
}
m_news.unlock();
return _(m.c_str());
} // getNextNewsMessage
// ----------------------------------------------------------------------------
/** Checks if the given condition list are all fulfilled.
* The conditions must be separated by ";", and each condition
* must be of the form "type comp version".
* Type must be 'stkversion'
* comp must be one of "<", "=", ">"
* version must be a valid STK version string
* \param cond The list of conditions
* \return True if all conditions are true.
*/
bool NewsManager::conditionFulfilled(const std::string &cond)
{
std::vector<std::string> cond_list;
cond_list = StringUtils::split(cond, ';');
for(unsigned int i=0; i<cond_list.size(); i++)
{
std::vector<std::string> cond = StringUtils::split(cond_list[i],' ');
if(cond.size()!=3)
{
Log::warn("NewsManager", "Invalid condition '%s' - assumed to "
"be true.", cond_list[i].c_str());
continue;
}
// Check for stkversion comparisons
// ================================
if(cond[0]=="stkversion")
{
int news_version = StringUtils::versionToInt(cond[2]);
int stk_version = StringUtils::versionToInt(STK_VERSION);
if(cond[1]=="=")
{
if(stk_version!=news_version) return false;
continue;
}
if(cond[1]=="<")
{
if(stk_version>=news_version) return false;
continue;
}
if(cond[1]==">")
{
if(stk_version<=news_version) return false;
continue;
}
Log::warn("NewsManager", "Invalid comparison in condition '%s' - "
"assumed true.", cond_list[i].c_str());
}
// Check for addons not installed
// ==============================
else if(cond[1]=="not" && cond[2]=="installed")
{
// The addons_manager cannot be accessed, since it's
// being initialised after the news manager. So a simple
// test is made to see if the directory exists. It is
// necessary to check for karts and tracks separately,
// since it's not possible to know if the addons is
// a kart or a track.
const std::string dir=file_manager->getAddonsDir();
if(file_manager->fileExists(dir+"/karts/"+cond[0]))
return false;
if(file_manager->fileExists(dir+"/tracks/"+cond[0]))
return false;
continue;
}
else
{
Log::warn("NewsManager", "Invalid condition '%s' - assumed to "
"be true.", cond_list[i].c_str());
continue;
}
} // for i < cond_list
return true;
} // conditionFulfilled
// ----------------------------------------------------------------------------