Merge branch 'feature/addon-manager'
authorIngo Ruhnke <grumbel@gmail.com>
Mon, 25 Aug 2014 07:52:57 +0000 (09:52 +0200)
committerIngo Ruhnke <grumbel@gmail.com>
Mon, 25 Aug 2014 07:52:57 +0000 (09:52 +0200)
15 files changed:
src/addon/addon.cpp
src/addon/addon.hpp
src/addon/addon_manager.cpp
src/addon/addon_manager.hpp
src/addon/downloader.cpp [new file with mode: 0644]
src/addon/downloader.hpp [new file with mode: 0644]
src/supertux/gameconfig.cpp
src/supertux/gameconfig.hpp
src/supertux/main.cpp
src/supertux/menu/addon_menu.cpp
src/supertux/menu/addon_menu.hpp
src/supertux/menu/contrib_menu.cpp
src/supertux/world.cpp
tools/build-addon-index.py [new file with mode: 0755]
tools/sexpr.py [new file with mode: 0755]

index 2ab094e..2688422 100644 (file)
@@ -1,5 +1,6 @@
 //  SuperTux - Add-on
 //  Copyright (C) 2007 Christoph Sommer <christoph.sommer@2007.expires.deltadevelopment.de>
+//                2014 Ingo Ruhnke <grumbel@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
 #include <stdexcept>
 #include <sstream>
 
-#include "addon/md5.hpp"
 #include "lisp/parser.hpp"
 #include "util/reader.hpp"
 #include "util/writer.hpp"
 #include "util/log.hpp"
 
