added extracting level and artwork sets from zip files at program start
[rocksndiamonds.git] / src / libgame / setup.c
index 45509d79019f31e6df678779eb462c4aaabd8903..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] =
@@ -99,9 +102,9 @@ static SetupFileHash *artworkinfo_cache_new = NULL;
 static boolean use_artworkinfo_cache = TRUE;
 
 
-/* ------------------------------------------------------------------------- */
-/* file functions                                                            */
-/* ------------------------------------------------------------------------- */
+// ----------------------------------------------------------------------------
+// file functions
+// ----------------------------------------------------------------------------
 
 static char *getLevelClassDescription(TreeInfo *ti)
 {
@@ -113,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;
 }
@@ -161,7 +158,7 @@ static char *getLevelSetupDir(char *level_subdir)
   return levelsetup_dir;
 }
 
-static char *getCacheDir()
+static char *getCacheDir(void)
 {
   static char *cache_dir = NULL;
 
@@ -171,6 +168,16 @@ static char *getCacheDir()
   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;
@@ -186,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;
@@ -207,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();
@@ -262,34 +323,14 @@ static char *getDefaultMusicDir(char *music_subdir)
   return music_dir;
 }
 
-#if 1
-static char *getDefaultArtworkSet(int type)
-{
-  return (type == TREE_TYPE_GRAPHICS_DIR ? "gfx_classic" :
-         type == TREE_TYPE_SOUNDS_DIR   ? "snd_classic" :
-         type == TREE_TYPE_MUSIC_DIR    ? "mus_classic" : "");
-}
-
-static char *getDefaultArtworkDir(int type)
-{
-  return (type == TREE_TYPE_GRAPHICS_DIR ?
-         getDefaultGraphicsDir("gfx_classic") :
-         type == TREE_TYPE_SOUNDS_DIR ?
-         getDefaultSoundsDir("snd_classic") :
-         type == TREE_TYPE_MUSIC_DIR ?
-         getDefaultMusicDir("mus_classic") : "");
-}
-
-#else
-
-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) :
@@ -298,9 +339,8 @@ static char *getDefaultArtworkDir(int type)
          type == TREE_TYPE_MUSIC_DIR ?
          getDefaultMusicDir(MUS_CLASSIC_SUBDIR) : "");
 }
-#endif
 
-static char *getUserGraphicsDir()
+static char *getUserGraphicsDir(void)
 {
   static char *usergraphics_dir = NULL;
 
@@ -310,7 +350,7 @@ static char *getUserGraphicsDir()
   return usergraphics_dir;
 }
 
-static char *getUserSoundsDir()
+static char *getUserSoundsDir(void)
 {
   static char *usersounds_dir = NULL;
 
@@ -320,7 +360,7 @@ static char *getUserSoundsDir()
   return usersounds_dir;
 }
 
-static char *getUserMusicDir()
+static char *getUserMusicDir(void)
 {
   static char *usermusic_dir = NULL;
 
@@ -334,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);
@@ -355,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
     {
@@ -386,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;
@@ -394,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;
@@ -402,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;
@@ -425,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;
 }
 
@@ -436,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;
 
@@ -452,7 +588,12 @@ char *getSetupFilename()
   return filename;
 }
 
-char *getEditorSetupFilename()
+char *getDefaultSetupFilename(void)
+{
+  return program.config_filename;
+}
+
+char *getEditorSetupFilename(void)
 {
   static char *filename = NULL;
 
@@ -468,7 +609,7 @@ char *getEditorSetupFilename()
   return filename;
 }
 
-char *getHelpAnimFilename()
+char *getHelpAnimFilename(void)
 {
   static char *filename = NULL;
 
@@ -479,7 +620,7 @@ char *getHelpAnimFilename()
   return filename;
 }
 
-char *getHelpTextFilename()
+char *getHelpTextFilename(void)
 {
   static char *filename = NULL;
 
@@ -490,7 +631,7 @@ char *getHelpTextFilename()
   return filename;
 }
 
-char *getLevelSetInfoFilename()
+char *getLevelSetInfoFilename(void)
 {
   static char *filename = NULL;
   char *basenames[] =
@@ -519,7 +660,7 @@ char *getLevelSetInfoFilename()
   return NULL;
 }
 
