added extracting level and artwork sets from zip files at program start
[rocksndiamonds.git] / src / libgame / setup.c
index ff8485bce818df7358ea49379fc43503cae5a7da..1ae27900c05b0e6e183268632491e9e0f7788c17 100644 (file)
@@ -1,21 +1,20 @@
-/***********************************************************
-* Artsoft Retro-Game Library                               *
-*----------------------------------------------------------*
-* (c) 1994-2006 Artsoft Entertainment                      *
-*               Holger Schemel                             *
-*               Detmolder Strasse 189                      *
-*               33604 Bielefeld                            *
-*               Germany                                    *
-*               e-mail: info@artsoft.org                   *
-*----------------------------------------------------------*
-* setup.c                                                  *
-***********************************************************/
+// ============================================================================
+// Artsoft Retro-Game Library
+// ----------------------------------------------------------------------------
+// (c) 1995-2014 by Artsoft Entertainment
+//                         Holger Schemel
+//                 info@artsoft.org
+//                 http://www.artsoft.org/
+// ----------------------------------------------------------------------------
+// setup.c
+// ============================================================================
 
 #include <sys/types.h>
 #include <sys/stat.h>
 #include <dirent.h>
 #include <string.h>
 #include <unistd.h>
+#include <errno.h>
 
 #include "platform.h"
 
 #include "text.h"
 #include "misc.h"
 #include "hash.h"
+#include "zip/miniunz.h"
 
 
+#define ENABLE_UNUSED_CODE     FALSE   // for currently unused functions
+#define DEBUG_NO_CONFIG_FILE   FALSE   // for extra-verbose debug output
+
 #define NUM_LEVELCLASS_DESC    8
 
 static char *levelclass_desc[NUM_LEVELCLASS_DESC] =
@@ -86,16 +89,22 @@ static char *levelclass_desc[NUM_LEVELCLASS_DESC] =
 
 #define MAX_COOKIE_LEN                         256
 
+
 static void setTreeInfoToDefaults(TreeInfo *, int);
+static TreeInfo *getTreeInfoCopy(TreeInfo *ti);
 static int compareTreeInfoEntries(const void *, const void *);
 
 static int token_value_position   = TOKEN_VALUE_POSITION_DEFAULT;
 static int token_comment_position = TOKEN_COMMENT_POSITION_DEFAULT;
 
+static SetupFileHash *artworkinfo_cache_old = NULL;
+static SetupFileHash *artworkinfo_cache_new = NULL;
+static boolean use_artworkinfo_cache = TRUE;
 
-/* ------------------------------------------------------------------------- */
-/* file functions                                                            */
-/* ------------------------------------------------------------------------- */
+
+// ----------------------------------------------------------------------------
+// file functions
+// ----------------------------------------------------------------------------
 
 static char *getLevelClassDescription(TreeInfo *ti)
 {
@@ -107,34 +116,28 @@ static char *getLevelClassDescription(TreeInfo *ti)
     return "Unknown Level Class";
 }
 
-static char *getUserLevelDir(char *level_subdir)
-{
-  static char *userlevel_dir = NULL;
-  char *data_dir = getUserGameDataDir();
-  char *userlevel_subdir = LEVELS_DIRECTORY;
-
-  checked_free(userlevel_dir);
-
-  if (level_subdir != NULL)
-    userlevel_dir = getPath3(data_dir, userlevel_subdir, level_subdir);
-  else
-    userlevel_dir = getPath2(data_dir, userlevel_subdir);
-
-  return userlevel_dir;
-}
-
 static char *getScoreDir(char *level_subdir)
 {
   static char *score_dir = NULL;
-  char *data_dir = getCommonDataDir();
+  static char *score_level_dir = NULL;
   char *score_subdir = SCORES_DIRECTORY;
 
-  checked_free(score_dir);
+  if (score_dir == NULL)
+  {
+    if (program.global_scores)
+      score_dir = getPath2(getCommonDataDir(),   score_subdir);
+    else
+      score_dir = getPath2(getUserGameDataDir(), score_subdir);
+  }
 
   if (level_subdir != NULL)
-    score_dir = getPath3(data_dir, score_subdir, level_subdir);
-  else
-    score_dir = getPath2(data_dir, score_subdir);
+  {
+    checked_free(score_level_dir);
+
+    score_level_dir = getPath2(score_dir, level_subdir);
+
+    return score_level_dir;
+  }
 
   return score_dir;
 }
@@ -155,6 +158,26 @@ static char *getLevelSetupDir(char *level_subdir)
   return levelsetup_dir;
 }
 
+static char *getCacheDir(void)
+{
+  static char *cache_dir = NULL;
+
+  if (cache_dir == NULL)
+    cache_dir = getPath2(getUserGameDataDir(), CACHE_DIRECTORY);
+
+  return cache_dir;
+}
+
+static char *getNetworkDir(void)
+{
+  static char *network_dir = NULL;
+
+  if (network_dir == NULL)
+    network_dir = getPath2(getUserGameDataDir(), NETWORK_DIRECTORY);
+
+  return network_dir;
+}
+
 static char *getLevelDirFromTreeInfo(TreeInfo *node)
 {
   static char *level_dir = NULL;
@@ -170,11 +193,65 @@ static char *getLevelDirFromTreeInfo(TreeInfo *node)
   return level_dir;
 }
 
-char *getCurrentLevelDir()
+char *getUserLevelDir(char *level_subdir)
+{
+  static char *userlevel_dir = NULL;
+  char *data_dir = getUserGameDataDir();
+  char *userlevel_subdir = LEVELS_DIRECTORY;
+
+  checked_free(userlevel_dir);
+
+  if (level_subdir != NULL)
+    userlevel_dir = getPath3(data_dir, userlevel_subdir, level_subdir);
+  else
+    userlevel_dir = getPath2(data_dir, userlevel_subdir);
+
+  return userlevel_dir;
+}
+
+char *getNetworkLevelDir(char *level_subdir)
+{
+  static char *network_level_dir = NULL;
+  char *data_dir = getNetworkDir();
+  char *networklevel_subdir = LEVELS_DIRECTORY;
+
+  checked_free(network_level_dir);
+
+  if (level_subdir != NULL)
+    network_level_dir = getPath3(data_dir, networklevel_subdir, level_subdir);
+  else
+    network_level_dir = getPath2(data_dir, networklevel_subdir);
+
+  return network_level_dir;
+}
+
+char *getCurrentLevelDir(void)
 {
   return getLevelDirFromTreeInfo(leveldir_current);
 }
 
+char *getNewUserLevelSubdir(void)
+{
+  static char *new_level_subdir = NULL;
+  char *subdir_prefix = getLoginName();
+  char subdir_suffix[10];
+  int max_suffix_number = 1000;
+  int i = 0;
+
+  while (++i < max_suffix_number)
+  {
+    sprintf(subdir_suffix, "_%d", i);
+
+    checked_free(new_level_subdir);
+    new_level_subdir = getStringCat2(subdir_prefix, subdir_suffix);
+
+    if (!directoryExists(getUserLevelDir(new_level_subdir)))
+      break;
+  }
+
+  return new_level_subdir;
+}
+
 static char *getTapeDir(char *level_subdir)
 {
   static char *tape_dir = NULL;
@@ -191,7 +268,7 @@ static char *getTapeDir(char *level_subdir)
   return tape_dir;
 }
 
-static char *getSolutionTapeDir()
+static char *getSolutionTapeDir(void)
 {
   static char *tape_dir = NULL;
   char *data_dir = getCurrentLevelDir();
@@ -246,14 +323,14 @@ static char *getDefaultMusicDir(char *music_subdir)
   return music_dir;
 }
 
-static char *getDefaultArtworkSet(int type)
+static char *getClassicArtworkSet(int type)
 {
   return (type == TREE_TYPE_GRAPHICS_DIR ? GFX_CLASSIC_SUBDIR :
          type == TREE_TYPE_SOUNDS_DIR   ? SND_CLASSIC_SUBDIR :
          type == TREE_TYPE_MUSIC_DIR    ? MUS_CLASSIC_SUBDIR : "");
 }
 
-static char *getDefaultArtworkDir(int type)
+static char *getClassicArtworkDir(int type)
 {
   return (type == TREE_TYPE_GRAPHICS_DIR ?
          getDefaultGraphicsDir(GFX_CLASSIC_SUBDIR) :
@@ -263,7 +340,7 @@ static char *getDefaultArtworkDir(int type)
          getDefaultMusicDir(MUS_CLASSIC_SUBDIR) : "");
 }
 
-static char *getUserGraphicsDir()
+static char *getUserGraphicsDir(void)
 {
   static char *usergraphics_dir = NULL;
 
@@ -273,7 +350,7 @@ static char *getUserGraphicsDir()
   return usergraphics_dir;
 }
 
-static char *getUserSoundsDir()
+static char *getUserSoundsDir(void)
 {
   static char *usersounds_dir = NULL;
 
@@ -283,7 +360,7 @@ static char *getUserSoundsDir()
   return usersounds_dir;
 }
 
-static char *getUserMusicDir()
+static char *getUserMusicDir(void)
 {
   static char *usermusic_dir = NULL;
 
@@ -297,6 +374,9 @@ static char *getSetupArtworkDir(TreeInfo *ti)
 {
   static char *artwork_dir = NULL;
 
+  if (ti == NULL)
+    return NULL;
+
   checked_free(artwork_dir);
 
   artwork_dir = getPath2(ti->basepath, ti->fullpath);
@@ -318,24 +398,32 @@ char *setLevelArtworkDir(TreeInfo *ti)
   checked_free(*artwork_path_ptr);
 
   if ((level_artwork = getTreeInfoFromIdentifier(ti, *artwork_set_ptr)))
+  {
     *artwork_path_ptr = getStringCopy(getSetupArtworkDir(level_artwork));
+  }
   else
   {
-    /* No (or non-existing) artwork configured in "levelinfo.conf". This would
-       normally result in using the artwork configured in the setup menu. But
-       if an artwork subdirectory exists (which might contain custom artwork
-       or an artwork configuration file), this level artwork must be treated
-       as relative to the default "classic" artwork, not to the artwork that
-       is currently configured in the setup menu. */
+    /*
+      No (or non-existing) artwork configured in "levelinfo.conf". This would
+      normally result in using the artwork configured in the setup menu. But
+      if an artwork subdirectory exists (which might contain custom artwork
+      or an artwork configuration file), this level artwork must be treated
+      as relative to the default "classic" artwork, not to the artwork that
+      is currently configured in the setup menu.
+
+      Update: For "special" versions of R'n'D (like "R'n'D jue"), do not use
+      the "default" artwork (which would be "jue0" for "R'n'D jue"), but use
+      the real "classic" artwork from the original R'n'D (like "gfx_classic").
+    */
 
     char *dir = getPath2(getCurrentLevelDir(), ARTWORK_DIRECTORY(ti->type));
 
     checked_free(*artwork_set_ptr);
 
-    if (fileExists(dir))
+    if (directoryExists(dir))
     {
-      *artwork_path_ptr = getStringCopy(getDefaultArtworkDir(ti->type));
-      *artwork_set_ptr = getStringCopy(getDefaultArtworkSet(ti->type));
+      *artwork_path_ptr = getStringCopy(getClassicArtworkDir(ti->type));
+      *artwork_set_ptr = getStringCopy(getClassicArtworkSet(ti->type));
     }
     else
     {
@@ -349,7 +437,7 @@ char *setLevelArtworkDir(TreeInfo *ti)
   return *artwork_set_ptr;
 }
 
-inline static char *getLevelArtworkSet(int type)
+static char *getLevelArtworkSet(int type)
 {
   if (leveldir_current == NULL)
     return NULL;
@@ -357,7 +445,7 @@ inline static char *getLevelArtworkSet(int type)
   return LEVELDIR_ARTWORK_SET(leveldir_current, type);
 }
 
-inline static char *getLevelArtworkDir(int type)
+static char *getLevelArtworkDir(int type)
 {
   if (leveldir_current == NULL)
     return UNDEFINED_FILENAME;
@@ -365,6 +453,76 @@ inline static char *getLevelArtworkDir(int type)
   return LEVELDIR_ARTWORK_PATH(leveldir_current, type);
 }
 
+char *getProgramMainDataPath(char *command_filename, char *base_path)
+{
+  // check if the program's main data base directory is configured
+  if (!strEqual(base_path, "."))
+    return base_path;
+
+  /* if the program is configured to start from current directory (default),
+     determine program package directory from program binary (some versions
+     of KDE/Konqueror and Mac OS X (especially "Mavericks") apparently do not
+     set the current working directory to the program package directory) */
+  char *main_data_path = getBasePath(command_filename);
+
+#if defined(PLATFORM_MACOSX)
+  if (strSuffix(main_data_path, MAC_APP_BINARY_SUBDIR))
+  {
+    char *main_data_path_old = main_data_path;
+
+    // cut relative path to Mac OS X application binary directory from path
+    main_data_path[strlen(main_data_path) -
+                  strlen(MAC_APP_BINARY_SUBDIR)] = '\0';
+
+    // cut trailing path separator from path (but not if path is root directory)
+    if (strSuffix(main_data_path, "/") && !strEqual(main_data_path, "/"))
+      main_data_path[strlen(main_data_path) - 1] = '\0';
+
+    // replace empty path with current directory
+    if (strEqual(main_data_path, ""))
+      main_data_path = ".";
+
+    // add relative path to Mac OS X application resources directory to path
+    main_data_path = getPath2(main_data_path, MAC_APP_FILES_SUBDIR);
+
+    free(main_data_path_old);
+  }
+#endif
+
+  return main_data_path;
+}
+
+char *getProgramConfigFilename(char *command_filename)
+{
+  char *command_filename_1 = getStringCopy(command_filename);
+
+  // strip trailing executable suffix from command filename
+  if (strSuffix(command_filename_1, ".exe"))
+    command_filename_1[strlen(command_filename_1) - 4] = '\0';
+
+  char *ro_base_path = getProgramMainDataPath(command_filename, RO_BASE_PATH);
+  char *conf_directory = getPath2(ro_base_path, CONF_DIRECTORY);
+
+  char *command_basepath = getBasePath(command_filename);
+  char *command_basename = getBaseNameNoSuffix(command_filename);
+  char *command_filename_2 = getPath2(command_basepath, command_basename);
+
+  char *config_filename_1 = getStringCat2(command_filename_1, ".conf");
+  char *config_filename_2 = getStringCat2(command_filename_2, ".conf");
+  char *config_filename_3 = getPath2(conf_directory, SETUP_FILENAME);
+
+  // 1st try: look for config file that exactly matches the binary filename
+  if (fileExists(config_filename_1))
+    return config_filename_1;
+
+  // 2nd try: look for config file that matches binary filename without suffix
+  if (fileExists(config_filename_2))
+    return config_filename_2;
+
+  // 3rd try: return setup config filename in global program config directory
+  return config_filename_3;
+}
+
 char *getTapeFilename(int nr)
 {
   static char *filename = NULL;
@@ -388,6 +546,19 @@ char *getSolutionTapeFilename(int nr)
   sprintf(basename, "%03d.%s", nr, TAPEFILE_EXTENSION);
   filename = getPath2(getSolutionTapeDir(), basename);
 
+  if (!fileExists(filename))
+  {
+    static char *filename_sln = NULL;
+
+    checked_free(filename_sln);
+
+    sprintf(basename, "%03d.sln", nr);
+    filename_sln = getPath2(getSolutionTapeDir(), basename);
+
+    if (fileExists(filename_sln))
+      return filename_sln;
+  }
+
   return filename;
 }
 
@@ -399,12 +570,14 @@ char *getScoreFilename(int nr)
   checked_free(filename);
 
   sprintf(basename, "%03d.%s", nr, SCOREFILE_EXTENSION);
-  filename = getPath2(getScoreDir(leveldir_current->subdir), basename);
+
+  // used instead of "leveldir_current->subdir" (for network games)
+  filename = getPath2(getScoreDir(levelset.identifier), basename);
 
   return filename;
 }
 
-char *getSetupFilename()
+char *getSetupFilename(void)
 {
   static char *filename = NULL;
 
@@ -415,7 +588,12 @@ char *getSetupFilename()
   return filename;
 }
 
-char *getEditorSetupFilename()
+char *getDefaultSetupFilename(void)
+{
+  return program.config_filename;
+}
+
+char *getEditorSetupFilename(void)
 {
   static char *filename = NULL;
 
@@ -431,7 +609,7 @@ char *getEditorSetupFilename()
   return filename;
 }
 
-char *getHelpAnimFilename()
+char *getHelpAnimFilename(void)
 {
   static char *filename = NULL;
 
@@ -442,7 +620,7 @@ char *getHelpAnimFilename()
   return filename;
 }
 
-char *getHelpTextFilename()
+char *getHelpTextFilename(void)
 {
   static char *filename = NULL;
 
@@ -453,7 +631,7 @@ char *getHelpTextFilename()
   return filename;
 }
 
-char *getLevelSetInfoFilename()
+char *getLevelSetInfoFilename(void)
 {
   static char *filename = NULL;
   char *basenames[] =
@@ -482,36 +660,85 @@ char *getLevelSetInfoFilename()
   return NULL;
 }
 
-static char *getCorrectedArtworkBasename(char *basename)
+static char *getLevelSetTitleMessageBasename(int nr, boolean initial)
+{
+  static char basename[32];
+
+  sprintf(basename, "%s_%d.txt",
+         (initial ? "titlemessage_initial" : "titlemessage"), nr + 1);
+
+  return basename;
+}
+
+char *getLevelSetTitleMessageFilename(int nr, boolean initial)
 {
-  char *basename_corrected = basename;
+  static char *filename = NULL;
+  char *basename;
+  boolean skip_setup_artwork = FALSE;
+
+  checked_free(filename);
+
+  basename = getLevelSetTitleMessageBasename(nr, initial);
 
-#if defined(PLATFORM_MSDOS)
-  if (program.filename_prefix != NULL)
+  if (!gfx.override_level_graphics)
   {
-    int prefix_len = strlen(program.filename_prefix);
+    // 1st try: look for special artwork in current level series directory
+    filename = getPath3(getCurrentLevelDir(), GRAPHICS_DIRECTORY, basename);
+    if (fileExists(filename))
+      return filename;
+
+    free(filename);
 
-    if (strncmp(basename, program.filename_prefix, prefix_len) == 0)
-      basename_corrected = &basename[prefix_len];
+    // 2nd try: look for message file in current level set directory
+    filename = getPath2(getCurrentLevelDir(), basename);
+    if (fileExists(filename))
+      return filename;
 
-    /* if corrected filename is still longer than standard MS-DOS filename
-       size (8 characters + 1 dot + 3 characters file extension), shorten
-       filename by writing file extension after 8th basename character */
-    if (strlen(basename_corrected) > 8 + 1 + 3)
-    {
-      static char *msdos_filename = NULL;
+    free(filename);
 
-      checked_free(msdos_filename);
+    // check if there is special artwork configured in level series config
+    if (getLevelArtworkSet(ARTWORK_TYPE_GRAPHICS) != NULL)
+    {
+      // 3rd try: look for special artwork configured in level series config
+      filename = getPath2(getLevelArtworkDir(ARTWORK_TYPE_GRAPHICS), basename);
+      if (fileExists(filename))
+       return filename;
 
-      msdos_filename = getStringCopy(basename_corrected);
-      strncpy(&msdos_filename[8], &basename[strlen(basename) - (1+3)], 1+3 +1);
+      free(filename);
 
-      basename_corrected = msdos_filename;
+      // take missing artwork configured in level set config from default
+      skip_setup_artwork = TRUE;
     }
   }
-#endif
 
-  return basename_corrected;
+  if (!skip_setup_artwork)
+  {
+    // 4th try: look for special artwork in configured artwork directory
+    filename = getPath2(getSetupArtworkDir(artwork.gfx_current), basename);
+    if (fileExists(filename))
+      return filename;
+
+    free(filename);
+  }
+
+  // 5th try: look for default artwork in new default artwork directory
+  filename = getPath2(getDefaultGraphicsDir(GFX_DEFAULT_SUBDIR), basename);
+  if (fileExists(filename))
+    return filename;
+
+  free(filename);
+
+  // 6th try: look for default artwork in old default artwork directory
+  filename = getPath2(options.graphics_directory, basename);
+  if (fileExists(filename))
+    return filename;
+
+  return NULL;         // cannot find specified artwork file anywhere
+}
+
+static char *getCorrectedArtworkBasename(char *basename)
+{
+  return basename;
 }
 
 char *getCustomImageFilename(char *basename)
@@ -523,53 +750,68 @@ char *getCustomImageFilename(char *basename)
 
   basename = getCorrectedArtworkBasename(basename);
 
-  if (!setup.override_level_graphics)
+  if (!gfx.override_level_graphics)
   {
-    /* 1st try: look for special artwork in current level series directory */
-    filename = getPath3(getCurrentLevelDir(), GRAPHICS_DIRECTORY, basename);
+    // 1st try: look for special artwork in current level series directory
+    filename = getImg3(getCurrentLevelDir(), GRAPHICS_DIRECTORY, basename);
     if (fileExists(filename))
       return filename;
 
     free(filename);
 
-    /* check if there is special artwork configured in level series config */
+    // check if there is special artwork configured in level series config
     if (getLevelArtworkSet(ARTWORK_TYPE_GRAPHICS) != NULL)
     {
-      /* 2nd try: look for special artwork configured in level series config */
-      filename = getPath2(getLevelArtworkDir(ARTWORK_TYPE_GRAPHICS), basename);
+      // 2nd try: look for special artwork configured in level series config
+      filename = getImg2(getLevelArtworkDir(ARTWORK_TYPE_GRAPHICS), basename);
       if (fileExists(filename))
        return filename;
 
       free(filename);
 
-      /* take missing artwork configured in level set config from default */
+      // take missing artwork configured in level set config from default
       skip_setup_artwork = TRUE;
     }
   }
 
   if (!skip_setup_artwork)
   {
-    /* 3rd try: look for special artwork in configured artwork directory */
-    filename = getPath2(getSetupArtworkDir(artwork.gfx_current), basename);
+    // 3rd try: look for special artwork in configured artwork directory
+    filename = getImg2(getSetupArtworkDir(artwork.gfx_current), basename);
     if (fileExists(filename))
       return filename;
 
     free(filename);
   }
 
-  /* 4th try: look for default artwork in new default artwork directory */
-  filename = getPath2(getDefaultGraphicsDir(GFX_CLASSIC_SUBDIR), basename);
+  // 4th try: look for default artwork in new default artwork directory
+  filename = getImg2(getDefaultGraphicsDir(GFX_DEFAULT_SUBDIR), basename);
   if (fileExists(filename))
     return filename;
 
   free(filename);
 
-  /* 5th try: look for default artwork in old default artwork directory */
-  filename = getPath2(options.graphics_directory, basename);
+  // 5th try: look for default artwork in old default artwork directory
+  filename = getImg2(options.graphics_directory, basename);
   if (fileExists(filename))
     return filename;
 
-  return NULL;         /* cannot find specified artwork file anywhere */
+  if (!strEqual(GFX_FALLBACK_FILENAME, UNDEFINED_FILENAME))
+  {
+    free(filename);
+
+    if (options.debug)
+      Error(ERR_WARN, "cannot find artwork file '%s' (using fallback)",
+           basename);
+
+    // 6th try: look for fallback artwork in old default artwork directory
+    // (needed to prevent errors when trying to access unused artwork files)
+    filename = getImg2(options.graphics_directory, GFX_FALLBACK_FILENAME);
+    if (fileExists(filename))
+      return filename;
+  }
+
+  return NULL;         // cannot find specified artwork file anywhere
 }
 
 char *getCustomSoundFilename(char *basename)
@@ -581,33 +823,33 @@ char *getCustomSoundFilename(char *basename)
 
   basename = getCorrectedArtworkBasename(basename);
 
-  if (!setup.override_level_sounds)
+  if (!gfx.override_level_sounds)
   {
-    /* 1st try: look for special artwork in current level series directory */
+    // 1st try: look for special artwork in current level series directory
     filename = getPath3(getCurrentLevelDir(), SOUNDS_DIRECTORY, basename);
     if (fileExists(filename))
       return filename;
 
     free(filename);
 
-    /* check if there is special artwork configured in level series config */
+    // check if there is special artwork configured in level series config
     if (getLevelArtworkSet(ARTWORK_TYPE_SOUNDS) != NULL)
     {
-      /* 2nd try: look for special artwork configured in level series config */
+      // 2nd try: look for special artwork configured in level series config
       filename = getPath2(getLevelArtworkDir(TREE_TYPE_SOUNDS_DIR), basename);
       if (fileExists(filename))
        return filename;
 
       free(filename);
 
-      /* take missing artwork configured in level set config from default */
+      // take missing artwork configured in level set config from default
       skip_setup_artwork = TRUE;
     }
   }
 
   if (!skip_setup_artwork)
   {
-    /* 3rd try: look for special artwork in configured artwork directory */
+    // 3rd try: look for special artwork in configured artwork directory
     filename = getPath2(getSetupArtworkDir(artwork.snd_current), basename);
     if (fileExists(filename))
       return filename;
@@ -615,19 +857,34 @@ char *getCustomSoundFilename(char *basename)
     free(filename);
   }
 
-  /* 4th try: look for default artwork in new default artwork directory */
-  filename = getPath2(getDefaultSoundsDir(SND_CLASSIC_SUBDIR), basename);
+  // 4th try: look for default artwork in new default artwork directory
+  filename = getPath2(getDefaultSoundsDir(SND_DEFAULT_SUBDIR), basename);
   if (fileExists(filename))
     return filename;
 
   free(filename);
 
-  /* 5th try: look for default artwork in old default artwork directory */
+  // 5th try: look for default artwork in old default artwork directory
   filename = getPath2(options.sounds_directory, basename);
   if (fileExists(filename))
     return filename;
 
-  return NULL;         /* cannot find specified artwork file anywhere */
+  if (!strEqual(SND_FALLBACK_FILENAME, UNDEFINED_FILENAME))
+  {
+    free(filename);
+
+    if (options.debug)
+      Error(ERR_WARN, "cannot find artwork file '%s' (using fallback)",
+           basename);
+
+    // 6th try: look for fallback artwork in old default artwork directory
+    // (needed to prevent errors when trying to access unused artwork files)
+    filename = getPath2(options.sounds_directory, SND_FALLBACK_FILENAME);
+    if (fileExists(filename))
+      return filename;
+  }
+
+  return NULL;         // cannot find specified artwork file anywhere
 }
 
 char *getCustomMusicFilename(char *basename)
@@ -639,33 +896,33 @@ char *getCustomMusicFilename(char *basename)
 
   basename = getCorrectedArtworkBasename(basename);
 
-  if (!setup.override_level_music)
+  if (!gfx.override_level_music)
   {
-    /* 1st try: look for special artwork in current level series directory */
+    // 1st try: look for special artwork in current level series directory
     filename = getPath3(getCurrentLevelDir(), MUSIC_DIRECTORY, basename);
     if (fileExists(filename))
       return filename;
 
     free(filename);
 
-    /* check if there is special artwork configured in level series config */
+    // check if there is special artwork configured in level series config
     if (getLevelArtworkSet(ARTWORK_TYPE_MUSIC) != NULL)
     {
-      /* 2nd try: look for special artwork configured in level series config */
+      // 2nd try: look for special artwork configured in level series config
       filename = getPath2(getLevelArtworkDir(TREE_TYPE_MUSIC_DIR), basename);
       if (fileExists(filename))
        return filename;
 
       free(filename);
 
-      /* take missing artwork configured in level set config from default */
+      // take missing artwork configured in level set config from default
       skip_setup_artwork = TRUE;
     }
   }
 
   if (!skip_setup_artwork)
   {
-    /* 3rd try: look for special artwork in configured artwork directory */
+    // 3rd try: look for special artwork in configured artwork directory
     filename = getPath2(getSetupArtworkDir(artwork.mus_current), basename);
     if (fileExists(filename))
       return filename;
@@ -673,19 +930,34 @@ char *getCustomMusicFilename(char *basename)
     free(filename);
   }
 
-  /* 4th try: look for default artwork in new default artwork directory */
-  filename = getPath2(getDefaultMusicDir(MUS_CLASSIC_SUBDIR), basename);
+  // 4th try: look for default artwork in new default artwork directory
+  filename = getPath2(getDefaultMusicDir(MUS_DEFAULT_SUBDIR), basename);
   if (fileExists(filename))
     return filename;
 
   free(filename);
 
-  /* 5th try: look for default artwork in old default artwork directory */
+  // 5th try: look for default artwork in old default artwork directory
   filename = getPath2(options.music_directory, basename);
   if (fileExists(filename))
     return filename;
 
-  return NULL;         /* cannot find specified artwork file anywhere */
+  if (!strEqual(MUS_FALLBACK_FILENAME, UNDEFINED_FILENAME))
+  {
+    free(filename);
+
+    if (options.debug)
+      Error(ERR_WARN, "cannot find artwork file '%s' (using fallback)",
+           basename);
+
+    // 6th try: look for fallback artwork in old default artwork directory
+    // (needed to prevent errors when trying to access unused artwork files)
+    filename = getPath2(options.music_directory, MUS_FALLBACK_FILENAME);
+    if (fileExists(filename))
+      return filename;
+  }
+
+  return NULL;         // cannot find specified artwork file anywhere
 }
 
 char *getCustomArtworkFilename(char *basename, int type)