-std::string
-Addon::get_md5() const
-{
-  if (!installed) {
-    if (stored_md5 == "") { log_warning << "Add-on not installed and no stored MD5 available" << std::endl; }
-    return stored_md5;
-  }
-
-  if (calculated_md5 != "") return calculated_md5;
+namespace {
 
-  if (installed_physfs_filename == "") throw std::runtime_error("Tried to calculate MD5 of Add-on with unknown filename");
+static const char* s_allowed_characters = "-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
 
-  // TODO: this does not work as expected for some files -- IFileStream seems to not always behave like an ifstream.
-  //IFileStream ifs(installed_physfs_filename);
-  //std::string md5 = MD5(ifs).hex_digest();
-
-  MD5 md5;
-  PHYSFS_file* file;
-  file = PHYSFS_openRead(installed_physfs_filename.c_str());
-  unsigned char buffer[1024];
-  while (true) {
-    PHYSFS_sint64 len = PHYSFS_read(file, buffer, 1, sizeof(buffer));
-    if (len <= 0) break;
-    md5.update(buffer, len);
+Addon::Type addon_type_from_string(const std::string& type)
+{
+  if (type == "world")
+  {
+    return Addon::WORLD;
+  }
+  else if (type == "worldmap")
+  {
+    return Addon::WORLDMAP;
+  }
+  else if (type == "levelset")
+  {
+    return Addon::LEVELSET;
+  }
+  else
+  {
+    throw std::runtime_error("not a valid Addon::Type: " + type);
   }
-  PHYSFS_close(file);
-
-  calculated_md5 = md5.hex_digest();
-  log_debug << "MD5 of " << title << ": " << calculated_md5 << std::endl;
-
-  return calculated_md5;
 }
 
-void
+} // namespace
+
+std::unique_ptr<Addon>
 Addon::parse(const Reader& lisp)
 {
-  try {
-    lisp.get("kind", kind);
-    lisp.get("title", title);
-    lisp.get("author", author);
-    lisp.get("license", license);
-    lisp.get("http-url", http_url);
-    lisp.get("file", suggested_filename);
-    lisp.get("md5", stored_md5);
-  } catch(std::exception& e) {
+  std::unique_ptr<Addon> addon(new Addon);
+
+  try
+  {
+    if (!lisp.get("id", addon->m_id))
+    {
+      throw std::runtime_error("(id ...) field missing from addon description");
+    }
+
+    if (addon->m_id.empty())
+    {
+      throw std::runtime_error("addon id is empty");
+    }
+
+    if (addon->m_id.find_first_not_of(s_allowed_characters) != std::string::npos)
+    {
+      throw std::runtime_error("addon id contains illegal characters: " + addon->m_id);
+    }
+
+    lisp.get("version", addon->m_version);
+
+    std::string type;
+    lisp.get("type", type);
+    addon->m_type = addon_type_from_string(type);
+
+    lisp.get("title", addon->m_title);
+    lisp.get("author", addon->m_author);
+    lisp.get("license", addon->m_license);
+    lisp.get("url", addon->m_url);
+    lisp.get("md5", addon->m_md5);
+
+    return addon;
+  }
+  catch(const std::exception& err)
+  {
     std::stringstream msg;
-    msg << "Problem when parsing addoninfo: " << e.what();
+    msg << "Problem when parsing addoninfo: " << err.what();
     throw std::runtime_error(msg.str());
   }
 }
 
-void
-Addon::parse(std::string fname)
+std::unique_ptr<Addon>
+Addon::parse(const std::string& fname)
 {
-  try {
+  try
+  {
     lisp::Parser parser;
     const lisp::Lisp* root = parser.parse(fname);
     const lisp::Lisp* addon = root->get_lisp("supertux-addoninfo");
-    if(!addon) throw std::runtime_error("file is not a supertux-addoninfo file.");
-    parse(*addon);
-  } catch(std::exception& e) {
+    if(!addon)
+    {
+      throw std::runtime_error("file is not a supertux-addoninfo file.");
+    }
+    else
+    {
+      return parse(*addon);
+    }
+  }
+  catch(const std::exception& err)
+  {
     std::stringstream msg;
-    msg << "Problem when reading addoninfo '" << fname << "': " << e.what();
+    msg << "Problem when reading addoninfo '" << fname << "': " << err.what();
     throw std::runtime_error(msg.str());
   }
 }
 
-void
-Addon::write(lisp::Writer& writer) const
+Addon::Addon() :
+  m_id(),
+  m_version(0),
+  m_type(),
+  m_title(),
+  m_author(),
+  m_license(),
+  m_url(),
+  m_md5(),
+  m_install_filename(),
+  m_enabled(false)
+{}
+
+std::string
+Addon::get_filename() const
 {
-  writer.start_list("supertux-addoninfo");
-  if (kind != "") writer.write("kind", kind);
-  if (title != "") writer.write("title", title);
-  if (author != "") writer.write("author", author);
-  if (license != "") writer.write("license", license);
-  if (http_url != "") writer.write("http-url", http_url);
-  if (suggested_filename != "") writer.write("file", suggested_filename);
-  if (stored_md5 != "") writer.write("md5", stored_md5);
-  writer.end_list("supertux-addoninfo");
+  return get_id() + ".zip";
 }
 
-void
-Addon::write(std::string fname) const
+std::string
+Addon::get_install_filename() const
+{
+  return m_install_filename;
+}
+
+bool
+Addon::is_installed() const
 {
-  lisp::Writer writer(fname);
-  write(writer);
+  return !m_install_filename.empty();
 }
 
 bool
-Addon::operator==(Addon addon2) const
+Addon::is_enabled() const
 {
-  std::string s1 = this->get_md5();
-  std::string s2 = addon2.get_md5();
+  return m_enabled;
+}
 
-  if ((s1 != "") && (s2 != "")) return (s1 == s2);
+void
+Addon::set_install_filename(const std::string& absolute_filename, const std::string& md5)
+{
+  m_install_filename = absolute_filename;
+  m_md5 = md5;
+}
 
-  if (this->title != addon2.title) return false;
-  if (this->author != addon2.author) return false;
-  if (this->kind != addon2.kind) return false;
-  return true;
+void
+Addon::set_enabled(bool v)
+{
+  m_enabled = v;
 }
 
+
 /* EOF */
index 69b0bd9..37da298 100644 (file)
@@ -1,5 +1,6 @@
 //  SuperTux - Add-on
 //  Copyright (C) 2007 Christoph Sommer <christoph.sommer@2007.expires.deltadevelopment.de>
+//                2014 Ingo Ruhnke <grumbel@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
 #ifndef HEADER_SUPERTUX_ADDON_ADDON_HPP
 #define HEADER_SUPERTUX_ADDON_ADDON_HPP
 
+#include <assert.h>
+#include <memory>
 #include <string>
 
 #include "util/reader_fwd.hpp"
-#include "util/writer_fwd.hpp"
 
-/**
- * Represents an (available or installed) Add-on, e.g. a level set
- */
 class Addon
 {
 public:
-  std::string kind;
-  std::string title;
-  std::string author;
-  std::string license;
-  std::string http_url;
-  /** filename suggested by addon author, e.g. "pak0.zip" */
-  std::string suggested_filename;
-  /** PhysFS filename on disk, e.g. "pak0.zip" */
-  std::string installed_physfs_filename;
-  /** complete path and filename on disk, e.g. "/home/sommer/.supertux2/pak0.zip" */
-  std::string installed_absolute_filename;
-  std::string stored_md5;
-  bool installed;
-  bool loaded;
-
-  /**
-   * Get MD5, based either on installed file's contents or stored value
-   */
-  std::string get_md5() const;
-
-  /**
-   * Read additional information from given contents of a (supertux-addoninfo ...) block
-   */
-  void parse(const Reader& lisp);
-
-  /**
-   * Read additional information from given file
-   */
-  void parse(std::string fname);
-
-  /**
-   * Writes out Add-on metainformation to a Lisp Writer
-   */
-  void write(Writer& writer) const;
-
-  /**
-   * Writes out Add-on metainformation to a file
-   */
-  void write(std::string fname) const;
-
-  /**
-   * Checks if Add-on is the same as given one.
-   * If available, checks MD5 sum, else relies on kind, author and title alone.
-   */
-  bool operator==(Addon addon2) const;
-
-protected:
-  friend class AddonManager;
-
-  mutable std::string calculated_md5;
-
-  Addon() :
-    kind(),
-    title(),
-    author(),
-    license(),
-    http_url(),
-    suggested_filename(),
-    installed_physfs_filename(),
-    installed_absolute_filename(),
-    stored_md5(),
-    installed(),
-    loaded(),
-    calculated_md5()
-  {};
+  static std::unique_ptr<Addon> parse(const Reader& lisp);
+  static std::unique_ptr<Addon> parse(const std::string& fname);
+
+  enum Type { WORLD, WORLDMAP, LEVELSET };
+
+private:
+  // fields provided by the addon.zip itself
+  std::string m_id;
+  int m_version;
+  Type m_type;
+  std::string m_title;
+  std::string m_author;
+  std::string m_license;
+
+  // additional fields provided for addons from an addon repository
+  std::string m_url;
+  std::string m_md5;
+
+  // fields filled by the AddonManager
+  std::string m_install_filename;
+  bool m_enabled;
+
+private:
+  Addon();
+
+public:
+  std::string get_id() const { return m_id; }
+  int get_version() const { return m_version; }
+
+  Type get_type() const { return m_type; }
+  std::string get_title() const { return m_title; }
+  std::string get_author() const { return m_author; }
+  std::string get_license() const { return m_license; }
+
+  std::string get_url() const { return m_url; }
+  std::string get_md5() const { return m_md5; }
+
+  std::string get_filename() const;
+  std::string get_install_filename() const;
+
+  bool is_installed() const;
+  bool is_enabled() const;
+
+  void set_install_filename(const std::string& absolute_filename, const std::string& md5);
+  void set_enabled(bool v);
+
+private:
+  Addon(const Addon&) = delete;
+  Addon& operator=(const Addon&) = delete;
 };
 
 #endif
index 5ae64df..ab56662 100644 (file)
@@ -1,5 +1,6 @@
 //  SuperTux - Add-on Manager
 //  Copyright (C) 2007 Christoph Sommer <christoph.sommer@2007.expires.deltadevelopment.de>
+//                2014 Ingo Ruhnke <grumbel@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
 #include <version.h>
 
 #include <algorithm>
+#include <iostream>
 #include <memory>
 #include <physfs.h>
 #include <sstream>
 #include <stdexcept>
+#include <stdio.h>
 #include <sys/stat.h>
 
-#ifdef HAVE_LIBCURL
-#  include <curl/curl.h>
-#  include <curl/easy.h>
-#endif
-
 #include "addon/addon.hpp"
+#include "addon/md5.hpp"
 #include "lisp/list_iterator.hpp"
 #include "lisp/parser.hpp"
+#include "util/file_system.hpp"
+#include "util/log.hpp"
 #include "util/reader.hpp"
 #include "util/writer.hpp"
-#include "util/log.hpp"
 
-#ifdef HAVE_LIBCURL
 namespace {
 
-size_t my_curl_string_append(void *ptr, size_t size, size_t nmemb, void *string_ptr)
-{
-  std::string& s = *static_cast<std::string*>(string_ptr);
-  std::string buf(static_cast<char*>(ptr), size * nmemb);
-  s += buf;
-  log_debug << "read " << size * nmemb << " bytes of data..." << std::endl;
-  return size * nmemb;
-}
-
-size_t my_curl_physfs_write(void *ptr, size_t size, size_t nmemb, void *f_p)
+MD5 md5_from_file(const std::string& filename)
 {
-  PHYSFS_file* f = static_cast<PHYSFS_file*>(f_p);
-  PHYSFS_sint64 written = PHYSFS_write(f, ptr, size, nmemb);
-  log_debug << "read " << size * nmemb << " bytes of data..." << std::endl;
-  return size * written;
-}
-
-}
-#endif
+  // TODO: this does not work as expected for some files -- IFileStream seems to not always behave like an ifstream.
+  //IFileStream ifs(installed_physfs_filename);
+  //std::string md5 = MD5(ifs).hex_digest();
+
+  MD5 md5;
+
+  unsigned char buffer[1024];
+  PHYSFS_file* file = PHYSFS_openRead(filename.c_str());
+  while (true)
+  {
+    PHYSFS_sint64 len = PHYSFS_read(file, buffer, 1, sizeof(buffer));
+    if (len <= 0) break;
+    md5.update(buffer, len);
+  }
+  PHYSFS_close(file);
 
-AddonManager::AddonManager(std::vector<std::string>& ignored_addon_filenames_) :
-  addons(),
-  ignored_addon_filenames(ignored_addon_filenames_)
-{
-#ifdef HAVE_LIBCURL
-  curl_global_init(CURL_GLOBAL_ALL);
-#endif
+  return md5;
 }
 
-AddonManager::~AddonManager()
+bool has_suffix(const std::string& str, const std::string& suffix)
 {
-#ifdef HAVE_LIBCURL
-  curl_global_cleanup();
-#endif
-
-  for (std::vector<Addon*>::iterator i = addons.begin(); i != addons.end(); i++) delete *i;
+  if (str.length() >= suffix.length())
+    return str.compare(str.length() - suffix.length(), suffix.length(), suffix) == 0;
+  else
+    return false;
 }
 
-std::vector<Addon*>
-AddonManager::get_addons()
+} // namespace
+
+AddonManager::AddonManager(const std::string& addon_directory,
+                           std::vector<Config::Addon>& addon_config) :
+  m_downloader(),
+  m_addon_directory(addon_directory),
+  m_repository_url("http://addons.supertux.googlecode.com/git/index-0_4_0.nfo"),
+  m_addon_config(addon_config),
+  m_installed_addons(),
+  m_repository_addons(),
+  m_has_been_updated(false)
 {
-  /*
-    for (std::vector<Addon>::iterator it = installed_addons.begin(); it != installed_addons.end(); ++it) {
-    Addon& addon = *it;
-    if (addon.md5 == "") addon.md5 = calculate_md5(addon);
-    }
-  */
-  return addons;
-}
+  PHYSFS_mkdir(m_addon_directory.c_str());
 
-void
-AddonManager::check_online()
-{
-#ifdef HAVE_LIBCURL
-  char error_buffer[CURL_ERROR_SIZE+1];
-
-  const char* baseUrl = "http://addons.supertux.googlecode.com/git/index-0_3_5.nfo";
-  std::string addoninfos = "";
-
-  CURL *curl_handle;
-  curl_handle = curl_easy_init();
-  curl_easy_setopt(curl_handle, CURLOPT_URL, baseUrl);
-  curl_easy_setopt(curl_handle, CURLOPT_USERAGENT, "SuperTux/" PACKAGE_VERSION " libcURL");
-  curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, my_curl_string_append);
-  curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, &addoninfos);
-  curl_easy_setopt(curl_handle, CURLOPT_ERRORBUFFER, error_buffer);
-  curl_easy_setopt(curl_handle, CURLOPT_NOPROGRESS, 1);
-  curl_easy_setopt(curl_handle, CURLOPT_NOSIGNAL, 1);
-  curl_easy_setopt(curl_handle, CURLOPT_FAILONERROR, 1);
-  curl_easy_setopt(curl_handle, CURLOPT_FOLLOWLOCATION, 1);
-  CURLcode result = curl_easy_perform(curl_handle);
-  curl_easy_cleanup(curl_handle);
-
-  if (result != CURLE_OK) {
-    std::string why = error_buffer[0] ? error_buffer : "unhandled error";
-    throw std::runtime_error("Downloading Add-on list failed: " + why);
-  }
+  add_installed_addons();
 
-  try {
-    lisp::Parser parser;
-    std::stringstream addoninfos_stream(addoninfos);
-    const lisp::Lisp* root = parser.parse(addoninfos_stream, "supertux-addons");
-
-    const lisp::Lisp* addons_lisp = root->get_lisp("supertux-addons");
-    if(!addons_lisp) throw std::runtime_error("Downloaded file is not an Add-on list");
-
-    lisp::ListIterator iter(addons_lisp);
-    while(iter.next())
+  // FIXME: We should also restore the order here
+  for(auto& addon : m_addon_config)
+  {
+    if (addon.enabled)
     {
-      const std::string& token = iter.item();
-      if(token != "supertux-addoninfo")
-      {
-        log_warning << "Unknown token '" << token << "' in Add-on list" << std::endl;
-        continue;
-      }
-      std::unique_ptr<Addon> addon(new Addon());
-      addon->parse(*(iter.lisp()));
-      addon->installed = false;
-      addon->loaded = false;
-
-      // make sure the list of known Add-ons does not already contain this one
-      bool exists = false;
-      for (std::vector<Addon*>::const_iterator i = addons.begin(); i != addons.end(); i++) {
-        if (**i == *addon) {
-          exists = true;
-          break;
-        }
-      }
-
-      if (exists)
+      try
       {
-        // do nothing
+        enable_addon(addon.id);
       }
-      else if (addon->suggested_filename.find_first_not_of("match.quiz-proxy_gwenblvdjfks0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ") != std::string::npos)
+      catch(const std::exception& err)
       {
-        // make sure the Add-on's file name does not contain weird characters
-        log_warning << "Add-on \"" << addon->title << "\" contains unsafe file name. Skipping." << std::endl;
-      }
-      else
-      {
-        addons.push_back(addon.release());
+        log_warning << "failed to enable addon from config: " << err.what() << std::endl;
       }
     }
-  } catch(std::exception& e) {
-    std::stringstream msg;
-    msg << "Problem when reading Add-on list: " << e.what();
-    throw std::runtime_error(msg.str());
   }
-
-#endif
 }
 
-void
-AddonManager::install(Addon* addon)
+AddonManager::~AddonManager()
 {
-#ifdef HAVE_LIBCURL
-
-  if (addon->installed) throw std::runtime_error("Tried installing installed Add-on");
-
-  // make sure the Add-on's file name does not contain weird characters
-  if (addon->suggested_filename.find_first_not_of("match.quiz-proxy_gwenblvdjfks0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ") != std::string::npos) {
-    throw std::runtime_error("Add-on has unsafe file name (\""+addon->suggested_filename+"\")");
+  // sync enabled/disabled addons into the config for saving
+  m_addon_config.clear();
+  for(auto& addon : m_installed_addons)
+  {
+    m_addon_config.push_back({addon->get_id(), addon->is_enabled()});
   }
+}
 
-  std::string fileName = addon->suggested_filename;
-
-  // make sure its file doesn't already exist
-  if (PHYSFS_exists(fileName.c_str())) {
-    fileName = addon->stored_md5 + "_" + addon->suggested_filename;
-    if (PHYSFS_exists(fileName.c_str())) {
-      throw std::runtime_error("Add-on of suggested filename already exists (\""+addon->suggested_filename+"\", \""+fileName+"\")");
-    }
+Addon&
+AddonManager::get_repository_addon(const AddonId& id)
+{
+  auto it = std::find_if(m_repository_addons.begin(), m_repository_addons.end(),
+                         [&id](const std::unique_ptr<Addon>& addon)
+                         {
+                           return addon->get_id() == id;
+                         });
+
+  if (it != m_repository_addons.end())
+  {
+    return **it;
+  }
+  else
+  {
+    throw std::runtime_error("Couldn't find repository Addon with id: " + id);
   }
+}
 
-  char error_buffer[CURL_ERROR_SIZE+1];
+Addon&
+AddonManager::get_installed_addon(const AddonId& id)
+{
+  auto it = std::find_if(m_installed_addons.begin(), m_installed_addons.end(),
+                         [&id](const std::unique_ptr<Addon>& addon)
+                         {
+                           return addon->get_id() == id;
+                         });
+
+  if (it != m_installed_addons.end())
+  {
+    return **it;
+  }
+  else
+  {
+    throw std::runtime_error("Couldn't find installed Addon with id: " + id);
+  }
+}
 
-  char* url = (char*)malloc(addon->http_url.length() + 1);
-  strncpy(url, addon->http_url.c_str(), addon->http_url.length() + 1);
+std::vector<AddonId>
+AddonManager::get_repository_addons() const
+{
+  std::vector<AddonId> results;
+  results.reserve(m_repository_addons.size());
+  std::transform(m_repository_addons.begin(), m_repository_addons.end(),
+                 std::back_inserter(results),
+                 [](const std::unique_ptr<Addon>& addon)
+                 {
+                   return addon->get_id();
+                 });
+  return results;
+}
 
-  PHYSFS_file* f = PHYSFS_openWrite(fileName.c_str());
 
-  log_debug << "Downloading \"" << url << "\"" << std::endl;
+std::vector<AddonId>
+AddonManager::get_installed_addons() const
+{
+  std::vector<AddonId> results;
+  results.reserve(m_installed_addons.size());
+  std::transform(m_installed_addons.begin(), m_installed_addons.end(),
+                 std::back_inserter(results),
+                 [](const std::unique_ptr<Addon>& addon)
+                 {
+                   return addon->get_id();
+                 });
+  return results;
+}
 
-  CURL *curl_handle;
-  curl_handle = curl_easy_init();
-  curl_easy_setopt(curl_handle, CURLOPT_URL, url);
-  curl_easy_setopt(curl_handle, CURLOPT_USERAGENT, "SuperTux/" PACKAGE_VERSION " libcURL");
-  curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, my_curl_physfs_write);
-  curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, f);
-  curl_easy_setopt(curl_handle, CURLOPT_ERRORBUFFER, error_buffer);
-  curl_easy_setopt(curl_handle, CURLOPT_NOPROGRESS, 1);
-  curl_easy_setopt(curl_handle, CURLOPT_NOSIGNAL, 1);
-  curl_easy_setopt(curl_handle, CURLOPT_FAILONERROR, 1);
-  CURLcode result = curl_easy_perform(curl_handle);
-  curl_easy_cleanup(curl_handle);
+bool
+AddonManager::has_online_support() const
+{
+  return true;
+}
 
-  PHYSFS_close(f);
+bool
+AddonManager::has_been_updated() const
+{
+  return m_has_been_updated;
+}
 
-  free(url);
+void
+AddonManager::check_online()
+{
+  std::string addoninfos = m_downloader.download(m_repository_url);
+  m_repository_addons = parse_addon_infos(addoninfos);
+  m_has_been_updated = true;
+}
 
-  if (result != CURLE_OK) {
-    PHYSFS_delete(fileName.c_str());
-    std::string why = error_buffer[0] ? error_buffer : "unhandled error";
-    throw std::runtime_error("Downloading Add-on failed: " + why);
+void
+AddonManager::install_addon(const AddonId& addon_id)
+{
+  { // remove addon if it already exists
+    auto it = std::find_if(m_installed_addons.begin(), m_installed_addons.end(),
+                           [&addon_id](const std::unique_ptr<Addon>& addon)
+                           {
+                             return addon->get_id() == addon_id;
+                           });
+    if (it != m_installed_addons.end())
+    {
+      log_debug << "reinstalling addon " << addon_id << std::endl;
+      if ((*it)->is_enabled())
+      {
+        disable_addon((*it)->get_id());
+      }
+      m_installed_addons.erase(it);
+    }
+    else
+    {
+      log_debug << "installing addon " << addon_id << std::endl;
+    }
   }
 
-  addon->installed = true;
-  addon->installed_physfs_filename = fileName;
-  static const std::string writeDir = PHYSFS_getWriteDir();
-  static const std::string dirSep = PHYSFS_getDirSeparator();
-  addon->installed_absolute_filename = writeDir + dirSep + fileName;
-  addon->loaded = false;
-
-  if (addon->get_md5() != addon->stored_md5) {
-    addon->installed = false;
-    PHYSFS_delete(fileName.c_str());
-    std::string why = "MD5 checksums differ";
-    throw std::runtime_error("Downloading Add-on failed: " + why);
-  }
+  Addon& repository_addon = get_repository_addon(addon_id);
 
-  log_debug << "Finished downloading \"" << addon->installed_absolute_filename << "\". Enabling Add-on." << std::endl;
+  std::string install_filename = FileSystem::join(m_addon_directory, repository_addon.get_filename());
 
-  enable(addon);
+  m_downloader.download(repository_addon.get_url(), install_filename);
 
-#else
-  (void) addon;
-#endif
+  MD5 md5 = md5_from_file(install_filename);
+  if (repository_addon.get_md5() != md5.hex_digest())
+  {
+    if (PHYSFS_delete(install_filename.c_str()) == 0)
+    {
+      log_warning << "PHYSFS_delete failed: " << PHYSFS_getLastError() << std::endl;
+    }
 
+    throw std::runtime_error("Downloading Add-on failed: MD5 checksums differ");
+  }
+  else
+  {
+    const char* realdir = PHYSFS_getRealDir(install_filename.c_str());
+    if (!realdir)
+    {
+      throw std::runtime_error("PHYSFS_getRealDir failed: " + install_filename);
+    }
+    else
+    {
+      add_installed_archive(install_filename, md5.hex_digest());
+    }
+  }
 }
 
 void
-AddonManager::remove(Addon* addon)
+AddonManager::uninstall_addon(const AddonId& addon_id)
 {
-  if (!addon->installed) throw std::runtime_error("Tried removing non-installed Add-on");
-
-  //FIXME: more checks
-
-  // make sure the Add-on's file name does not contain weird characters
-  if (addon->installed_physfs_filename.find_first_not_of("match.quiz-proxy_gwenblvdjfks0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ") != std::string::npos) {
-    throw std::runtime_error("Add-on has unsafe file name (\""+addon->installed_physfs_filename+"\")");
+  log_debug << "uninstalling addon " << addon_id << std::endl;
+  Addon& addon = get_installed_addon(addon_id);
+  if (addon.is_enabled())
+  {
+    disable_addon(addon_id);
   }
-
-  unload(addon);
-
-  log_debug << "deleting file \"" << addon->installed_absolute_filename << "\"" << std::endl;
-  PHYSFS_delete(addon->installed_absolute_filename.c_str());
-  addon->installed = false;
-
-  // FIXME: As we don't know anything more about it (e.g. where to get it), remove it from list of known Add-ons
+  log_debug << "deleting file \"" << addon.get_install_filename() << "\"" << std::endl;
+  PHYSFS_delete(addon.get_install_filename().c_str());
+  m_installed_addons.erase(std::remove_if(m_installed_addons.begin(), m_installed_addons.end(),
+                                          [&addon](const std::unique_ptr<Addon>& rhs)
+                                          {
+                                            return addon.get_id() == rhs->get_id();
+                                          }),
+                           m_installed_addons.end());
 }
 
 void
-AddonManager::disable(Addon* addon)
+AddonManager::enable_addon(const AddonId& addon_id)
 {
-  unload(addon);
-
-  std::string fileName = addon->installed_physfs_filename;
-  if (std::find(ignored_addon_filenames.begin(), ignored_addon_filenames.end(), fileName) == ignored_addon_filenames.end()) {
-    ignored_addon_filenames.push_back(fileName);
+  log_debug << "enabling addon " << addon_id << std::endl;
+  Addon& addon = get_installed_addon(addon_id);
+  if (addon.is_enabled())
+  {
+    log_warning << "Tried enabling already enabled Add-on" << std::endl;
+  }
+  else
+  {
+    log_debug << "Adding archive \"" << addon.get_install_filename() << "\" to search path" << std::endl;
+    //int PHYSFS_mount(addon.installed_install_filename.c_str(), "addons/", 0)
+    if (PHYSFS_addToSearchPath(addon.get_install_filename().c_str(), 0) == 0)
+    {
+      log_warning << "Could not add " << addon.get_install_filename() << " to search path: "
+                  << PHYSFS_getLastError() << std::endl;
+    }
+    else
+    {
+      addon.set_enabled(true);
+    }
   }
 }
 
 void
-AddonManager::enable(Addon* addon)
+AddonManager::disable_addon(const AddonId& addon_id)
 {
-  load(addon);
-
-  std::string fileName = addon->installed_physfs_filename;
-  std::vector<std::string>::iterator i = std::find(ignored_addon_filenames.begin(), ignored_addon_filenames.end(), fileName);
-  if (i != ignored_addon_filenames.end()) {
-    ignored_addon_filenames.erase(i);
+  log_debug << "disabling addon " << addon_id << std::endl;
+  Addon& addon = get_installed_addon(addon_id);
+  if (!addon.is_enabled())
+  {
+    log_warning << "Tried disabling already disabled Add-On" << std::endl;
+  }
+  else
+  {
+    log_debug << "Removing archive \"" << addon.get_install_filename() << "\" from search path" << std::endl;
+    if (PHYSFS_removeFromSearchPath(addon.get_install_filename().c_str()) == 0)
+    {
+      log_warning << "Could not remove " << addon.get_install_filename() << " from search path: "
+                  << PHYSFS_getLastError() << std::endl;
+    }
+    else
+    {
+      addon.set_enabled(false);
+    }
   }
 }
 
-void
-AddonManager::unload(Addon* addon)
+std::vector<std::string>
+AddonManager::scan_for_archives() const
 {
-  if (!addon->installed) throw std::runtime_error("Tried unloading non-installed Add-on");
-  if (!addon->loaded) return;
+  std::vector<std::string> archives;
 
-  log_debug << "Removing archive \"" << addon->installed_absolute_filename << "\" from search path" << std::endl;
-  if (PHYSFS_removeFromSearchPath(addon->installed_absolute_filename.c_str()) == 0) {
-    log_warning << "Could not remove " << addon->installed_absolute_filename << " from search path. Ignoring." << std::endl;
-    return;
+  // Search for archives and add them to the search path
+  std::unique_ptr<char*, decltype(&PHYSFS_freeList)>
+    rc(PHYSFS_enumerateFiles(m_addon_directory.c_str()),
+       PHYSFS_freeList);
+  for(char** i = rc.get(); *i != 0; ++i)
+  {
+    if (has_suffix(*i, ".zip"))
+    {
+      std::string archive = FileSystem::join(m_addon_directory, *i);
+      if (PHYSFS_exists(archive.c_str()))
+      {
+        archives.push_back(archive);
+      }
+    }
   }
 
-  addon->loaded = false;
+  return archives;
 }
 
-void
-AddonManager::load(Addon* addon)
+std::string
+AddonManager::scan_for_info(const std::string& archive_os_path) const
 {
-  if (!addon->installed) throw std::runtime_error("Tried loading non-installed Add-on");
-  if (addon->loaded) return;
+  std::unique_ptr<char*, decltype(&PHYSFS_freeList)>
+    rc2(PHYSFS_enumerateFiles("/"),
+        PHYSFS_freeList);
+  for(char** j = rc2.get(); *j != 0; ++j)
+  {
+    if (has_suffix(*j, ".nfo"))
+    {
+      std::string nfo_filename = FileSystem::join("/", *j);
 
-  log_debug << "Adding archive \"" << addon->installed_absolute_filename << "\" to search path" << std::endl;
-  if (PHYSFS_addToSearchPath(addon->installed_absolute_filename.c_str(), 0) == 0) {
-    log_warning << "Could not add " << addon->installed_absolute_filename << " to search path. Ignoring." << std::endl;
-    return;
+      // make sure it's in the current archive_os_path
+      const char* realdir = PHYSFS_getRealDir(nfo_filename.c_str());
+      if (!realdir)
+      {
+        log_warning << "PHYSFS_getRealDir() failed for " << nfo_filename << ": " << PHYSFS_getLastError() << std::endl;
+      }
+      else
+      {
+        if (realdir == archive_os_path)
+        {
+          return nfo_filename;
+        }
+      }
+    }
   }
 
-  addon->loaded = true;
+  return std::string();
 }
 
 void
-AddonManager::load_addons()
+AddonManager::add_installed_archive(const std::string& archive, const std::string& md5)
 {
-  // unload all Addons and forget about them
-  for (std::vector<Addon*>::iterator i = addons.begin(); i != addons.end(); i++) {
-    if ((*i)->installed && (*i)->loaded) unload(*i);
-    delete *i;
+  const char* realdir = PHYSFS_getRealDir(archive.c_str());
+  if (!realdir)
+  {
+    log_warning << "PHYSFS_getRealDir() failed for " << archive << ": "
+                << PHYSFS_getLastError() << std::endl;
   }
-  addons.clear();
-
-  // Search for archives and add them to the search path
-  char** rc = PHYSFS_enumerateFiles("/");
+  else
+  {
+    std::string os_path = FileSystem::join(realdir, archive);
 
-  for(char** i = rc; *i != 0; ++i) {
+    PHYSFS_addToSearchPath(os_path.c_str(), 0);
 
-    // get filename of potential archive
-    std::string fileName = *i;
+    std::string nfo_filename = scan_for_info(os_path);
 
-    const std::string archiveDir = PHYSFS_getRealDir(fileName.c_str());
-    static const std::string dirSep = PHYSFS_getDirSeparator();
-    std::string fullFilename = archiveDir + dirSep + fileName;
-
-    /*
-    // make sure it's in the writeDir
-    static const std::string writeDir = PHYSFS_getWriteDir();
-    if (fileName.compare(0, writeDir.length(), writeDir) != 0) continue;
-    */
-
-    // make sure it looks like an archive
-    static const std::string archiveExt = ".zip";
-    if (fullFilename.compare(fullFilename.length()-archiveExt.length(), archiveExt.length(), archiveExt) != 0) continue;
-
-    // make sure it exists
-    struct stat stats;
-    if (stat(fullFilename.c_str(), &stats) != 0) continue;
-
-    // make sure it's an actual file
-    if (!S_ISREG(stats.st_mode)) continue;
-
-    log_debug << "Found archive \"" << fullFilename << "\"" << std::endl;
-
-    // add archive to search path
-    PHYSFS_addToSearchPath(fullFilename.c_str(), 0);
-
-    // Search for infoFiles
-    std::string infoFileName = "";
-    char** rc2 = PHYSFS_enumerateFiles("/");
-    for(char** j = rc2; *j != 0; ++j) {
+    if (nfo_filename.empty())
+    {
+      log_warning << "Couldn't find .nfo file for " << os_path << std::endl;
+    }
+    else
+    {
+      try
+      {
+        std::unique_ptr<Addon> addon = Addon::parse(nfo_filename);
+        addon->set_install_filename(os_path, md5);
+        m_installed_addons.push_back(std::move(addon));
+      }
+      catch (const std::runtime_error& e)
+      {
+        log_warning << "Could not load add-on info for " << archive << ": " << e.what() << std::endl;
+      }
+    }
 
-      // get filename of potential infoFile
-      std::string potentialInfoFileName = *j;
+    PHYSFS_removeFromSearchPath(os_path.c_str());
+  }
+}
 
-      // make sure it looks like an infoFile
-      static const std::string infoExt = ".nfo";
-      if (potentialInfoFileName.length() <= infoExt.length())
-        continue;
+void
+AddonManager::add_installed_addons()
+{
+  auto archives = scan_for_archives();
 
-      if (potentialInfoFileName.compare(potentialInfoFileName.length()-infoExt.length(), infoExt.length(), infoExt) != 0)
-        continue;
+  for(auto archive : archives)
+  {
+    MD5 md5 = md5_from_file(archive);
+    add_installed_archive(archive, md5.hex_digest());
+  }
+}
 
-      // make sure it's in the current archive
-      std::string infoFileDir = PHYSFS_getRealDir(potentialInfoFileName.c_str());
-      if (infoFileDir != fullFilename) continue;
+AddonManager::AddonList
+AddonManager::parse_addon_infos(const std::string& addoninfos) const
+{
+  AddonList m_addons;
 
-      // found infoFileName
-      infoFileName = potentialInfoFileName;
-      break;
+  try
+  {
+    lisp::Parser parser;
+    std::stringstream addoninfos_stream(addoninfos);
+    const lisp::Lisp* root = parser.parse(addoninfos_stream, "supertux-addons");
+    const lisp::Lisp* addons_lisp = root->get_lisp("supertux-addons");
+    if(!addons_lisp)
+    {
+      throw std::runtime_error("Downloaded file is not an Add-on list");
     }
-    PHYSFS_freeList(rc2);
-
-    // if we have an infoFile, it's an Addon
-    if (infoFileName != "") {
-      try {
-        Addon* addon = new Addon();
-        addon->parse(infoFileName);
-        addon->installed = true;
-        addon->installed_physfs_filename = fileName;
-        addon->installed_absolute_filename = fullFilename;
-        addon->loaded = true;
-        addons.push_back(addon);
-
-        // check if the Addon is disabled
-        if (std::find(ignored_addon_filenames.begin(), ignored_addon_filenames.end(), fileName) != ignored_addon_filenames.end())
+    else
+    {
+      lisp::ListIterator iter(addons_lisp);
+      while(iter.next())
+      {
+        const std::string& token = iter.item();
+        if(token != "supertux-addoninfo")
         {
-          unload(addon);
+          log_warning << "Unknown token '" << token << "' in Add-on list" << std::endl;
+        }
+        else
+        {
+          std::unique_ptr<Addon> addon = Addon::parse(*iter.lisp());
+          m_addons.push_back(std::move(addon));
         }
-
-      } catch (const std::runtime_error& e) {
-        log_warning << "Could not load add-on info for " << fullFilename << ", loading as unmanaged:" << e.what() << std::endl;
       }
-    }
 
+      return m_addons;
+    }
+  }
+  catch(const std::exception& e)
+  {
+    std::stringstream msg;
+    msg << "Problem when reading Add-on list: " << e.what();
+    throw std::runtime_error(msg.str());
   }
 
-  PHYSFS_freeList(rc);
+  return m_addons;
 }
 
 /* EOF */
index 9387f4e..b87d7ce 100644 (file)
@@ -1,5 +1,6 @@
 //  SuperTux - Add-on Manager
 //  Copyright (C) 2007 Christoph Sommer <christoph.sommer@2007.expires.deltadevelopment.de>
+//                2014 Ingo Ruhnke <grumbel@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
 #ifndef HEADER_SUPERTUX_ADDON_ADDON_MANAGER_HPP
 #define HEADER_SUPERTUX_ADDON_ADDON_MANAGER_HPP
 
+#include <memory>
 #include <string>
 #include <vector>
 
+#include "addon/downloader.hpp"
+#include "supertux/gameconfig.hpp"
 #include "util/currenton.hpp"
 #include "util/reader_fwd.hpp"
 #include "util/writer_fwd.hpp"
 
 class Addon;
+class AddonRepository;
 
-/**
- * Checks for, installs and removes Add-ons
- */
+typedef std::string AddonId;
+
+/** Checks for, installs and removes Add-ons */
 class AddonManager : public Currenton<AddonManager>
 {
+private:
+  typedef std::vector<std::unique_ptr<Addon> > AddonList;
+
+  Downloader m_downloader;
+  std::string m_addon_directory;
+  std::string m_repository_url;
+  std::vector<Config::Addon>& m_addon_config;
+
+  AddonList m_installed_addons;
+  AddonList m_repository_addons;
+
+  bool m_has_been_updated;
+
 public:
-  AddonManager(std::vector<std::string>& ignored_addon_filenames_);
+  AddonManager(const std::string& addon_directory,
+               std::vector<Config::Addon>& addon_config);
   ~AddonManager();
 
-  /**
-   * returns a list of installed Add-ons
-   */
-  std::vector<Addon*> get_addons();
-
-  /**
-   * downloads list of available Add-ons
-   */
+  bool has_online_support() const;
+  bool has_been_updated() const;
   void check_online();
 
-  /**
-   * Download and install Add-on
-   */
-  void install(Addon* addon);
-
-  /**
-   * Physically delete Add-on
-   */
-  void remove(Addon* addon);
-
-  /**
-   * Unload Add-on and mark as not to be loaded automatically
-   */
-  void disable(Addon* addon);
-
-  /**
-   * Load Add-on and mark as to be loaded automatically
-   */
-  void enable(Addon* addon);
-
-  /**
-   * Remove Add-on from search path
-   */
-  void unload(Addon* addon);
-
-  /**
-   * Add Add-on to search path
-   */
-  void load(Addon* addon);
-
-  /**
-   * Loads all enabled Add-ons, i.e. adds them to the search path
-   */
-  void load_addons();
+  std::vector<AddonId> get_repository_addons() const;
+  std::vector<AddonId> get_installed_addons() const;
+
+  Addon& get_repository_addon(const AddonId& addon);
+  Addon& get_installed_addon(const AddonId& addon);
+
+  void install_addon(const AddonId& addon_id);
+  void uninstall_addon(const AddonId& addon_id);
+
+  void enable_addon(const AddonId& addon_id);
+  void disable_addon(const AddonId& addon_id);
+
+private:
+  std::vector<std::string> scan_for_archives() const;
+  void add_installed_addons();
+  AddonList parse_addon_infos(const std::string& addoninfos) const;
+
+  /** add \a archive, given as physfs path, to the list of installed
+      archives */
+  void add_installed_archive(const std::string& archive, const std::string& md5);
+
+  /** search for an .nfo file in the top level directory that
+      originates from \a archive, \a archive is a OS path */
+  std::string scan_for_info(const std::string& archive) const;
 
 private:
-  std::vector<Addon*> addons;
-  std::vector<std::string>& ignored_addon_filenames;
+  AddonManager(const AddonManager&) = delete;
+  AddonManager& operator=(const AddonManager&) = delete;
 };
 
 #endif
diff --git a/src/addon/downloader.cpp b/src/addon/downloader.cpp
new file mode 100644 (file)
index 0000000..ac80e73
--- /dev/null
@@ -0,0 +1,105 @@
+//  SuperTux
+//  Copyright (C) 2007 Christoph Sommer <christoph.sommer@2007.expires.deltadevelopment.de>
+//                2014 Ingo Ruhnke <grumbel@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 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, see <http://www.gnu.org/licenses/>.
+
+#include "addon/downloader.hpp"
+
+#include <curl/curl.h>
+#include <curl/easy.h>
+#include <physfs.h>
+#include <memory>
+#include <stdexcept>
+
+#include "util/log.hpp"
+#include "version.h"
+
+namespace {
+
+size_t my_curl_string_append(void* ptr, size_t size, size_t nmemb, void* userdata)
+{
+  std::string& s = *static_cast<std::string*>(userdata);
+  std::string buf(static_cast<char*>(ptr), size * nmemb);
+  s += buf;
+  log_debug << "read " << size * nmemb << " bytes of data..." << std::endl;
+  return size * nmemb;
+}
+
+size_t my_curl_physfs_write(void* ptr, size_t size, size_t nmemb, void* userdata)
+{
+  PHYSFS_file* f = static_cast<PHYSFS_file*>(userdata);
+  PHYSFS_sint64 written = PHYSFS_write(f, ptr, size, nmemb);
+  log_debug << "read " << size * nmemb << " bytes of data..." << std::endl;
+  return size * written;
+}
+
+} // namespace
+
+Downloader::Downloader()
+{
+  curl_global_init(CURL_GLOBAL_ALL);
+}
+
+Downloader::~Downloader()
+{
+  curl_global_cleanup();
+}
+
+void
+Downloader::download(const std::string& url,
+                     size_t (*write_func)(void* ptr, size_t size, size_t nmemb, void* userdata),
+                     void* userdata)
+{
+  log_info << "Downloading " << url << std::endl;
+
+  char error_buffer[CURL_ERROR_SIZE+1];
+
+  CURL* curl_handle = curl_easy_init();
+  curl_easy_setopt(curl_handle, CURLOPT_URL, url.c_str());
+  curl_easy_setopt(curl_handle, CURLOPT_USERAGENT, "SuperTux/" PACKAGE_VERSION " libcURL");
+  curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, write_func);
+  curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, userdata);
+  curl_easy_setopt(curl_handle, CURLOPT_ERRORBUFFER, error_buffer);
+  curl_easy_setopt(curl_handle, CURLOPT_NOPROGRESS, 1);
+  curl_easy_setopt(curl_handle, CURLOPT_NOSIGNAL, 1);
+  curl_easy_setopt(curl_handle, CURLOPT_FAILONERROR, 1);
+  curl_easy_setopt(curl_handle, CURLOPT_FOLLOWLOCATION, 1);
+  CURLcode result = curl_easy_perform(curl_handle);
+  curl_easy_cleanup(curl_handle);
+
+  if (result != CURLE_OK)
+  {
+    std::string why = error_buffer[0] ? error_buffer : "unhandled error";
+    throw std::runtime_error(url + ": download failed: " + why);
+  }
+}
+
+std::string
+Downloader::download(const std::string& url)
+{
+  std::string result;
+  download(url, my_curl_string_append, &result);
+  return result;
+}
+
+void
+Downloader::download(const std::string& url, const std::string& filename)
+{
+  std::unique_ptr<PHYSFS_file, int(*)(PHYSFS_File*)> fout(PHYSFS_openWrite(filename.c_str()),
+                                                          PHYSFS_close);
+  download(url, my_curl_physfs_write, fout.get());
+}
+
+/* EOF */
diff --git a/src/addon/downloader.hpp b/src/addon/downloader.hpp
new file mode 100644 (file)
index 0000000..dd69976
--- /dev/null
@@ -0,0 +1,47 @@
+//  SuperTux
+//  Copyright (C) 2007 Christoph Sommer <christoph.sommer@2007.expires.deltadevelopment.de>
+//                2014 Ingo Ruhnke <grumbel@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 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, see <http://www.gnu.org/licenses/>.
+
+#ifndef HEADER_SUPERTUX_ADDON_DOWNLOADER_HPP
+#define HEADER_SUPERTUX_ADDON_DOWNLOADER_HPP
+
+#include <string>
+
+class Downloader
+{
+private:
+public:
+  Downloader();
+  ~Downloader();
+
+  /** Download \a url and return the result as string */
+  std::string download(const std::string& url);
+
+  /** Download \a url and store the result in \a filename */
+  void download(const std::string& url, const std::string& filename);
+
+  void download(const std::string& url,
+                size_t (*write_func)(void* ptr, size_t size, size_t nmemb, void* userdata),
+                void* userdata);
+
+private:
+  Downloader(const Downloader&) = delete;
+  Downloader& operator=(const Downloader&) = delete;
+};
+
+#endif
+
+/* EOF */
index eea7955..879be82 100644 (file)
 #include "addon/addon_manager.hpp"
 #include "control/input_manager.hpp"
 #include "lisp/writer.hpp"
+#include "lisp/list_iterator.hpp"
 #include "lisp/parser.hpp"
 #include "util/reader.hpp"
+#include "util/log.hpp"
 #include "supertux/globals.hpp"
 
 Config::Config() :
@@ -47,7 +49,7 @@ Config::Config() :
   locale(),
   keyboard_config(),
   joystick_config(),
-  disabled_addon_filenames(),
+  addons(),
   developer_mode(false)
 {
 }
@@ -121,7 +123,25 @@ Config::load()
   const lisp::Lisp* config_addons_lisp = config_lisp->get_lisp("addons");
   if (config_addons_lisp)
   {
-    config_addons_lisp->get("disabled-addons", disabled_addon_filenames);
+    lisp::ListIterator iter(config_addons_lisp);
+    while(iter.next())
+    {
+      const std::string& token = iter.item();
+      if (token == "addon")
+      {
+        std::string id;
+        bool enabled = false;
+        if (iter.lisp()->get("id", id) &&
+            iter.lisp()->get("enabled", enabled))
+        {
+          addons.push_back({id, enabled});
+        }
+      }
+      else
+      {
+        log_warning << "Unknown token in config file: " << token << std::endl;
+      }
+    }
   }
 }
 
@@ -175,7 +195,13 @@ Config::save()
   writer.end_list("control");
 
   writer.start_list("addons");
-  writer.write("disabled-addons", disabled_addon_filenames);
+  for(auto addon : addons)
+  {
+    writer.start_list("addon");
+    writer.write("id", addon.id);
+    writer.write("enabled", addon.enabled);
+    writer.end_list("addon");
+  }
   writer.end_list("addons");
 
   writer.end_list("supertux-config");
index 9f9ffd2..3b7dd06 100644 (file)
@@ -72,7 +72,12 @@ public:
   KeyboardConfig keyboard_config;
   JoystickConfig joystick_config;
 
-  std::vector<std::string> disabled_addon_filenames;
+  struct Addon
+  {
+    std::string id;
+    bool enabled;
+  };
+  std::vector<Addon> addons;
 
   bool developer_mode;
 };
index 5d3747d..6dc7c37 100644 (file)
@@ -326,8 +326,7 @@ Main::launch_game()
   Resources resources;
 
   timelog("addons");
-  AddonManager addon_manager(g_config->disabled_addon_filenames);
-  addon_manager.load_addons();
+  AddonManager addon_manager("addons", g_config->addons);
 
   timelog(0);
 
index a658a96..91636f2 100644 (file)
 
 namespace {
 
-bool generate_addons_menu_sorter(const Addon* a1, const Addon* a2)
+#define IS_REPOSITORY_MENU_ID(idx) ((idx - MNID_ADDON_LIST_START) % 2 == 0)
+#define IS_INSTALLED_MENU_ID(idx) ((idx - MNID_ADDON_LIST_START) % 2 == 1)
+
+#define MAKE_REPOSITORY_MENU_ID(idx) (MNID_ADDON_LIST_START + 2*idx+0)
+#define MAKE_INSTALLED_MENU_ID(idx) (MNID_ADDON_LIST_START + 2*idx+1)
+
+#define UNPACK_REPOSITORY_MENU_ID(idx) (((idx - MNID_ADDON_LIST_START) - 0) / 2)
+#define UNPACK_INSTALLED_MENU_ID(idx) (((idx - MNID_ADDON_LIST_START) - 1) / 2)
+
+std::string addon_type_to_translated_string(Addon::Type type)
 {
-  return a1->title < a2->title;
+  switch (type)
+  {
+    case Addon::LEVELSET:
+      return _("Levelset");
+
+    case Addon::WORLDMAP:
+      return _("Worldmap");
+
+    case Addon::WORLD:
+      return _("World");
+
+    default:
+      return _("Unknown");
+  }
+}
+
+std::string generate_menu_item_text(const Addon& addon)
+{
+  std::string text;
+  std::string type = addon_type_to_translated_string(addon.get_type());
+
+  if(!addon.get_author().empty())
+  {
+    text = str(boost::format(_("%s \"%s\" by \"%s\""))
+               % type % addon.get_title() % addon.get_author());
+  }
+  else
+  {
+    // Only addon type and name, no need for translation.
+    text = str(boost::format("%s \"%s\"")
+               % type % addon.get_title());
+  }
+
+  return text;
 }
 
 } // namespace
 
 AddonMenu::AddonMenu() :
-  m_addons()
+  m_addon_manager(*AddonManager::current()),
+  m_installed_addons(),
+  m_repository_addons()
 {
   refresh();
 }
@@ -44,75 +88,95 @@ AddonMenu::AddonMenu() :
 void
 AddonMenu::refresh()
 {
-  clear();
-
-  AddonManager& adm = *AddonManager::current();
-
-  // refresh list of addons
-  m_addons = adm.get_addons();
+  m_installed_addons = m_addon_manager.get_installed_addons();
+  m_repository_addons = m_addon_manager.get_repository_addons();
+
+#ifdef GRUMBEL
+  std::sort(m_addons.begin(), m_addons.end(),
+            [](const Addon& lhs, const Addon& rhs)
+            {
+              return lhs.title < lhs.title;
+            });
+#endif
 
-  // sort list
-  std::sort(m_addons.begin(), m_addons.end(), generate_addons_menu_sorter);
+  rebuild_menu();
+}
 
+void
+AddonMenu::rebuild_menu()
+{
+  clear();
   add_label(_("Add-ons"));
   add_hl();
 
-  // FIXME: don't use macro, use AddonManager::online_available() or so
-#ifdef HAVE_LIBCURL
-  add_entry(0, std::string(_("Check Online")));
-#else
-  add_inactive(0, std::string(_("Check Online (disabled)")));
-#endif
-
-  //add_hl();
 
-  for (unsigned int i = 0; i < m_addons.size(); i++)
+  if (m_installed_addons.empty())
   {
-    const Addon& addon = *m_addons[i];
-    std::string text = "";
-
-    if (!addon.kind.empty())
+    add_inactive(MNID_NOTHING_NEW, _("No Addons installed"));
+  }
+  else
+  {
+    int idx = 0;
+    for (const auto& addon_id : m_installed_addons)
     {
-      std::string kind = addon.kind;
-      if(addon.kind == "Levelset") {
-        kind = _("Levelset");
-      }
-      else if(addon.kind == "Worldmap") {
-        kind = _("Worldmap");
-      }
-      else if(addon.kind == "World") {
-        kind = _("World");
-      }
-      else if(addon.kind == "Level") {
-        kind = _("Level");
-      }
+      const Addon& addon = m_addon_manager.get_installed_addon(addon_id);
+      std::string text = generate_menu_item_text(addon);
+      add_toggle(MAKE_INSTALLED_MENU_ID(idx), text, addon.is_enabled());
+      idx += 1;
+    }
+  }
+
+  add_hl();
 
-      if(!addon.author.empty())
+  {
+    bool have_new_stuff = false;
+    int idx = 0;
+    for (const auto& addon_id : m_repository_addons)
+    {
+      const Addon& addon = m_addon_manager.get_repository_addon(addon_id);
+      try
       {
-        text = str(boost::format(_("%s \"%s\" by \"%s\""))
-                   % kind % addon.title % addon.author);
+        // addon is already installed, so check if they are the same
+        Addon& installed_addon = m_addon_manager.get_installed_addon(addon_id);
+        if (installed_addon.get_md5() == addon.get_md5() ||
+            installed_addon.get_version() > addon.get_version())
+        {
+          log_debug << "ignoring already installed addon " << installed_addon.get_id() << std::endl;
+        }
+        else
+        {
+          log_debug << installed_addon.get_id() << " is installed, but updated: '"
+                    << installed_addon.get_md5() << "' vs '" << addon.get_md5() << "'  '"
+                    << installed_addon.get_version() << "' vs '" << addon.get_version() << "'"
+                    << std::endl;
+          std::string text = generate_menu_item_text(addon);
+          add_entry(MAKE_REPOSITORY_MENU_ID(idx), "Install " + text + " *NEW*");
+          have_new_stuff = true;
+        }
       }
-      else
+      catch(const std::exception& err)
       {
-        // Only addon type and name, no need for translation.
-        text = str(boost::format("%s \"%s\"")
-                   % kind % addon.title);
+        // addon is not installed
+        std::string text = generate_menu_item_text(addon);
+        add_entry(MAKE_REPOSITORY_MENU_ID(idx), "Install " + text);
+        have_new_stuff = true;
       }
+      idx += 1;
     }
-    else
+
+    if (!have_new_stuff && m_addon_manager.has_been_updated())
     {
-      if (!addon.author.empty())
-      {
-        text = str(boost::format(_("\"%s\" by \"%s\""))
-                   % addon.title % addon.author);
-      }
-      else {
-        // Only addon name, no need for translation.
-        text = str(boost::format("\"%s\"")
-                   % addon.title);
-      }
+      add_inactive(MNID_NOTHING_NEW, _("No new Addons found"));
     }
-    add_toggle(ADDON_LIST_START_ID + i, text, addon.loaded);
+  }
+
+  if (!m_addon_manager.has_online_support())
+  {
+    add_inactive(MNID_CHECK_ONLINE, std::string(_("Check Online (disabled)")));
+  }
+  else
+  {
+    add_entry(MNID_CHECK_ONLINE, std::string(_("Check Online")));
   }
 
   add_hl();
@@ -122,69 +186,61 @@ AddonMenu::refresh()
 void
 AddonMenu::menu_action(MenuItem* item)
 {
-  int index = item->id;
-
-  if (index == -1)
-  {
-    // do nothing
-  }
-  else if (index == 0) // check if "Check Online" was chosen
+  if (item->id == MNID_CHECK_ONLINE) // check if "Check Online" was chosen
   {
     try
     {
-      AddonManager::current()->check_online();
+      m_addon_manager.check_online();
       refresh();
-      set_active_item(index);
     }
     catch (std::exception& e)
     {
       log_warning << "Check for available Add-ons failed: " << e.what() << std::endl;
     }
   }
-  else
+  else if (MNID_ADDON_LIST_START <= item->id)
   {
-    // if one of the Addons listed was chosen, take appropriate action
-    if ((index >= ADDON_LIST_START_ID) && (index < ADDON_LIST_START_ID) + m_addons.size())
+    if (IS_INSTALLED_MENU_ID(item->id))
     {
-      Addon& addon = *m_addons[index - ADDON_LIST_START_ID];
-      if (!addon.installed)
-      {
-        try
-        {
-          AddonManager::current()->install(&addon);
-        }
-        catch (std::exception& e)
-        {
-          log_warning << "Installing Add-on failed: " << e.what() << std::endl;
-        }
-        set_toggled(index, addon.loaded);
-      }
-      else if (!addon.loaded)
+      int idx = UNPACK_INSTALLED_MENU_ID(item->id);
+      if (0 <= idx && idx < static_cast<int>(m_installed_addons.size()))
       {
-        try
+        const Addon& addon = m_addon_manager.get_installed_addon(m_installed_addons[idx]);
+        if(addon.is_enabled())
         {
-          AddonManager::current()->enable(&addon);
+          m_addon_manager.disable_addon(addon.get_id());
+          set_toggled(item->id, addon.is_enabled());
         }
-        catch (std::exception& e)
+        else
         {
-          log_warning << "Enabling Add-on failed: " << e.what() << std::endl;
+          m_addon_manager.enable_addon(addon.get_id());
+          set_toggled(item->id, addon.is_enabled());
         }
-        set_toggled(index, addon.loaded);
       }
-      else
+    }
+    else if (IS_REPOSITORY_MENU_ID(item->id))
+    {
+      int idx = UNPACK_REPOSITORY_MENU_ID(item->id);
+      if (0 <= idx && idx < static_cast<int>(m_repository_addons.size()))
       {
+        const Addon& addon = m_addon_manager.get_repository_addon(m_repository_addons[idx]);
         try
         {
-          AddonManager::current()->disable(&addon);
+          m_addon_manager.install_addon(addon.get_id());
+          m_addon_manager.enable_addon(addon.get_id());
         }
-        catch (std::exception& e)
+        catch(const std::exception& err)
         {
-          log_warning << "Disabling Add-on failed: " << e.what() << std::endl;
+          log_warning << "Enabling addon failed: " << err.what() << std::endl;
         }
-        set_toggled(index, addon.loaded);
+        refresh();
       }
     }
   }
+  else
+  {
+       log_warning << "Unknown menu item clicked: " << item->id << std::endl;
+  }
 }
 
 /* EOF */
index 640c5bc..741ad67 100644 (file)
 
 #include "gui/menu.hpp"
 
-enum {
-  ADDON_LIST_START_ID = 10
-};
-
 class Addon;
+class AddonManager;
 
 class AddonMenu : public Menu
 {
 private:
-  std::vector<Addon*> m_addons;
+  enum {
+    MNID_CHECK_ONLINE,
+    MNID_NOTHING_NEW,
+    MNID_ADDON_LIST_START = 10
+  };
+
+private:
+  AddonManager& m_addon_manager;
+  std::vector<std::string> m_installed_addons;
+  std::vector<std::string> m_repository_addons;
 
 public:
   AddonMenu();
@@ -37,6 +43,9 @@ public:
   void menu_action(MenuItem* item) override;
 
 private:
+  void rebuild_menu();
+
+private:
   AddonMenu(const AddonMenu&);
   AddonMenu& operator=(const AddonMenu&);
 };
index 802c0ea..8bbc717 100644 (file)
 ContribMenu::ContribMenu() :
   m_contrib_worlds()
 {
-  /** Generating contrib levels list by making use of Level Subset  */
+  // Generating contrib levels list by making use of Level Subset
   std::vector<std::string> level_worlds;
-  char** files = PHYSFS_enumerateFiles("levels/");
-  for(const char* const* filename = files; *filename != 0; ++filename) {
-    std::string filepath = std::string("levels/") + *filename;
+
+  std::unique_ptr<char*, decltype(&PHYSFS_freeList)>
+    files(PHYSFS_enumerateFiles("levels"),
+          PHYSFS_freeList);
+  for(const char* const* filename = files.get(); *filename != 0; ++filename)
+  {
+    std::string filepath = FileSystem::join("levels", *filename);
     if(PHYSFS_isDirectory(filepath.c_str()))
+    {
       level_worlds.push_back(filepath);
+    }
   }
-  PHYSFS_freeList(files);
 
   add_label(_("Contrib Levels"));
   add_hl();
index b6734cc..cb87b32 100644 (file)
@@ -43,6 +43,9 @@ World::load(const std::string& directory)
   { // generate savegame filename
     std::string worlddirname = FileSystem::basename(directory);
     std::ostringstream stream;
+#ifdef GRUMBEL
+    // sanitize this!
+#endif
     stream << "profile" << g_config->profile << "/" << worlddirname << ".stsg";
     std::string slotfile = stream.str();
     world->m_savegame_filename = stream.str();
diff --git a/tools/build-addon-index.py b/tools/build-addon-index.py
new file mode 100755 (executable)
index 0000000..5e1e2f7
--- /dev/null
@@ -0,0 +1,133 @@
+#!/usr/bin/env python
+#
+# SuperTux
+# Copyright (C) 2014 Ingo Ruhnke <grumbel@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 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, see <http://www.gnu.org/licenses/>.
+
+
+import argparse
+import glob
+import hashlib
+import os
+import subprocess
+import sys
+
+import sexpr
+
+
+def escape_str(str):
+    return "\"%s\"" % str.replace("\"", "\\\"")
+
+
+class Addon:
+    def __init__(self, filename):
+        lst = sexpr.parse(filename)
+        if lst[0][0] != "supertux-addoninfo":
+            raise Exception("not a supertux-addoninfo: %s" % lst[0][0])
+        else:
+            tags = {}
+            for k, v in lst[0][1:]:
+                if k == "id":
+                    self.id = v
+                elif k == "version":
+                    self.version = int(v)
+                elif k == "type":
+                    self.type = v
+                elif k == "title":
+                    self.title = v
+                elif k == "author":
+                    self.author = v
+                elif k == "license":
+                    self.license = v
+                else:
+                    raise Exception("unknown tag: %s" % k)
+
+            self.md5 = ""
+            self.url = ""
+                    
+    def write(self, fout):
+        fout.write("  (supertux-addoninfo\n")
+        fout.write("    (id %s)\n" % escape_str(self.id))
+        fout.write("    (version %d)\n" % self.version)
+        fout.write("    (type %s)\n" % escape_str(self.type))
+        fout.write("    (title %s)\n" % escape_str(self.title))
+        fout.write("    (author %s)\n" % escape_str(self.author))
+        fout.write("    (license %s)\n" % escape_str(self.license))
+        fout.write("    (url %s)\n" % escape_str(self.url))
+        fout.write("    (md5 %s)\n" % escape_str(self.md5))
+        fout.write("   )\n")
+
+  
+def process_addon(fout, addon_dir, nfo, base_url, zipdir):
+    # print addon_dir, nfo
+    with open(nfo) as fin:
+        addon = Addon(fin.read())
+
+    zipfile = addon.id + "_v" + str(addon.version) + ".zip"
+
+    # see http://pivotallabs.com/barriers-deterministic-reproducible-zip-files/
+    os.remove(os.path.join(zipdir, zipfile))
+    zipout = os.path.relpath(os.path.join(zipdir, zipfile), addon_dir)
+    subprocess.call(["zip", "-X", "-r", "--quiet", zipout, "."], cwd=addon_dir)
+
+    with open(os.path.join(zipdir, zipfile), 'rb') as fin:
+        addon.md5 = hashlib.md5(fin.read()).hexdigest()
+
+    addon.url = base_url + zipfile
+
+    addon.write(fout)
+
+
+def generate_index(fout, directory, base_url, zipdir):
+    fout.write(";; automatically generated by build-addon-index.py\n")
+    fout.write("(supertux-addons\n")
+    for addon_dir in os.listdir(directory):
+        addon_dir = os.path.join(directory, addon_dir)
+        if os.path.isdir(addon_dir):
+            print addon_dir
+            nfos = glob.glob(os.path.join(addon_dir, "*.nfo"))
+            if len(nfos) == 0:
+                raise Exception(".nfo file missing from %s" % addon_dir)
+            elif len(nfos) > 1:
+                raise Exception("to many .nfo files in %s" % addon_dir)
+            else:
+                try:
+                    process_addon(fout, addon_dir, nfos[0], base_url, zipdir)
+                except Exception, e:
+                    sys.stderr.write("%s: ignoring addon because: %s\n" % (addon_dir, e))
+    fout.write(")\n\n;; EOF ;;\n")
+
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser(description='Addon Index/Zip Generator')
+    parser.add_argument('DIRECTORY',  type=str, nargs=1,
+                        help="directory containing the mods")
+    parser.add_argument('-o', '--output', metavar='FILE', type=str, required=False,
+                        help="output file")
+    parser.add_argument('-z', '--zipdir', metavar="DIR", type=str, required=True,
+                        help="generate zip files")
+    parser.add_argument('-u', '--url', metavar='FILE', type=str,
+                        default="http://addons.supertux.googlecode.com/git/repository/",
+                        help="base url")
+    args = parser.parse_args()
+
+    if args.output is None:
+        fout = sys.stdout
+        generate_index(fout, args.DIRECTORY[0], args.url, args.zipdir)
+    else:
+        with open(args.output, "w") as fout:
+            generate_index(fout, args.DIRECTORY[0], args.url, args.zipdir)
+
+# EOF #
diff --git a/tools/sexpr.py b/tools/sexpr.py
new file mode 100755 (executable)
index 0000000..4c69f92
--- /dev/null
@@ -0,0 +1,112 @@
+#!/usr/bin/env python
+
+# Copyright (c) 2014 Ingo Ruhnke <grumbel@gmail.com>
+#
+# This software is provided 'as-is', without any express or implied
+# warranty. In no event will the authors be held liable for any damages
+# arising from the use of this software.
+#
+# Permission is granted to anyone to use this software for any purpose,
+# including commercial applications, and to alter it and redistribute it
+# freely, subject to the following restrictions:
+#
+# 1. The origin of this software must not be misrepresented; you must not
+#    claim that you wrote the original software. If you use this software
+#    in a product, an acknowledgment in the product documentation would be
+#    appreciated but is not required.
+# 2. Altered source versions must be plainly marked as such, and must not be
+#    misrepresented as being the original software.
+# 3. This notice may not be removed or altered from any source distribution.
+
+import re
+import codecs
+
+def parse(text):
+    stack = [[]]
+    state = 'list'
+    i = 0
+    line = 1
+    column = 0
+    while i < len(text):
+        c = text[i]
+        if c == '\n':
+            line += 1
+            column = 0
+        else:
+            column += 1
+
+        if state == 'list':
+            if c == '(':
+                stack.append([])
+            elif c == ')':
+                stack[-2].append(stack.pop())
+            elif c == "\"":
+                state = 'string'
+                atom = ""
+            elif c == ";":
+                state = 'comment'
+            elif c.isalpha():
+                state = 'symbol'
+                atom = c
+            elif c.isdigit():
+                state = 'number'
+                atom = c
+            elif c.isspace():
+                pass
+            else:
+                raise Exception("%d:%d: error: unexpected character: '%s'" % (line, column, c))
+
+        elif state == 'comment':
+            if c == '\n':
+                state = 'list'
+            else:
+                pass
+
+        elif state == 'string':
+            if c == "\\":
+                i += 1
+                atom += text[i]
+            elif c == "\"":
+                stack[-1].append(atom)
+                state = 'list'
+            else:
+                atom += c
+
+        elif state == 'number':
+            if not c.isdigit() or c != ".":
+                stack[-1].append(int(atom))
+                state = 'list'
+                i -= 1
+            else:
+                atom += c
+
+        elif state == 'symbol':
+            if c.isspace() or c == '(' or c == ')':
+                stack[-1].append(atom)
+                state = 'list'
+                i -= 1
+            else:
+                atom += c
+
+        # print c, stack
+
+        i += 1
+
+    if len(stack) == 1:
+        return stack[0]
+    else:
+        raise Exception("error: list not closed")
+
+if __name__ == "__main__":
+    print "parsing..."
+    result = parse(r'(() ("bar" foo) ()) () bar ')
+    print "1.", result
+    print "2.", parse(""";;comment
+    ("Hello World" 5 1 123) ("Hello" 123 123 "foobar") ;; comment""")
+    print "3.", parse(r'(8(8)8)')
+    print "4.", parse(r'')
+    print "5.", parse(r'  ')
+    with codecs.open("white.stf", encoding='utf-8') as fin:
+        print "6.", parse(fin.read())
+
+# EOF #