-char *getLevelSetTitleMessageBasename(int nr, boolean initial)
+static char *getLevelSetTitleMessageBasename(int nr, boolean initial)
 {
   static char basename[32];
 
@@ -541,38 +682,38 @@ char *getLevelSetTitleMessageFilename(int nr, boolean initial)
 
   if (!gfx.override_level_graphics)
   {
-    /* 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(), GRAPHICS_DIRECTORY, basename);
     if (fileExists(filename))
       return filename;
 
     free(filename);
 
-    /* 2nd try: look for message file in current level set directory */
+    // 2nd try: look for message file in current level set directory
     filename = getPath2(getCurrentLevelDir(), 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)
     {
-      /* 3rd try: look for special artwork configured in level series config */
+      // 3rd try: look for special artwork configured in level series config
       filename = getPath2(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)
   {
-    /* 4th try: look for special artwork in configured artwork directory */
+    // 4th try: look for special artwork in configured artwork directory
     filename = getPath2(getSetupArtworkDir(artwork.gfx_current), basename);
     if (fileExists(filename))
       return filename;
@@ -580,51 +721,24 @@ char *getLevelSetTitleMessageFilename(int nr, boolean initial)
     free(filename);
   }
 
-  /* 5th try: look for default artwork in new default artwork directory */
-  filename = getPath2(getDefaultGraphicsDir(GFX_CLASSIC_SUBDIR), basename);
+  // 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 */
+  // 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 */
+  return NULL;         // cannot find specified artwork file anywhere
 }
 
 static char *getCorrectedArtworkBasename(char *basename)
 {
-  char *basename_corrected = basename;
-
-#if defined(PLATFORM_MSDOS)
-  if (program.filename_prefix != NULL)
-  {
-    int prefix_len = strlen(program.filename_prefix);
-
-    if (strncmp(basename, program.filename_prefix, prefix_len) == 0)
-      basename_corrected = &basename[prefix_len];
-
-    /* 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;
-
-      checked_free(msdos_filename);
-
-      msdos_filename = getStringCopy(basename_corrected);
-      strncpy(&msdos_filename[8], &basename[strlen(basename) - (1+3)], 1+3 +1);
-
-      basename_corrected = msdos_filename;
-    }
-  }
-#endif
-
-  return basename_corrected;
+  return basename;
 }
 
 char *getCustomImageFilename(char *basename)
@@ -638,66 +752,66 @@ char *getCustomImageFilename(char *basename)
 
   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;
 
-#if CREATE_SPECIAL_EDITION
-  free(filename);
+  if (!strEqual(GFX_FALLBACK_FILENAME, UNDEFINED_FILENAME))
+  {
+    free(filename);
 
-  /* !!! INSERT WARNING HERE TO REPORT MISSING ARTWORK FILES !!! */
-#if 0
-  printf("::: MISSING ARTWORK FILE '%s'\n", basename);
-#endif
+    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.graphics_directory, GFX_FALLBACK_FILENAME);
-  if (fileExists(filename))
-    return filename;
-#endif
+    // 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 */
+  return NULL;         // cannot find specified artwork file anywhere
 }
 
 char *getCustomSoundFilename(char *basename)
@@ -711,31 +825,31 @@ char *getCustomSoundFilename(char *basename)
 
   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;
@@ -743,29 +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;
 
-#if CREATE_SPECIAL_EDITION
-  free(filename);
+  if (!strEqual(SND_FALLBACK_FILENAME, UNDEFINED_FILENAME))
+  {
+    free(filename);
 
-  /* 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;
-#endif
+    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 */
+  return NULL;         // cannot find specified artwork file anywhere
 }
 
 char *getCustomMusicFilename(char *basename)
@@ -779,31 +898,31 @@ char *getCustomMusicFilename(char *basename)
 
   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;
@@ -811,29 +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;
 
-#if CREATE_SPECIAL_EDITION
-  free(filename);
+  if (!strEqual(MUS_FALLBACK_FILENAME, UNDEFINED_FILENAME))
+  {
+    free(filename);
 
-  /* 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;
-#endif
+    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 */
+  return NULL;         // cannot find specified artwork file anywhere
 }
 
 char *getCustomArtworkFilename(char *basename, int type)
@@ -873,51 +997,51 @@ char *getCustomMusicDirectory(void)
 
   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)
@@ -929,16 +1053,22 @@ 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);
@@ -948,6 +1078,17 @@ void InitUserLevelDirectory(char *level_subdir)
   }
 }
 
+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);
@@ -955,18 +1096,18 @@ void InitLevelSetupDirectory(char *level_subdir)
   createDirectory(getLevelSetupDir(level_subdir), "level setup", PERMS_PRIVATE);
 }
 
-void InitCacheDirectory()
+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));
 }
@@ -1009,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;
 }
 
@@ -1027,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;
 }
 
@@ -1100,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;
 
@@ -1113,16 +1254,10 @@ TreeInfo *cloneTreeNode(TreeInfo **node_top, TreeInfo *node_parent,
     return cloneTreeNode(node_top, node_parent, node->next,
                         skip_sets_without_levels);
 
-#if 1
-  node_new = getTreeInfoCopy(node);            /* copy complete node */
-#else
-  node_new = newTreeInfo();
-
-  *node_new = *node;                           /* copy complete node */
-#endif
+  node_new = getTreeInfoCopy(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,
@@ -1134,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);
 
@@ -1180,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);
@@ -1202,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;
 