@@ -723,53 +995,53 @@ char *getCustomMusicDirectory(void)
 
   checked_free(directory);
 
-  if (!setup.override_level_music)
+  if (!gfx.override_level_music)
   {
-    /* 1st try: look for special artwork in current level series directory */
+    // 1st try: look for special artwork in current level series directory
     directory = getPath2(getCurrentLevelDir(), MUSIC_DIRECTORY);
-    if (fileExists(directory))
+    if (directoryExists(directory))
       return directory;
 
     free(directory);
 
-    /* check if there is special artwork configured in level series config */
+    // check if there is special artwork configured in level series config
     if (getLevelArtworkSet(ARTWORK_TYPE_MUSIC) != NULL)
     {
-      /* 2nd try: look for special artwork configured in level series config */
+      // 2nd try: look for special artwork configured in level series config
       directory = getStringCopy(getLevelArtworkDir(TREE_TYPE_MUSIC_DIR));
-      if (fileExists(directory))
+      if (directoryExists(directory))
        return directory;
 
       free(directory);
 
-      /* take missing artwork configured in level set config from default */
+      // take missing artwork configured in level set config from default
       skip_setup_artwork = TRUE;
     }
   }
 
   if (!skip_setup_artwork)
   {
-    /* 3rd try: look for special artwork in configured artwork directory */
+    // 3rd try: look for special artwork in configured artwork directory
     directory = getStringCopy(getSetupArtworkDir(artwork.mus_current));
-    if (fileExists(directory))
+    if (directoryExists(directory))
       return directory;
 
     free(directory);
   }
 
-  /* 4th try: look for default artwork in new default artwork directory */
-  directory = getStringCopy(getDefaultMusicDir(MUS_CLASSIC_SUBDIR));
-  if (fileExists(directory))
+  // 4th try: look for default artwork in new default artwork directory
+  directory = getStringCopy(getDefaultMusicDir(MUS_DEFAULT_SUBDIR));
+  if (directoryExists(directory))
     return directory;
 
   free(directory);
 
-  /* 5th try: look for default artwork in old default artwork directory */
+  // 5th try: look for default artwork in old default artwork directory
   directory = getStringCopy(options.music_directory);
-  if (fileExists(directory))
+  if (directoryExists(directory))
     return directory;
 
-  return NULL;         /* cannot find specified artwork file anywhere */
+  return NULL;         // cannot find specified artwork file anywhere
 }
 
 void InitTapeDirectory(char *level_subdir)
@@ -781,38 +1053,61 @@ void InitTapeDirectory(char *level_subdir)
 
 void InitScoreDirectory(char *level_subdir)
 {
-  createDirectory(getCommonDataDir(), "common data", PERMS_PUBLIC);
-  createDirectory(getScoreDir(NULL), "main score", PERMS_PUBLIC);
-  createDirectory(getScoreDir(level_subdir), "level score", PERMS_PUBLIC);
+  int permissions = (program.global_scores ? PERMS_PUBLIC : PERMS_PRIVATE);
+
+  if (program.global_scores)
+    createDirectory(getCommonDataDir(), "common data", permissions);
+  else
+    createDirectory(getUserGameDataDir(), "user data", permissions);
+
+  createDirectory(getScoreDir(NULL), "main score", permissions);
+  createDirectory(getScoreDir(level_subdir), "level score", permissions);
 }
 
-static void SaveUserLevelInfo();
+static void SaveUserLevelInfo(void);
 
 void InitUserLevelDirectory(char *level_subdir)
 {
-  if (!fileExists(getUserLevelDir(level_subdir)))
+  if (!directoryExists(getUserLevelDir(level_subdir)))
   {
     createDirectory(getUserGameDataDir(), "user data", PERMS_PRIVATE);
     createDirectory(getUserLevelDir(NULL), "main user level", PERMS_PRIVATE);
-    createDirectory(getUserLevelDir(level_subdir), "user level",PERMS_PRIVATE);
+    createDirectory(getUserLevelDir(level_subdir), "user level", PERMS_PRIVATE);
 
     SaveUserLevelInfo();
   }
 }
 
+void InitNetworkLevelDirectory(char *level_subdir)
+{
+  if (!directoryExists(getNetworkLevelDir(level_subdir)))
+  {
+    createDirectory(getUserGameDataDir(), "user data", PERMS_PRIVATE);
+    createDirectory(getNetworkDir(), "network data", PERMS_PRIVATE);
+    createDirectory(getNetworkLevelDir(NULL), "main network level", PERMS_PRIVATE);
+    createDirectory(getNetworkLevelDir(level_subdir), "network level", PERMS_PRIVATE);
+  }
+}
+
 void InitLevelSetupDirectory(char *level_subdir)
 {
   createDirectory(getUserGameDataDir(), "user data", PERMS_PRIVATE);
   createDirectory(getLevelSetupDir(NULL), "main level setup", PERMS_PRIVATE);
-  createDirectory(getLevelSetupDir(level_subdir), "level setup",PERMS_PRIVATE);
+  createDirectory(getLevelSetupDir(level_subdir), "level setup", PERMS_PRIVATE);
+}
+
+static void InitCacheDirectory(void)
+{
+  createDirectory(getUserGameDataDir(), "user data", PERMS_PRIVATE);
+  createDirectory(getCacheDir(), "cache data", PERMS_PRIVATE);
 }
 
 
-/* ------------------------------------------------------------------------- */
-/* some functions to handle lists of level and artwork directories           */
-/* ------------------------------------------------------------------------- */
+// ----------------------------------------------------------------------------
+// some functions to handle lists of level and artwork directories
+// ----------------------------------------------------------------------------
 
-TreeInfo *newTreeInfo()
+TreeInfo *newTreeInfo(void)
 {
   return checked_calloc(sizeof(TreeInfo));
 }
@@ -855,16 +1150,16 @@ TreeInfo *getFirstValidTreeInfoEntry(TreeInfo *node)
   if (node == NULL)
     return NULL;
 
-  if (node->node_group)                /* enter level group (step down into tree) */
+  if (node->node_group)                // enter level group (step down into tree)
     return getFirstValidTreeInfoEntry(node->node_group);
-  else if (node->parent_link)  /* skip start entry of level group */
+  else if (node->parent_link)  // skip start entry of level group
   {
-    if (node->next)            /* get first real level series entry */
+    if (node->next)            // get first real level series entry
       return getFirstValidTreeInfoEntry(node->next);
-    else                       /* leave empty level group and go on */
+    else                       // leave empty level group and go on
       return getFirstValidTreeInfoEntry(node->node_parent->next);
   }
-  else                         /* this seems to be a regular level series */
+  else                         // this seems to be a regular level series
     return node;
 }
 
@@ -873,9 +1168,9 @@ TreeInfo *getTreeInfoFirstGroupEntry(TreeInfo *node)
   if (node == NULL)
     return NULL;
 
-  if (node->node_parent == NULL)               /* top level group */
+  if (node->node_parent == NULL)               // top level group
     return *node->node_top;
-  else                                         /* sub level group */
+  else                                         // sub level group
     return node->node_parent->node_group;
 }
 
@@ -946,8 +1241,8 @@ TreeInfo *getTreeInfoFromIdentifier(TreeInfo *node, char *identifier)
   return NULL;
 }
 
