changed using 'external' app data directory on Android version, if possible
[rocksndiamonds.git] / src / libgame / setup.c
index d69c6537e238a42b925e202751fe558bf0216e2d..ced73a837cff9f80921172c23e6eefa2419bda85 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"
 
@@ -31,6 +30,8 @@
 #include "hash.h"
 
 
+#define ENABLE_UNUSED_CODE     FALSE   /* for currently unused functions */
+
 #define NUM_LEVELCLASS_DESC    8
 
 static char *levelclass_desc[NUM_LEVELCLASS_DESC] =
@@ -132,15 +133,25 @@ static char *getUserLevelDir(char *level_subdir)
 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;
 }
@@ -313,6 +324,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);
@@ -334,7 +348,9 @@ 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
   {
     /*
@@ -354,7 +370,7 @@ char *setLevelArtworkDir(TreeInfo *ti)
 
     checked_free(*artwork_set_ptr);
 
-    if (fileExists(dir))
+    if (directoryExists(dir))
     {
       *artwork_path_ptr = getStringCopy(getClassicArtworkDir(ti->type));
       *artwork_set_ptr = getStringCopy(getClassicArtworkSet(ti->type));
@@ -387,6 +403,29 @@ inline static char *getLevelArtworkDir(int type)
   return LEVELDIR_ARTWORK_PATH(leveldir_current, type);
 }
 
+char *getProgramConfigFilename(char *command_filename_ptr)
+{
+  char *command_filename_1 = getStringCopy(command_filename_ptr);
+
+  // strip trailing executable suffix from command filename
+  if (strSuffix(command_filename_1, ".exe"))
+    command_filename_1[strlen(command_filename_1) - 4] = '\0';
+
+  char *command_basepath = getBasePath(command_filename_ptr);
+  char *command_basename = getBaseNameNoSuffix(command_filename_ptr);
+  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");
+
+  // 1st try: look for config file that exactly matches the binary filename
+  if (fileExists(config_filename_1))
+    return config_filename_1;
+
+  // 2nd try: return config filename that matches binary filename without suffix
+  return config_filename_2;
+}
+
 char *getTapeFilename(int nr)
 {
   static char *filename = NULL;
@@ -410,6 +449,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;
 }
 
@@ -437,6 +489,11 @@ char *getSetupFilename()
   return filename;
 }
 
+char *getDefaultSetupFilename()
+{
+  return program.config_filename;
+}
+
 char *getEditorSetupFilename()
 {
   static char *filename = NULL;
@@ -582,34 +639,7 @@ char *getLevelSetTitleMessageFilename(int nr, boolean initial)
 
 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)
@@ -624,7 +654,7 @@ 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);
+    filename = getImg3(getCurrentLevelDir(), GRAPHICS_DIRECTORY, basename);
     if (fileExists(filename))
       return filename;
 
@@ -634,7 +664,7 @@ char *getCustomImageFilename(char *basename)
     if (getLevelArtworkSet(ARTWORK_TYPE_GRAPHICS) != NULL)
     {
       /* 2nd try: look for special artwork configured in level series config */
-      filename = getPath2(getLevelArtworkDir(ARTWORK_TYPE_GRAPHICS), basename);
+      filename = getImg2(getLevelArtworkDir(ARTWORK_TYPE_GRAPHICS), basename);
       if (fileExists(filename))
        return filename;
 
@@ -648,7 +678,7 @@ char *getCustomImageFilename(char *basename)
   if (!skip_setup_artwork)
   {
     /* 3rd try: look for special artwork in configured artwork directory */
-    filename = getPath2(getSetupArtworkDir(artwork.gfx_current), basename);
+    filename = getImg2(getSetupArtworkDir(artwork.gfx_current), basename);
     if (fileExists(filename))
       return filename;
 
@@ -656,31 +686,31 @@ char *getCustomImageFilename(char *basename)
   }
 
   /* 4th try: look for default artwork in new default artwork directory */
-  filename = getPath2(getDefaultGraphicsDir(GFX_DEFAULT_SUBDIR), basename);
+  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);
+  filename = getImg2(options.graphics_directory, basename);
   if (fileExists(filename))
     return filename;
 
-#if defined(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 */
 }
@@ -740,15 +770,20 @@ char *getCustomSoundFilename(char *basename)
   if (fileExists(filename))
     return filename;
 
-#if defined(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 */
 }
@@ -808,15 +843,20 @@ char *getCustomMusicFilename(char *basename)
   if (fileExists(filename))
     return filename;
 
-#if defined(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 */
 }