@@ -1214,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)
   {
@@ -1245,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
@@ -1274,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;
 
@@ -1333,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;
@@ -1365,38 +1508,22 @@ char *getUserGameDataDir(void)
 {
   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;
 }
 
-void updateUserGameDataDir()
-{
-#if defined(PLATFORM_MACOSX)
-  char *userdata_dir_old = getPath2(getHomeDir(), program.userdata_subdir_unix);
-  char *userdata_dir_new = getUserGameDataDir();       /* do not free() this */
-
-  /* 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);
-#endif
-}
-
-char *getSetupDir()
+char *getSetupDir(void)
 {
   return getUserGameDataDir();
 }
@@ -1419,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)
@@ -1453,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;
@@ -1471,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;
@@ -1487,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)
@@ -1509,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;
@@ -1600,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)
 {
@@ -1612,6 +1784,7 @@ static void printSetupFileList(SetupFileList *list)
   printSetupFileList(list->next);
 }
 #endif
+#endif
 
 #ifdef DEBUG
 DEFINE_HASHTABLE_INSERT(insert_hash_entry, char, char);
@@ -1625,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
@@ -1650,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;
 }
@@ -1660,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);
@@ -1676,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)
@@ -1696,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");
@@ -1710,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)
@@ -1721,6 +1895,7 @@ static void printSetupFileHash(SetupFileHash *hash)
   END_HASH_ITERATION(hash, itr)
 }
 #endif
+#endif
 
 #define ALLOW_TOKEN_VALUE_SEPARATOR_BEING_WHITESPACE           1
 #define CHECK_TOKEN_VALUE_SEPARATOR__WARN_IF_MISSING           0
@@ -1743,7 +1918,7 @@ static boolean getTokenValueFromSetupLineExt(char *line,
   static char line_copy[MAX_LINE_LEN + 1], line_raw_copy[MAX_LINE_LEN + 1];
   char *token, *value, *line_ptr;
 
-  /* when externally invoked via ReadTokenValueFromLine(), copy line buffers */
+  // when externally invoked via ReadTokenValueFromLine(), copy line buffers
   if (line_raw == NULL)
   {
     strncpy(line_copy, line, MAX_LINE_LEN);
@@ -1754,7 +1929,7 @@ static boolean getTokenValueFromSetupLineExt(char *line,
     line_raw = line_raw_copy;
   }
 
-  /* cut trailing comment from input line */
+  // cut trailing comment from input line
   for (line_ptr = line; *line_ptr; line_ptr++)
   {
     if (*line_ptr == '#')
@@ -1764,37 +1939,33 @@ static boolean getTokenValueFromSetupLineExt(char *line,
     }
   }
 
-  /* cut trailing whitespaces from input line */
+  // 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';
 
-  /* ignore empty lines */
+  // ignore empty lines
   if (*line == '\0')
     return FALSE;
 
-  /* cut leading whitespaces from token */
+  // cut leading whitespaces from token
   for (token = line; *token; token++)
     if (*token != ' ' && *token != '\t')
       break;
 
-  /* start with empty value as reliable default */
+  // start with empty value as reliable default
   value = "";
 
   token_value_separator_found = FALSE;
 
-  /* find end of token to determine start of value */
+  // find end of token to determine start of value
   for (line_ptr = token; *line_ptr; line_ptr++)
   {
-#if 1
-    /* first look for an explicit token/value separator, like ':' or '=' */
+    // first look for an explicit token/value separator, like ':' or '='
     if (*line_ptr == ':' || *line_ptr == '=')
-#else
-    if (*line_ptr == ' ' || *line_ptr == '\t' || *line_ptr == ':')
-#endif
     {
-      *line_ptr = '\0';                        /* terminate token string */
-      value = line_ptr + 1;            /* set beginning of value */
+      *line_ptr = '\0';                        // terminate token string
+      value = line_ptr + 1;            // set beginning of value
 
       token_value_separator_found = TRUE;
 
@@ -1803,15 +1974,15 @@ static boolean getTokenValueFromSetupLineExt(char *line,
   }
 
 #if ALLOW_TOKEN_VALUE_SEPARATOR_BEING_WHITESPACE
-  /* fallback: if no token/value separator found, also allow whitespaces */
+  // 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 == ' ' || *line_ptr == '\t')
       {
-       *line_ptr = '\0';               /* terminate token string */
-       value = line_ptr + 1;           /* set beginning of value */
+       *line_ptr = '\0';               // terminate token string
+       value = line_ptr + 1;           // set beginning of value
 
        token_value_separator_found = TRUE;
 
@@ -1848,21 +2019,16 @@ static boolean getTokenValueFromSetupLineExt(char *line,
   }
 #endif
 
-  /* cut trailing whitespaces from token */
+  // 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 */
+  // cut leading whitespaces from value
   for (; *value; value++)
     if (*value != ' ' && *value != '\t')
       break;
 
-#if 0
-  if (*value == '\0')
-    value = "true";    /* treat tokens without value as "true" */
-#endif
-
   *token_ptr = token;
   *value_ptr = value;
 
@@ -1871,14 +2037,13 @@ static boolean getTokenValueFromSetupLineExt(char *line,
 
 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 */
+  // 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);
 }
 
-#if 1
 static boolean loadSetupFileData(void *setup_file_data, char *filename,
                                 boolean top_recursion_level, boolean is_hash)
 {
@@ -1887,7 +2052,7 @@ static boolean loadSetupFileData(void *setup_file_data, char *filename,
   char *token, *value, *line_ptr;
   void *insert_ptr = NULL;
   boolean read_continued_line = FALSE;
-  FILE *file;
+  File *file;
   int line_nr = 0, token_count = 0, include_count = 0;
 
 #if CHECK_TOKEN_VALUE_SEPARATOR__WARN_IF_MISSING
@@ -1898,66 +2063,60 @@ static boolean loadSetupFileData(void *setup_file_data, char *filename,
   token_already_exists_warning = FALSE;
 #endif
 
-  if (!(file = fopen(filename, MODE_READ)))
+  if (!(file = openFile(filename, MODE_READ)))
   {
-    Error(ERR_WARN, "cannot open configuration file '%s'", filename);
+#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 */
+  // 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) */
+  // 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) */
+  // mark this file as already included (to prevent including it again)
   setHashEntry(include_filename_hash, getBaseNamePtr(filename), "true");
 
-  while (!feof(file))
+  while (!checkEndOfFile(file))
   {
-    /* read next line of input file */
-    if (!fgets(line, MAX_LINE_LEN, 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 */
+    // 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) */
+    // 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 == '\n' || *line_ptr == '\r') && *(line_ptr + 1) == '\0')
        *line_ptr = '\0';
 
-    /* copy raw input line for later use (mainly debugging output) */
+    // copy raw input line for later use (mainly debugging output)
     strcpy(line_raw, line);
 
     if (read_continued_line)
     {
-#if 0
-      /* !!! ??? WHY ??? !!! */
-      /* cut leading whitespaces from input line */
-      for (line_ptr = line; *line_ptr; line_ptr++)
-       if (*line_ptr != ' ' && *line_ptr != '\t')
-         break;
-#endif
-
-      /* append new line to existing line, if there is enough space */
+      // 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);
 
-      strcpy(line, previous_line);     /* copy storage buffer to line */
+      strcpy(line, previous_line);     // copy storage buffer to line
 
       read_continued_line = FALSE;
     }
 
-    /* if the last character is '\', continue at next line */
+    // if the last character is '\', continue at next line
     if (strlen(line) > 0 && line[strlen(line) - 1] == '\\')
     {
-      line[strlen(line) - 1] = '\0';   /* cut off trailing backslash */
-      strcpy(previous_line, line);     /* copy line to storage buffer */
+      line[strlen(line) - 1] = '\0';   // cut off trailing backslash
+      strcpy(previous_line, line);     // copy line to storage buffer
 
       read_continued_line = TRUE;
 
@@ -1978,10 +2137,6 @@ static boolean loadSetupFileData(void *setup_file_data, char *filename,
          char *basename = getBaseName(value);
          char *filename_include = getPath2(basepath, basename);
 
-#if 0
-         Error(ERR_INFO, "[including file '%s']", filename_include);
-#endif
-
          loadSetupFileData(setup_file_data, filename_include, FALSE, is_hash);
 
          free(basepath);
@@ -2032,7 +2187,7 @@ static boolean loadSetupFileData(void *setup_file_data, char *filename,
     }
   }
 
-  fclose(file);
+  closeFile(file);
 
 #if CHECK_TOKEN_VALUE_SEPARATOR__WARN_IF_MISSING
   if (token_value_separator_warning)
@@ -2053,259 +2208,26 @@ static boolean loadSetupFileData(void *setup_file_data, char *filename,
   return TRUE;
 }
 
-#else
-
-static boolean loadSetupFileData(void *setup_file_data, char *filename,
-                                boolean top_recursion_level, boolean is_hash)
+static void saveSetupFileHash(SetupFileHash *hash, char *filename)
 {
-  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;
-  int token_count = 0;
-
-#if CHECK_TOKEN_VALUE_SEPARATOR__WARN_IF_MISSING
-  token_value_separator_warning = FALSE;
-#endif
 
-  if (!(file = fopen(filename, MODE_READ)))
+  if (!(file = fopen(filename, MODE_WRITE)))
   {
-    Error(ERR_WARN, "cannot open configuration file '%s'", filename);
+    Error(ERR_WARN, "cannot write configuration file '%s'", filename);
 
-    return FALSE;
+    return;
   }
 
-  /* use "insert pointer" to store list end for constant insertion complexity */
-  if (!is_hash)
-    insert_ptr = setup_file_data;
+  BEGIN_HASH_ITERATION(hash, itr)
+  {
+    fprintf(file, "%s\n", getFormattedSetupEntry(HASH_ITERATION_TOKEN(itr),
+                                                HASH_ITERATION_VALUE(itr)));
+  }
+  END_HASH_ITERATION(hash, itr)
 
-  /* 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 (!feof(file))
-  {
-    /* read next line of input file */
-    if (!fgets(line, MAX_LINE_LEN, file))
-      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 == '\n' || *line_ptr == '\r') && *(line_ptr + 1) == '\0')
-       *line_ptr = '\0';
-
-    /* copy raw input line for later use (mainly debugging output) */
-    strcpy(line_raw, line);
-
-    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;
-
-      /* 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);
-
-      strcpy(line, previous_line);     /* copy storage buffer to line */
-
-      read_continued_line = FALSE;
-    }
-
-    /* if the last character is '\', continue at next line */
-    if (strlen(line) > 0 && line[strlen(line) - 1] == '\\')
-    {
-      line[strlen(line) - 1] = '\0';   /* cut off trailing backslash */
-      strcpy(previous_line, line);     /* copy line to storage buffer */
-
-      read_continued_line = TRUE;
-
-      continue;
-    }
-
-    /* cut trailing comment from input line */
-    for (line_ptr = line; *line_ptr; line_ptr++)
-    {
-      if (*line_ptr == '#')
-      {
-       *line_ptr = '\0';
-       break;
-      }
-    }
-
-    /* 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';
-
-    /* ignore empty lines */
-    if (*line == '\0')
-      continue;
-
-    /* cut leading whitespaces from token */
-    for (token = line; *token; token++)
-      if (*token != ' ' && *token != '\t')
-       break;
-
-    /* start with empty value as reliable default */
-    value = "";
-
-    token_value_separator_found = FALSE;
-
-    /* find end of token to determine start of value */
-    for (line_ptr = token; *line_ptr; line_ptr++)
-    {
-#if 1
-      /* first look for an explicit token/value separator, like ':' or '=' */
-      if (*line_ptr == ':' || *line_ptr == '=')
-#else
-      if (*line_ptr == ' ' || *line_ptr == '\t' || *line_ptr == ':')
-#endif
-      {
-       *line_ptr = '\0';               /* terminate token string */
-       value = line_ptr + 1;           /* set beginning of value */
-
-       token_value_separator_found = TRUE;
-
-       break;
-      }
-    }
-
-#if ALLOW_TOKEN_VALUE_SEPARATOR_BEING_WHITESPACE
-    /* fallback: if no token/value separator found, also allow whitespaces */
-    if (!token_value_separator_found)
-    {
-      for (line_ptr = token; *line_ptr; line_ptr++)
-      {
-       if (*line_ptr == ' ' || *line_ptr == '\t')
-       {
-         *line_ptr = '\0';             /* terminate token string */
-         value = line_ptr + 1;         /* set beginning of value */
-
-         token_value_separator_found = TRUE;
-
-         break;
-       }
-      }
-
-#if CHECK_TOKEN_VALUE_SEPARATOR__WARN_IF_MISSING
-      if (token_value_separator_found)
-      {
-       if (!token_value_separator_warning)
-       {
-         Error(ERR_INFO_LINE, "-");
-         Error(ERR_WARN, "missing token/value separator(s) in config file:");
-         Error(ERR_INFO, "- config file: '%s'", filename);
-
-         token_value_separator_warning = TRUE;
-       }
-
-       Error(ERR_INFO, "- line %d: '%s'", line_nr, 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;
-
-#if 0
-    if (*value == '\0')
-      value = "true";  /* treat tokens without value as "true" */
-#endif
-
-    if (*token)
-    {
-      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);
-
-#if 0
-         Error(ERR_INFO, "[including file '%s']", filename_include);
-#endif
-
-         loadSetupFileData(setup_file_data, filename_include, FALSE, is_hash);
-
-         free(basepath);
-         free(basename);
-         free(filename_include);
-       }
-       else
-       {
-         Error(ERR_WARN, "ignoring already processed file '%s'", value);
-       }
-      }
-      else
-      {
-       if (is_hash)
-         setHashEntry((SetupFileHash *)setup_file_data, token, value);
-       else
-         insert_ptr = addListEntry((SetupFileList *)insert_ptr, token, value);
-
-       token_count++;
-      }
-    }
-  }
-
-  fclose(file);
-
-#if CHECK_TOKEN_VALUE_SEPARATOR__WARN_IF_MISSING
-  if (token_value_separator_warning)
-    Error(ERR_INFO_LINE, "-");
-#endif
-
-  if (token_count == 0)
-    Error(ERR_WARN, "configuration file '%s' is empty", filename);
-
-  if (top_recursion_level)
-    freeSetupFileHash(include_filename_hash);
-
-  return TRUE;
-}
-#endif
-
-void saveSetupFileHash(SetupFileHash *hash, char *filename)
-{
-  FILE *file;
-
-  if (!(file = fopen(filename, MODE_WRITE)))
-  {
-    Error(ERR_WARN, "cannot write configuration file '%s'", filename);
-
-    return;
-  }
-
-  BEGIN_HASH_ITERATION(hash, itr)
-  {
-    fprintf(file, "%s\n", getFormattedSetupEntry(HASH_ITERATION_TOKEN(itr),
-                                                HASH_ITERATION_VALUE(itr)));
-  }
-  END_HASH_ITERATION(hash, itr)
-
-  fclose(file);
-}
+  fclose(file);
+}
 
 SetupFileList *loadSetupFileList(char *filename)
 {
@@ -2321,7 +2243,7 @@ SetupFileList *loadSetupFileList(char *filename)
 
   first_valid_list_entry = setup_file_list->next;
 
-  /* free empty list header */
+  // free empty list header
   setup_file_list->next = NULL;
   freeSetupFileList(setup_file_list);
 
@@ -2342,63 +2264,59 @@ SetupFileHash *loadSetupFileHash(char *filename)
   return setup_file_hash;
 }
 
-void checkSetupFileHashIdentifier(SetupFileHash *setup_file_hash,
-                                 char *filename, char *identifier)
-{
-  char *value = getHashEntry(setup_file_hash, TOKEN_STR_FILE_IDENTIFIER);
-
-  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);
-}
 
-
-/* ========================================================================= */
-/* 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_YEAR                   4
-#define LEVELINFO_TOKEN_IMPORTED_FROM          5
-#define LEVELINFO_TOKEN_IMPORTED_BY            6
-#define LEVELINFO_TOKEN_TESTED_BY              7
-#define LEVELINFO_TOKEN_LEVELS                 8
-#define LEVELINFO_TOKEN_FIRST_LEVEL            9
-#define LEVELINFO_TOKEN_SORT_PRIORITY          10
-#define LEVELINFO_TOKEN_LATEST_ENGINE          11
-#define LEVELINFO_TOKEN_LEVEL_GROUP            12
-#define LEVELINFO_TOKEN_READONLY               13
-#define LEVELINFO_TOKEN_GRAPHICS_SET_ECS       14
-#define LEVELINFO_TOKEN_GRAPHICS_SET_AGA       15
-#define LEVELINFO_TOKEN_GRAPHICS_SET           16
-#define LEVELINFO_TOKEN_SOUNDS_SET             17
-#define LEVELINFO_TOKEN_MUSIC_SET              18
-#define LEVELINFO_TOKEN_FILENAME               19
-#define LEVELINFO_TOKEN_FILETYPE               20
-#define LEVELINFO_TOKEN_HANDICAP               21
-#define LEVELINFO_TOKEN_SKIP_LEVELS            22
-
-#define NUM_LEVELINFO_TOKENS                   23
+#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"             },
@@ -2415,18 +2333,22 @@ 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 */
+  // 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"              },
@@ -2463,8 +2385,12 @@ static void setTreeInfoToDefaults(TreeInfo *ti, int type)
   ti->author = getStringCopy(ANONYMOUS_NAME);
   ti->year = NULL;
 
-  ti->sort_priority = LEVELCLASS_UNDEFINED;    /* default: least priority */
-  ti->latest_engine = FALSE;                   /* default: get from level */
+  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->parent_link = FALSE;
   ti->in_user_dir = FALSE;
   ti->user_defined = FALSE;
@@ -2491,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;
@@ -2513,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;
 
@@ -2534,6 +2462,10 @@ static void setTreeInfoToDefaultsFromParent(TreeInfo *ti, TreeInfo *parent)
   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;
   ti->parent_link = FALSE;
@@ -2550,26 +2482,28 @@ static void setTreeInfoToDefaultsFromParent(TreeInfo *ti, TreeInfo *parent)
     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;
   }
 }
 
@@ -2577,7 +2511,7 @@ static TreeInfo *getTreeInfoCopy(TreeInfo *ti)
 {
   TreeInfo *ti_copy = newTreeInfo();
 
-  /* copy all values from the original structure */
+  // copy all values from the original structure
 
   ti_copy->type                        = ti->type;
 
@@ -2597,6 +2531,11 @@ static TreeInfo *getTreeInfoCopy(TreeInfo *ti)
   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);
@@ -2613,6 +2552,8 @@ static TreeInfo *getTreeInfoCopy(TreeInfo *ti)
   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;
@@ -2637,7 +2578,7 @@ static TreeInfo *getTreeInfoCopy(TreeInfo *ti)
   return ti_copy;
 }
 
-static void freeTreeInfo(TreeInfo *ti)
+void freeTreeInfo(TreeInfo *ti)
 {
   if (ti == NULL)
     return;
@@ -2652,6 +2593,10 @@ static void freeTreeInfo(TreeInfo *ti)
   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);
 
   checked_free(ti->infotext);
@@ -2674,8 +2619,18 @@ 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);
 }
 
@@ -2688,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:
@@ -2696,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;
@@ -2713,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;
   }
@@ -2722,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)
@@ -2730,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);
@@ -2756,12 +2721,12 @@ static int compareTreeInfoEntries(const void *object1, const void *object2)
   return compare_result;
 }
 