-TreeInfo *cloneTreeNode(TreeInfo **node_top, TreeInfo *node_parent,
-                       TreeInfo *node, boolean skip_sets_without_levels)
+static TreeInfo *cloneTreeNode(TreeInfo **node_top, TreeInfo *node_parent,
+                              TreeInfo *node, boolean skip_sets_without_levels)
 {
   TreeInfo *node_new;
 
@@ -959,12 +1254,10 @@ TreeInfo *cloneTreeNode(TreeInfo **node_top, TreeInfo *node_parent,
     return cloneTreeNode(node_top, node_parent, node->next,
                         skip_sets_without_levels);
 
-  node_new = newTreeInfo();
+  node_new = getTreeInfoCopy(node);            // copy complete node
 
-  *node_new = *node;                           /* copy complete node */
-
-  node_new->node_top = node_top;               /* correct top node link */
-  node_new->node_parent = node_parent;         /* correct parent node link */
+  node_new->node_top = node_top;               // correct top node link
+  node_new->node_parent = node_parent;         // correct parent node link
 
   if (node->level_group)
     node_new->node_group = cloneTreeNode(node_top, node_new, node->node_group,
@@ -976,7 +1269,7 @@ TreeInfo *cloneTreeNode(TreeInfo **node_top, TreeInfo *node_parent,
   return node_new;
 }
 
-void cloneTree(TreeInfo **ti_new, TreeInfo *ti, boolean skip_empty_sets)
+static void cloneTree(TreeInfo **ti_new, TreeInfo *ti, boolean skip_empty_sets)
 {
   TreeInfo *ti_cloned = cloneTreeNode(ti_new, NULL, ti, skip_empty_sets);
 
@@ -1022,8 +1315,13 @@ void dumpTreeInfo(TreeInfo *node, int depth)
     for (i = 0; i < (depth + 1) * 3; i++)
       printf(" ");
 
+    printf("'%s' / '%s'\n", node->identifier, node->name);
+
+    /*
+    // use for dumping artwork info tree
     printf("subdir == '%s' ['%s', '%s'] [%d])\n",
           node->subdir, node->fullpath, node->basepath, node->in_user_dir);
+    */
 
     if (node->node_group != NULL)
       dumpTreeInfo(node->node_group, depth + 1);
@@ -1044,11 +1342,11 @@ void sortTreeInfoBySortFunction(TreeInfo **node_first,
   if (num_nodes == 0)
     return;
 
-  /* allocate array for sorting structure pointers */
+  // allocate array for sorting structure pointers
   sort_array = checked_calloc(num_nodes * sizeof(TreeInfo *));
 
-  /* writing structure pointers to sorting array */
-  while (i < num_nodes && node)                /* double boundary check... */
+  // writing structure pointers to sorting array
+  while (i < num_nodes && node)                // double boundary check...
   {
     sort_array[i] = node;
 
@@ -1056,21 +1354,21 @@ void sortTreeInfoBySortFunction(TreeInfo **node_first,
     node = node->next;
   }
 
-  /* sorting the structure pointers in the sorting array */
+  // sorting the structure pointers in the sorting array
   qsort(sort_array, num_nodes, sizeof(TreeInfo *),
        compare_function);
 
-  /* update the linkage of list elements with the sorted node array */
+  // update the linkage of list elements with the sorted node array
   for (i = 0; i < num_nodes - 1; i++)
     sort_array[i]->next = sort_array[i + 1];
   sort_array[num_nodes - 1]->next = NULL;
 
-  /* update the linkage of the main list anchor pointer */
+  // update the linkage of the main list anchor pointer
   *node_first = sort_array[0];
 
   free(sort_array);
 
-  /* now recursively sort the level group structures */
+  // now recursively sort the level group structures
   node = *node_first;
   while (node)
   {
@@ -1087,9 +1385,9 @@ void sortTreeInfo(TreeInfo **node_first)
 }
 
 
-/* ========================================================================= */
-/* some stuff from "files.c"                                                 */
-/* ========================================================================= */
+// ============================================================================
+// some stuff from "files.c"
+// ============================================================================
 
 #if defined(PLATFORM_WIN32)
 #ifndef S_IRGRP
@@ -1116,24 +1414,27 @@ void sortTreeInfo(TreeInfo **node_first)
 #ifndef S_ISGID
 #define S_ISGID 0
 #endif
-#endif /* PLATFORM_WIN32 */
+#endif // PLATFORM_WIN32
 
-/* file permissions for newly written files */
+// file permissions for newly written files
 #define MODE_R_ALL             (S_IRUSR | S_IRGRP | S_IROTH)
 #define MODE_W_ALL             (S_IWUSR | S_IWGRP | S_IWOTH)
 #define MODE_X_ALL             (S_IXUSR | S_IXGRP | S_IXOTH)
 
 #define MODE_W_PRIVATE         (S_IWUSR)
-#define MODE_W_PUBLIC          (S_IWUSR | S_IWGRP)
+#define MODE_W_PUBLIC_FILE     (S_IWUSR | S_IWGRP)
 #define MODE_W_PUBLIC_DIR      (S_IWUSR | S_IWGRP | S_ISGID)
 
 #define DIR_PERMS_PRIVATE      (MODE_R_ALL | MODE_X_ALL | MODE_W_PRIVATE)
 #define DIR_PERMS_PUBLIC       (MODE_R_ALL | MODE_X_ALL | MODE_W_PUBLIC_DIR)
+#define DIR_PERMS_PUBLIC_ALL   (MODE_R_ALL | MODE_X_ALL | MODE_W_ALL)
 
 #define FILE_PERMS_PRIVATE     (MODE_R_ALL | MODE_W_PRIVATE)
-#define FILE_PERMS_PUBLIC      (MODE_R_ALL | MODE_W_PUBLIC)
+#define FILE_PERMS_PUBLIC      (MODE_R_ALL | MODE_W_PUBLIC_FILE)
+#define FILE_PERMS_PUBLIC_ALL  (MODE_R_ALL | MODE_W_ALL)
 
-char *getHomeDir()
+
+char *getHomeDir(void)
 {
   static char *dir = NULL;
 
@@ -1175,7 +1476,7 @@ char *getCommonDataDir(void)
     char *dir = checked_malloc(MAX_PATH + 1);
 
     if (SUCCEEDED(SHGetFolderPath(NULL, CSIDL_COMMON_DOCUMENTS, NULL, 0, dir))
-       && !strEqual(dir, ""))          /* empty for Windows 95/98 */
+       && !strEqual(dir, ""))          // empty for Windows 95/98
       common_data_dir = getPath2(dir, program.userdata_subdir);
     else
       common_data_dir = options.rw_base_directory;
@@ -1205,39 +1506,24 @@ char *getPersonalDataDir(void)
 
 char *getUserGameDataDir(void)
 {
-  if (program.userdata_path == NULL)
-    program.userdata_path = getPath2(getPersonalDataDir(),
-                                    program.userdata_subdir);
-
-  return program.userdata_path;
-}
-
-void updateUserGameDataDir()
-{
-#if defined(PLATFORM_MACOSX)
-  char *userdata_dir_old = getPath2(getHomeDir(), program.userdata_subdir_unix);
-  char *userdata_dir_new = getUserGameDataDir();
-
-  /* convert old Unix style game data directory to Mac OS X style, if needed */
-  if (fileExists(userdata_dir_old) && !fileExists(userdata_dir_new))
-  {
-    if (rename(userdata_dir_old, userdata_dir_new) != 0)
-    {
-      Error(ERR_WARN, "cannot move game data directory '%s' to '%s'",
-           userdata_dir_old, userdata_dir_new);
-
-      /* continue using Unix style data directory -- this should not happen */
-      program.userdata_path = getPath2(getPersonalDataDir(),
-                                      program.userdata_subdir_unix);
-    }
-  }
-
-  free(userdata_dir_old);
-  free(userdata_dir_new);
+  static char *user_game_data_dir = NULL;
+
+#if defined(PLATFORM_ANDROID)
+  if (user_game_data_dir == NULL)
+    user_game_data_dir = (char *)(SDL_AndroidGetExternalStorageState() &
+                                 SDL_ANDROID_EXTERNAL_STORAGE_WRITE ?
+                                 SDL_AndroidGetExternalStoragePath() :
+                                 SDL_AndroidGetInternalStoragePath());
+#else
+  if (user_game_data_dir == NULL)
+    user_game_data_dir = getPath2(getPersonalDataDir(),
+                                 program.userdata_subdir);
 #endif
+
+  return user_game_data_dir;
 }
 
-char *getSetupDir()
+char *getSetupDir(void)
 {
   return getUserGameDataDir();
 }
@@ -1260,32 +1546,64 @@ static int posix_mkdir(const char *pathname, mode_t mode)
 #endif
 }
 
+static boolean posix_process_running_setgid(void)
+{
+#if defined(PLATFORM_UNIX)
+  return (getgid() != getegid());
+#else
+  return FALSE;
+#endif
+}
+
 void createDirectory(char *dir, char *text, int permission_class)
 {
-  /* leave "other" permissions in umask untouched, but ensure group parts
-     of USERDATA_DIR_MODE are not masked */
+  if (directoryExists(dir))
+    return;
+
+  // leave "other" permissions in umask untouched, but ensure group parts
+  // of USERDATA_DIR_MODE are not masked
   mode_t dir_mode = (permission_class == PERMS_PRIVATE ?
                     DIR_PERMS_PRIVATE : DIR_PERMS_PUBLIC);
-  mode_t normal_umask = posix_umask(0);
+  mode_t last_umask = posix_umask(0);
   mode_t group_umask = ~(dir_mode & S_IRWXG);
-  posix_umask(normal_umask & group_umask);
+  int running_setgid = posix_process_running_setgid();
+
+  if (permission_class == PERMS_PUBLIC)
+  {
+    // if we're setgid, protect files against "other"
+    // else keep umask(0) to make the dir world-writable
+
+    if (running_setgid)
+      posix_umask(last_umask & group_umask);
+    else
+      dir_mode = DIR_PERMS_PUBLIC_ALL;
+  }
 
-  if (!fileExists(dir))
-    if (posix_mkdir(dir, dir_mode) != 0)
-      Error(ERR_WARN, "cannot create %s directory '%s'", text, dir);
+  if (posix_mkdir(dir, dir_mode) != 0)
+    Error(ERR_WARN, "cannot create %s directory '%s': %s",
+         text, dir, strerror(errno));
 
-  posix_umask(normal_umask);           /* reset normal umask */
+  if (permission_class == PERMS_PUBLIC && !running_setgid)
+    chmod(dir, dir_mode);
+
+  posix_umask(last_umask);             // restore previous umask
 }
 
-void InitUserDataDirectory()
+void InitUserDataDirectory(void)
 {
   createDirectory(getUserGameDataDir(), "user data", PERMS_PRIVATE);
 }
 
 void SetFilePermissions(char *filename, int permission_class)
 {
-  chmod(filename, (permission_class == PERMS_PRIVATE ?
-                  FILE_PERMS_PRIVATE : FILE_PERMS_PUBLIC));
+  int running_setgid = posix_process_running_setgid();
+  int perms = (permission_class == PERMS_PRIVATE ?
+              FILE_PERMS_PRIVATE : FILE_PERMS_PUBLIC);
+
+  if (permission_class == PERMS_PUBLIC && !running_setgid)
+    perms = FILE_PERMS_PUBLIC_ALL;
+
+  chmod(filename, perms);
 }
 
 char *getCookie(char *file_type)
@@ -1294,15 +1612,26 @@ char *getCookie(char *file_type)
 
   if (strlen(program.cookie_prefix) + 1 +
       strlen(file_type) + strlen("_FILE_VERSION_x.x") > MAX_COOKIE_LEN)
-    return "[COOKIE ERROR]";   /* should never happen */
+    return "[COOKIE ERROR]";   // should never happen
 
   sprintf(cookie, "%s_%s_FILE_VERSION_%d.%d",
          program.cookie_prefix, file_type,
-         program.version_major, program.version_minor);
+         program.version_super, program.version_major);
 
   return cookie;
 }
 
+void fprintFileHeader(FILE *file, char *basename)
+{
+  char *prefix = "# ";
+  char *sep1 = "=";
+
+  fprintf_line_with_prefix(file, prefix, sep1, 77);
+  fprintf(file, "%s%s\n", prefix, basename);
+  fprintf_line_with_prefix(file, prefix, sep1, 77);
+  fprintf(file, "\n");
+}
+
 int getFileVersionFromCookieString(const char *cookie)
 {
   const char *ptr_cookie1, *ptr_cookie2;
@@ -1312,7 +1641,7 @@ int getFileVersionFromCookieString(const char *cookie)
   const int len_pattern1 = strlen(pattern1);
   const int len_pattern2 = strlen(pattern2);
   const int len_pattern = len_pattern1 + len_pattern2;
-  int version_major, version_minor;
+  int version_super, version_major;
 
   if (len_cookie <= len_pattern)
     return -1;
@@ -1328,10 +1657,10 @@ int getFileVersionFromCookieString(const char *cookie)
       ptr_cookie2[2] < '0' || ptr_cookie2[2] > '9')
     return -1;
 
-  version_major = ptr_cookie2[0] - '0';
-  version_minor = ptr_cookie2[2] - '0';
+  version_super = ptr_cookie2[0] - '0';
+  version_major = ptr_cookie2[2] - '0';
 
-  return VERSION_IDENT(version_major, version_minor, 0, 0);
+  return VERSION_IDENT(version_super, version_major, 0, 0);
 }
 
 boolean checkCookieString(const char *cookie, const char *template)
@@ -1350,25 +1679,26 @@ boolean checkCookieString(const char *cookie, const char *template)
   return TRUE;
 }
 
-/* ------------------------------------------------------------------------- */
-/* setup file list and hash handling functions                               */
-/* ------------------------------------------------------------------------- */
+
+// ----------------------------------------------------------------------------
+// setup file list and hash handling functions
+// ----------------------------------------------------------------------------
 
 char *getFormattedSetupEntry(char *token, char *value)
 {
   int i;
   static char entry[MAX_LINE_LEN];
 
-  /* if value is an empty string, just return token without value */
+  // if value is an empty string, just return token without value
   if (*value == '\0')
     return token;
 
-  /* start with the token and some spaces to format output line */
+  // start with the token and some spaces to format output line
   sprintf(entry, "%s:", token);
   for (i = strlen(entry); i < token_value_position; i++)
     strcat(entry, " ");
 
-  /* continue with the token's value */
+  // continue with the token's value
   strcat(entry, value);
 
   return entry;
@@ -1441,6 +1771,7 @@ SetupFileList *addListEntry(SetupFileList *list, char *token, char *value)
     return addListEntry(list->next, token, value);
 }
 
+#if ENABLE_UNUSED_CODE
 #ifdef DEBUG
 static void printSetupFileList(SetupFileList *list)
 {
@@ -1453,6 +1784,7 @@ static void printSetupFileList(SetupFileList *list)
   printSetupFileList(list->next);
 }
 #endif
+#endif
 
 #ifdef DEBUG
 DEFINE_HASHTABLE_INSERT(insert_hash_entry, char, char);
@@ -1466,7 +1798,7 @@ DEFINE_HASHTABLE_REMOVE(remove_hash_entry, char, char);
 #define remove_hash_entry hashtable_remove
 #endif
 
-static unsigned int get_hash_from_key(void *key)
+unsigned int get_hash_from_key(void *key)
 {
   /*
     djb2
@@ -1491,7 +1823,7 @@ static unsigned int get_hash_from_key(void *key)
   int c;
 
   while ((c = *str++))
-    hash = ((hash << 5) + hash) + c;   /* hash * 33 + c */
+    hash = ((hash << 5) + hash) + c;   // hash * 33 + c
 
   return hash;
 }
@@ -1501,7 +1833,7 @@ static int keys_are_equal(void *key1, void *key2)
   return (strEqual((char *)key1, (char *)key2));
 }
 
-SetupFileHash *newSetupFileHash()
+SetupFileHash *newSetupFileHash(void)
 {
   SetupFileHash *new_hash =
     create_hashtable(16, 0.75, get_hash_from_key, keys_are_equal);
@@ -1517,7 +1849,7 @@ void freeSetupFileHash(SetupFileHash *hash)
   if (hash == NULL)
     return;
 
-  hashtable_destroy(hash, 1);  /* 1 == also free values stored in hash */
+  hashtable_destroy(hash, 1);  // 1 == also free values stored in hash
 }
 
 char *getHashEntry(SetupFileHash *hash, char *token)
@@ -1537,7 +1869,7 @@ void setHashEntry(SetupFileHash *hash, char *token, char *value)
 
   value_copy = getStringCopy(value);
 
-  /* change value; if it does not exist, insert it as new */
+  // change value; if it does not exist, insert it as new
   if (!change_hash_entry(hash, token, value_copy))
     if (!insert_hash_entry(hash, getStringCopy(token), value_copy))
       Error(ERR_EXIT, "cannot insert into hash -- aborting");
@@ -1551,7 +1883,8 @@ char *removeHashEntry(SetupFileHash *hash, char *token)
   return remove_hash_entry(hash, token);
 }
 
-#if 0
+#if ENABLE_UNUSED_CODE
+#if DEBUG
 static void printSetupFileHash(SetupFileHash *hash)
 {
   BEGIN_HASH_ITERATION(hash, itr)
@@ -1562,213 +1895,431 @@ static void printSetupFileHash(SetupFileHash *hash)
   END_HASH_ITERATION(hash, itr)
 }
 #endif
+#endif
 