@@ -860,7 +900,7 @@ char *getCustomMusicDirectory(void)
   {
     /* 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);
@@ -870,7 +910,7 @@ char *getCustomMusicDirectory(void)
     {
       /* 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);
@@ -884,7 +924,7 @@ char *getCustomMusicDirectory(void)
   {
     /* 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);
@@ -892,14 +932,14 @@ char *getCustomMusicDirectory(void)
 
   /* 4th try: look for default artwork in new default artwork directory */
   directory = getStringCopy(getDefaultMusicDir(MUS_DEFAULT_SUBDIR));
-  if (fileExists(directory))
+  if (directoryExists(directory))
     return directory;
 
   free(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 */
@@ -914,16 +954,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();
 
 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);
@@ -1098,13 +1144,7 @@ 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->node_top = node_top;               /* correct top node link */
   node_new->node_parent = node_parent;         /* correct parent node link */
@@ -1165,8 +1205,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);
@@ -1267,14 +1312,17 @@ void sortTreeInfo(TreeInfo **node_first)
 #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()
 {
@@ -1350,37 +1398,21 @@ 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()
 {
   return getUserGameDataDir();
@@ -1404,21 +1436,47 @@ static int posix_mkdir(const char *pathname, mode_t mode)
 #endif
 }
 
+static boolean posix_process_running_setgid()
+{
+#if defined(PLATFORM_UNIX)
+  return (getgid() != getegid());
+#else
+  return FALSE;
+#endif
+}
+
 void createDirectory(char *dir, char *text, int permission_class)
 {
+  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 (!fileExists(dir))
-    if (posix_mkdir(dir, dir_mode) != 0)
-      Error(ERR_WARN, "cannot create %s directory '%s'", text, dir);
+  if (permission_class == PERMS_PUBLIC)
+  {
+    /* if we're setgid, protect files against "other" */
+    /* else keep umask(0) to make the dir world-writable */
 
-  posix_umask(normal_umask);           /* reset normal umask */
+    if (running_setgid)
+      posix_umask(last_umask & group_umask);
+    else
+      dir_mode = DIR_PERMS_PUBLIC_ALL;
+  }
+
+  if (posix_mkdir(dir, dir_mode) != 0)
+    Error(ERR_WARN, "cannot create %s directory '%s': %s",
+         text, dir, strerror(errno));
+
+  if (permission_class == PERMS_PUBLIC && !running_setgid)
+    chmod(dir, dir_mode);
+
+  posix_umask(last_umask);             /* restore previous umask */
 }
 
 void InitUserDataDirectory()
@@ -1428,8 +1486,14 @@ void InitUserDataDirectory()
 
 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)
@@ -1447,6 +1511,17 @@ char *getCookie(char *file_type)
   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;
@@ -1494,6 +1569,7 @@ boolean checkCookieString(const char *cookie, const char *template)
   return TRUE;
 }
 
+
 /* ------------------------------------------------------------------------- */
 /* setup file list and hash handling functions                               */
 /* ------------------------------------------------------------------------- */
@@ -1585,6 +1661,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)
 {
@@ -1597,6 +1674,7 @@ static void printSetupFileList(SetupFileList *list)
   printSetupFileList(list->next);
 }
 #endif
+#endif
 
 #ifdef DEBUG
 DEFINE_HASHTABLE_INSERT(insert_hash_entry, char, char);
@@ -1610,7 +1688,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
@@ -1695,7 +1773,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)
@@ -1706,6 +1785,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
@@ -1771,12 +1851,8 @@ static boolean getTokenValueFromSetupLineExt(char *line,
   /* 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 */
@@ -1843,11 +1919,6 @@ static boolean getTokenValueFromSetupLineExt(char *line,
     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;
 
@@ -1863,7 +1934,6 @@ boolean getTokenValueFromSetupLine(char *line, char **token, char **value)
   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)
 {
@@ -1872,7 +1942,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
@@ -1883,7 +1953,7 @@ 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);
 
@@ -1901,10 +1971,10 @@ static boolean loadSetupFileData(void *setup_file_data, char *filename,
   /* 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))
+    if (!getStringFromFile(file, line, MAX_LINE_LEN))
       break;
 
     /* check if line was completely read and is terminated by line break */
@@ -1921,14 +1991,6 @@ static boolean loadSetupFileData(void *setup_file_data, char *filename,
 
     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 */
       if (strlen(previous_line) + strlen(line_ptr) < MAX_LINE_LEN)
        strcat(previous_line, line_ptr);
@@ -1963,10 +2025,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);
@@ -2017,7 +2075,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)
@@ -2038,239 +2096,6 @@ 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 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)))
-  {
-    Error(ERR_WARN, "cannot open configuration file '%s'", filename);
-
-    return FALSE;
-  }
-
-  /* use "insert pointer" to store list end for constant insertion complexity */
-  if (!is_hash)
-    insert_ptr = setup_file_data;
-
-  /* on top invocation, create hash to mark included files (to prevent loops) */
-  if (top_recursion_level)
-    include_filename_hash = newSetupFileHash();
-
-  /* mark this file as already included (to prevent including it again) */
-  setHashEntry(include_filename_hash, getBaseNamePtr(filename), "true");
-
-  while (!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;
@@ -2327,17 +2152,6 @@ 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                                                          */
@@ -2369,10 +2183,11 @@ void checkSetupFileHashIdentifier(SetupFileHash *setup_file_hash,
 #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 LEVELINFO_TOKEN_SPECIAL_FLAGS          21
+#define LEVELINFO_TOKEN_HANDICAP               22
+#define LEVELINFO_TOKEN_SKIP_LEVELS            23
 
-#define NUM_LEVELINFO_TOKENS                   23
+#define NUM_LEVELINFO_TOKENS                   24
 
 static LevelDirTree ldi;
 
@@ -2400,6 +2215,7 @@ 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"           }
 };
@@ -2476,6 +2292,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;
@@ -2547,12 +2365,14 @@ static void setTreeInfoToDefaultsFromParent(TreeInfo *ti, TreeInfo *parent)
     ti->level_filename = NULL;
     ti->level_filetype = NULL;
 
+    ti->special_flags = getStringCopy(parent->special_flags);
+
     ti->levels = 0;
     ti->first_level = 0;
     ti->last_level = 0;
     ti->level_group = FALSE;
     ti->handicap_level = 0;
-    ti->readonly = TRUE;
+    ti->readonly = parent->readonly;
     ti->handicap = TRUE;
     ti->skip_levels = FALSE;
   }