-static void createParentTreeInfoNode(TreeInfo *node_parent)
+static TreeInfo *createParentTreeInfoNode(TreeInfo *node_parent)
 {
   TreeInfo *ti_new;
 
   if (node_parent == NULL)
-    return;
+    return NULL;
 
   ti_new = newTreeInfo();
   setTreeInfoToDefaults(ti_new, node_parent->type);
@@ -2773,7 +2738,7 @@ static void createParentTreeInfoNode(TreeInfo *node_parent)
   setString(&ti_new->name, ".. (parent directory)");
   setString(&ti_new->name_sorting, ti_new->name);
 
-  setString(&ti_new->subdir, "..");
+  setString(&ti_new->subdir, STRING_PARENT_DIRECTORY);
   setString(&ti_new->fullpath, node_parent->fullpath);
 
   ti_new->sort_priority = node_parent->sort_priority;
@@ -2782,14 +2747,52 @@ static void createParentTreeInfoNode(TreeInfo *node_parent)
   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()
+// ----------------------------------------------------------------------------
+// functions for handling level and custom artwork info cache
+// ----------------------------------------------------------------------------
+
+static void LoadArtworkInfoCache(void)
 {
   InitCacheDirectory();
 
@@ -2797,10 +2800,10 @@ static void LoadArtworkInfoCache()
   {
     char *filename = getPath2(getCacheDir(), ARTWORKINFO_CACHE_FILE);
 
-    /* try to load artwork info hash from already existing 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 no artwork info cache file was found, start with empty hash
     if (artworkinfo_cache_old == NULL)
       artworkinfo_cache_old = newSetupFileHash();
 
@@ -2811,7 +2814,7 @@ static void LoadArtworkInfoCache()
     artworkinfo_cache_new = newSetupFileHash();
 }
 
-static void SaveArtworkInfoCache()
+static void SaveArtworkInfoCache(void)
 {
   char *filename = getPath2(getCacheDir(), ARTWORKINFO_CACHE_FILE);
 
@@ -2833,7 +2836,7 @@ static char *getCacheTokenPrefix(char *prefix1, char *prefix2)
   return prefix;
 }
 
-/* (identical to above function, but separate string buffer needed -- nasty) */
+// (identical to above function, but separate string buffer needed -- nasty)
 static char *getCacheToken(char *prefix, char *suffix)
 {
   static char *token = NULL;
@@ -2845,14 +2848,9 @@ static char *getCacheToken(char *prefix, char *suffix)
   return token;
 }
 
-static char *getFileTimestamp(char *filename)
+static char *getFileTimestampString(char *filename)
 {
-  struct stat file_status;
-
-  if (stat(filename, &file_status) != 0)       /* cannot stat file */
-    return getStringCopy(i_to_a(0));
-
-  return getStringCopy(i_to_a(file_status.st_mtime));
+  return getStringCopy(i_to_a(getFileTimestampEpochSeconds(filename)));
 }
 
 static boolean modifiedFileTimestamp(char *filename, char *timestamp_string)
@@ -2862,7 +2860,7 @@ static boolean modifiedFileTimestamp(char *filename, char *timestamp_string)
   if (timestamp_string == NULL)
     return TRUE;
 
-  if (stat(filename, &file_status) != 0)       /* cannot stat file */
+  if (stat(filename, &file_status) != 0)       // cannot stat file
     return TRUE;
 
   return (file_status.st_mtime != atoi(timestamp_string));
@@ -2888,55 +2886,39 @@ static TreeInfo *getArtworkInfoCacheEntry(LevelDirTree *level_node, int type)
     artwork_info = newTreeInfo();
     setTreeInfoToDefaults(artwork_info, type);
 
-    /* set all structure fields according to the token/value pairs */
+    // 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);
 
-      setSetupInfo(artworkinfo_tokens, i, value);
-
-      /* check if cache entry for this item is invalid or incomplete */
-      if (value == NULL)
-      {
-#if 1
-       Error(ERR_WARN, "cache entry '%s' invalid", token);
-#endif
-
-       cached = FALSE;
-      }
+      // if defined, use value from cache, else keep default value
+      if (value != NULL)
+       setSetupInfo(artworkinfo_tokens, i, value);
     }
 
     *artwork_info = ldi;
-  }
 
-  if (cached)
-  {
     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 */
+    // 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 */
+    // 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;
 
-#if 0
-    if (!cached)
-      printf("::: '%s': INVALIDATED FROM CACHE BY TIMESTAMP\n", identifier);
-#endif
-
     checked_free(filename_levelinfo);
     checked_free(filename_artworkinfo);
   }
@@ -2969,8 +2951,8 @@ static void setArtworkInfoCacheEntry(TreeInfo *artwork_info,
                                        LEVELINFO_FILENAME);
     char *filename_artworkinfo = getPath2(getSetupArtworkDir(artwork_info),
                                          ARTWORKINFO_FILENAME(type));
-    char *timestamp_levelinfo = getFileTimestamp(filename_levelinfo);
-    char *timestamp_artworkinfo = getFileTimestamp(filename_artworkinfo);
+    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);
@@ -2996,11 +2978,153 @@ static void setArtworkInfoCacheEntry(TreeInfo *artwork_info,
 }
 
 