-static void *loadSetupFileData(char *filename, boolean use_hash)
+#define ALLOW_TOKEN_VALUE_SEPARATOR_BEING_WHITESPACE           1
+#define CHECK_TOKEN_VALUE_SEPARATOR__WARN_IF_MISSING           0
+#define CHECK_TOKEN__WARN_IF_ALREADY_EXISTS_IN_HASH            0
+
+static boolean token_value_separator_found = FALSE;
+#if CHECK_TOKEN_VALUE_SEPARATOR__WARN_IF_MISSING
+static boolean token_value_separator_warning = FALSE;
+#endif
+#if CHECK_TOKEN__WARN_IF_ALREADY_EXISTS_IN_HASH
+static boolean token_already_exists_warning = FALSE;
+#endif
+
+static boolean getTokenValueFromSetupLineExt(char *line,
+                                            char **token_ptr, char **value_ptr,
+                                            char *filename, char *line_raw,
+                                            int line_nr,
+                                            boolean separator_required)
 {
-  char line[MAX_LINE_LEN], previous_line[MAX_LINE_LEN];
+  static char line_copy[MAX_LINE_LEN + 1], line_raw_copy[MAX_LINE_LEN + 1];
   char *token, *value, *line_ptr;
-  void *setup_file_data, *insert_ptr = NULL;
-  boolean read_continued_line = FALSE;
-  FILE *file;
 
-  if (!(file = fopen(filename, MODE_READ)))
+  // when externally invoked via ReadTokenValueFromLine(), copy line buffers
+  if (line_raw == NULL)
   {
-    Error(ERR_WARN, "cannot open configuration file '%s'", filename);
+    strncpy(line_copy, line, MAX_LINE_LEN);
+    line_copy[MAX_LINE_LEN] = '\0';
+    line = line_copy;
 
-    return NULL;
+    strcpy(line_raw_copy, line_copy);
+    line_raw = line_raw_copy;
   }
 
-  if (use_hash)
-    setup_file_data = newSetupFileHash();
-  else
-    insert_ptr = setup_file_data = newSetupFileList("", "");
-
-  while (!feof(file))
+  // cut trailing comment from input line
+  for (line_ptr = line; *line_ptr; line_ptr++)
   {
-    /* read next line of input file */
-    if (!fgets(line, MAX_LINE_LEN, file))
+    if (*line_ptr == '#')
+    {
+      *line_ptr = '\0';
       break;
+    }
+  }
 
-    /* cut trailing newline or carriage return */
-    for (line_ptr = &line[strlen(line)]; line_ptr >= line; line_ptr--)
-      if ((*line_ptr == '\n' || *line_ptr == '\r') && *(line_ptr + 1) == '\0')
-       *line_ptr = '\0';
+  // cut trailing whitespaces from input line
+  for (line_ptr = &line[strlen(line)]; line_ptr >= line; line_ptr--)
+    if ((*line_ptr == ' ' || *line_ptr == '\t') && *(line_ptr + 1) == '\0')
+      *line_ptr = '\0';
 
-    if (read_continued_line)
-    {
-      /* cut leading whitespaces from input line */
-      for (line_ptr = line; *line_ptr; line_ptr++)
-       if (*line_ptr != ' ' && *line_ptr != '\t')
-         break;
+  // ignore empty lines
+  if (*line == '\0')
+    return FALSE;
 
-      /* append new line to existing line, if there is enough space */
-      if (strlen(previous_line) + strlen(line_ptr) < MAX_LINE_LEN)
-       strcat(previous_line, line_ptr);
+  // cut leading whitespaces from token
+  for (token = line; *token; token++)
+    if (*token != ' ' && *token != '\t')
+      break;
 
-      strcpy(line, previous_line);     /* copy storage buffer to line */
+  // start with empty value as reliable default
+  value = "";
 
-      read_continued_line = FALSE;
-    }
+  token_value_separator_found = FALSE;
 
-    /* if the last character is '\', continue at next line */
-    if (strlen(line) > 0 && line[strlen(line) - 1] == '\\')
+  // find end of token to determine start of value
+  for (line_ptr = token; *line_ptr; line_ptr++)
+  {
+    // first look for an explicit token/value separator, like ':' or '='
+    if (*line_ptr == ':' || *line_ptr == '=')
     {
-      line[strlen(line) - 1] = '\0';   /* cut off trailing backslash */
-      strcpy(previous_line, line);     /* copy line to storage buffer */
+      *line_ptr = '\0';                        // terminate token string
+      value = line_ptr + 1;            // set beginning of value
 
-      read_continued_line = TRUE;
+      token_value_separator_found = TRUE;
 
-      continue;
+      break;
     }
+  }
 
-    /* cut trailing comment from input line */
-    for (line_ptr = line; *line_ptr; line_ptr++)
+#if ALLOW_TOKEN_VALUE_SEPARATOR_BEING_WHITESPACE
+  // fallback: if no token/value separator found, also allow whitespaces
+  if (!token_value_separator_found && !separator_required)
+  {
+    for (line_ptr = token; *line_ptr; line_ptr++)
     {
-      if (*line_ptr == '#')
+      if (*line_ptr == ' ' || *line_ptr == '\t')
       {
-       *line_ptr = '\0';
+       *line_ptr = '\0';               // terminate token string
+       value = line_ptr + 1;           // set beginning of value
+
+       token_value_separator_found = TRUE;
+
        break;
       }
     }
 
-    /* cut trailing whitespaces from input line */
+#if CHECK_TOKEN_VALUE_SEPARATOR__WARN_IF_MISSING
+    if (token_value_separator_found)
+    {
+      if (!token_value_separator_warning)
+      {
+       Error(ERR_INFO_LINE, "-");
+
+       if (filename != NULL)
+       {
+         Error(ERR_WARN, "missing token/value separator(s) in config file:");
+         Error(ERR_INFO, "- config file: '%s'", filename);
+       }
+       else
+       {
+         Error(ERR_WARN, "missing token/value separator(s):");
+       }
+
+       token_value_separator_warning = TRUE;
+      }
+
+      if (filename != NULL)
+       Error(ERR_INFO, "- line %d: '%s'", line_nr, line_raw);
+      else
+       Error(ERR_INFO, "- line: '%s'", line_raw);
+    }
+#endif
+  }
+#endif
+
+  // cut trailing whitespaces from token
+  for (line_ptr = &token[strlen(token)]; line_ptr >= token; line_ptr--)
+    if ((*line_ptr == ' ' || *line_ptr == '\t') && *(line_ptr + 1) == '\0')
+      *line_ptr = '\0';
+
+  // cut leading whitespaces from value
+  for (; *value; value++)
+    if (*value != ' ' && *value != '\t')
+      break;
+
+  *token_ptr = token;
+  *value_ptr = value;
+
+  return TRUE;
+}
+
+boolean getTokenValueFromSetupLine(char *line, char **token, char **value)
+{
+  // while the internal (old) interface does not require a token/value
+  // separator (for downwards compatibility with existing files which
+  // don't use them), it is mandatory for the external (new) interface
+
+  return getTokenValueFromSetupLineExt(line, token, value, NULL, NULL, 0, TRUE);
+}
+
+static boolean loadSetupFileData(void *setup_file_data, char *filename,
+                                boolean top_recursion_level, boolean is_hash)
+{
+  static SetupFileHash *include_filename_hash = NULL;
+  char line[MAX_LINE_LEN], line_raw[MAX_LINE_LEN], previous_line[MAX_LINE_LEN];
+  char *token, *value, *line_ptr;
+  void *insert_ptr = NULL;
+  boolean read_continued_line = FALSE;
+  File *file;
+  int line_nr = 0, token_count = 0, include_count = 0;
+
+#if CHECK_TOKEN_VALUE_SEPARATOR__WARN_IF_MISSING
+  token_value_separator_warning = FALSE;
+#endif
+
+#if CHECK_TOKEN__WARN_IF_ALREADY_EXISTS_IN_HASH
+  token_already_exists_warning = FALSE;
+#endif
+
+  if (!(file = openFile(filename, MODE_READ)))
+  {
+#if DEBUG_NO_CONFIG_FILE
+    Error(ERR_DEBUG, "cannot open configuration file '%s'", filename);
+#endif
+
+    return FALSE;
+  }
+
+  // use "insert pointer" to store list end for constant insertion complexity
+  if (!is_hash)
+    insert_ptr = setup_file_data;
+
+  // on top invocation, create hash to mark included files (to prevent loops)
+  if (top_recursion_level)
+    include_filename_hash = newSetupFileHash();
+
+  // mark this file as already included (to prevent including it again)
+  setHashEntry(include_filename_hash, getBaseNamePtr(filename), "true");
+
+  while (!checkEndOfFile(file))
+  {
+    // read next line of input file
+    if (!getStringFromFile(file, line, MAX_LINE_LEN))
+      break;
+
+    // check if line was completely read and is terminated by line break
+    if (strlen(line) > 0 && line[strlen(line) - 1] == '\n')
+      line_nr++;
+
+    // cut trailing line break (this can be newline and/or carriage return)
     for (line_ptr = &line[strlen(line)]; line_ptr >= line; line_ptr--)
-      if ((*line_ptr == ' ' || *line_ptr == '\t') && *(line_ptr + 1) == '\0')
+      if ((*line_ptr == '\n' || *line_ptr == '\r') && *(line_ptr + 1) == '\0')
        *line_ptr = '\0';
 
-    /* ignore empty lines */
-    if (*line == '\0')
-      continue;
+    // copy raw input line for later use (mainly debugging output)
+    strcpy(line_raw, line);
 
-    /* cut leading whitespaces from token */
-    for (token = line; *token; token++)
-      if (*token != ' ' && *token != '\t')
-       break;
+    if (read_continued_line)
+    {
+      // append new line to existing line, if there is enough space
+      if (strlen(previous_line) + strlen(line_ptr) < MAX_LINE_LEN)
+       strcat(previous_line, line_ptr);
 
-    /* start with empty value as reliable default */
-    value = "";
+      strcpy(line, previous_line);     // copy storage buffer to line
 
-    /* find end of token to determine start of value */
-    for (line_ptr = token; *line_ptr; line_ptr++)
+      read_continued_line = FALSE;
+    }
+
+    // if the last character is '\', continue at next line
+    if (strlen(line) > 0 && line[strlen(line) - 1] == '\\')
     {
-      if (*line_ptr == ' ' || *line_ptr == '\t' || *line_ptr == ':')
-      {
-       *line_ptr = '\0';               /* terminate token string */
-       value = line_ptr + 1;           /* set beginning of value */
+      line[strlen(line) - 1] = '\0';   // cut off trailing backslash
+      strcpy(previous_line, line);     // copy line to storage buffer
 
-       break;
-      }
-    }
+      read_continued_line = TRUE;
 
-    /* cut leading whitespaces from value */
-    for (; *value; value++)
-      if (*value != ' ' && *value != '\t')
-       break;
+      continue;
+    }
 
-#if 0
-    if (*value == '\0')
-      value = "true";  /* treat tokens without value as "true" */
-#endif
+    if (!getTokenValueFromSetupLineExt(line, &token, &value, filename,
+                                      line_raw, line_nr, FALSE))
+      continue;
 
     if (*token)
     {
-      if (use_hash)
-       setHashEntry((SetupFileHash *)setup_file_data, token, value);
+      if (strEqual(token, "include"))
+      {
+       if (getHashEntry(include_filename_hash, value) == NULL)
+       {
+         char *basepath = getBasePath(filename);
+         char *basename = getBaseName(value);
+         char *filename_include = getPath2(basepath, basename);
+
+         loadSetupFileData(setup_file_data, filename_include, FALSE, is_hash);
+
+         free(basepath);
+         free(basename);
+         free(filename_include);
+
+         include_count++;
+       }
+       else
+       {
+         Error(ERR_WARN, "ignoring already processed file '%s'", value);
+       }
+      }
       else
-       insert_ptr = addListEntry((SetupFileList *)insert_ptr, token, value);
+      {
+       if (is_hash)
+       {
+#if CHECK_TOKEN__WARN_IF_ALREADY_EXISTS_IN_HASH
+         char *old_value =
+           getHashEntry((SetupFileHash *)setup_file_data, token);
+
+         if (old_value != NULL)
+         {
+           if (!token_already_exists_warning)
+           {
+             Error(ERR_INFO_LINE, "-");
+             Error(ERR_WARN, "duplicate token(s) found in config file:");
+             Error(ERR_INFO, "- config file: '%s'", filename);
+
+             token_already_exists_warning = TRUE;
+           }
+
+           Error(ERR_INFO, "- token: '%s' (in line %d)", token, line_nr);
+           Error(ERR_INFO, "  old value: '%s'", old_value);
+           Error(ERR_INFO, "  new value: '%s'", value);
+         }
+#endif
+
+         setHashEntry((SetupFileHash *)setup_file_data, token, value);
+       }
+       else
+       {
+         insert_ptr = addListEntry((SetupFileList *)insert_ptr, token, value);
+       }
+
+       token_count++;
+      }
     }
   }
 
-  fclose(file);
+  closeFile(file);
 
-  if (use_hash)
-  {
-    if (hashtable_count((SetupFileHash *)setup_file_data) == 0)
-      Error(ERR_WARN, "configuration file '%s' is empty", filename);
-  }
-  else
+#if CHECK_TOKEN_VALUE_SEPARATOR__WARN_IF_MISSING
+  if (token_value_separator_warning)
+    Error(ERR_INFO_LINE, "-");
+#endif
+
+#if CHECK_TOKEN__WARN_IF_ALREADY_EXISTS_IN_HASH
+  if (token_already_exists_warning)
+    Error(ERR_INFO_LINE, "-");
+#endif
+
+  if (token_count == 0 && include_count == 0)
+    Error(ERR_WARN, "configuration file '%s' is empty", filename);
+
+  if (top_recursion_level)
+    freeSetupFileHash(include_filename_hash);
+
+  return TRUE;
+}
+
+static void saveSetupFileHash(SetupFileHash *hash, char *filename)
+{
+  FILE *file;
+
+  if (!(file = fopen(filename, MODE_WRITE)))
   {
-    SetupFileList *setup_file_list = (SetupFileList *)setup_file_data;
-    SetupFileList *first_valid_list_entry = setup_file_list->next;
+    Error(ERR_WARN, "cannot write configuration file '%s'", filename);
 
-    /* free empty list header */
-    setup_file_list->next = NULL;
-    freeSetupFileList(setup_file_list);
-    setup_file_data = first_valid_list_entry;
+    return;
+  }
 
-    if (first_valid_list_entry == NULL)
-      Error(ERR_WARN, "configuration file '%s' is empty", filename);
+  BEGIN_HASH_ITERATION(hash, itr)
+  {
+    fprintf(file, "%s\n", getFormattedSetupEntry(HASH_ITERATION_TOKEN(itr),
+                                                HASH_ITERATION_VALUE(itr)));
   }
+  END_HASH_ITERATION(hash, itr)
 
-  return setup_file_data;
+  fclose(file);
 }
 
 SetupFileList *loadSetupFileList(char *filename)
 {
-  return (SetupFileList *)loadSetupFileData(filename, FALSE);
+  SetupFileList *setup_file_list = newSetupFileList("", "");
+  SetupFileList *first_valid_list_entry;
+
+  if (!loadSetupFileData(setup_file_list, filename, TRUE, FALSE))
+  {
+    freeSetupFileList(setup_file_list);
+
+    return NULL;
+  }
+
+  first_valid_list_entry = setup_file_list->next;
+
+  // free empty list header
+  setup_file_list->next = NULL;
+  freeSetupFileList(setup_file_list);
+
+  return first_valid_list_entry;
 }
 
 SetupFileHash *loadSetupFileHash(char *filename)
 {
-  return (SetupFileHash *)loadSetupFileData(filename, TRUE);
-}
+  SetupFileHash *setup_file_hash = newSetupFileHash();
 
-void checkSetupFileHashIdentifier(SetupFileHash *setup_file_hash,
-                                 char *filename, char *identifier)
-{
-  char *value = getHashEntry(setup_file_hash, TOKEN_STR_FILE_IDENTIFIER);
+  if (!loadSetupFileData(setup_file_hash, filename, TRUE, TRUE))
+  {
+    freeSetupFileHash(setup_file_hash);
 
-  if (value == NULL)
-    Error(ERR_WARN, "config file '%s' has no file identifier", filename);
-  else if (!checkCookieString(value, identifier))
-    Error(ERR_WARN, "config file '%s' has wrong file identifier", filename);
+    return NULL;
+  }
+
+  return setup_file_hash;
 }
 
 
-/* ========================================================================= */
-/* setup file stuff                                                          */
-/* ========================================================================= */
+// ============================================================================
+// setup file stuff
+// ============================================================================
 
 #define TOKEN_STR_LAST_LEVEL_SERIES            "last_level_series"
 #define TOKEN_STR_LAST_PLAYED_LEVEL            "last_played_level"
 #define TOKEN_STR_HANDICAP_LEVEL               "handicap_level"
 
-/* level directory info */
+// level directory info
 #define LEVELINFO_TOKEN_IDENTIFIER             0
 #define LEVELINFO_TOKEN_NAME                   1
 #define LEVELINFO_TOKEN_NAME_SORTING           2
 #define LEVELINFO_TOKEN_AUTHOR                 3
-#define LEVELINFO_TOKEN_IMPORTED_FROM          4
-#define LEVELINFO_TOKEN_IMPORTED_BY            5
-#define LEVELINFO_TOKEN_LEVELS                 6
-#define LEVELINFO_TOKEN_FIRST_LEVEL            7
-#define LEVELINFO_TOKEN_SORT_PRIORITY          8
-#define LEVELINFO_TOKEN_LATEST_ENGINE          9
-#define LEVELINFO_TOKEN_LEVEL_GROUP            10
-#define LEVELINFO_TOKEN_READONLY               11
-#define LEVELINFO_TOKEN_GRAPHICS_SET_ECS       12
-#define LEVELINFO_TOKEN_GRAPHICS_SET_AGA       13
-#define LEVELINFO_TOKEN_GRAPHICS_SET           14
-#define LEVELINFO_TOKEN_SOUNDS_SET             15
-#define LEVELINFO_TOKEN_MUSIC_SET              16
-#define LEVELINFO_TOKEN_FILENAME               17
-#define LEVELINFO_TOKEN_FILETYPE               18
-#define LEVELINFO_TOKEN_HANDICAP               19
-#define LEVELINFO_TOKEN_SKIP_LEVELS            20
-
-#define NUM_LEVELINFO_TOKENS                   21
+#define LEVELINFO_TOKEN_YEAR                   4
+#define LEVELINFO_TOKEN_PROGRAM_TITLE          5
+#define LEVELINFO_TOKEN_PROGRAM_COPYRIGHT      6
+#define LEVELINFO_TOKEN_PROGRAM_COMPANY                7
+#define LEVELINFO_TOKEN_IMPORTED_FROM          8
+#define LEVELINFO_TOKEN_IMPORTED_BY            9
+#define LEVELINFO_TOKEN_TESTED_BY              10
+#define LEVELINFO_TOKEN_LEVELS                 11
+#define LEVELINFO_TOKEN_FIRST_LEVEL            12
+#define LEVELINFO_TOKEN_SORT_PRIORITY          13
+#define LEVELINFO_TOKEN_LATEST_ENGINE          14
+#define LEVELINFO_TOKEN_LEVEL_GROUP            15
+#define LEVELINFO_TOKEN_READONLY               16
+#define LEVELINFO_TOKEN_GRAPHICS_SET_ECS       17
+#define LEVELINFO_TOKEN_GRAPHICS_SET_AGA       18
+#define LEVELINFO_TOKEN_GRAPHICS_SET           19
+#define LEVELINFO_TOKEN_SOUNDS_SET             20
+#define LEVELINFO_TOKEN_MUSIC_SET              21
+#define LEVELINFO_TOKEN_FILENAME               22
+#define LEVELINFO_TOKEN_FILETYPE               23
+#define LEVELINFO_TOKEN_SPECIAL_FLAGS          24
+#define LEVELINFO_TOKEN_HANDICAP               25
+#define LEVELINFO_TOKEN_SKIP_LEVELS            26
+
+#define NUM_LEVELINFO_TOKENS                   27
 
 static LevelDirTree ldi;
 
 static struct TokenInfo levelinfo_tokens[] =
 {
-  /* level directory info */
+  // level directory info
   { TYPE_STRING,       &ldi.identifier,        "identifier"            },
   { TYPE_STRING,       &ldi.name,              "name"                  },
   { TYPE_STRING,       &ldi.name_sorting,      "name_sorting"          },
   { TYPE_STRING,       &ldi.author,            "author"                },
+  { TYPE_STRING,       &ldi.year,              "year"                  },
+  { TYPE_STRING,       &ldi.program_title,     "program_title"         },
+  { TYPE_STRING,       &ldi.program_copyright, "program_copyright"     },
+  { TYPE_STRING,       &ldi.program_company,   "program_company"       },
   { TYPE_STRING,       &ldi.imported_from,     "imported_from"         },
   { TYPE_STRING,       &ldi.imported_by,       "imported_by"           },
+  { TYPE_STRING,       &ldi.tested_by,         "tested_by"             },
   { TYPE_INTEGER,      &ldi.levels,            "levels"                },
   { TYPE_INTEGER,      &ldi.first_level,       "first_level"           },
   { TYPE_INTEGER,      &ldi.sort_priority,     "sort_priority"         },
@@ -1782,10 +2333,32 @@ static struct TokenInfo levelinfo_tokens[] =
   { TYPE_STRING,       &ldi.music_set,         "music_set"             },
   { TYPE_STRING,       &ldi.level_filename,    "filename"              },
   { TYPE_STRING,       &ldi.level_filetype,    "filetype"              },
+  { TYPE_STRING,       &ldi.special_flags,     "special_flags"         },
   { TYPE_BOOLEAN,      &ldi.handicap,          "handicap"              },
   { TYPE_BOOLEAN,      &ldi.skip_levels,       "skip_levels"           }
 };
 
