2008-01-08 14:52:32 +01:00
|
|
|
/*****************************************************************************
|
|
|
|
* Eliot
|
2012-10-07 16:25:41 +02:00
|
|
|
* Copyright (C) 2007-2012 Olivier Teulière
|
2008-01-08 14:52:32 +01:00
|
|
|
* Authors: Olivier Teulière <ipkiss @@ gmail.com>
|
|
|
|
*
|
|
|
|
* 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 St, Fifth Floor, Boston, MA 02110-1301 USA
|
|
|
|
*****************************************************************************/
|
|
|
|
|
2008-09-22 23:21:38 +02:00
|
|
|
#include "config.h"
|
|
|
|
|
2008-01-08 14:52:32 +01:00
|
|
|
#include <cstdlib>
|
2008-09-22 23:21:38 +02:00
|
|
|
#ifdef HAVE_LIBCONFIG
|
|
|
|
# define LIBCONFIG_STATIC
|
|
|
|
# include <libconfig.h++>
|
|
|
|
#endif
|
|
|
|
#ifdef WIN32
|
|
|
|
# include <windows.h>
|
|
|
|
# include <shlobj.h>
|
|
|
|
#else
|
|
|
|
# if defined(HAVE_SYS_STAT_H) && defined(HAVE_SYS_TYPES_H)
|
|
|
|
# include <sys/stat.h>
|
|
|
|
# include <sys/types.h>
|
|
|
|
# endif
|
|
|
|
#endif
|
2008-01-08 14:52:32 +01:00
|
|
|
|
|
|
|
#include "settings.h"
|
2008-09-05 23:31:30 +02:00
|
|
|
#include "game_exception.h"
|
2008-01-08 14:52:32 +01:00
|
|
|
|
2008-09-22 23:21:38 +02:00
|
|
|
using namespace libconfig;
|
|
|
|
|
2008-01-08 14:52:32 +01:00
|
|
|
|
2012-02-18 22:26:52 +01:00
|
|
|
INIT_LOGGER(game, Settings);
|
|
|
|
|
|
|
|
|
2008-01-08 14:52:32 +01:00
|
|
|
Settings *Settings::m_instance = NULL;
|
|
|
|
|
|
|
|
|
|
|
|
Settings & Settings::Instance()
|
|
|
|
{
|
|
|
|
if (m_instance == NULL)
|
|
|
|
{
|
|
|
|
m_instance = new Settings;
|
|
|
|
}
|
|
|
|
return *m_instance;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void Settings::Destroy()
|
|
|
|
{
|
|
|
|
delete m_instance;
|
|
|
|
m_instance = NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2012-06-09 15:38:34 +02:00
|
|
|
// Return true if the given path exists and is a directory)
|
|
|
|
static bool is_directory(const string &path)
|
|
|
|
{
|
|
|
|
#ifdef WIN32
|
|
|
|
DWORD attrib = GetFileAttributes(path.c_str());
|
|
|
|
return (attrib != INVALID_FILE_ATTRIBUTES &&
|
|
|
|
(attrib & FILE_ATTRIBUTE_DIRECTORY));
|
|
|
|
#else
|
|
|
|
#if defined(HAVE_SYS_STAT_H) && defined(HAVE_SYS_TYPES_H)
|
|
|
|
struct stat sb;
|
|
|
|
int res = stat(path.c_str(), &sb);
|
|
|
|
return res == 0 && S_ISDIR(sb.st_mode);
|
|
|
|
#else
|
|
|
|
// Unimplemented
|
|
|
|
return false;
|
|
|
|
#endif
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create a directory.
|
|
|
|
// Return true in case of success, false otherwise.
|
|
|
|
static bool my_mkdir(const string &dir)
|
|
|
|
{
|
|
|
|
#ifdef WIN32
|
|
|
|
// The value '248' comes from MSDN
|
|
|
|
char tmp[248];
|
|
|
|
snprintf(tmp, sizeof(tmp), "%s", dir.c_str());
|
|
|
|
return CreateDirectory(tmp, NULL);
|
|
|
|
#else
|
|
|
|
#if defined(HAVE_SYS_STAT_H) && defined(HAVE_SYS_TYPES_H)
|
|
|
|
// Create the directory with mode 0700
|
|
|
|
return mkdir(dir.c_str(), S_IRWXU) == 0;
|
|
|
|
#else
|
|
|
|
// Unimplemented
|
|
|
|
return false;
|
|
|
|
#endif
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create a directory, like mkdir -p
|
|
|
|
// We ignore potential errors...
|
|
|
|
static void full_mkdir(const string &dir)
|
|
|
|
{
|
|
|
|
// Remove trailing '/'
|
|
|
|
string copy = dir;
|
|
|
|
string::size_type pos = dir.find_last_not_of('/');
|
|
|
|
if (pos != string::npos && pos != dir.size() - 1)
|
|
|
|
copy.erase(pos + 1, dir.size() - 1 - pos);
|
|
|
|
|
|
|
|
// Create intermediate directories
|
|
|
|
pos = 0;
|
|
|
|
while ((pos = copy.find('/', pos)) != string::npos)
|
|
|
|
{
|
|
|
|
// Ignore potential errors...
|
|
|
|
my_mkdir(copy.substr(0, pos));
|
|
|
|
++pos;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create the final directory
|
|
|
|
my_mkdir(copy);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2012-04-12 21:46:53 +02:00
|
|
|
string Settings::GetConfigFileDir()
|
2008-09-22 23:21:38 +02:00
|
|
|
{
|
2012-06-09 15:38:34 +02:00
|
|
|
string dirName;
|
2008-09-22 23:21:38 +02:00
|
|
|
#ifdef WIN32
|
2012-04-12 21:46:53 +02:00
|
|
|
char szPath[MAX_PATH];
|
|
|
|
// Get the AppData directory
|
|
|
|
if (SHGetFolderPath(NULL, CSIDL_APPDATA | CSIDL_FLAG_CREATE,
|
|
|
|
NULL, 0, szPath) == S_OK)
|
|
|
|
{
|
2012-06-09 15:38:34 +02:00
|
|
|
dirName = szPath + string("/eliot");
|
2012-04-12 21:46:53 +02:00
|
|
|
}
|
2008-09-22 23:21:38 +02:00
|
|
|
#else
|
2012-04-12 21:46:53 +02:00
|
|
|
// Follow the XDG Base Directory Specification (from freedesktop.org)
|
|
|
|
// XXX: In fact we don't follow it to the letter, because the location
|
|
|
|
// of the config file could be different when reading and writing.
|
|
|
|
// But in the case of Eliot it's not very important (we don't try to
|
|
|
|
// merge config files)...
|
|
|
|
const char *configDir = getenv("XDG_CONFIG_HOME");
|
|
|
|
if (configDir != NULL)
|
2012-06-09 15:38:34 +02:00
|
|
|
dirName = configDir;
|
2012-04-12 21:46:53 +02:00
|
|
|
else
|
|
|
|
{
|
|
|
|
// Fallback to the default value: $HOME/.config
|
|
|
|
configDir = getenv("HOME");
|
|
|
|
if (configDir)
|
2012-06-09 15:38:34 +02:00
|
|
|
dirName = configDir + string("/.config");
|
2012-04-12 21:46:53 +02:00
|
|
|
}
|
2012-06-09 15:38:34 +02:00
|
|
|
dirName += "/eliot";
|
|
|
|
#endif
|
2008-09-22 23:21:38 +02:00
|
|
|
|
2012-06-09 15:38:34 +02:00
|
|
|
if (dirName != "")
|
|
|
|
dirName += "/";
|
|
|
|
|
|
|
|
// Try to create the directory if it doesn't exist.
|
|
|
|
// If the directory cannot be created, saving the
|
|
|
|
// configuration file will definitely fail...
|
|
|
|
if (!is_directory(dirName))
|
2012-04-12 21:46:53 +02:00
|
|
|
{
|
2012-06-09 15:38:34 +02:00
|
|
|
full_mkdir(dirName);
|
2012-04-12 21:46:53 +02:00
|
|
|
}
|
2008-09-22 23:21:38 +02:00
|
|
|
|
2012-06-09 15:38:34 +02:00
|
|
|
return dirName;
|
2012-04-12 21:46:53 +02:00
|
|
|
}
|
2009-01-22 19:30:22 +01:00
|
|
|
|
|
|
|
|
2012-04-12 21:46:53 +02:00
|
|
|
namespace
|
|
|
|
{
|
2010-01-28 23:23:47 +01:00
|
|
|
#ifdef HAVE_LIBCONFIG
|
2009-01-22 19:30:22 +01:00
|
|
|
template<typename T>
|
|
|
|
void copySetting(const Config &srcConf, Config &dstConf, const char *path)
|
|
|
|
{
|
|
|
|
if (srcConf.exists(path))
|
|
|
|
{
|
|
|
|
T t;
|
|
|
|
srcConf.lookupValue(path, t);
|
|
|
|
dstConf.lookup(path) = t;
|
|
|
|
}
|
|
|
|
}
|
2010-01-28 23:23:47 +01:00
|
|
|
#endif
|
2008-09-22 23:21:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2008-01-08 14:52:32 +01:00
|
|
|
Settings::Settings()
|
|
|
|
{
|
2008-09-22 23:21:38 +02:00
|
|
|
#ifdef HAVE_LIBCONFIG
|
2012-04-12 21:46:53 +02:00
|
|
|
m_fileName = GetConfigFileDir() + "eliot.cfg";
|
2008-09-22 23:21:38 +02:00
|
|
|
m_conf = new Config;
|
2008-01-08 14:52:32 +01:00
|
|
|
|
2008-09-22 23:21:38 +02:00
|
|
|
// ============== General options ==============
|
2008-01-08 14:52:32 +01:00
|
|
|
|
|
|
|
// ============== Training mode options ==============
|
2009-01-22 19:30:22 +01:00
|
|
|
Setting &training = m_conf->getRoot().add("training", Setting::TypeGroup);
|
|
|
|
|
|
|
|
// Number of search results kept in a search
|
|
|
|
training.add("search-limit", Setting::TypeInt) = 100;
|
2008-01-08 14:52:32 +01:00
|
|
|
|
|
|
|
// ============== Duplicate mode options ==============
|
2008-09-22 23:21:38 +02:00
|
|
|
Setting &dupli = m_conf->getRoot().add("duplicate", Setting::TypeGroup);
|
2008-01-08 14:52:32 +01:00
|
|
|
|
|
|
|
// Minimum number of players in a duplicate game needed to apply a "solo" bonus
|
|
|
|
// (16 is the ODS value)
|
2008-09-22 23:21:38 +02:00
|
|
|
dupli.add("solo-players", Setting::TypeInt) = 16;
|
2008-01-08 14:52:32 +01:00
|
|
|
// Number of points granted for a solo (10 is the ODS value)
|
2008-09-22 23:21:38 +02:00
|
|
|
dupli.add("solo-value", Setting::TypeInt) = 10;
|
2008-01-08 14:52:32 +01:00
|
|
|
|
|
|
|
// If true, Eliot complains when the player does something illegal
|
|
|
|
// If false, the word is accepted (with a score of 0) and the player does
|
|
|
|
// not get a second chance
|
2008-09-22 23:21:38 +02:00
|
|
|
dupli.add("reject-invalid", Setting::TypeBoolean) = true;
|
2008-01-08 14:52:32 +01:00
|
|
|
|
|
|
|
// ============== Freegame mode options ==============
|
2008-09-22 23:21:38 +02:00
|
|
|
Setting &freegame = m_conf->getRoot().add("freegame", Setting::TypeGroup);
|
2008-01-08 14:52:32 +01:00
|
|
|
|
|
|
|
// If true, Eliot complains when the player does something illegal
|
|
|
|
// If false, the word is accepted (with a score of 0) and the player does
|
|
|
|
// not get a second chance.
|
|
|
|
// Trying to change letters or to pass the turn in an incorrect way will
|
|
|
|
// be rejected in any case.
|
2008-09-22 23:21:38 +02:00
|
|
|
freegame.add("reject-invalid", Setting::TypeBoolean) = true;
|
|
|
|
|
2012-03-05 22:15:42 +01:00
|
|
|
// ============== Arbitration mode options ==============
|
|
|
|
Setting &arbitration = m_conf->getRoot().add("arbitration", Setting::TypeGroup);
|
|
|
|
|
2012-04-16 23:02:11 +02:00
|
|
|
// If true, a random rack is defined, otherwise the rack is left untouched
|
|
|
|
arbitration.add("fill-rack", Setting::TypeBoolean) = true;
|
|
|
|
|
2012-12-05 23:02:12 +01:00
|
|
|
// If true, solos are automatically given when appropriate
|
|
|
|
// If false, the arbitrator has full control (but must do everything manually)
|
|
|
|
arbitration.add("solo-auto", Setting::TypeBoolean) = true;
|
|
|
|
// Minimum number of players in a duplicate game needed to apply a "solo" bonus
|
|
|
|
// (16 is the ODS value)
|
|
|
|
arbitration.add("solo-players", Setting::TypeInt) = 16;
|
2012-04-30 08:48:32 +02:00
|
|
|
|
2012-10-05 12:52:42 +02:00
|
|
|
// Number of points granted for a solo (10 is the ODS value)
|
|
|
|
arbitration.add("solo-value", Setting::TypeInt) = 10;
|
|
|
|
|
2012-03-23 08:12:03 +01:00
|
|
|
// Default value of a penalty
|
2012-10-05 12:52:42 +02:00
|
|
|
arbitration.add("penalty-value", Setting::TypeInt) = 5;
|
2012-03-23 08:12:03 +01:00
|
|
|
|
2012-04-30 08:48:32 +02:00
|
|
|
// Maximum number of warnings before getting penalties
|
|
|
|
arbitration.add("warnings-limit", Setting::TypeInt) = 3;
|
2012-03-05 22:15:42 +01:00
|
|
|
|
2012-12-05 23:02:12 +01:00
|
|
|
// Number of search results kept in a search
|
|
|
|
arbitration.add("search-limit", Setting::TypeInt) = 100;
|
|
|
|
|
2013-01-13 22:52:52 +01:00
|
|
|
// ============== Topping mode options ==============
|
|
|
|
Setting &topping = m_conf->getRoot().add("topping", Setting::TypeGroup);
|
|
|
|
|
2013-01-16 15:51:09 +01:00
|
|
|
// If true, a score penalty equal to the number of elapsed seconds
|
|
|
|
// is given to the player at each turn
|
|
|
|
topping.add("elapsed-penalty", Setting::TypeBoolean) = true;
|
|
|
|
|
|
|
|
// Additional penalty points given to the player when the timer expires
|
2013-01-13 22:52:52 +01:00
|
|
|
topping.add("timeout-penalty", Setting::TypeInt) = 60;
|
|
|
|
|
2008-09-22 23:21:38 +02:00
|
|
|
// Try to read the values from the configuration file
|
|
|
|
try
|
|
|
|
{
|
2009-01-22 19:30:22 +01:00
|
|
|
// We cannot call readFile() on m_conf, as it removes the previous
|
|
|
|
// settings. So we create a temporary config, and copy the settings
|
|
|
|
// one by one...
|
|
|
|
Config tmpConf;
|
|
|
|
tmpConf.readFile(m_fileName.c_str());
|
|
|
|
copySetting<int>(tmpConf, *m_conf, "training.search-limit");
|
|
|
|
copySetting<int>(tmpConf, *m_conf, "duplicate.solo-players");
|
|
|
|
copySetting<int>(tmpConf, *m_conf, "duplicate.solo-value");
|
|
|
|
copySetting<bool>(tmpConf, *m_conf, "duplicate.reject-invalid");
|
|
|
|
copySetting<bool>(tmpConf, *m_conf, "freegame.reject-invalid");
|
2012-04-16 23:02:11 +02:00
|
|
|
copySetting<bool>(tmpConf, *m_conf, "arbitration.fill-rack");
|
2012-03-05 22:15:42 +01:00
|
|
|
copySetting<int>(tmpConf, *m_conf, "arbitration.search-limit");
|
2012-12-05 23:02:12 +01:00
|
|
|
copySetting<bool>(tmpConf, *m_conf, "arbitration.solo-auto");
|
|
|
|
copySetting<int>(tmpConf, *m_conf, "arbitration.solo-players");
|
2012-05-05 19:58:05 +02:00
|
|
|
copySetting<int>(tmpConf, *m_conf, "arbitration.solo-value");
|
2012-10-05 12:52:42 +02:00
|
|
|
copySetting<int>(tmpConf, *m_conf, "arbitration.penalty-value");
|
|
|
|
copySetting<int>(tmpConf, *m_conf, "arbitration.warnings-limit");
|
2013-01-18 17:32:00 +01:00
|
|
|
copySetting<bool>(tmpConf, *m_conf, "topping.elapsed-penalty");
|
2013-01-13 22:52:52 +01:00
|
|
|
copySetting<int>(tmpConf, *m_conf, "topping.timeout-penalty");
|
2008-09-22 23:21:38 +02:00
|
|
|
}
|
2012-12-05 23:02:12 +01:00
|
|
|
catch (const std::exception &e)
|
2008-09-22 23:21:38 +02:00
|
|
|
{
|
2012-12-05 23:02:12 +01:00
|
|
|
// Only log the exception
|
|
|
|
LOG_ERROR("Error reading config file: " << e.what());
|
2008-09-22 23:21:38 +02:00
|
|
|
}
|
|
|
|
#endif
|
2008-01-08 14:52:32 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2008-09-22 23:21:38 +02:00
|
|
|
Settings::~Settings()
|
2008-01-08 14:52:32 +01:00
|
|
|
{
|
2008-09-22 23:21:38 +02:00
|
|
|
#ifdef HAVE_LIBCONFIG
|
|
|
|
delete m_conf;
|
|
|
|
#endif
|
2008-01-08 14:52:32 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2008-09-22 23:21:38 +02:00
|
|
|
void Settings::save() const
|
2008-01-08 14:52:32 +01:00
|
|
|
{
|
2008-09-22 23:21:38 +02:00
|
|
|
#ifdef HAVE_LIBCONFIG
|
|
|
|
try
|
|
|
|
{
|
|
|
|
m_conf->writeFile(m_fileName.c_str());
|
|
|
|
}
|
|
|
|
catch (FileIOException &e)
|
|
|
|
{
|
|
|
|
throw GameException("The configuration file cannot be written (" +
|
|
|
|
m_fileName + ")");
|
|
|
|
}
|
|
|
|
#endif
|
2008-01-08 14:52:32 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2008-09-22 23:21:38 +02:00
|
|
|
void Settings::setBool(const string &iName, bool iValue)
|
2008-01-08 14:52:32 +01:00
|
|
|
{
|
2008-09-22 23:21:38 +02:00
|
|
|
setValue<bool>(iName, iValue);
|
2008-01-08 14:52:32 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2008-09-22 23:21:38 +02:00
|
|
|
bool Settings::getBool(const string &iName) const
|
2008-01-08 14:52:32 +01:00
|
|
|
{
|
2008-09-22 23:21:38 +02:00
|
|
|
#ifdef HAVE_LIBCONFIG
|
|
|
|
try
|
|
|
|
{
|
|
|
|
return m_conf->lookup(iName);
|
|
|
|
}
|
|
|
|
catch (SettingNotFoundException &e)
|
|
|
|
{
|
|
|
|
throw GameException("No such option: " + iName);
|
|
|
|
}
|
|
|
|
#else
|
|
|
|
// Dummy implementation
|
|
|
|
return true;
|
|
|
|
#endif
|
2008-01-08 14:52:32 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2008-09-22 23:21:38 +02:00
|
|
|
void Settings::setInt(const string &iName, int iValue)
|
2008-01-08 14:52:32 +01:00
|
|
|
{
|
2008-09-22 23:21:38 +02:00
|
|
|
setValue<int>(iName, iValue);
|
2008-01-08 14:52:32 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2008-09-22 23:21:38 +02:00
|
|
|
int Settings::getInt(const string &iName) const
|
2008-01-08 14:52:32 +01:00
|
|
|
{
|
2008-09-22 23:21:38 +02:00
|
|
|
#ifdef HAVE_LIBCONFIG
|
|
|
|
try
|
|
|
|
{
|
|
|
|
return m_conf->lookup(iName);
|
|
|
|
}
|
|
|
|
catch (SettingNotFoundException &e)
|
2008-01-08 14:52:32 +01:00
|
|
|
{
|
2008-09-05 23:31:30 +02:00
|
|
|
throw GameException("No such option: " + iName);
|
2008-01-08 14:52:32 +01:00
|
|
|
}
|
2008-09-22 23:21:38 +02:00
|
|
|
#else
|
|
|
|
// Dummy implementation
|
2009-01-22 19:30:22 +01:00
|
|
|
if (iName == "training.search-limit")
|
|
|
|
return 100;
|
|
|
|
else if (iName == "duplicate.solo-players")
|
2008-09-22 23:21:38 +02:00
|
|
|
return 16;
|
2009-01-22 19:30:22 +01:00
|
|
|
else if (iName == "duplicate.solo-value")
|
2008-09-22 23:21:38 +02:00
|
|
|
return 10;
|
2012-03-05 22:15:42 +01:00
|
|
|
else if (iName == "arbitration.search-limit")
|
|
|
|
return 100;
|
2012-12-05 23:02:12 +01:00
|
|
|
else if (iName == "arbitration.solo-players")
|
|
|
|
return 16;
|
2012-10-05 12:52:42 +02:00
|
|
|
else if (iName == "arbitration.solo-value")
|
|
|
|
return 5;
|
|
|
|
else if (iName == "arbitration.penalty-value")
|
2012-03-23 08:12:03 +01:00
|
|
|
return 5;
|
2012-04-30 08:48:32 +02:00
|
|
|
else if (iName == "arbitration.warnings-limit")
|
|
|
|
return 3;
|
2013-01-16 15:51:09 +01:00
|
|
|
else if (iName == "topping.timeout-penalty")
|
2013-01-13 22:52:52 +01:00
|
|
|
return 60;
|
2008-09-22 23:21:38 +02:00
|
|
|
return 0;
|
|
|
|
#endif
|
2008-01-08 14:52:32 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2008-09-22 23:21:38 +02:00
|
|
|
template<class T>
|
|
|
|
void Settings::setValue(const string &iName, T iValue)
|
2008-01-08 14:52:32 +01:00
|
|
|
{
|
2008-09-22 23:21:38 +02:00
|
|
|
#ifdef HAVE_LIBCONFIG
|
|
|
|
try
|
|
|
|
{
|
|
|
|
m_conf->lookup(iName) = iValue;
|
|
|
|
}
|
|
|
|
catch (SettingNotFoundException &e)
|
2008-01-08 14:52:32 +01:00
|
|
|
{
|
2008-09-25 22:39:37 +02:00
|
|
|
#ifdef DEBUG
|
2008-09-05 23:31:30 +02:00
|
|
|
throw GameException("No such option: " + iName);
|
2008-09-25 22:39:37 +02:00
|
|
|
#endif
|
2008-01-08 14:52:32 +01:00
|
|
|
}
|
2008-09-22 23:21:38 +02:00
|
|
|
#endif
|
2008-01-08 14:52:32 +01:00
|
|
|
}
|
|
|
|
|