-/* -------------------------------------------------------------------------- */
-/* functions for loading level info and custom artwork info                   */
-/* -------------------------------------------------------------------------- */
+// ----------------------------------------------------------------------------
+// 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;
+
+    // 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;
+
+    i++;
+  }
+
+  // check if valid configuration filename was found in zip file
+  if (!found_top_dir_conf_filename)
+    return FALSE;
+
+  return TRUE;
+}
+
+static boolean ExtractZipFileIntoDirectory(char *zip_filename, char *directory,
+                                          int tree_type)
+{
+  boolean zip_file_valid = CheckZipFileForDirectory(zip_filename, directory,
+                                                   tree_type);
+
+  Error(ERR_DEBUG, "zip file '%s': %s", zip_filename,
+       (zip_file_valid ? "EXTRACT" : "REJECT"));
+
+  if (!zip_file_valid)
+    return FALSE;
+
+  char **zip_entries = zip_extract(zip_filename, directory);
+
+  boolean zip_file_extracted = (zip_entries != NULL);
+
+  if (zip_file_extracted)
+    Error(ERR_DEBUG, "zip file successfully extracted!");
+  else
+    Error(ERR_DEBUG, "zip file could not be extracted!");
+
+  return zip_file_extracted;
+}
+
+static void ProcessZipFilesInDirectory(char *directory, int tree_type)
+{
+  Directory *dir;
+  DirectoryEntry *dir_entry;
+
+  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,
@@ -3008,17 +3132,13 @@ static boolean LoadLevelInfoFromLevelConf(TreeInfo **node_first,
                                          char *level_directory,
                                          char *directory_name)
 {
-#if 0
-  static unsigned long progress_delay = 0;
-  unsigned long progress_delay_value = 100;    /* (in milliseconds) */
-#endif
   char *directory_path = getPath2(level_directory, directory_name);
   char *filename = getPath2(directory_path, LEVELINFO_FILENAME);
   SetupFileHash *setup_file_hash;
   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);
@@ -3031,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);
@@ -3048,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,
@@ -3067,29 +3186,24 @@ static boolean LoadLevelInfoFromLevelConf(TreeInfo **node_first,
   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()) ||