+static struct TokenInfo artworkinfo_tokens[] =
+{
+  // artwork directory info
+  { TYPE_STRING,       &ldi.identifier,        "identifier"            },
+  { TYPE_STRING,       &ldi.subdir,            "subdir"                },
+  { TYPE_STRING,       &ldi.name,              "name"                  },
+  { TYPE_STRING,       &ldi.name_sorting,      "name_sorting"          },
+  { TYPE_STRING,       &ldi.author,            "author"                },
+  { TYPE_STRING,       &ldi.program_title,     "program_title"         },
+  { TYPE_STRING,       &ldi.program_copyright, "program_copyright"     },
+  { TYPE_STRING,       &ldi.program_company,   "program_company"       },
+  { TYPE_INTEGER,      &ldi.sort_priority,     "sort_priority"         },
+  { TYPE_STRING,       &ldi.basepath,          "basepath"              },
+  { TYPE_STRING,       &ldi.fullpath,          "fullpath"              },
+  { TYPE_BOOLEAN,      &ldi.in_user_dir,       "in_user_dir"           },
+  { TYPE_INTEGER,      &ldi.color,             "color"                 },
+  { TYPE_STRING,       &ldi.class_desc,        "class_desc"            },
+
+  { -1,                        NULL,                   NULL                    },
+};
+
 static void setTreeInfoToDefaults(TreeInfo *ti, int type)
 {
   ti->type = type;
@@ -1810,9 +2383,14 @@ static void setTreeInfoToDefaults(TreeInfo *ti, int type)
   ti->name = getStringCopy(ANONYMOUS_NAME);
   ti->name_sorting = NULL;
   ti->author = getStringCopy(ANONYMOUS_NAME);
+  ti->year = NULL;
+
+  ti->program_title = NULL;
+  ti->program_copyright = NULL;
+  ti->program_company = NULL;
 
-  ti->sort_priority = LEVELCLASS_UNDEFINED;    /* default: least priority */
-  ti->latest_engine = FALSE;                   /* default: get from level */
+  ti->sort_priority = LEVELCLASS_UNDEFINED;    // default: least priority
+  ti->latest_engine = FALSE;                   // default: get from level
   ti->parent_link = FALSE;
   ti->in_user_dir = FALSE;
   ti->user_defined = FALSE;
@@ -1825,6 +2403,7 @@ static void setTreeInfoToDefaults(TreeInfo *ti, int type)
   {
     ti->imported_from = NULL;
     ti->imported_by = NULL;
+    ti->tested_by = NULL;
 
     ti->graphics_set_ecs = NULL;
     ti->graphics_set_aga = NULL;
@@ -1838,6 +2417,8 @@ static void setTreeInfoToDefaults(TreeInfo *ti, int type)
     ti->level_filename = NULL;
     ti->level_filetype = NULL;
 
+    ti->special_flags = NULL;
+
     ti->levels = 0;
     ti->first_level = 0;
     ti->last_level = 0;
@@ -1860,7 +2441,7 @@ static void setTreeInfoToDefaultsFromParent(TreeInfo *ti, TreeInfo *parent)
     return;
   }
 
-  /* copy all values from the parent structure */
+  // copy all values from the parent structure
 
   ti->type = parent->type;
 
@@ -1879,6 +2460,11 @@ static void setTreeInfoToDefaultsFromParent(TreeInfo *ti, TreeInfo *parent)
   ti->name = getStringCopy(ANONYMOUS_NAME);
   ti->name_sorting = NULL;
   ti->author = getStringCopy(parent->author);
+  ti->year = getStringCopy(parent->year);
+
+  ti->program_title = getStringCopy(parent->program_title);
+  ti->program_copyright = getStringCopy(parent->program_copyright);
+  ti->program_company = getStringCopy(parent->program_company);
 
   ti->sort_priority = parent->sort_priority;
   ti->latest_engine = parent->latest_engine;
@@ -1894,32 +2480,109 @@ static void setTreeInfoToDefaultsFromParent(TreeInfo *ti, TreeInfo *parent)
   {
     ti->imported_from = getStringCopy(parent->imported_from);
     ti->imported_by = getStringCopy(parent->imported_by);
+    ti->tested_by = getStringCopy(parent->tested_by);
 
-    ti->graphics_set_ecs = NULL;
-    ti->graphics_set_aga = NULL;
-    ti->graphics_set = NULL;
-    ti->sounds_set = NULL;
-    ti->music_set = NULL;
+    ti->graphics_set_ecs = getStringCopy(parent->graphics_set_ecs);
+    ti->graphics_set_aga = getStringCopy(parent->graphics_set_aga);
+    ti->graphics_set = getStringCopy(parent->graphics_set);
+    ti->sounds_set = getStringCopy(parent->sounds_set);
+    ti->music_set = getStringCopy(parent->music_set);
     ti->graphics_path = getStringCopy(UNDEFINED_FILENAME);
     ti->sounds_path = getStringCopy(UNDEFINED_FILENAME);
     ti->music_path = getStringCopy(UNDEFINED_FILENAME);
 
-    ti->level_filename = NULL;
-    ti->level_filetype = NULL;
+    ti->level_filename = getStringCopy(parent->level_filename);
+    ti->level_filetype = getStringCopy(parent->level_filetype);
 
-    ti->levels = 0;
-    ti->first_level = 0;
-    ti->last_level = 0;
+    ti->special_flags = getStringCopy(parent->special_flags);
+
+    ti->levels = parent->levels;
+    ti->first_level = parent->first_level;
+    ti->last_level = parent->last_level;
     ti->level_group = FALSE;
-    ti->handicap_level = 0;
-    ti->readonly = TRUE;
-    ti->handicap = TRUE;
-    ti->skip_levels = FALSE;
+    ti->handicap_level = parent->handicap_level;
+    ti->readonly = parent->readonly;
+    ti->handicap = parent->handicap;
+    ti->skip_levels = parent->skip_levels;
   }
 }
 