@@ -2598,6 +2418,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;
@@ -2622,7 +2444,7 @@ static TreeInfo *getTreeInfoCopy(TreeInfo *ti)
   return ti_copy;
 }
 
-static void freeTreeInfo(TreeInfo *ti)
+void freeTreeInfo(TreeInfo *ti)
 {
   if (ti == NULL)
     return;
@@ -2659,8 +2481,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);
 }
 
@@ -2681,6 +2513,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;
@@ -2707,7 +2543,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)
@@ -2715,7 +2551,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);
@@ -2741,12 +2579,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);
@@ -2758,7 +2596,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;
@@ -2767,6 +2605,44 @@ 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, node_first->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;
 }
 
 
@@ -2830,14 +2706,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)
@@ -2885,9 +2756,7 @@ static TreeInfo *getArtworkInfoCacheEntry(LevelDirTree *level_node, int type)
       /* 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;
       }
@@ -2917,11 +2786,6 @@ static TreeInfo *getArtworkInfoCacheEntry(LevelDirTree *level_node, int type)
     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);
   }
@@ -2954,8 +2818,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);
@@ -2993,10 +2857,6 @@ 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;
@@ -3033,9 +2893,6 @@ 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 */
   ldi = *leveldir_new;
   for (i = 0; i < NUM_LEVELINFO_TOKENS; i++)