@@ -3107,38 +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 1
-#if 1
-  DrawInitTextExt(leveldir_new->name, 150, FC_YELLOW,
-                 leveldir_new->level_group);
-#else
-  if (leveldir_new->level_group ||
-      DelayReached(&progress_delay, progress_delay_value))
-    DrawInitText(leveldir_new->name, 150, FC_YELLOW);
-#endif
-#else
   DrawInitText(leveldir_new->name, 150, FC_YELLOW);
-#endif
-
-#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
 
   pushTreeInfo(node_first, leveldir_new);
 
@@ -3146,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);
 
-    /* recursively 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);
   }
@@ -3164,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;
     }
 
@@ -3208,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, ".");
   }
@@ -3223,7 +3317,7 @@ static void LoadLevelInfoFromLevelDir(TreeInfo **node_first,
          level_directory);
 }
 
-boolean AdjustGraphicsForEMC()
+boolean AdjustGraphicsForEMC(void)
 {
   boolean settings_changed = FALSE;
 
@@ -3233,7 +3327,7 @@ boolean AdjustGraphicsForEMC()
   return settings_changed;
 }
 
-void LoadLevelInfo()
+void LoadLevelInfo(void)
 {
   InitUserLevelDirectory(getLoginName());
 
@@ -3242,6 +3336,8 @@ void LoadLevelInfo()
   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
@@ -3251,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)
@@ -3259,7 +3355,7 @@ void LoadLevelInfo()
 
   sortTreeInfo(&leveldir_first);
 
-#if 0
+#if ENABLE_UNUSED_CODE
   dumpTreeInfo(leveldir_first, 0);
 #endif
 }
@@ -3278,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);
@@ -3321,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,
@@ -3344,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);
@@ -3358,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, "."))
     {
@@ -3378,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,
@@ -3393,10 +3486,6 @@ static boolean LoadArtworkInfoFromArtworkConf(TreeInfo **node_first,
     setString(&artwork_new->name_sorting, artwork_new->name);
   }
 
-#if 0
-  DrawInitText(artwork_new->name, 150, FC_YELLOW);
-#endif
-
   pushTreeInfo(node_first, artwork_new);
 
   freeSetupFileHash(setup_file_hash);
@@ -3411,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);
@@ -3467,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);
@@ -3483,7 +3578,7 @@ static TreeInfo *getDummyArtworkInfo(int type)
   return artwork_new;
 }
 
-void LoadArtworkInfo()
+void LoadArtworkInfo(void)
 {
   LoadArtworkInfoCache();
 
@@ -3517,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);
 
@@ -3530,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);
 
@@ -3538,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);
 
@@ -3546,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);
@@ -3556,27 +3651,23 @@ 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)
 {
-#if 0
-  static unsigned long progress_delay = 0;
-  unsigned long progress_delay_value = 100;    /* (in milliseconds) */
-#endif
   int type = (*artwork_node)->type;
 