-static void freeTreeInfo(TreeInfo *ti)
+static TreeInfo *getTreeInfoCopy(TreeInfo *ti)
+{
+  TreeInfo *ti_copy = newTreeInfo();
+
+  // copy all values from the original structure
+
+  ti_copy->type                        = ti->type;
+
+  ti_copy->node_top            = ti->node_top;
+  ti_copy->node_parent         = ti->node_parent;
+  ti_copy->node_group          = ti->node_group;
+  ti_copy->next                        = ti->next;
+
+  ti_copy->cl_first            = ti->cl_first;
+  ti_copy->cl_cursor           = ti->cl_cursor;
+
+  ti_copy->subdir              = getStringCopy(ti->subdir);
+  ti_copy->fullpath            = getStringCopy(ti->fullpath);
+  ti_copy->basepath            = getStringCopy(ti->basepath);
+  ti_copy->identifier          = getStringCopy(ti->identifier);
+  ti_copy->name                        = getStringCopy(ti->name);
+  ti_copy->name_sorting                = getStringCopy(ti->name_sorting);
+  ti_copy->author              = getStringCopy(ti->author);
+  ti_copy->year                        = getStringCopy(ti->year);
+
+  ti_copy->program_title       = getStringCopy(ti->program_title);
+  ti_copy->program_copyright   = getStringCopy(ti->program_copyright);
+  ti_copy->program_company     = getStringCopy(ti->program_company);
+
+  ti_copy->imported_from       = getStringCopy(ti->imported_from);
+  ti_copy->imported_by         = getStringCopy(ti->imported_by);
+  ti_copy->tested_by           = getStringCopy(ti->tested_by);
+
+  ti_copy->graphics_set_ecs    = getStringCopy(ti->graphics_set_ecs);
+  ti_copy->graphics_set_aga    = getStringCopy(ti->graphics_set_aga);
+  ti_copy->graphics_set                = getStringCopy(ti->graphics_set);
+  ti_copy->sounds_set          = getStringCopy(ti->sounds_set);
+  ti_copy->music_set           = getStringCopy(ti->music_set);
+  ti_copy->graphics_path       = getStringCopy(ti->graphics_path);
+  ti_copy->sounds_path         = getStringCopy(ti->sounds_path);
+  ti_copy->music_path          = getStringCopy(ti->music_path);
+
+  ti_copy->level_filename      = getStringCopy(ti->level_filename);
+  ti_copy->level_filetype      = getStringCopy(ti->level_filetype);
+
+  ti_copy->special_flags       = getStringCopy(ti->special_flags);
+
+  ti_copy->levels              = ti->levels;
+  ti_copy->first_level         = ti->first_level;
+  ti_copy->last_level          = ti->last_level;
+  ti_copy->sort_priority       = ti->sort_priority;
+
+  ti_copy->latest_engine       = ti->latest_engine;
+
+  ti_copy->level_group         = ti->level_group;
+  ti_copy->parent_link         = ti->parent_link;
+  ti_copy->in_user_dir         = ti->in_user_dir;
+  ti_copy->user_defined                = ti->user_defined;
+  ti_copy->readonly            = ti->readonly;
+  ti_copy->handicap            = ti->handicap;
+  ti_copy->skip_levels         = ti->skip_levels;
+
+  ti_copy->color               = ti->color;
+  ti_copy->class_desc          = getStringCopy(ti->class_desc);
+  ti_copy->handicap_level      = ti->handicap_level;
+
+  ti_copy->infotext            = getStringCopy(ti->infotext);
+
+  return ti_copy;
+}
+
+void freeTreeInfo(TreeInfo *ti)
 {
+  if (ti == NULL)
+    return;
+
   checked_free(ti->subdir);
   checked_free(ti->fullpath);
   checked_free(ti->basepath);
@@ -1928,6 +2591,11 @@ static void freeTreeInfo(TreeInfo *ti)
   checked_free(ti->name);
   checked_free(ti->name_sorting);
   checked_free(ti->author);
+  checked_free(ti->year);
+
+  checked_free(ti->program_title);
+  checked_free(ti->program_copyright);
+  checked_free(ti->program_company);
 
   checked_free(ti->class_desc);
 
@@ -1937,6 +2605,7 @@ static void freeTreeInfo(TreeInfo *ti)
   {
     checked_free(ti->imported_from);
     checked_free(ti->imported_by);
+    checked_free(ti->tested_by);
 
     checked_free(ti->graphics_set_ecs);
     checked_free(ti->graphics_set_aga);
@@ -1950,7 +2619,19 @@ static void freeTreeInfo(TreeInfo *ti)
 
     checked_free(ti->level_filename);
     checked_free(ti->level_filetype);
+
+    checked_free(ti->special_flags);
   }
+
+  // recursively free child node
+  if (ti->node_group)
+    freeTreeInfo(ti->node_group);
+
+  // recursively free next node
+  if (ti->next)
+    freeTreeInfo(ti->next);
+
+  checked_free(ti);
 }
 
 void setSetupInfo(struct TokenInfo *token_info,
@@ -1962,7 +2643,7 @@ void setSetupInfo(struct TokenInfo *token_info,
   if (token_value == NULL)
     return;
 
-  /* set setup field to corresponding token value */
+  // set setup field to corresponding token value
   switch (token_type)
   {
     case TYPE_BOOLEAN:
@@ -1970,6 +2651,10 @@ void setSetupInfo(struct TokenInfo *token_info,
       *(boolean *)setup_value = get_boolean_from_string(token_value);
       break;
 
+    case TYPE_SWITCH3:
+      *(int *)setup_value = get_switch3_from_string(token_value);
+      break;
+
     case TYPE_KEY:
       *(Key *)setup_value = getKeyFromKeyName(token_value);
       break;
@@ -1987,6 +2672,10 @@ void setSetupInfo(struct TokenInfo *token_info,
       *(char **)setup_value = getStringCopy(token_value);
       break;
 
+    case TYPE_PLAYER:
+      *(int *)setup_value = get_player_nr_from_string(token_value);
+      break;
+
     default:
       break;
   }
@@ -1996,7 +2685,7 @@ static int compareTreeInfoEntries(const void *object1, const void *object2)
 {
   const TreeInfo *entry1 = *((TreeInfo **)object1);
   const TreeInfo *entry2 = *((TreeInfo **)object2);
-  int class_sorting1, class_sorting2;
+  int class_sorting1 = 0, class_sorting2 = 0;
   int compare_result;
 
   if (entry1->type == TREE_TYPE_LEVEL_DIR)
@@ -2004,7 +2693,9 @@ static int compareTreeInfoEntries(const void *object1, const void *object2)
     class_sorting1 = LEVELSORTING(entry1);
     class_sorting2 = LEVELSORTING(entry2);
   }
-  else
+  else if (entry1->type == TREE_TYPE_GRAPHICS_DIR ||
+          entry1->type == TREE_TYPE_SOUNDS_DIR ||
+          entry1->type == TREE_TYPE_MUSIC_DIR)
   {
     class_sorting1 = ARTWORKSORTING(entry1);
     class_sorting2 = ARTWORKSORTING(entry2);
@@ -2014,51 +2705,426 @@ static int compareTreeInfoEntries(const void *object1, const void *object2)
     compare_result = (entry1->parent_link ? -1 : +1);
   else if (entry1->sort_priority == entry2->sort_priority)
   {
-    char *name1 = getStringToLower(entry1->name_sorting);
-    char *name2 = getStringToLower(entry2->name_sorting);
+    char *name1 = getStringToLower(entry1->name_sorting);
+    char *name2 = getStringToLower(entry2->name_sorting);
+
+    compare_result = strcmp(name1, name2);
+
+    free(name1);
+    free(name2);
+  }
+  else if (class_sorting1 == class_sorting2)
+    compare_result = entry1->sort_priority - entry2->sort_priority;
+  else
+    compare_result = class_sorting1 - class_sorting2;
+
+  return compare_result;
+}
+
+static TreeInfo *createParentTreeInfoNode(TreeInfo *node_parent)
+{
+  TreeInfo *ti_new;
+
+  if (node_parent == NULL)
+    return NULL;
+
+  ti_new = newTreeInfo();
+  setTreeInfoToDefaults(ti_new, node_parent->type);
+
+  ti_new->node_parent = node_parent;
+  ti_new->parent_link = TRUE;
+
+  setString(&ti_new->identifier, node_parent->identifier);
+  setString(&ti_new->name, ".. (parent directory)");
+  setString(&ti_new->name_sorting, ti_new->name);
+
+  setString(&ti_new->subdir, STRING_PARENT_DIRECTORY);
+  setString(&ti_new->fullpath, node_parent->fullpath);
+
+  ti_new->sort_priority = node_parent->sort_priority;
+  ti_new->latest_engine = node_parent->latest_engine;
+
+  setString(&ti_new->class_desc, getLevelClassDescription(ti_new));
+
+  pushTreeInfo(&node_parent->node_group, ti_new);
+
+  return ti_new;
+}
+
+static TreeInfo *createTopTreeInfoNode(TreeInfo *node_first)
+{
+  TreeInfo *ti_new, *ti_new2;
+
+  if (node_first == NULL)
+    return NULL;
+
+  ti_new = newTreeInfo();
+  setTreeInfoToDefaults(ti_new, TREE_TYPE_LEVEL_DIR);
+
+  ti_new->node_parent = NULL;
+  ti_new->parent_link = FALSE;
+
+  setString(&ti_new->identifier, node_first->identifier);
+  setString(&ti_new->name, "level sets");
+  setString(&ti_new->name_sorting, ti_new->name);
+
+  setString(&ti_new->subdir, STRING_TOP_DIRECTORY);
+  setString(&ti_new->fullpath, ".");
+
+  ti_new->sort_priority = node_first->sort_priority;;
+  ti_new->latest_engine = node_first->latest_engine;
+
+  setString(&ti_new->class_desc, "level sets");
+
+  ti_new->node_group = node_first;
+  ti_new->level_group = TRUE;
+
+  ti_new2 = createParentTreeInfoNode(ti_new);
+
+  setString(&ti_new2->name, ".. (main menu)");
+  setString(&ti_new2->name_sorting, ti_new2->name);
+
+  return ti_new;
+}
+
+
+// ----------------------------------------------------------------------------
+// functions for handling level and custom artwork info cache
+// ----------------------------------------------------------------------------
+
+static void LoadArtworkInfoCache(void)
+{
+  InitCacheDirectory();
+
+  if (artworkinfo_cache_old == NULL)
+  {
+    char *filename = getPath2(getCacheDir(), ARTWORKINFO_CACHE_FILE);
+
+    // try to load artwork info hash from already existing cache file
+    artworkinfo_cache_old = loadSetupFileHash(filename);
+
+    // if no artwork info cache file was found, start with empty hash
+    if (artworkinfo_cache_old == NULL)
+      artworkinfo_cache_old = newSetupFileHash();
+
+    free(filename);
+  }
+
+  if (artworkinfo_cache_new == NULL)
+    artworkinfo_cache_new = newSetupFileHash();
+}
+
+static void SaveArtworkInfoCache(void)
+{
+  char *filename = getPath2(getCacheDir(), ARTWORKINFO_CACHE_FILE);
+
+  InitCacheDirectory();
+
+  saveSetupFileHash(artworkinfo_cache_new, filename);
+
+  free(filename);
+}
+
+static char *getCacheTokenPrefix(char *prefix1, char *prefix2)
+{
+  static char *prefix = NULL;
+
+  checked_free(prefix);
+
+  prefix = getStringCat2WithSeparator(prefix1, prefix2, ".");
+
+  return prefix;
+}
+
+// (identical to above function, but separate string buffer needed -- nasty)
+static char *getCacheToken(char *prefix, char *suffix)
+{
+  static char *token = NULL;
+
+  checked_free(token);
+
+  token = getStringCat2WithSeparator(prefix, suffix, ".");
+
+  return token;
+}
+
+static char *getFileTimestampString(char *filename)
+{
+  return getStringCopy(i_to_a(getFileTimestampEpochSeconds(filename)));
+}
+
+static boolean modifiedFileTimestamp(char *filename, char *timestamp_string)
+{
+  struct stat file_status;
+
+  if (timestamp_string == NULL)
+    return TRUE;
+
+  if (stat(filename, &file_status) != 0)       // cannot stat file
+    return TRUE;
+
+  return (file_status.st_mtime != atoi(timestamp_string));
+}
+
+static TreeInfo *getArtworkInfoCacheEntry(LevelDirTree *level_node, int type)
+{
+  char *identifier = level_node->subdir;
+  char *type_string = ARTWORK_DIRECTORY(type);
+  char *token_prefix = getCacheTokenPrefix(type_string, identifier);
+  char *token_main = getCacheToken(token_prefix, "CACHED");
+  char *cache_entry = getHashEntry(artworkinfo_cache_old, token_main);
+  boolean cached = (cache_entry != NULL && strEqual(cache_entry, "true"));
+  TreeInfo *artwork_info = NULL;
+
+  if (!use_artworkinfo_cache)
+    return NULL;
+
+  if (cached)
+  {
+    int i;
+
+    artwork_info = newTreeInfo();
+    setTreeInfoToDefaults(artwork_info, type);
+
+    // set all structure fields according to the token/value pairs
+    ldi = *artwork_info;
+    for (i = 0; artworkinfo_tokens[i].type != -1; i++)
+    {
+      char *token = getCacheToken(token_prefix, artworkinfo_tokens[i].text);
+      char *value = getHashEntry(artworkinfo_cache_old, token);
+
+      // if defined, use value from cache, else keep default value
+      if (value != NULL)
+       setSetupInfo(artworkinfo_tokens, i, value);
+    }
+
+    *artwork_info = ldi;
+
+    char *filename_levelinfo = getPath2(getLevelDirFromTreeInfo(level_node),
+                                       LEVELINFO_FILENAME);
+    char *filename_artworkinfo = getPath2(getSetupArtworkDir(artwork_info),
+                                         ARTWORKINFO_FILENAME(type));
+
+    // check if corresponding "levelinfo.conf" file has changed
+    token_main = getCacheToken(token_prefix, "TIMESTAMP_LEVELINFO");
+    cache_entry = getHashEntry(artworkinfo_cache_old, token_main);
+
+    if (modifiedFileTimestamp(filename_levelinfo, cache_entry))
+      cached = FALSE;
+
+    // check if corresponding "<artworkinfo>.conf" file has changed
+    token_main = getCacheToken(token_prefix, "TIMESTAMP_ARTWORKINFO");
+    cache_entry = getHashEntry(artworkinfo_cache_old, token_main);
+
+    if (modifiedFileTimestamp(filename_artworkinfo, cache_entry))
+      cached = FALSE;
+
+    checked_free(filename_levelinfo);
+    checked_free(filename_artworkinfo);
+  }
+
+  if (!cached && artwork_info != NULL)
+  {
+    freeTreeInfo(artwork_info);
+
+    return NULL;
+  }
+
+  return artwork_info;
+}
+
+static void setArtworkInfoCacheEntry(TreeInfo *artwork_info,
+                                    LevelDirTree *level_node, int type)
+{
+  char *identifier = level_node->subdir;
+  char *type_string = ARTWORK_DIRECTORY(type);
+  char *token_prefix = getCacheTokenPrefix(type_string, identifier);
+  char *token_main = getCacheToken(token_prefix, "CACHED");
+  boolean set_cache_timestamps = TRUE;
+  int i;
+
+  setHashEntry(artworkinfo_cache_new, token_main, "true");
+
+  if (set_cache_timestamps)
+  {
+    char *filename_levelinfo = getPath2(getLevelDirFromTreeInfo(level_node),
+                                       LEVELINFO_FILENAME);
+    char *filename_artworkinfo = getPath2(getSetupArtworkDir(artwork_info),
+                                         ARTWORKINFO_FILENAME(type));
+    char *timestamp_levelinfo = getFileTimestampString(filename_levelinfo);
+    char *timestamp_artworkinfo = getFileTimestampString(filename_artworkinfo);
+
+    token_main = getCacheToken(token_prefix, "TIMESTAMP_LEVELINFO");
+    setHashEntry(artworkinfo_cache_new, token_main, timestamp_levelinfo);
+
+    token_main = getCacheToken(token_prefix, "TIMESTAMP_ARTWORKINFO");
+    setHashEntry(artworkinfo_cache_new, token_main, timestamp_artworkinfo);
+
+    checked_free(filename_levelinfo);
+    checked_free(filename_artworkinfo);
+    checked_free(timestamp_levelinfo);
+    checked_free(timestamp_artworkinfo);
+  }
+
+  ldi = *artwork_info;
+  for (i = 0; artworkinfo_tokens[i].type != -1; i++)
+  {
+    char *token = getCacheToken(token_prefix, artworkinfo_tokens[i].text);
+    char *value = getSetupValue(artworkinfo_tokens[i].type,
+                               artworkinfo_tokens[i].value);
+    if (value != NULL)
+      setHashEntry(artworkinfo_cache_new, token, value);
+  }
+}
+
+
+// ----------------------------------------------------------------------------
+// functions for loading level info and custom artwork info
+// ----------------------------------------------------------------------------
+
+static boolean CheckZipFileForDirectory(char *zip_filename, char *directory,
+                                       int tree_type)
+{
+  static char *top_dir_path = NULL;
+  static char *top_dir_conf_filename = NULL;
+
+  checked_free(top_dir_path);
+  checked_free(top_dir_conf_filename);
+
+  top_dir_path = NULL;
+  top_dir_conf_filename = NULL;
+
+  char *conf_basename = (tree_type == TREE_TYPE_LEVEL_DIR ? LEVELINFO_FILENAME :
+                        ARTWORKINFO_FILENAME(tree_type));
+
+  // check if valid configuration filename determined
+  if (conf_basename == NULL || strEqual(conf_basename, ""))
+    return FALSE;
+
+  char **zip_entries = zip_list(zip_filename);
+
+  // check if zip file successfully opened
+  if (zip_entries == NULL || zip_entries[0] == NULL)
+    return FALSE;
+
+  // first zip file entry is expected to be top level directory
+  char *top_dir = zip_entries[0];
+
+  // check if valid top level directory found in zip file
+  if (!strSuffix(top_dir, "/"))
+    return FALSE;
+
+  // get path of extracted top level directory
+  top_dir_path = getPath2(directory, top_dir);
+
+  // remove trailing directory separator from top level directory path
+  // (required to be able to check for file and directory in next step)
+  top_dir_path[strlen(top_dir_path) - 1] = '\0';
+
+  // check if zip file's top level directory already exists in target directory
+  if (fileExists(top_dir_path))                // (checks for file and directory)
+    return FALSE;
+
+  // get filename of configuration file in top level directory
+  top_dir_conf_filename = getStringCat2(top_dir, conf_basename);
+
+  boolean found_top_dir_conf_filename = FALSE;
+  int i = 0;
+
+  while (zip_entries[i] != NULL)
+  {
+    // check if every zip file entry is below top level directory
+    if (!strPrefix(zip_entries[i], top_dir))
+      return FALSE;
 
-    compare_result = strcmp(name1, name2);
+    // check if this zip file entry is the configuration filename
+    if (strEqual(zip_entries[i], top_dir_conf_filename))
+      found_top_dir_conf_filename = TRUE;
 
-    free(name1);
-    free(name2);
+    i++;
   }
-  else if (class_sorting1 == class_sorting2)
-    compare_result = entry1->sort_priority - entry2->sort_priority;
-  else
-    compare_result = class_sorting1 - class_sorting2;
 
-  return compare_result;
+  // check if valid configuration filename was found in zip file
+  if (!found_top_dir_conf_filename)
+    return FALSE;
+
+  return TRUE;
 }
 
-static void createParentTreeInfoNode(TreeInfo *node_parent)
+static boolean ExtractZipFileIntoDirectory(char *zip_filename, char *directory,
+                                          int tree_type)
 {
-  TreeInfo *ti_new;
+  boolean zip_file_valid = CheckZipFileForDirectory(zip_filename, directory,
+                                                   tree_type);
 
-  if (node_parent == NULL)
-    return;
+  Error(ERR_DEBUG, "zip file '%s': %s", zip_filename,
+       (zip_file_valid ? "EXTRACT" : "REJECT"));
 
-  ti_new = newTreeInfo();
-  setTreeInfoToDefaults(ti_new, node_parent->type);
+  if (!zip_file_valid)
+    return FALSE;
 
-  ti_new->node_parent = node_parent;
-  ti_new->parent_link = TRUE;
+  char **zip_entries = zip_extract(zip_filename, directory);
 
-  setString(&ti_new->identifier, node_parent->identifier);
-  setString(&ti_new->name, ".. (parent directory)");
-  setString(&ti_new->name_sorting, ti_new->name);
+  boolean zip_file_extracted = (zip_entries != NULL);
 
-  setString(&ti_new->subdir, "..");
-  setString(&ti_new->fullpath, node_parent->fullpath);
+  if (zip_file_extracted)
+    Error(ERR_DEBUG, "zip file successfully extracted!");
+  else
+    Error(ERR_DEBUG, "zip file could not be extracted!");
 
-  ti_new->sort_priority = node_parent->sort_priority;
-  ti_new->latest_engine = node_parent->latest_engine;
+  return zip_file_extracted;
+}
 
-  setString(&ti_new->class_desc, getLevelClassDescription(ti_new));
+static void ProcessZipFilesInDirectory(char *directory, int tree_type)
+{
+  Directory *dir;
+  DirectoryEntry *dir_entry;
 
-  pushTreeInfo(&node_parent->node_group, ti_new);
+  if ((dir = openDirectory(directory)) == NULL)
+  {
+    // display error if directory is main "options.graphics_directory" etc.
+    if (tree_type == TREE_TYPE_LEVEL_DIR ||
+       directory == OPTIONS_ARTWORK_DIRECTORY(tree_type))
+      Error(ERR_WARN, "cannot read directory '%s'", directory);
+
+    return;
+  }
+
+  while ((dir_entry = readDirectory(dir)) != NULL)     // loop all entries
+  {
+    // skip non-zip files (and also directories with zip extension)
+    if (!strSuffixLower(dir_entry->basename, ".zip") || dir_entry->is_directory)
+      continue;
+
+    char *zip_filename = getPath2(directory, dir_entry->basename);
+    char *zip_filename_extracted = getStringCat2(zip_filename, ".extracted");
+    char *zip_filename_rejected  = getStringCat2(zip_filename, ".rejected");
+
+    // check if zip file hasn't already been extracted or rejected
+    if (!fileExists(zip_filename_extracted) &&
+       !fileExists(zip_filename_rejected))
+    {
+      boolean zip_file_extracted = ExtractZipFileIntoDirectory(zip_filename,
+                                                              directory,
+                                                              tree_type);
+      char *marker_filename = (zip_file_extracted ? zip_filename_extracted :
+                              zip_filename_rejected);
+      FILE *marker_file;
+
+      // create empty file to mark zip file as extracted or rejected
+      if ((marker_file = fopen(marker_filename, MODE_WRITE)))
+       fclose(marker_file);
+
+      free(zip_filename);
+      free(zip_filename_extracted);
+      free(zip_filename_rejected);
+    }
+  }
+
+  closeDirectory(dir);
 }
 
-/* forward declaration for recursive call by "LoadLevelInfoFromLevelDir()" */
+// forward declaration for recursive call by "LoadLevelInfoFromLevelDir()"
 static void LoadLevelInfoFromLevelDir(TreeInfo **, TreeInfo *, char *);
 
 static boolean LoadLevelInfoFromLevelConf(TreeInfo **node_first,
@@ -2072,7 +3138,7 @@ static boolean LoadLevelInfoFromLevelConf(TreeInfo **node_first,
   LevelDirTree *leveldir_new = NULL;
   int i;
 
-  /* unless debugging, silently ignore directories without "levelinfo.conf" */
+  // unless debugging, silently ignore directories without "levelinfo.conf"
   if (!options.debug && !fileExists(filename))
   {
     free(directory_path);
@@ -2085,7 +3151,9 @@ static boolean LoadLevelInfoFromLevelConf(TreeInfo **node_first,
 
   if (setup_file_hash == NULL)
   {
+#if DEBUG_NO_CONFIG_FILE
     Error(ERR_WARN, "ignoring level directory '%s'", directory_path);
+#endif
 
     free(directory_path);
     free(filename);
@@ -2102,10 +3170,7 @@ static boolean LoadLevelInfoFromLevelConf(TreeInfo **node_first,
 
   leveldir_new->subdir = getStringCopy(directory_name);
 
-  checkSetupFileHashIdentifier(setup_file_hash, filename,
-                              getCookie("LEVELINFO"));
-
-  /* set all structure fields according to the token/value pairs */
+  // set all structure fields according to the token/value pairs
   ldi = *leveldir_new;
   for (i = 0; i < NUM_LEVELINFO_TOKENS; i++)
     setSetupInfo(levelinfo_tokens, i,
@@ -2115,37 +3180,30 @@ static boolean LoadLevelInfoFromLevelConf(TreeInfo **node_first,
   if (strEqual(leveldir_new->name, ANONYMOUS_NAME))
     setString(&leveldir_new->name, leveldir_new->subdir);
 
-  DrawInitText(leveldir_new->name, 150, FC_YELLOW);
-
   if (leveldir_new->identifier == NULL)
     leveldir_new->identifier = getStringCopy(leveldir_new->subdir);
 
   if (leveldir_new->name_sorting == NULL)
     leveldir_new->name_sorting = getStringCopy(leveldir_new->name);
 
-  if (node_parent == NULL)             /* top level group */
+  if (node_parent == NULL)             // top level group
   {
     leveldir_new->basepath = getStringCopy(level_directory);
     leveldir_new->fullpath = getStringCopy(leveldir_new->subdir);
   }
-  else                                 /* sub level group */
+  else                                 // sub level group
   {
     leveldir_new->basepath = getStringCopy(node_parent->basepath);
     leveldir_new->fullpath = getPath2(node_parent->fullpath, directory_name);
   }
 
-#if 0
-  if (leveldir_new->levels < 1)
-    leveldir_new->levels = 1;
-#endif
-
   leveldir_new->last_level =
     leveldir_new->first_level + leveldir_new->levels - 1;
 
   leveldir_new->in_user_dir =
     (!strEqual(leveldir_new->basepath, options.level_directory));
 
-  /* adjust some settings if user's private level directory was detected */
+  // adjust some settings if user's private level directory was detected
   if (leveldir_new->sort_priority == LEVELCLASS_UNDEFINED &&
       leveldir_new->in_user_dir &&
       (strEqual(leveldir_new->subdir, getLoginName()) ||
@@ -2163,25 +3221,11 @@ static boolean LoadLevelInfoFromLevelConf(TreeInfo **node_first,
 
   setString(&leveldir_new->class_desc, getLevelClassDescription(leveldir_new));
 
-  leveldir_new->handicap_level =       /* set handicap to default value */
+  leveldir_new->handicap_level =       // set handicap to default value
     (leveldir_new->user_defined || !leveldir_new->handicap ?
      leveldir_new->last_level : leveldir_new->first_level);
 
-#if 0
-  /* !!! don't skip sets without levels (else artwork base sets are missing) */
-#if 1
-  if (leveldir_new->levels < 1 && !leveldir_new->level_group)
-  {
-    /* skip level sets without levels (which are probably artwork base sets) */
-
-    freeSetupFileHash(setup_file_hash);
-    free(directory_path);
-    free(filename);
-
-    return FALSE;
-  }
-#endif
-#endif
+  DrawInitText(leveldir_new->name, 150, FC_YELLOW);
 
   pushTreeInfo(node_first, leveldir_new);
 
@@ -2189,10 +3233,10 @@ static boolean LoadLevelInfoFromLevelConf(TreeInfo **node_first,
 
   if (leveldir_new->level_group)
   {
-    /* create node to link back to current level directory */
+    // create node to link back to current level directory
     createParentTreeInfoNode(leveldir_new);
 
-    /* step into sub-directory and look for more level series */
+    // recursively step into sub-directory and look for more level series
     LoadLevelInfoFromLevelDir(&leveldir_new->node_group,
                              leveldir_new, directory_path);
   }
@@ -2207,35 +3251,42 @@ static void LoadLevelInfoFromLevelDir(TreeInfo **node_first,
                                      TreeInfo *node_parent,
                                      char *level_directory)
 {
-  DIR *dir;
-  struct dirent *dir_entry;
+  // ---------- 1st stage: process any level set zip files ----------
+
+  ProcessZipFilesInDirectory(level_directory, TREE_TYPE_LEVEL_DIR);
+
+  // ---------- 2nd stage: check for level set directories ----------
+
+  Directory *dir;
+  DirectoryEntry *dir_entry;
   boolean valid_entry_found = FALSE;
 
-  if ((dir = opendir(level_directory)) == NULL)
+  if ((dir = openDirectory(level_directory)) == NULL)
   {
     Error(ERR_WARN, "cannot read level directory '%s'", level_directory);
+
     return;
   }
 
-  while ((dir_entry = readdir(dir)) != NULL)   /* loop until last dir entry */
+  while ((dir_entry = readDirectory(dir)) != NULL)     // loop all entries
   {
-    struct stat file_status;
-    char *directory_name = dir_entry->d_name;
+    char *directory_name = dir_entry->basename;
     char *directory_path = getPath2(level_directory, directory_name);
 
-    /* skip entries for current and parent directory */
+    // skip entries for current and parent directory
     if (strEqual(directory_name, ".") ||
        strEqual(directory_name, ".."))
     {
       free(directory_path);
+
       continue;
     }
 
-    /* find out if directory entry is itself a directory */
-    if (stat(directory_path, &file_status) != 0 ||     /* cannot stat file */
-       (file_status.st_mode & S_IFMT) != S_IFDIR)      /* not a directory */
+    // find out if directory entry is itself a directory
+    if (!dir_entry->is_directory)                      // not a directory
     {
       free(directory_path);
+
       continue;
     }
 
@@ -2251,12 +3302,12 @@ static void LoadLevelInfoFromLevelDir(TreeInfo **node_first,
                                                    directory_name);
   }
 
-  closedir(dir);
+  closeDirectory(dir);
 
-  /* special case: top level directory may directly contain "levelinfo.conf" */
+  // special case: top level directory may directly contain "levelinfo.conf"
   if (node_parent == NULL && !valid_entry_found)
   {
-    /* check if this directory directly contains a file "levelinfo.conf" */
+    // check if this directory directly contains a file "levelinfo.conf"
     valid_entry_found |= LoadLevelInfoFromLevelConf(node_first, node_parent,
                                                    level_directory, ".");
   }
@@ -2266,7 +3317,7 @@ static void LoadLevelInfoFromLevelDir(TreeInfo **node_first,
          level_directory);
 }
 
-boolean AdjustGraphicsForEMC()
+boolean AdjustGraphicsForEMC(void)
 {
   boolean settings_changed = FALSE;
 
@@ -2276,15 +3327,17 @@ boolean AdjustGraphicsForEMC()
   return settings_changed;
 }
 
-void LoadLevelInfo()
+void LoadLevelInfo(void)
 {
   InitUserLevelDirectory(getLoginName());
 
-  DrawInitText("Loading level series:", 120, FC_GREEN);
+  DrawInitText("Loading level series", 120, FC_GREEN);
 
   LoadLevelInfoFromLevelDir(&leveldir_first, NULL, options.level_directory);
   LoadLevelInfoFromLevelDir(&leveldir_first, NULL, getUserLevelDir(NULL));
 
+  leveldir_first = createTopTreeInfoNode(leveldir_first);
+
   /* after loading all level set information, clone the level directory tree
      and remove all level sets without levels (these may still contain artwork
      to be offered in the setup menu as "custom artwork", and are therefore
@@ -2294,7 +3347,7 @@ void LoadLevelInfo()
 
   AdjustGraphicsForEMC();
 
-  /* before sorting, the first entries will be from the user directory */
+  // before sorting, the first entries will be from the user directory
   leveldir_current = getFirstValidTreeInfoEntry(leveldir_first);
 
   if (leveldir_first == NULL)
@@ -2302,7 +3355,7 @@ void LoadLevelInfo()
 
   sortTreeInfo(&leveldir_first);
 
-#if 0
+#if ENABLE_UNUSED_CODE
   dumpTreeInfo(leveldir_first, 0);
 #endif
 }
@@ -2321,32 +3374,33 @@ static boolean LoadArtworkInfoFromArtworkConf(TreeInfo **node_first,
   if (fileExists(filename))
     setup_file_hash = loadSetupFileHash(filename);
 
-  if (setup_file_hash == NULL) /* no config file -- look for artwork files */
+  if (setup_file_hash == NULL) // no config file -- look for artwork files
   {
-    DIR *dir;
-    struct dirent *dir_entry;
+    Directory *dir;
+    DirectoryEntry *dir_entry;
     boolean valid_file_found = FALSE;
 
-    if ((dir = opendir(directory_path)) != NULL)
+    if ((dir = openDirectory(directory_path)) != NULL)
     {
-      while ((dir_entry = readdir(dir)) != NULL)
+      while ((dir_entry = readDirectory(dir)) != NULL)
       {
-       char *entry_name = dir_entry->d_name;
-
-       if (FileIsArtworkType(entry_name, type))
+       if (FileIsArtworkType(dir_entry->filename, type))
        {
          valid_file_found = TRUE;
+
          break;
        }
       }
 
-      closedir(dir);
+      closeDirectory(dir);
     }
 
     if (!valid_file_found)
     {
+#if DEBUG_NO_CONFIG_FILE
       if (!strEqual(directory_name, "."))
        Error(ERR_WARN, "ignoring artwork directory '%s'", directory_path);
+#endif
 
       free(directory_path);
       free(filename);
@@ -2364,13 +3418,9 @@ static boolean LoadArtworkInfoFromArtworkConf(TreeInfo **node_first,
 
   artwork_new->subdir = getStringCopy(directory_name);
 
-  if (setup_file_hash) /* (before defining ".color" and ".class_desc") */
+  if (setup_file_hash) // (before defining ".color" and ".class_desc")
   {
-#if 0
-    checkSetupFileHashIdentifier(setup_file_hash, filename, getCookie("..."));
-#endif
-
-    /* set all structure fields according to the token/value pairs */
+    // set all structure fields according to the token/value pairs
     ldi = *artwork_new;
     for (i = 0; i < NUM_LEVELINFO_TOKENS; i++)
       setSetupInfo(levelinfo_tokens, i,
@@ -2380,10 +3430,6 @@ static boolean LoadArtworkInfoFromArtworkConf(TreeInfo **node_first,
     if (strEqual(artwork_new->name, ANONYMOUS_NAME))
       setString(&artwork_new->name, artwork_new->subdir);
 
-#if 0
-    DrawInitText(artwork_new->name, 150, FC_YELLOW);
-#endif
-
     if (artwork_new->identifier == NULL)
       artwork_new->identifier = getStringCopy(artwork_new->subdir);
 
@@ -2391,12 +3437,12 @@ static boolean LoadArtworkInfoFromArtworkConf(TreeInfo **node_first,
       artwork_new->name_sorting = getStringCopy(artwork_new->name);
   }
 
-  if (node_parent == NULL)             /* top level group */
+  if (node_parent == NULL)             // top level group
   {
     artwork_new->basepath = getStringCopy(base_directory);
     artwork_new->fullpath = getStringCopy(artwork_new->subdir);
   }
-  else                                 /* sub level group */
+  else                                 // sub level group
   {
     artwork_new->basepath = getStringCopy(node_parent->basepath);
     artwork_new->fullpath = getPath2(node_parent->fullpath, directory_name);
@@ -2405,12 +3451,12 @@ static boolean LoadArtworkInfoFromArtworkConf(TreeInfo **node_first,
   artwork_new->in_user_dir =
     (!strEqual(artwork_new->basepath, OPTIONS_ARTWORK_DIRECTORY(type)));
 
-  /* (may use ".sort_priority" from "setup_file_hash" above) */
+  // (may use ".sort_priority" from "setup_file_hash" above)
   artwork_new->color = ARTWORKCOLOR(artwork_new);
 
   setString(&artwork_new->class_desc, getLevelClassDescription(artwork_new));
 
-  if (setup_file_hash == NULL) /* (after determining ".user_defined") */
+  if (setup_file_hash == NULL) // (after determining ".user_defined")
   {
     if (strEqual(artwork_new->subdir, "."))
     {
@@ -2425,7 +3471,7 @@ static boolean LoadArtworkInfoFromArtworkConf(TreeInfo **node_first,
        artwork_new->sort_priority = ARTWORKCLASS_CLASSICS;
       }
 
-      /* set to new values after changing ".sort_priority" */
+      // set to new values after changing ".sort_priority"
       artwork_new->color = ARTWORKCOLOR(artwork_new);
 
       setString(&artwork_new->class_desc,
@@ -2440,8 +3486,6 @@ static boolean LoadArtworkInfoFromArtworkConf(TreeInfo **node_first,
     setString(&artwork_new->name_sorting, artwork_new->name);
   }
 
-  DrawInitText(artwork_new->name, 150, FC_YELLOW);
-
   pushTreeInfo(node_first, artwork_new);
 
   freeSetupFileHash(setup_file_hash);
@@ -2456,52 +3500,58 @@ static void LoadArtworkInfoFromArtworkDir(TreeInfo **node_first,
                                          TreeInfo *node_parent,
                                          char *base_directory, int type)
 {
-  DIR *dir;
-  struct dirent *dir_entry;
+  // ---------- 1st stage: process any artwork set zip files ----------
+
+  ProcessZipFilesInDirectory(base_directory, type);
+
+  // ---------- 2nd stage: check for artwork set directories ----------
+
+  Directory *dir;
+  DirectoryEntry *dir_entry;
   boolean valid_entry_found = FALSE;
 
-  if ((dir = opendir(base_directory)) == NULL)
+  if ((dir = openDirectory(base_directory)) == NULL)
   {
-    /* display error if directory is main "options.graphics_directory" etc. */
+    // display error if directory is main "options.graphics_directory" etc.
     if (base_directory == OPTIONS_ARTWORK_DIRECTORY(type))
       Error(ERR_WARN, "cannot read directory '%s'", base_directory);
 
     return;
   }
 
-  while ((dir_entry = readdir(dir)) != NULL)   /* loop until last dir entry */
+  while ((dir_entry = readDirectory(dir)) != NULL)     // loop all entries
   {
-    struct stat file_status;
-    char *directory_name = dir_entry->d_name;
+    char *directory_name = dir_entry->basename;
     char *directory_path = getPath2(base_directory, directory_name);
 
-    /* skip directory entries for current and parent directory */
+    // skip directory entries for current and parent directory
     if (strEqual(directory_name, ".") ||
        strEqual(directory_name, ".."))
     {
       free(directory_path);
+
       continue;
     }
 
-    /* skip directory entries which are not a directory or are not accessible */
-    if (stat(directory_path, &file_status) != 0 ||     /* cannot stat file */
-       (file_status.st_mode & S_IFMT) != S_IFDIR)      /* not a directory */
+    // skip directory entries which are not a directory
+    if (!dir_entry->is_directory)                      // not a directory
     {
       free(directory_path);
+
       continue;
     }
 
     free(directory_path);
 
-    /* check if this directory contains artwork with or without config file */
+    // check if this directory contains artwork with or without config file
     valid_entry_found |= LoadArtworkInfoFromArtworkConf(node_first, node_parent,
                                                        base_directory,
                                                        directory_name, type);
   }
 
-  closedir(dir);
+  closeDirectory(dir);
 
-  /* check if this directory directly contains artwork itself */
+  // check if this directory directly contains artwork itself
   valid_entry_found |= LoadArtworkInfoFromArtworkConf(node_first, node_parent,
                                                      base_directory, ".",
                                                      type);
@@ -2512,7 +3562,7 @@ static void LoadArtworkInfoFromArtworkDir(TreeInfo **node_first,
 
 static TreeInfo *getDummyArtworkInfo(int type)
 {
-  /* this is only needed when there is completely no artwork available */
+  // this is only needed when there is completely no artwork available
   TreeInfo *artwork_new = newTreeInfo();
 
   setTreeInfoToDefaults(artwork_new, type);
@@ -2528,9 +3578,11 @@ static TreeInfo *getDummyArtworkInfo(int type)
   return artwork_new;
 }
 
-void LoadArtworkInfo()
+void LoadArtworkInfo(void)
 {
-  DrawInitText("Looking for custom artwork:", 120, FC_GREEN);
+  LoadArtworkInfoCache();
+
+  DrawInitText("Looking for custom artwork", 120, FC_GREEN);
 
   LoadArtworkInfoFromArtworkDir(&artwork.gfx_first, NULL,
                                options.graphics_directory,
@@ -2560,12 +3612,12 @@ void LoadArtworkInfo()
   if (artwork.mus_first == NULL)
     artwork.mus_first = getDummyArtworkInfo(TREE_TYPE_MUSIC_DIR);
 
-  /* before sorting, the first entries will be from the user directory */
+  // before sorting, the first entries will be from the user directory
   artwork.gfx_current =
     getTreeInfoFromIdentifier(artwork.gfx_first, setup.graphics_set);
   if (artwork.gfx_current == NULL)
     artwork.gfx_current =
-      getTreeInfoFromIdentifier(artwork.gfx_first, GFX_CLASSIC_SUBDIR);
+      getTreeInfoFromIdentifier(artwork.gfx_first, GFX_DEFAULT_SUBDIR);
   if (artwork.gfx_current == NULL)
     artwork.gfx_current = getFirstValidTreeInfoEntry(artwork.gfx_first);
 
@@ -2573,7 +3625,7 @@ void LoadArtworkInfo()
     getTreeInfoFromIdentifier(artwork.snd_first, setup.sounds_set);
   if (artwork.snd_current == NULL)
     artwork.snd_current =
-      getTreeInfoFromIdentifier(artwork.snd_first, SND_CLASSIC_SUBDIR);
+      getTreeInfoFromIdentifier(artwork.snd_first, SND_DEFAULT_SUBDIR);
   if (artwork.snd_current == NULL)
     artwork.snd_current = getFirstValidTreeInfoEntry(artwork.snd_first);
 
@@ -2581,7 +3633,7 @@ void LoadArtworkInfo()
     getTreeInfoFromIdentifier(artwork.mus_first, setup.music_set);
   if (artwork.mus_current == NULL)
     artwork.mus_current =
-      getTreeInfoFromIdentifier(artwork.mus_first, MUS_CLASSIC_SUBDIR);
+      getTreeInfoFromIdentifier(artwork.mus_first, MUS_DEFAULT_SUBDIR);
   if (artwork.mus_current == NULL)
     artwork.mus_current = getFirstValidTreeInfoEntry(artwork.mus_first);
 
@@ -2589,7 +3641,7 @@ void LoadArtworkInfo()
   artwork.snd_current_identifier = artwork.snd_current->identifier;
   artwork.mus_current_identifier = artwork.mus_current->identifier;
 
-#if 0
+#if ENABLE_UNUSED_CODE
   printf("graphics set == %s\n\n", artwork.gfx_current_identifier);
   printf("sounds set == %s\n\n", artwork.snd_current_identifier);
   printf("music set == %s\n\n", artwork.mus_current_identifier);
@@ -2599,47 +3651,62 @@ void LoadArtworkInfo()
   sortTreeInfo(&artwork.snd_first);
   sortTreeInfo(&artwork.mus_first);
 
-#if 0
+#if ENABLE_UNUSED_CODE
   dumpTreeInfo(artwork.gfx_first, 0);
   dumpTreeInfo(artwork.snd_first, 0);
   dumpTreeInfo(artwork.mus_first, 0);
 #endif
 }
 
-void LoadArtworkInfoFromLevelInfo(ArtworkDirTree **artwork_node,
-                                 LevelDirTree *level_node)
+static void LoadArtworkInfoFromLevelInfo(ArtworkDirTree **artwork_node,
+                                        LevelDirTree *level_node)
 {
-  /* recursively check all level directories for artwork sub-directories */
+  int type = (*artwork_node)->type;
+
+  // recursively check all level directories for artwork sub-directories
 
   while (level_node)
   {
-    /* check all tree entries for artwork, but skip parent link entries */
+    // check all tree entries for artwork, but skip parent link entries
     if (!level_node->parent_link)
     {
-      TreeInfo *topnode_last = *artwork_node;
-      char *path = getPath2(getLevelDirFromTreeInfo(level_node),
-                           ARTWORK_DIRECTORY((*artwork_node)->type));
+      TreeInfo *artwork_new = getArtworkInfoCacheEntry(level_node, type);
+      boolean cached = (artwork_new != NULL);
 
-      LoadArtworkInfoFromArtworkDir(artwork_node, NULL, path,
-                                   (*artwork_node)->type);
-
-      if (topnode_last != *artwork_node)
+      if (cached)
+      {
+       pushTreeInfo(artwork_node, artwork_new);
+      }
+      else
       {
-       free((*artwork_node)->identifier);
-       free((*artwork_node)->name);
-       free((*artwork_node)->name_sorting);
+       TreeInfo *topnode_last = *artwork_node;
+       char *path = getPath2(getLevelDirFromTreeInfo(level_node),
+                             ARTWORK_DIRECTORY(type));
+
+       LoadArtworkInfoFromArtworkDir(artwork_node, NULL, path, type);
+
+       if (topnode_last != *artwork_node)      // check for newly added node
+       {
+         artwork_new = *artwork_node;
+
+         setString(&artwork_new->identifier,   level_node->subdir);
+         setString(&artwork_new->name,         level_node->name);
+         setString(&artwork_new->name_sorting, level_node->name_sorting);
 
-       (*artwork_node)->identifier   = getStringCopy(level_node->subdir);
-       (*artwork_node)->name         = getStringCopy(level_node->name);
-       (*artwork_node)->name_sorting = getStringCopy(level_node->name);
+         artwork_new->sort_priority = level_node->sort_priority;
+         artwork_new->color = LEVELCOLOR(artwork_new);
+       }
 
-       (*artwork_node)->sort_priority = level_node->sort_priority;
-       (*artwork_node)->color = LEVELCOLOR((*artwork_node));
+       free(path);
       }
 
-      free(path);
+      // insert artwork info (from old cache or filesystem) into new cache
+      if (artwork_new != NULL)
+       setArtworkInfoCacheEntry(artwork_new, level_node, type);
     }
 
+    DrawInitText(level_node->name, 150, FC_YELLOW);
+
     if (level_node->node_group != NULL)
       LoadArtworkInfoFromLevelInfo(artwork_node, level_node->node_group);
 
@@ -2647,15 +3714,26 @@ void LoadArtworkInfoFromLevelInfo(ArtworkDirTree **artwork_node,
   }
 }
 
-void LoadLevelArtworkInfo()
+void LoadLevelArtworkInfo(void)
 {
-  DrawInitText("Looking for custom level artwork:", 120, FC_GREEN);
+  print_timestamp_init("LoadLevelArtworkInfo");
+
+  DrawInitText("Looking for custom level artwork", 120, FC_GREEN);
+
+  print_timestamp_time("DrawTimeText");
 
   LoadArtworkInfoFromLevelInfo(&artwork.gfx_first, leveldir_first_all);
+  print_timestamp_time("LoadArtworkInfoFromLevelInfo (gfx)");
   LoadArtworkInfoFromLevelInfo(&artwork.snd_first, leveldir_first_all);
+  print_timestamp_time("LoadArtworkInfoFromLevelInfo (snd)");
   LoadArtworkInfoFromLevelInfo(&artwork.mus_first, leveldir_first_all);
+  print_timestamp_time("LoadArtworkInfoFromLevelInfo (mus)");
 
-  /* needed for reloading level artwork not known at ealier stage */
+  SaveArtworkInfoCache();
+
+  print_timestamp_time("SaveArtworkInfoCache");
+
+  // needed for reloading level artwork not known at ealier stage
 
   if (!strEqual(artwork.gfx_current_identifier, setup.graphics_set))
   {
@@ -2663,7 +3741,7 @@ void LoadLevelArtworkInfo()
       getTreeInfoFromIdentifier(artwork.gfx_first, setup.graphics_set);
     if (artwork.gfx_current == NULL)
       artwork.gfx_current =
-       getTreeInfoFromIdentifier(artwork.gfx_first, GFX_CLASSIC_SUBDIR);
+       getTreeInfoFromIdentifier(artwork.gfx_first, GFX_DEFAULT_SUBDIR);
     if (artwork.gfx_current == NULL)
       artwork.gfx_current = getFirstValidTreeInfoEntry(artwork.gfx_first);
   }
@@ -2674,7 +3752,7 @@ void LoadLevelArtworkInfo()
       getTreeInfoFromIdentifier(artwork.snd_first, setup.sounds_set);
     if (artwork.snd_current == NULL)
       artwork.snd_current =
-       getTreeInfoFromIdentifier(artwork.snd_first, SND_CLASSIC_SUBDIR);
+       getTreeInfoFromIdentifier(artwork.snd_first, SND_DEFAULT_SUBDIR);
     if (artwork.snd_current == NULL)
       artwork.snd_current = getFirstValidTreeInfoEntry(artwork.snd_first);
   }
@@ -2685,52 +3763,227 @@ void LoadLevelArtworkInfo()
       getTreeInfoFromIdentifier(artwork.mus_first, setup.music_set);
     if (artwork.mus_current == NULL)
       artwork.mus_current =
-       getTreeInfoFromIdentifier(artwork.mus_first, MUS_CLASSIC_SUBDIR);
+       getTreeInfoFromIdentifier(artwork.mus_first, MUS_DEFAULT_SUBDIR);
     if (artwork.mus_current == NULL)
       artwork.mus_current = getFirstValidTreeInfoEntry(artwork.mus_first);
   }
 
+  print_timestamp_time("getTreeInfoFromIdentifier");
+
   sortTreeInfo(&artwork.gfx_first);
   sortTreeInfo(&artwork.snd_first);
   sortTreeInfo(&artwork.mus_first);
 
-#if 0
+  print_timestamp_time("sortTreeInfo");
+
+#if ENABLE_UNUSED_CODE
   dumpTreeInfo(artwork.gfx_first, 0);
   dumpTreeInfo(artwork.snd_first, 0);
   dumpTreeInfo(artwork.mus_first, 0);
 #endif
+
+  print_timestamp_done("LoadLevelArtworkInfo");
+}
+
+static boolean AddUserLevelSetToLevelInfoExt(char *level_subdir_new)
+{
+  // get level info tree node of first (original) user level set
+  char *level_subdir_old = getLoginName();
+  LevelDirTree *leveldir_old = getTreeInfoFromIdentifier(leveldir_first,
+                                                        level_subdir_old);
+  if (leveldir_old == NULL)            // should not happen
+    return FALSE;
+
+  int draw_deactivation_mask = GetDrawDeactivationMask();
+
+  // override draw deactivation mask (temporarily disable drawing)
+  SetDrawDeactivationMask(REDRAW_ALL);
+
+  // load new level set config and add it next to first user level set
+  LoadLevelInfoFromLevelConf(&leveldir_old->next, NULL,
+                            leveldir_old->basepath, level_subdir_new);
+
+  // set draw deactivation mask to previous value
+  SetDrawDeactivationMask(draw_deactivation_mask);
+
+  // get level info tree node of newly added user level set
+  LevelDirTree *leveldir_new = getTreeInfoFromIdentifier(leveldir_first,
+                                                        level_subdir_new);
+  if (leveldir_new == NULL)            // should not happen
+    return FALSE;
+
+  // correct top link and parent node link of newly created tree node
+  leveldir_new->node_top    = leveldir_old->node_top;
+  leveldir_new->node_parent = leveldir_old->node_parent;
+
+  // sort level info tree to adjust position of newly added level set
+  sortTreeInfo(&leveldir_first);
+
+  return TRUE;
+}
+
+void AddUserLevelSetToLevelInfo(char *level_subdir_new)
+{
+  if (!AddUserLevelSetToLevelInfoExt(level_subdir_new))
+    Error(ERR_EXIT, "internal level set structure corrupted -- aborting");
+}
+
+char *getArtworkIdentifierForUserLevelSet(int type)
+{
+  char *classic_artwork_set = getClassicArtworkSet(type);
+
+  // check for custom artwork configured in "levelinfo.conf"
+  char *leveldir_artwork_set =
+    *LEVELDIR_ARTWORK_SET_PTR(leveldir_current, type);
+  boolean has_leveldir_artwork_set =
+    (leveldir_artwork_set != NULL && !strEqual(leveldir_artwork_set,
+                                              classic_artwork_set));
+
+  // check for custom artwork in sub-directory "graphics" etc.
+  TreeInfo *artwork_first_node = ARTWORK_FIRST_NODE(artwork, type);
+  char *leveldir_identifier = leveldir_current->identifier;
+  boolean has_artwork_subdir =
+    (getTreeInfoFromIdentifier(artwork_first_node,
+                              leveldir_identifier) != NULL);
+
+  return (has_leveldir_artwork_set ? leveldir_artwork_set :
+         has_artwork_subdir       ? leveldir_identifier :
+         classic_artwork_set);
+}
+
+TreeInfo *getArtworkTreeInfoForUserLevelSet(int type)
+{
+  char *artwork_set = getArtworkIdentifierForUserLevelSet(type);
+  TreeInfo *artwork_first_node = ARTWORK_FIRST_NODE(artwork, type);
+
+  return getTreeInfoFromIdentifier(artwork_first_node, artwork_set);
+}
+
+boolean checkIfCustomArtworkExistsForCurrentLevelSet(void)
+{
+  char *graphics_set =
+    getArtworkIdentifierForUserLevelSet(ARTWORK_TYPE_GRAPHICS);
+  char *sounds_set =
+    getArtworkIdentifierForUserLevelSet(ARTWORK_TYPE_SOUNDS);
+  char *music_set =
+    getArtworkIdentifierForUserLevelSet(ARTWORK_TYPE_MUSIC);
+
+  return (!strEqual(graphics_set, GFX_CLASSIC_SUBDIR) ||
+         !strEqual(sounds_set,   SND_CLASSIC_SUBDIR) ||
+         !strEqual(music_set,    MUS_CLASSIC_SUBDIR));
+}
+
+boolean UpdateUserLevelSet(char *level_subdir, char *level_name,
+                          char *level_author, int num_levels)
+{
+  char *filename = getPath2(getUserLevelDir(level_subdir), LEVELINFO_FILENAME);
+  char *filename_tmp = getStringCat2(filename, ".tmp");
+  FILE *file = NULL;
+  FILE *file_tmp = NULL;
+  char line[MAX_LINE_LEN];
+  boolean success = FALSE;
+  LevelDirTree *leveldir = getTreeInfoFromIdentifier(leveldir_first,
+                                                    level_subdir);
+  // update values in level directory tree
+
+  if (level_name != NULL)
+    setString(&leveldir->name, level_name);
+
+  if (level_author != NULL)
+    setString(&leveldir->author, level_author);
+
+  if (num_levels != -1)
+    leveldir->levels = num_levels;
+
+  // update values that depend on other values
+
+  setString(&leveldir->name_sorting, leveldir->name);
+
+  leveldir->last_level = leveldir->first_level + leveldir->levels - 1;
+
+  // sort order of level sets may have changed
+  sortTreeInfo(&leveldir_first);
+
+  if ((file     = fopen(filename,     MODE_READ)) &&
+      (file_tmp = fopen(filename_tmp, MODE_WRITE)))
+  {
+    while (fgets(line, MAX_LINE_LEN, file))
+    {
+      if (strPrefix(line, "name:") && level_name != NULL)
+       fprintf(file_tmp, "%-32s%s\n", "name:", level_name);
+      else if (strPrefix(line, "author:") && level_author != NULL)
+       fprintf(file_tmp, "%-32s%s\n", "author:", level_author);
+      else if (strPrefix(line, "levels:") && num_levels != -1)
+       fprintf(file_tmp, "%-32s%d\n", "levels:", num_levels);
+      else
+       fputs(line, file_tmp);
+    }
+
+    success = TRUE;
+  }
+
+  if (file)
+    fclose(file);
+
+  if (file_tmp)
+    fclose(file_tmp);
+
+  if (success)
+    success = (rename(filename_tmp, filename) == 0);
+
+  free(filename);
+  free(filename_tmp);
+
+  return success;
 }
 
-static void SaveUserLevelInfo()
+boolean CreateUserLevelSet(char *level_subdir, char *level_name,
+                          char *level_author, int num_levels,
+                          boolean use_artwork_set)
 {
   LevelDirTree *level_info;
   char *filename;
   FILE *file;
   int i;
 
-  filename = getPath2(getUserLevelDir(getLoginName()), LEVELINFO_FILENAME);
+  // create user level sub-directory, if needed
+  createDirectory(getUserLevelDir(level_subdir), "user level", PERMS_PRIVATE);
+
+  filename = getPath2(getUserLevelDir(level_subdir), LEVELINFO_FILENAME);
 
   if (!(file = fopen(filename, MODE_WRITE)))
   {
     Error(ERR_WARN, "cannot write level info file '%s'", filename);
     free(filename);
-    return;
+
+    return FALSE;
   }
 
   level_info = newTreeInfo();
 
-  /* always start with reliable default values */
+  // always start with reliable default values
   setTreeInfoToDefaults(level_info, TREE_TYPE_LEVEL_DIR);
 
-  setString(&level_info->name, getLoginName());
-  setString(&level_info->author, getRealName());
-  level_info->levels = 100;
+  setString(&level_info->name, level_name);
+  setString(&level_info->author, level_author);
+  level_info->levels = num_levels;
   level_info->first_level = 1;
+  level_info->sort_priority = LEVELCLASS_PRIVATE_START;
+  level_info->readonly = FALSE;
+
+  if (use_artwork_set)
+  {
+    level_info->graphics_set =
+      getStringCopy(getArtworkIdentifierForUserLevelSet(ARTWORK_TYPE_GRAPHICS));
+    level_info->sounds_set =
+      getStringCopy(getArtworkIdentifierForUserLevelSet(ARTWORK_TYPE_SOUNDS));
+    level_info->music_set =
+      getStringCopy(getArtworkIdentifierForUserLevelSet(ARTWORK_TYPE_MUSIC));
+  }
 
   token_value_position = TOKEN_VALUE_POSITION_SHORT;
 
-  fprintf(file, "%s\n\n", getFormattedSetupEntry(TOKEN_STR_FILE_IDENTIFIER,
-                                                getCookie("LEVELINFO")));
+  fprintFileHeader(file, LEVELINFO_FILENAME);
 
   ldi = *level_info;
   for (i = 0; i < NUM_LEVELINFO_TOKENS; i++)
@@ -2738,11 +3991,18 @@ static void SaveUserLevelInfo()
     if (i == LEVELINFO_TOKEN_NAME ||
        i == LEVELINFO_TOKEN_AUTHOR ||
        i == LEVELINFO_TOKEN_LEVELS ||
-       i == LEVELINFO_TOKEN_FIRST_LEVEL)
+       i == LEVELINFO_TOKEN_FIRST_LEVEL ||
+       i == LEVELINFO_TOKEN_SORT_PRIORITY ||
+       i == LEVELINFO_TOKEN_READONLY ||
+       (use_artwork_set && (i == LEVELINFO_TOKEN_GRAPHICS_SET ||
+                            i == LEVELINFO_TOKEN_SOUNDS_SET ||
+                            i == LEVELINFO_TOKEN_MUSIC_SET)))
       fprintf(file, "%s\n", getSetupLine(levelinfo_tokens, "", i));
 
-    /* just to make things nicer :) */
-    if (i == LEVELINFO_TOKEN_AUTHOR)
+    // just to make things nicer :)
+    if (i == LEVELINFO_TOKEN_AUTHOR ||
+       i == LEVELINFO_TOKEN_FIRST_LEVEL ||
+       (use_artwork_set && i == LEVELINFO_TOKEN_READONLY))
       fprintf(file, "\n");     
   }
 
@@ -2754,6 +4014,13 @@ static void SaveUserLevelInfo()
 
   freeTreeInfo(level_info);
   free(filename);
+
+  return TRUE;
+}
+
+static void SaveUserLevelInfo(void)
+{
+  CreateUserLevelSet(getLoginName(), getLoginName(), getRealName(), 100, FALSE);
 }
 
 char *getSetupValue(int type, void *value)
@@ -2773,10 +4040,20 @@ char *getSetupValue(int type, void *value)
       strcpy(value_string, (*(boolean *)value ? "on" : "off"));
       break;
 
+    case TYPE_SWITCH3:
+      strcpy(value_string, (*(int *)value == AUTO  ? "auto" :
+                           *(int *)value == FALSE ? "off" : "on"));
+      break;
+
     case TYPE_YES_NO:
       strcpy(value_string, (*(boolean *)value ? "yes" : "no"));
       break;
 
+    case TYPE_YES_NO_AUTO:
+      strcpy(value_string, (*(int *)value == AUTO  ? "auto" :
+                           *(int *)value == FALSE ? "no" : "yes"));
+      break;
+
     case TYPE_ECS_AGA:
       strcpy(value_string, (*(boolean *)value ? "AGA" : "ECS"));
       break;
@@ -2794,9 +4071,16 @@ char *getSetupValue(int type, void *value)
       break;
 
     case TYPE_STRING:
+      if (*(char **)value == NULL)
+       return NULL;
+
       strcpy(value_string, *(char **)value);
       break;
 
+    case TYPE_PLAYER:
+      sprintf(value_string, "player_%d", *(int *)value + 1);
+      break;
+
     default:
       value_string[0] = '\0';
       break;
@@ -2818,10 +4102,10 @@ char *getSetupLine(struct TokenInfo *token_info, char *prefix, int token_nr)
   char *token_text = token_info[token_nr].text;
   char *value_string = getSetupValue(token_type, setup_value);
 
-  /* build complete token string */
+  // build complete token string
   sprintf(token_string, "%s%s", prefix, token_text);
 
-  /* build setup entry line */
+  // build setup entry line
   line = getFormattedSetupEntry(token_string, value_string);
 
   if (token_type == TYPE_KEY_X11)
@@ -2829,11 +4113,11 @@ char *getSetupLine(struct TokenInfo *token_info, char *prefix, int token_nr)
     Key key = *(Key *)setup_value;
     char *keyname = getKeyNameFromKey(key);
 
-    /* add comment, if useful */
+    // add comment, if useful
     if (!strEqual(keyname, "(undefined)") &&
        !strEqual(keyname, "(unknown)"))
     {
-      /* add at least one whitespace */
+      // add at least one whitespace
       strcat(line, " ");
       for (i = strlen(line); i < token_comment_position; i++)
        strcat(line, " ");
@@ -2846,18 +4130,26 @@ char *getSetupLine(struct TokenInfo *token_info, char *prefix, int token_nr)
   return line;
 }
 
-void LoadLevelSetup_LastSeries()
+void LoadLevelSetup_LastSeries(void)
 {
-  /* ----------------------------------------------------------------------- */
-  /* ~/.<program>/levelsetup.conf                                            */
-  /* ----------------------------------------------------------------------- */
+  // --------------------------------------------------------------------------
+  // ~/.<program>/levelsetup.conf
+  // --------------------------------------------------------------------------
 
   char *filename = getPath2(getSetupDir(), LEVELSETUP_FILENAME);
   SetupFileHash *level_setup_hash = NULL;
 
-  /* always start with reliable default values */
+  // always start with reliable default values
   leveldir_current = getFirstValidTreeInfoEntry(leveldir_first);
 
+  if (!strEqual(DEFAULT_LEVELSET, UNDEFINED_LEVELSET))
+  {
+    leveldir_current = getTreeInfoFromIdentifier(leveldir_first,
+                                                DEFAULT_LEVELSET);
+    if (leveldir_current == NULL)
+      leveldir_current = getFirstValidTreeInfoEntry(leveldir_first);
+  }
+
   if ((level_setup_hash = loadSetupFileHash(filename)))
   {
     char *last_level_series =
@@ -2868,22 +4160,25 @@ void LoadLevelSetup_LastSeries()
     if (leveldir_current == NULL)
       leveldir_current = getFirstValidTreeInfoEntry(leveldir_first);
 
-    checkSetupFileHashIdentifier(level_setup_hash, filename,
-                                getCookie("LEVELSETUP"));
-
     freeSetupFileHash(level_setup_hash);
   }
   else
-    Error(ERR_WARN, "using default setup values");
+  {
+    Error(ERR_DEBUG, "using default setup values");
+  }
 
   free(filename);
 }
 
-void SaveLevelSetup_LastSeries()
+static void SaveLevelSetup_LastSeries_Ext(boolean deactivate_last_level_series)
 {
-  /* ----------------------------------------------------------------------- */
-  /* ~/.<program>/levelsetup.conf                                            */
-  /* ----------------------------------------------------------------------- */
+  // --------------------------------------------------------------------------
+  // ~/.<program>/levelsetup.conf
+  // --------------------------------------------------------------------------
+
+  // check if the current level directory structure is available at this point
+  if (leveldir_current == NULL)
+    return;
 
   char *filename = getPath2(getSetupDir(), LEVELSETUP_FILENAME);
   char *level_subdir = leveldir_current->subdir;
@@ -2894,12 +4189,17 @@ void SaveLevelSetup_LastSeries()
   if (!(file = fopen(filename, MODE_WRITE)))
   {
     Error(ERR_WARN, "cannot write setup file '%s'", filename);
+
     free(filename);
+
     return;
   }
 
-  fprintf(file, "%s\n\n", getFormattedSetupEntry(TOKEN_STR_FILE_IDENTIFIER,
-                                                getCookie("LEVELSETUP")));
+  fprintFileHeader(file, LEVELSETUP_FILENAME);
+
+  if (deactivate_last_level_series)
+    fprintf(file, "# %s\n# ", "the following level set may have caused a problem and was deactivated");
+
   fprintf(file, "%s\n", getFormattedSetupEntry(TOKEN_STR_LAST_LEVEL_SERIES,
                                               level_subdir));
 
@@ -2910,71 +4210,59 @@ void SaveLevelSetup_LastSeries()
   free(filename);
 }
 
-static void checkSeriesInfo()
+void SaveLevelSetup_LastSeries(void)
+{
+  SaveLevelSetup_LastSeries_Ext(FALSE);
+}
+
+void SaveLevelSetup_LastSeries_Deactivate(void)
+{
+  SaveLevelSetup_LastSeries_Ext(TRUE);
+}
+
+static void checkSeriesInfo(void)
 {
   static char *level_directory = NULL;
-  DIR *dir;
-  struct dirent *dir_entry;
+  Directory *dir;
 
-  /* check for more levels besides the 'levels' field of 'levelinfo.conf' */
+  // check for more levels besides the 'levels' field of 'levelinfo.conf'
 
   level_directory = getPath2((leveldir_current->in_user_dir ?
                              getUserLevelDir(NULL) :
                              options.level_directory),
                             leveldir_current->fullpath);
 
-  if ((dir = opendir(level_directory)) == NULL)
+  if ((dir = openDirectory(level_directory)) == NULL)
   {
     Error(ERR_WARN, "cannot read level directory '%s'", level_directory);
-    return;
-  }
 
-  while ((dir_entry = readdir(dir)) != NULL)   /* last directory entry */
-  {
-    if (strlen(dir_entry->d_name) > 4 &&
-       dir_entry->d_name[3] == '.' &&
-       strEqual(&dir_entry->d_name[4], LEVELFILE_EXTENSION))
-    {
-      char levelnum_str[4];
-      int levelnum_value;
-
-      strncpy(levelnum_str, dir_entry->d_name, 3);
-      levelnum_str[3] = '\0';
-
-      levelnum_value = atoi(levelnum_str);
-
-#if 0
-      if (levelnum_value < leveldir_current->first_level)
-      {
-       Error(ERR_WARN, "additional level %d found", levelnum_value);
-       leveldir_current->first_level = levelnum_value;
-      }
-      else if (levelnum_value > leveldir_current->last_level)
-      {
-       Error(ERR_WARN, "additional level %d found", levelnum_value);
-       leveldir_current->last_level = levelnum_value;
-      }
-#endif
-    }
+    return;
   }
 
-  closedir(dir);
+  closeDirectory(dir);
 }
 
-void LoadLevelSetup_SeriesInfo()
+void LoadLevelSetup_SeriesInfo(void)
 {
   char *filename;
   SetupFileHash *level_setup_hash = NULL;
   char *level_subdir = leveldir_current->subdir;
+  int i;
 
-  /* always start with reliable default values */
+  // always start with reliable default values
   level_nr = leveldir_current->first_level;
 
-  checkSeriesInfo(leveldir_current);
+  for (i = 0; i < MAX_LEVELS; i++)
+  {
+    LevelStats_setPlayed(i, 0);
+    LevelStats_setSolved(i, 0);
+  }
+
+  checkSeriesInfo();
 
-  /* ----------------------------------------------------------------------- */
-  /* ~/.<program>/levelsetup/<level series>/levelsetup.conf                  */
-  /* ----------------------------------------------------------------------- */
+  // --------------------------------------------------------------------------
+  // ~/.<program>/levelsetup/<level series>/levelsetup.conf
+  // --------------------------------------------------------------------------
 
   level_subdir = leveldir_current->subdir;
 
@@ -2984,6 +4272,8 @@ void LoadLevelSetup_SeriesInfo()
   {
     char *token_value;
 
+    // get last played level in this level set
+
     token_value = getHashEntry(level_setup_hash, TOKEN_STR_LAST_PLAYED_LEVEL);
 
     if (token_value)
@@ -2996,6 +4286,8 @@ void LoadLevelSetup_SeriesInfo()
        level_nr = leveldir_current->last_level;
     }
 
+    // get handicap level in this level set
+
     token_value = getHashEntry(level_setup_hash, TOKEN_STR_HANDICAP_LEVEL);
 
     if (token_value)
@@ -3013,28 +4305,53 @@ void LoadLevelSetup_SeriesInfo()
       leveldir_current->handicap_level = level_nr;
     }
 
-    checkSetupFileHashIdentifier(level_setup_hash, filename,
-                                getCookie("LEVELSETUP"));
+    // get number of played and solved levels in this level set
+
+    BEGIN_HASH_ITERATION(level_setup_hash, itr)
+    {
+      char *token = HASH_ITERATION_TOKEN(itr);
+      char *value = HASH_ITERATION_VALUE(itr);
+
+      if (strlen(token) == 3 &&
+         token[0] >= '0' && token[0] <= '9' &&
+         token[1] >= '0' && token[1] <= '9' &&
+         token[2] >= '0' && token[2] <= '9')
+      {
+       int level_nr = atoi(token);
+
+       if (value != NULL)
+         LevelStats_setPlayed(level_nr, atoi(value));  // read 1st column
+
+       value = strchr(value, ' ');
+
+       if (value != NULL)
+         LevelStats_setSolved(level_nr, atoi(value));  // read 2nd column
+      }
+    }
+    END_HASH_ITERATION(hash, itr)
 
     freeSetupFileHash(level_setup_hash);
   }
   else
-    Error(ERR_WARN, "using default setup values");
+  {
+    Error(ERR_DEBUG, "using default setup values");
+  }
 
   free(filename);
 }
 
-void SaveLevelSetup_SeriesInfo()
+void SaveLevelSetup_SeriesInfo(void)
 {
   char *filename;
   char *level_subdir = leveldir_current->subdir;
   char *level_nr_str = int2str(level_nr, 0);
   char *handicap_level_str = int2str(leveldir_current->handicap_level, 0);
   FILE *file;
+  int i;
 
-  /* ----------------------------------------------------------------------- */
-  /* ~/.<program>/levelsetup/<level series>/levelsetup.conf                  */
-  /* ----------------------------------------------------------------------- */
+  // --------------------------------------------------------------------------
+  // ~/.<program>/levelsetup/<level series>/levelsetup.conf
+  // --------------------------------------------------------------------------
 
   InitLevelSetupDirectory(level_subdir);
 
@@ -3047,12 +4364,28 @@ void SaveLevelSetup_SeriesInfo()
     return;
   }
 
-  fprintf(file, "%s\n\n", getFormattedSetupEntry(TOKEN_STR_FILE_IDENTIFIER,
-                                                getCookie("LEVELSETUP")));
+  fprintFileHeader(file, LEVELSETUP_FILENAME);
+
   fprintf(file, "%s\n", getFormattedSetupEntry(TOKEN_STR_LAST_PLAYED_LEVEL,
                                               level_nr_str));
-  fprintf(file, "%s\n", getFormattedSetupEntry(TOKEN_STR_HANDICAP_LEVEL,
-                                              handicap_level_str));
+  fprintf(file, "%s\n\n", getFormattedSetupEntry(TOKEN_STR_HANDICAP_LEVEL,
+                                                handicap_level_str));
+
+  for (i = leveldir_current->first_level; i <= leveldir_current->last_level;
+       i++)
+  {
+    if (LevelStats_getPlayed(i) > 0 ||
+       LevelStats_getSolved(i) > 0)
+    {
+      char token[16];
+      char value[16];
+
+      sprintf(token, "%03d", i);
+      sprintf(value, "%d %d", LevelStats_getPlayed(i), LevelStats_getSolved(i));
+
+      fprintf(file, "%s\n", getFormattedSetupEntry(token, value));
+    }
+  }
 
   fclose(file);
 
@@ -3060,3 +4393,37 @@ void SaveLevelSetup_SeriesInfo()
 
   free(filename);
 }
+
+int LevelStats_getPlayed(int nr)
+{
+  return (nr >= 0 && nr < MAX_LEVELS ? level_stats[nr].played : 0);
+}
+
+int LevelStats_getSolved(int nr)
+{
+  return (nr >= 0 && nr < MAX_LEVELS ? level_stats[nr].solved : 0);
+}
+
+void LevelStats_setPlayed(int nr, int value)
+{
+  if (nr >= 0 && nr < MAX_LEVELS)
+    level_stats[nr].played = value;
+}
+
+void LevelStats_setSolved(int nr, int value)
+{
+  if (nr >= 0 && nr < MAX_LEVELS)
+    level_stats[nr].solved = value;
+}
+
+void LevelStats_incPlayed(int nr)
+{
+  if (nr >= 0 && nr < MAX_LEVELS)
+    level_stats[nr].played++;
+}
+
+void LevelStats_incSolved(int nr)
+{
+  if (nr >= 0 && nr < MAX_LEVELS)
+    level_stats[nr].solved++;
+}