@@ -3063,11 +2920,6 @@ static boolean LoadLevelInfoFromLevelConf(TreeInfo **node_first,
     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;
 
@@ -3096,34 +2948,7 @@ static boolean LoadLevelInfoFromLevelConf(TreeInfo **node_first,
     (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);
 
@@ -3149,20 +2974,20 @@ static void LoadLevelInfoFromLevelDir(TreeInfo **node_first,
                                      TreeInfo *node_parent,
                                      char *level_directory)
 {
-  DIR *dir;
-  struct dirent *dir_entry;
+  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 */
@@ -3170,14 +2995,15 @@ static void LoadLevelInfoFromLevelDir(TreeInfo **node_first,
        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 */
+    if (!dir_entry->is_directory)                      /* not a directory */
     {
       free(directory_path);
+
       continue;
     }
 
@@ -3193,7 +3019,7 @@ static void LoadLevelInfoFromLevelDir(TreeInfo **node_first,
                                                    directory_name);
   }
 
-  closedir(dir);
+  closeDirectory(dir);
 
   /* special case: top level directory may directly contain "levelinfo.conf" */
   if (node_parent == NULL && !valid_entry_found)
@@ -3227,6 +3053,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
@@ -3244,7 +3072,7 @@ void LoadLevelInfo()
 
   sortTreeInfo(&leveldir_first);
 
-#if 0
+#if ENABLE_UNUSED_CODE
   dumpTreeInfo(leveldir_first, 0);
 #endif
 }
@@ -3265,24 +3093,23 @@ static boolean LoadArtworkInfoFromArtworkConf(TreeInfo **node_first,
 
   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)
@@ -3308,10 +3135,6 @@ static boolean LoadArtworkInfoFromArtworkConf(TreeInfo **node_first,
 
   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 */
     ldi = *artwork_new;
     for (i = 0; i < NUM_LEVELINFO_TOKENS; i++)
@@ -3378,10 +3201,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);
@@ -3396,11 +3215,11 @@ static void LoadArtworkInfoFromArtworkDir(TreeInfo **node_first,
                                          TreeInfo *node_parent,
                                          char *base_directory, int type)
 {
-  DIR *dir;
-  struct dirent *dir_entry;
+  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. */
     if (base_directory == OPTIONS_ARTWORK_DIRECTORY(type))
@@ -3409,10 +3228,9 @@ static void LoadArtworkInfoFromArtworkDir(TreeInfo **node_first,
     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 */
@@ -3420,14 +3238,15 @@ static void LoadArtworkInfoFromArtworkDir(TreeInfo **node_first,
        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;
     }
 
@@ -3439,7 +3258,7 @@ static void LoadArtworkInfoFromArtworkDir(TreeInfo **node_first,
                                                        directory_name, type);
   }
 
-  closedir(dir);
+  closeDirectory(dir);
 
   /* check if this directory directly contains artwork itself */
   valid_entry_found |= LoadArtworkInfoFromArtworkConf(node_first, node_parent,
@@ -3531,7 +3350,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);
@@ -3541,7 +3360,7 @@ 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);
@@ -3551,10 +3370,6 @@ void LoadArtworkInfo()
 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 */
@@ -3599,14 +3414,7 @@ void LoadArtworkInfoFromLevelInfo(ArtworkDirTree **artwork_node,
        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);
@@ -3617,14 +3425,23 @@ void LoadArtworkInfoFromLevelInfo(ArtworkDirTree **artwork_node,
 
 void LoadLevelArtworkInfo()
 {
+  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();
 
+  print_timestamp_time("SaveArtworkInfoCache");
+
   /* needed for reloading level artwork not known at ealier stage */
 
   if (!strEqual(artwork.gfx_current_identifier, setup.graphics_set))
@@ -3660,15 +3477,21 @@ void LoadLevelArtworkInfo()
       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 void SaveUserLevelInfo()
@@ -3699,8 +3522,7 @@ static void SaveUserLevelInfo()
 
   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++)
@@ -3743,10 +3565,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;
@@ -3831,12 +3663,13 @@ void LoadLevelSetup_LastSeries()
   /* always start with reliable default values */
   leveldir_current = getFirstValidTreeInfoEntry(leveldir_first);
 
-#if defined(CREATE_SPECIAL_EDITION_RND_JUE)
-  leveldir_current = getTreeInfoFromIdentifier(leveldir_first,
-                                              "jue_start");
-  if (leveldir_current == NULL)
-    leveldir_current = getFirstValidTreeInfoEntry(leveldir_first);
-#endif
+  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)))
   {
@@ -3848,9 +3681,6 @@ void LoadLevelSetup_LastSeries()
     if (leveldir_current == NULL)
       leveldir_current = getFirstValidTreeInfoEntry(leveldir_first);
 
-    checkSetupFileHashIdentifier(level_setup_hash, filename,
-                                getCookie("LEVELSETUP"));
-
     freeSetupFileHash(level_setup_hash);
   }
   else
@@ -3859,12 +3689,16 @@ void LoadLevelSetup_LastSeries()
   free(filename);
 }
 
-void SaveLevelSetup_LastSeries()
+static void SaveLevelSetup_LastSeries_Ext(boolean deactivate_last_level_series)
 {
   /* ----------------------------------------------------------------------- */
   /* ~/.<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;
   FILE *file;
@@ -3874,12 +3708,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));
 
@@ -3890,11 +3729,20 @@ void SaveLevelSetup_LastSeries()
   free(filename);
 }
 
+void SaveLevelSetup_LastSeries()
+{
+  SaveLevelSetup_LastSeries_Ext(FALSE);
+}
+
+void SaveLevelSetup_LastSeries_Deactivate()
+{
+  SaveLevelSetup_LastSeries_Ext(TRUE);
+}
+
 static void checkSeriesInfo()
 {
   static char *level_directory = NULL;
-  DIR *dir;
-  struct dirent *dir_entry;
+  Directory *dir;
 
   /* check for more levels besides the 'levels' field of 'levelinfo.conf' */
 
@@ -3903,42 +3751,14 @@ static void checkSeriesInfo()
                              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()
@@ -3946,11 +3766,18 @@ void LoadLevelSetup_SeriesInfo()
   char *filename;
   SetupFileHash *level_setup_hash = NULL;
   char *level_subdir = leveldir_current->subdir;
+  int i;
 
   /* 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                  */
@@ -3964,6 +3791,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)
@@ -3976,6 +3805,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)
@@ -3993,8 +3824,30 @@ 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);
   }
@@ -4011,6 +3864,7 @@ void SaveLevelSetup_SeriesInfo()
   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                  */
@@ -4027,12 +3881,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);
 
@@ -4040,3 +3910,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++;
+}