-  /* recursively check all level directories for artwork sub-directories */
+  // 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 *artwork_new = getArtworkInfoCacheEntry(level_node, type);
@@ -3594,7 +3685,7 @@ void LoadArtworkInfoFromLevelInfo(ArtworkDirTree **artwork_node,
 
        LoadArtworkInfoFromArtworkDir(artwork_node, NULL, path, type);
 
-       if (topnode_last != *artwork_node)      /* check for newly added node */
+       if (topnode_last != *artwork_node)      // check for newly added node
        {
          artwork_new = *artwork_node;
 
@@ -3609,19 +3700,12 @@ void LoadArtworkInfoFromLevelInfo(ArtworkDirTree **artwork_node,
        free(path);
       }
 
-      /* insert artwork info (from old cache or filesystem) into new cache */
+      // insert artwork info (from old cache or filesystem) into new cache
       if (artwork_new != NULL)
        setArtworkInfoCacheEntry(artwork_new, level_node, type);
     }
 
-#if 1
-    DrawInitTextExt(level_node->name, 150, FC_YELLOW,
-                   level_node->level_group);
-#else
-    if (level_node->level_group ||
-       DelayReached(&progress_delay, progress_delay_value))
-      DrawInitText(level_node->name, 150, FC_YELLOW);
-#endif
+    DrawInitText(level_node->name, 150, FC_YELLOW);
 
     if (level_node->node_group != NULL)
       LoadArtworkInfoFromLevelInfo(artwork_node, level_node->node_group);
@@ -3630,17 +3714,26 @@ void LoadArtworkInfoFromLevelInfo(ArtworkDirTree **artwork_node,
   }
 }
 
-void LoadLevelArtworkInfo()
+void LoadLevelArtworkInfo(void)
 {
+  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)");
 
   SaveArtworkInfoCache();
 
-  /* needed for reloading level artwork not known at ealier stage */
+  print_timestamp_time("SaveArtworkInfoCache");
+
+  // needed for reloading level artwork not known at ealier stage
 
   if (!strEqual(artwork.gfx_current_identifier, setup.graphics_set))
   {
@@ -3648,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);
   }
@@ -3659,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);
   }
@@ -3670,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));
 }
 
-static void SaveUserLevelInfo()
+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;
+}
+
+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++)
@@ -3723,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");     
   }
 
@@ -3739,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)
@@ -3758,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;
@@ -3785,6 +4077,10 @@ char *getSetupValue(int type, void *value)
       strcpy(value_string, *(char **)value);
       break;
 
+    case TYPE_PLAYER:
+      sprintf(value_string, "player_%d", *(int *)value + 1);
+      break;
+
     default:
       value_string[0] = '\0';
       break;
@@ -3806,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)
@@ -3817,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, " ");
@@ -3834,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 =
@@ -3856,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;
@@ -3882,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));
 
@@ -3898,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;
 
@@ -3972,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)
@@ -3984,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)
@@ -4001,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);
 
@@ -4035,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);
 
@@ -4048,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++;
+}