added optional button to restart game (door, panel and touch variants)
[rocksndiamonds.git] / src / libgame / setup.c
index 8f98ff010db474365fb4c7fd9500e27a5d35f167..b419875a580e8fd1f87eab476ec4b496f232db7c 100644 (file)
@@ -19,6 +19,7 @@
 #include "platform.h"
 
 #include "setup.h"
+#include "sound.h"
 #include "joystick.h"
 #include "text.h"
 #include "misc.h"
@@ -43,46 +44,19 @@ static char *levelclass_desc[NUM_LEVELCLASS_DESC] =
   "DX Boulderdash"
 };
 
-
-#define LEVELCOLOR(n)  (IS_LEVELCLASS_TUTORIAL(n) ?            FC_BLUE :    \
-                        IS_LEVELCLASS_CLASSICS(n) ?            FC_RED :     \
-                        IS_LEVELCLASS_BD(n) ?                  FC_YELLOW :  \
-                        IS_LEVELCLASS_EM(n) ?                  FC_YELLOW :  \
-                        IS_LEVELCLASS_SP(n) ?                  FC_YELLOW :  \
-                        IS_LEVELCLASS_DX(n) ?                  FC_YELLOW :  \
-                        IS_LEVELCLASS_SB(n) ?                  FC_YELLOW :  \
-                        IS_LEVELCLASS_CONTRIB(n) ?             FC_GREEN :   \
-                        IS_LEVELCLASS_PRIVATE(n) ?             FC_RED :     \
-                        FC_BLUE)
-
-#define LEVELSORTING(n)        (IS_LEVELCLASS_TUTORIAL(n) ?            0 :     \
-                        IS_LEVELCLASS_CLASSICS(n) ?            1 :     \
-                        IS_LEVELCLASS_BD(n) ?                  2 :     \
-                        IS_LEVELCLASS_EM(n) ?                  3 :     \
-                        IS_LEVELCLASS_SP(n) ?                  4 :     \
-                        IS_LEVELCLASS_DX(n) ?                  5 :     \
-                        IS_LEVELCLASS_SB(n) ?                  6 :     \
-                        IS_LEVELCLASS_CONTRIB(n) ?             7 :     \
-                        IS_LEVELCLASS_PRIVATE(n) ?             8 :     \
-                        9)
-
-#define ARTWORKCOLOR(n)        (IS_ARTWORKCLASS_CLASSICS(n) ?          FC_RED :     \
-                        IS_ARTWORKCLASS_CONTRIB(n) ?           FC_GREEN :   \
-                        IS_ARTWORKCLASS_PRIVATE(n) ?           FC_RED :     \
-                        IS_ARTWORKCLASS_LEVEL(n) ?             FC_YELLOW :  \
-                        FC_BLUE)
-
-#define ARTWORKSORTING(n) (IS_ARTWORKCLASS_CLASSICS(n) ?       0 :     \
-                          IS_ARTWORKCLASS_LEVEL(n) ?           1 :     \
-                          IS_ARTWORKCLASS_CONTRIB(n) ?         2 :     \
-                          IS_ARTWORKCLASS_PRIVATE(n) ?         3 :     \
-                          9)
-
 #define TOKEN_VALUE_POSITION_SHORT             32
 #define TOKEN_VALUE_POSITION_DEFAULT           40
 #define TOKEN_COMMENT_POSITION_DEFAULT         60
 
-#define MAX_COOKIE_LEN                         256
+#define TREE_NODE_TYPE_DEFAULT                 0
+#define TREE_NODE_TYPE_PARENT                  1
+#define TREE_NODE_TYPE_GROUP                   2
+#define TREE_NODE_TYPE_COPY                    3
+
+#define TREE_NODE_TYPE(ti)     (ti->node_group  ? TREE_NODE_TYPE_GROUP  : \
+                                ti->parent_link ? TREE_NODE_TYPE_PARENT : \
+                                ti->is_copy     ? TREE_NODE_TYPE_COPY   : \
+                                TREE_NODE_TYPE_DEFAULT)
 
 
 static void setTreeInfoToDefaults(TreeInfo *, int);
@@ -95,13 +69,25 @@ static int token_comment_position = TOKEN_COMMENT_POSITION_DEFAULT;
 static SetupFileHash *artworkinfo_cache_old = NULL;
 static SetupFileHash *artworkinfo_cache_new = NULL;
 static SetupFileHash *optional_tokens_hash = NULL;
+static SetupFileHash *missing_file_hash = NULL;
 static boolean use_artworkinfo_cache = TRUE;
+static boolean update_artworkinfo_cache = FALSE;
 
 
 // ----------------------------------------------------------------------------
 // file functions
 // ----------------------------------------------------------------------------
 
+static void WarnUsingFallback(char *filename)
+{
+  if (getHashEntry(missing_file_hash, filename) == NULL)
+  {
+    setHashEntry(missing_file_hash, filename, "");
+
+    Debug("setup", "cannot find artwork file '%s' (using fallback)", filename);
+  }
+}
+
 static char *getLevelClassDescription(TreeInfo *ti)
 {
   int position = ti->sort_priority / 100;
@@ -112,6 +98,16 @@ static char *getLevelClassDescription(TreeInfo *ti)
     return "Unknown Level Class";
 }
 
+static char *getCacheDir(void)
+{
+  static char *cache_dir = NULL;
+
+  if (cache_dir == NULL)
+    cache_dir = getPath2(getMainUserGameDataDir(), CACHE_DIRECTORY);
+
+  return cache_dir;
+}
+
 static char *getScoreDir(char *level_subdir)
 {
   static char *score_dir = NULL;
@@ -119,13 +115,29 @@ static char *getScoreDir(char *level_subdir)
   char *score_subdir = SCORES_DIRECTORY;
 
   if (score_dir == NULL)
+    score_dir = getPath2(getMainUserGameDataDir(), score_subdir);
+
+  if (level_subdir != NULL)
   {
-    if (program.global_scores)
-      score_dir = getPath2(getCommonDataDir(),       score_subdir);
-    else
-      score_dir = getPath2(getMainUserGameDataDir(), score_subdir);
+    checked_free(score_level_dir);
+
+    score_level_dir = getPath2(score_dir, level_subdir);
+
+    return score_level_dir;
   }
 
+  return score_dir;
+}
+
+static char *getScoreCacheDir(char *level_subdir)
+{
+  static char *score_dir = NULL;
+  static char *score_level_dir = NULL;
+  char *score_subdir = SCORES_DIRECTORY;
+
+  if (score_dir == NULL)
+    score_dir = getPath2(getCacheDir(), score_subdir);
+
   if (level_subdir != NULL)
   {
     checked_free(score_level_dir);
@@ -138,6 +150,32 @@ static char *getScoreDir(char *level_subdir)
   return score_dir;
 }
 
+static char *getScoreTapeDir(char *level_subdir, int nr)
+{
+  static char *score_tape_dir = NULL;
+  char tape_subdir[MAX_FILENAME_LEN];
+
+  checked_free(score_tape_dir);
+
+  sprintf(tape_subdir, "%03d", nr);
+  score_tape_dir = getPath2(getScoreDir(level_subdir), tape_subdir);
+
+  return score_tape_dir;
+}
+
+static char *getScoreCacheTapeDir(char *level_subdir, int nr)
+{
+  static char *score_cache_tape_dir = NULL;
+  char tape_subdir[MAX_FILENAME_LEN];
+
+  checked_free(score_cache_tape_dir);
+
+  sprintf(tape_subdir, "%03d", nr);
+  score_cache_tape_dir = getPath2(getScoreCacheDir(level_subdir), tape_subdir);
+
+  return score_cache_tape_dir;
+}
+
 static char *getUserSubdir(int nr)
 {
   static char user_subdir[16] = { 0 };
@@ -180,16 +218,6 @@ static char *getLevelSetupDir(char *level_subdir)
   return levelsetup_dir;
 }
 
-static char *getCacheDir(void)
-{
-  static char *cache_dir = NULL;
-
-  if (cache_dir == NULL)
-    cache_dir = getPath2(getMainUserGameDataDir(), CACHE_DIRECTORY);
-
-  return cache_dir;
-}
-
 static char *getNetworkDir(void)
 {
   static char *network_dir = NULL;
@@ -274,7 +302,7 @@ char *getNewUserLevelSubdir(void)
   return new_level_subdir;
 }
 
-static char *getTapeDir(char *level_subdir)
+char *getTapeDir(char *level_subdir)
 {
   static char *tape_dir = NULL;
   char *data_dir = getUserGameDataDir();
@@ -487,7 +515,7 @@ char *getProgramMainDataPath(char *command_filename, char *base_path)
      set the current working directory to the program package directory) */
   char *main_data_path = getBasePath(command_filename);
 
-#if defined(PLATFORM_MACOSX)
+#if defined(PLATFORM_MAC)
   if (strSuffix(main_data_path, MAC_APP_BINARY_SUBDIR))
   {
     char *main_data_path_old = main_data_path;
@@ -529,8 +557,8 @@ char *getProgramConfigFilename(char *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 *base_path = getProgramMainDataPath(command_filename, BASE_PATH);
+    char *conf_directory = getPath2(base_path, CONF_DIRECTORY);
 
     char *command_basepath = getBasePath(command_filename);
     char *command_basename = getBaseNameNoSuffix(command_filename);
@@ -540,7 +568,7 @@ char *getProgramConfigFilename(char *command_filename)
     config_filename_2 = getStringCat2(command_filename_2, ".conf");
     config_filename_3 = getPath2(conf_directory, SETUP_FILENAME);
 
-    checked_free(ro_base_path);
+    checked_free(base_path);
     checked_free(conf_directory);
 
     checked_free(command_basepath);
@@ -564,6 +592,34 @@ char *getProgramConfigFilename(char *command_filename)
   return config_filename_3;
 }
 
+static char *getPlatformConfigFilename(char *config_filename)
+{
+  static char *platform_config_filename = NULL;
+  static boolean initialized = FALSE;
+
+  if (!initialized)
+  {
+    char *config_basepath = getBasePath(config_filename);
+    char *config_basename = getBaseNameNoSuffix(config_filename);
+    char *config_filename_prefix = getPath2(config_basepath, config_basename);
+    char *platform_string_lower = getStringToLower(PLATFORM_STRING);
+    char *platform_suffix = getStringCat2("-", platform_string_lower);
+
+    platform_config_filename = getStringCat3(config_filename_prefix,
+                                            platform_suffix, ".conf");
+
+    checked_free(config_basepath);
+    checked_free(config_basename);
+    checked_free(config_filename_prefix);
+    checked_free(platform_string_lower);
+    checked_free(platform_suffix);
+
+    initialized = TRUE;
+  }
+
+  return platform_config_filename;
+}
+
 char *getTapeFilename(int nr)
 {
   static char *filename = NULL;
@@ -577,7 +633,20 @@ char *getTapeFilename(int nr)
   return filename;
 }
 
-char *getSolutionTapeFilename(int nr)
+char *getTemporaryTapeFilename(void)
+{
+  static char *filename = NULL;
+  char basename[MAX_FILENAME_LEN];
+
+  checked_free(filename);
+
+  sprintf(basename, "tmp.%s", TAPEFILE_EXTENSION);
+  filename = getPath2(getTapeDir(NULL), basename);
+
+  return filename;
+}
+
+char *getDefaultSolutionTapeFilename(int nr)
 {
   static char *filename = NULL;
   char basename[MAX_FILENAME_LEN];
@@ -587,17 +656,32 @@ char *getSolutionTapeFilename(int nr)
   sprintf(basename, "%03d.%s", nr, TAPEFILE_EXTENSION);
   filename = getPath2(getSolutionTapeDir(), basename);
 
-  if (!fileExists(filename))
-  {
-    static char *filename_sln = NULL;
+  return filename;
+}
 
-    checked_free(filename_sln);
+char *getSokobanSolutionTapeFilename(int nr)
+{
+  static char *filename = NULL;
+  char basename[MAX_FILENAME_LEN];
+
+  checked_free(filename);
+
+  sprintf(basename, "%03d.sln", nr);
+  filename = getPath2(getSolutionTapeDir(), basename);
 
-    sprintf(basename, "%03d.sln", nr);
-    filename_sln = getPath2(getSolutionTapeDir(), basename);
+  return filename;
+}
+
+char *getSolutionTapeFilename(int nr)
+{
+  char *filename = getDefaultSolutionTapeFilename(nr);
+
+  if (!fileExists(filename))
+  {
+    char *filename2 = getSokobanSolutionTapeFilename(nr);
 
-    if (fileExists(filename_sln))
-      return filename_sln;
+    if (fileExists(filename2))
+      return filename2;
   }
 
   return filename;
@@ -618,6 +702,64 @@ char *getScoreFilename(int nr)
   return filename;
 }
 
+char *getScoreCacheFilename(int nr)
+{
+  static char *filename = NULL;
+  char basename[MAX_FILENAME_LEN];
+
+  checked_free(filename);
+
+  sprintf(basename, "%03d.%s", nr, SCOREFILE_EXTENSION);
+
+  // used instead of "leveldir_current->subdir" (for network games)
+  filename = getPath2(getScoreCacheDir(levelset.identifier), basename);
+
+  return filename;
+}
+
+char *getScoreTapeBasename(char *name)
+{
+  static char basename[MAX_FILENAME_LEN];
+  char basename_raw[MAX_FILENAME_LEN];
+  char timestamp[20];
+
+  sprintf(timestamp, "%s", getCurrentTimestamp());
+  sprintf(basename_raw, "%s-%s", timestamp, name);
+  sprintf(basename, "%s-%08x", timestamp, get_hash_from_key(basename_raw));
+
+  return basename;
+}
+
+char *getScoreTapeFilename(char *basename_no_ext, int nr)
+{
+  static char *filename = NULL;
+  char basename[MAX_FILENAME_LEN];
+
+  checked_free(filename);
+
+  sprintf(basename, "%s.%s", basename_no_ext, TAPEFILE_EXTENSION);
+
+  // used instead of "leveldir_current->subdir" (for network games)
+  filename = getPath2(getScoreTapeDir(levelset.identifier, nr), basename);
+
+  return filename;
+}
+
+char *getScoreCacheTapeFilename(char *basename_no_ext, int nr)
+{
+  static char *filename = NULL;
+  char basename[MAX_FILENAME_LEN];
+
+  checked_free(filename);
+
+  sprintf(basename, "%s.%s", basename_no_ext, TAPEFILE_EXTENSION);
+
+  // used instead of "leveldir_current->subdir" (for network games)
+  filename = getPath2(getScoreCacheTapeDir(levelset.identifier, nr), basename);
+
+  return filename;
+}
+
 char *getSetupFilename(void)
 {
   static char *filename = NULL;
@@ -634,6 +776,11 @@ char *getDefaultSetupFilename(void)
   return program.config_filename;
 }
 
+char *getPlatformSetupFilename(void)
+{
+  return getPlatformConfigFilename(program.config_filename);
+}
+
 char *getEditorSetupFilename(void)
 {
   static char *filename = NULL;
@@ -672,9 +819,34 @@ char *getHelpTextFilename(void)
   return filename;
 }
 
-char *getLevelSetInfoFilename(void)
+static char *getLevelSetInfoBasename(int nr)
 {
+  static char basename[32];
+
+  sprintf(basename, "levelset_%d.txt", nr + 1);
+
+  return basename;
+}
+
+char *getLevelSetInfoFilename(int nr)
+{
+  char *basename = getLevelSetInfoBasename(nr);
+  static char *info_subdir = NULL;
   static char *filename = NULL;
+
+  if (info_subdir == NULL)
+    info_subdir = getPath2(DOCS_DIRECTORY, LEVELSET_INFO_DIRECTORY);
+
+  checked_free(filename);
+
+  // look for level set info file the current level set directory
+  filename = getPath3(getCurrentLevelDir(), info_subdir, basename);
+  if (fileExists(filename))
+    return filename;
+
+  if (nr > 0)
+    return NULL;
+
   char *basenames[] =
   {
     "README",
@@ -777,6 +949,65 @@ char *getLevelSetTitleMessageFilename(int nr, boolean initial)
   return NULL;         // cannot find specified artwork file anywhere
 }
 
+static char *getCreditsBasename(int nr)
+{
+  static char basename[32];
+
+  sprintf(basename, "credits_%d.txt", nr + 1);
+
+  return basename;
+}
+
+char *getCreditsFilename(int nr, boolean global)
+{
+  char *basename = getCreditsBasename(nr);
+  char *basepath = NULL;
+  static char *credits_subdir = NULL;
+  static char *filename = NULL;
+
+  if (credits_subdir == NULL)
+    credits_subdir = getPath2(DOCS_DIRECTORY, CREDITS_DIRECTORY);
+
+  checked_free(filename);
+
+  // look for credits file in the game's base or current level set directory
+  basepath = (global ? options.base_directory : getCurrentLevelDir());
+
+  filename = getPath3(basepath, credits_subdir, basename);
+  if (fileExists(filename))
+    return filename;
+
+  return NULL;         // cannot find credits file
+}
+
+static char *getProgramInfoBasename(int nr)
+{
+  static char basename[32];
+
+  sprintf(basename, "program_%d.txt", nr + 1);
+
+  return basename;
+}
+
+char *getProgramInfoFilename(int nr)
+{
+  char *basename = getProgramInfoBasename(nr);
+  static char *info_subdir = NULL;
+  static char *filename = NULL;
+
+  if (info_subdir == NULL)
+    info_subdir = getPath2(DOCS_DIRECTORY, PROGRAM_INFO_DIRECTORY);
+
+  checked_free(filename);
+
+  // look for program info file in the game's base directory
+  filename = getPath3(options.base_directory, info_subdir, basename);
+  if (fileExists(filename))
+    return filename;
+
+  return NULL;         // cannot find program info file
+}
+
 static char *getCorrectedArtworkBasename(char *basename)
 {
   return basename;
@@ -841,7 +1072,7 @@ char *getCustomImageFilename(char *basename)
   {
     free(filename);
 
-    Warn("cannot find artwork file '%s' (using fallback)", basename);
+    WarnUsingFallback(basename);
 
     // 6th try: look for fallback artwork in old default artwork directory
     // (needed to prevent errors when trying to access unused artwork files)
@@ -912,7 +1143,7 @@ char *getCustomSoundFilename(char *basename)
   {
     free(filename);
 
-    Warn("cannot find artwork file '%s' (using fallback)", basename);
+    WarnUsingFallback(basename);
 
     // 6th try: look for fallback artwork in old default artwork directory
     // (needed to prevent errors when trying to access unused artwork files)
@@ -983,7 +1214,7 @@ char *getCustomMusicFilename(char *basename)
   {
     free(filename);
 
-    Warn("cannot find artwork file '%s' (using fallback)", basename);
+    WarnUsingFallback(basename);
 
     // 6th try: look for fallback artwork in old default artwork directory
     // (needed to prevent errors when trying to access unused artwork files)
@@ -1023,7 +1254,58 @@ char *getCustomArtworkLevelConfigFilename(int type)
   return filename;
 }
 
-char *getCustomMusicDirectory(void)
+static boolean directoryExists_CheckMusic(char *directory, boolean check_music)
+{
+  if (!directoryExists(directory))
+    return FALSE;
+
+  if (!check_music)
+    return TRUE;
+
+  Directory *dir;
+  DirectoryEntry *dir_entry;
+  int num_music = getMusicListSize();
+  boolean music_found = FALSE;
+
+  if ((dir = openDirectory(directory)) == NULL)
+    return FALSE;
+
+  while ((dir_entry = readDirectory(dir)) != NULL)     // loop all entries
+  {
+    char *basename = dir_entry->basename;
+    boolean music_already_used = FALSE;
+    int i;
+
+    // skip all music files that are configured in music config file
+    for (i = 0; i < num_music; i++)
+    {
+      struct FileInfo *music = getMusicListEntry(i);
+
+      if (strEqual(basename, music->filename))
+      {
+       music_already_used = TRUE;
+
+       break;
+      }
+    }
+
+    if (music_already_used)
+      continue;
+
+    if (FileIsMusic(dir_entry->filename))
+    {
+      music_found = TRUE;
+
+      break;
+    }
+  }
+
+  closeDirectory(dir);
+
+  return music_found;
+}
+
+static char *getCustomMusicDirectoryExt(boolean check_music)
 {
   static char *directory = NULL;
   boolean skip_setup_artwork = FALSE;
@@ -1034,7 +1316,7 @@ char *getCustomMusicDirectory(void)
   {
     // 1st try: look for special artwork in current level series directory
     directory = getPath2(getCurrentLevelDir(), MUSIC_DIRECTORY);
-    if (directoryExists(directory))
+    if (directoryExists_CheckMusic(directory, check_music))
       return directory;
 
     free(directory);
@@ -1044,7 +1326,9 @@ char *getCustomMusicDirectory(void)
     {
       // 2nd try: look for special artwork configured in level series config
       directory = getStringCopy(getLevelArtworkDir(TREE_TYPE_MUSIC_DIR));
-      if (directoryExists(directory))
+
+      // directory also valid if no unconfigured music found (no game music)
+      if (directoryExists_CheckMusic(directory, FALSE))
        return directory;
 
       free(directory);
@@ -1058,7 +1342,9 @@ char *getCustomMusicDirectory(void)
   {
     // 3rd try: look for special artwork in configured artwork directory
     directory = getStringCopy(getSetupArtworkDir(artwork.mus_current));
-    if (directoryExists(directory))
+
+    // directory also valid if no unconfigured music found (no game music)
+    if (directoryExists_CheckMusic(directory, FALSE))
       return directory;
 
     free(directory);
@@ -1066,37 +1352,104 @@ char *getCustomMusicDirectory(void)
 
   // 4th try: look for default artwork in new default artwork directory
   directory = getStringCopy(getDefaultMusicDir(MUS_DEFAULT_SUBDIR));
-  if (directoryExists(directory))
+  if (directoryExists_CheckMusic(directory, check_music))
     return directory;
 
   free(directory);
 
   // 5th try: look for default artwork in old default artwork directory
   directory = getStringCopy(options.music_directory);
-  if (directoryExists(directory))
+  if (directoryExists_CheckMusic(directory, check_music))
     return directory;
 
   return NULL;         // cannot find specified artwork file anywhere
 }
 
+char *getCustomMusicDirectory(void)
+{
+  return getCustomMusicDirectoryExt(FALSE);
+}
+
+char *getCustomMusicDirectory_NoConf(void)
+{
+  return getCustomMusicDirectoryExt(TRUE);
+}
+
+void MarkTapeDirectoryUploadsAsComplete(char *level_subdir)
+{
+  char *filename = getPath2(getTapeDir(level_subdir), UPLOADED_FILENAME);
+
+  touchFile(filename);
+
+  checked_free(filename);
+}
+
+void MarkTapeDirectoryUploadsAsIncomplete(char *level_subdir)
+{
+  char *filename = getPath2(getTapeDir(level_subdir), UPLOADED_FILENAME);
+
+  unlink(filename);
+
+  checked_free(filename);
+}
+
+boolean CheckTapeDirectoryUploadsComplete(char *level_subdir)
+{
+  char *filename = getPath2(getTapeDir(level_subdir), UPLOADED_FILENAME);
+  boolean success = fileExists(filename);
+
+  checked_free(filename);
+
+  return success;
+}
+
+void InitMissingFileHash(void)
+{
+  if (missing_file_hash == NULL)
+    freeSetupFileHash(missing_file_hash);
+
+  missing_file_hash = newSetupFileHash();
+}
+
 void InitTapeDirectory(char *level_subdir)
 {
-  createDirectory(getUserGameDataDir(), "user data", PERMS_PRIVATE);
-  createDirectory(getTapeDir(NULL), "main tape", PERMS_PRIVATE);
-  createDirectory(getTapeDir(level_subdir), "level tape", PERMS_PRIVATE);
+  boolean new_tape_dir = !directoryExists(getTapeDir(level_subdir));
+
+  createDirectory(getUserGameDataDir(), "user data");
+  createDirectory(getTapeDir(NULL), "main tape");
+  createDirectory(getTapeDir(level_subdir), "level tape");
+
+  if (new_tape_dir)
+    MarkTapeDirectoryUploadsAsComplete(level_subdir);
 }
 
 void InitScoreDirectory(char *level_subdir)
 {
-  int permissions = (program.global_scores ? PERMS_PUBLIC : PERMS_PRIVATE);
+  createDirectory(getMainUserGameDataDir(), "main user data");
+  createDirectory(getScoreDir(NULL), "main score");
+  createDirectory(getScoreDir(level_subdir), "level score");
+}
 
-  if (program.global_scores)
-    createDirectory(getCommonDataDir(), "common data", permissions);
-  else
-    createDirectory(getMainUserGameDataDir(), "main user data", permissions);
+void InitScoreCacheDirectory(char *level_subdir)
+{
+  createDirectory(getMainUserGameDataDir(), "main user data");
+  createDirectory(getCacheDir(), "cache data");
+  createDirectory(getScoreCacheDir(NULL), "main score");
+  createDirectory(getScoreCacheDir(level_subdir), "level score");
+}
+
+void InitScoreTapeDirectory(char *level_subdir, int nr)
+{
+  InitScoreDirectory(level_subdir);
+
+  createDirectory(getScoreTapeDir(level_subdir, nr), "score tape");
+}
+
+void InitScoreCacheTapeDirectory(char *level_subdir, int nr)
+{
+  InitScoreCacheDirectory(level_subdir);
 
-  createDirectory(getScoreDir(NULL), "main score", permissions);
-  createDirectory(getScoreDir(level_subdir), "level score", permissions);
+  createDirectory(getScoreCacheTapeDir(level_subdir, nr), "score tape");
 }
 
 static void SaveUserLevelInfo(void);
@@ -1105,12 +1458,15 @@ void InitUserLevelDirectory(char *level_subdir)
 {
   if (!directoryExists(getUserLevelDir(level_subdir)))
   {
-    createDirectory(getMainUserGameDataDir(), "main user data", PERMS_PRIVATE);
-    createDirectory(getUserLevelDir(NULL), "main user level", PERMS_PRIVATE);
-    createDirectory(getUserLevelDir(level_subdir), "user level", PERMS_PRIVATE);
+    createDirectory(getMainUserGameDataDir(), "main user data");
+    createDirectory(getUserLevelDir(NULL), "main user level");
 
     if (setup.internal.create_user_levelset)
+    {
+      createDirectory(getUserLevelDir(level_subdir), "user level");
+
       SaveUserLevelInfo();
+    }
   }
 }
 
@@ -1118,24 +1474,24 @@ void InitNetworkLevelDirectory(char *level_subdir)
 {
   if (!directoryExists(getNetworkLevelDir(level_subdir)))
   {
-    createDirectory(getMainUserGameDataDir(), "main 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);
+    createDirectory(getMainUserGameDataDir(), "main user data");
+    createDirectory(getNetworkDir(), "network data");
+    createDirectory(getNetworkLevelDir(NULL), "main network level");
+    createDirectory(getNetworkLevelDir(level_subdir), "network level");
   }
 }
 
 void InitLevelSetupDirectory(char *level_subdir)
 {
-  createDirectory(getUserGameDataDir(), "user data", PERMS_PRIVATE);
-  createDirectory(getLevelSetupDir(NULL), "main level setup", PERMS_PRIVATE);
-  createDirectory(getLevelSetupDir(level_subdir), "level setup", PERMS_PRIVATE);
+  createDirectory(getUserGameDataDir(), "user data");
+  createDirectory(getLevelSetupDir(NULL), "main level setup");
+  createDirectory(getLevelSetupDir(level_subdir), "level setup");
 }
 
 static void InitCacheDirectory(void)
 {
-  createDirectory(getMainUserGameDataDir(), "main user data", PERMS_PRIVATE);
-  createDirectory(getCacheDir(), "cache data", PERMS_PRIVATE);
+  createDirectory(getMainUserGameDataDir(), "main user data");
+  createDirectory(getCacheDir(), "cache data");
 }
 
 
@@ -1188,25 +1544,52 @@ int numTreeInfo(TreeInfo *node)
 
 boolean validLevelSeries(TreeInfo *node)
 {
-  return (node != NULL && !node->node_group && !node->parent_link);
+  // in a number of cases, tree node is no valid level set
+  if (node == NULL || node->node_group || node->parent_link || node->is_copy)
+    return FALSE;
+
+  return TRUE;
 }
 
-TreeInfo *getFirstValidTreeInfoEntry(TreeInfo *node)
+TreeInfo *getValidLevelSeries(TreeInfo *node, TreeInfo *default_node)
+{
+  if (validLevelSeries(node))
+    return node;
+  else if (node->is_copy)
+    return getTreeInfoFromIdentifier(leveldir_first, node->identifier);
+  else
+    return getFirstValidTreeInfoEntry(default_node);
+}
+
+static TreeInfo *getValidTreeInfoEntryExt(TreeInfo *node, boolean get_next_node)
 {
   if (node == NULL)
     return NULL;
 
-  if (node->node_group)                // enter level group (step down into tree)
+  if (node->node_group)                // enter node group (step down into tree)
     return getFirstValidTreeInfoEntry(node->node_group);
-  else if (node->parent_link)  // skip start entry of level group
-  {
-    if (node->next)            // get first real level series entry
-      return getFirstValidTreeInfoEntry(node->next);
-    else                       // leave empty level group and go on
-      return getFirstValidTreeInfoEntry(node->node_parent->next);
-  }
-  else                         // this seems to be a regular level series
+
+  if (node->parent_link)       // skip first node (back link) of node group
+    get_next_node = TRUE;
+
+  if (!get_next_node)          // get current regular tree node
     return node;
+
+  // get next regular tree node, or step up until one is found
+  while (node->next == NULL && node->node_parent != NULL)
+    node = node->node_parent;
+
+  return getFirstValidTreeInfoEntry(node->next);
+}
+
+TreeInfo *getFirstValidTreeInfoEntry(TreeInfo *node)
+{
+  return getValidTreeInfoEntryExt(node, FALSE);
+}
+
+TreeInfo *getNextValidTreeInfoEntry(TreeInfo *node)
+{
+  return getValidTreeInfoEntryExt(node, TRUE);
 }
 
 TreeInfo *getTreeInfoFirstGroupEntry(TreeInfo *node)
@@ -1260,29 +1643,25 @@ TreeInfo *getTreeInfoFromPos(TreeInfo *node, int pos)
 }
 
 static TreeInfo *getTreeInfoFromIdentifierExt(TreeInfo *node, char *identifier,
-                                             boolean include_node_groups)
+                                             int node_type_wanted)
 {
   if (identifier == NULL)
     return NULL;
 
   while (node)
   {
+    if (TREE_NODE_TYPE(node) == node_type_wanted &&
+       strEqual(identifier, node->identifier))
+      return node;
+
     if (node->node_group)
     {
-      if (include_node_groups && strEqual(identifier, node->identifier))
-       return node;
-
       TreeInfo *node_group = getTreeInfoFromIdentifierExt(node->node_group,
                                                          identifier,
-                                                         include_node_groups);
+                                                         node_type_wanted);
       if (node_group)
        return node_group;
     }
-    else if (!node->parent_link)
-    {
-      if (strEqual(identifier, node->identifier))
-       return node;
-    }
 
     node = node->next;
   }
@@ -1292,7 +1671,7 @@ static TreeInfo *getTreeInfoFromIdentifierExt(TreeInfo *node, char *identifier,
 
 TreeInfo *getTreeInfoFromIdentifier(TreeInfo *node, char *identifier)
 {
-  return getTreeInfoFromIdentifierExt(node, identifier, FALSE);
+  return getTreeInfoFromIdentifierExt(node, identifier, TREE_NODE_TYPE_DEFAULT);
 }
 
 static TreeInfo *cloneTreeNode(TreeInfo **node_top, TreeInfo *node_parent,
@@ -1330,30 +1709,56 @@ static void cloneTree(TreeInfo **ti_new, TreeInfo *ti, boolean skip_empty_sets)
   *ti_new = ti_cloned;
 }
 
-static boolean adjustTreeGraphicsForEMC(TreeInfo *node)
+static boolean adjustTreeArtworkForEMC(char **artwork_set_1,
+                                      char **artwork_set_2,
+                                      char **artwork_set, boolean prefer_2)
 {
-  boolean settings_changed = FALSE;
+  // do nothing if neither special artwork set 1 nor 2 are defined
+  if (!*artwork_set_1 && !*artwork_set_2)
+    return FALSE;
 
-  while (node)
+  boolean want_1 = (prefer_2 == FALSE);
+  boolean want_2 = (prefer_2 == TRUE);
+  boolean has_only_1 = (!*artwork_set && !*artwork_set_2);
+  boolean has_only_2 = (!*artwork_set && !*artwork_set_1);
+  char *artwork_set_new = NULL;
+
+  // replace missing special artwork 1 or 2 with (optional) standard artwork
+
+  if (!*artwork_set_1)
+    setString(artwork_set_1, *artwork_set);
+
+  if (!*artwork_set_2)
+    setString(artwork_set_2, *artwork_set);
+
+  // set standard artwork to either special artwork 1 or 2, as requested
+
+  if (*artwork_set_1 && (want_1 || has_only_1))
+    artwork_set_new = *artwork_set_1;
+
+  if (*artwork_set_2 && (want_2 || has_only_2))
+    artwork_set_new = *artwork_set_2;
+
+  if (artwork_set_new && !strEqual(*artwork_set, artwork_set_new))
   {
-    boolean want_ecs = (setup.prefer_aga_graphics == FALSE);
-    boolean want_aga = (setup.prefer_aga_graphics == TRUE);
-    boolean has_only_ecs = (!node->graphics_set && !node->graphics_set_aga);
-    boolean has_only_aga = (!node->graphics_set && !node->graphics_set_ecs);
-    char *graphics_set = NULL;
+    setString(artwork_set, artwork_set_new);
 
-    if (node->graphics_set_ecs && (want_ecs || has_only_ecs))
-      graphics_set = node->graphics_set_ecs;
+    return TRUE;
+  }
 
-    if (node->graphics_set_aga && (want_aga || has_only_aga))
-      graphics_set = node->graphics_set_aga;
+  return FALSE;
+}
 
-    if (graphics_set && !strEqual(node->graphics_set, graphics_set))
-    {
-      setString(&node->graphics_set, graphics_set);
-      settings_changed = TRUE;
-    }
+static boolean adjustTreeGraphicsForEMC(TreeInfo *node)
+{
+  boolean settings_changed = FALSE;
 
+  while (node)
+  {
+    settings_changed |= adjustTreeArtworkForEMC(&node->graphics_set_ecs,
+                                               &node->graphics_set_aga,
+                                               &node->graphics_set,
+                                               setup.prefer_aga_graphics);
     if (node->node_group != NULL)
       settings_changed |= adjustTreeGraphicsForEMC(node->node_group);
 
@@ -1369,24 +1774,10 @@ static boolean adjustTreeSoundsForEMC(TreeInfo *node)
 
   while (node)
   {
-    boolean want_default = (setup.prefer_lowpass_sounds == FALSE);
-    boolean want_lowpass = (setup.prefer_lowpass_sounds == TRUE);
-    boolean has_only_default = (!node->sounds_set && !node->sounds_set_lowpass);
-    boolean has_only_lowpass = (!node->sounds_set && !node->sounds_set_default);
-    char *sounds_set = NULL;
-
-    if (node->sounds_set_default && (want_default || has_only_default))
-      sounds_set = node->sounds_set_default;
-
-    if (node->sounds_set_lowpass && (want_lowpass || has_only_lowpass))
-      sounds_set = node->sounds_set_lowpass;
-
-    if (sounds_set && !strEqual(node->sounds_set, sounds_set))
-    {
-      setString(&node->sounds_set, sounds_set);
-      settings_changed = TRUE;
-    }
-
+    settings_changed |= adjustTreeArtworkForEMC(&node->sounds_set_default,
+                                               &node->sounds_set_lowpass,
+                                               &node->sounds_set,
+                                               setup.prefer_lowpass_sounds);
     if (node->node_group != NULL)
       settings_changed |= adjustTreeSoundsForEMC(node->node_group);
 
@@ -1396,9 +1787,10 @@ static boolean adjustTreeSoundsForEMC(TreeInfo *node)
   return settings_changed;
 }
 
-void dumpTreeInfo(TreeInfo *node, int depth)
+int dumpTreeInfo(TreeInfo *node, int depth)
 {
   char bullet_list[] = { '-', '*', 'o' };
+  int num_leaf_nodes = 0;
   int i;
 
   if (depth == 0)
@@ -1414,7 +1806,11 @@ void dumpTreeInfo(TreeInfo *node, int depth)
     DebugContinued("tree", "%c '%s' ['%s] [PARENT: '%s'] %s\n",
                   bullet, node->name, node->identifier,
                   (node->node_parent ? node->node_parent->identifier : "-"),
-                  (node->node_group ? "[GROUP]" : ""));
+                  (node->node_group ? "[GROUP]" :
+                   node->is_copy ? "[COPY]" : ""));
+
+    if (!node->node_group && !node->parent_link)
+      num_leaf_nodes++;
 
     /*
     // use for dumping artwork info tree
@@ -1423,10 +1819,15 @@ void dumpTreeInfo(TreeInfo *node, int depth)
     */
 
     if (node->node_group != NULL)
-      dumpTreeInfo(node->node_group, depth + 1);
+      num_leaf_nodes += dumpTreeInfo(node->node_group, depth + 1);
 
     node = node->next;
   }
+
+  if (depth == 0)
+    Debug("tree", "Summary: %d leaf nodes found", num_leaf_nodes);
+
+  return num_leaf_nodes;
 }
 
 void sortTreeInfoBySortFunction(TreeInfo **node_first,
@@ -1488,7 +1889,7 @@ void sortTreeInfo(TreeInfo **node_first)
 // some stuff from "files.c"
 // ============================================================================
 
-#if defined(PLATFORM_WIN32)
+#if defined(PLATFORM_WINDOWS)
 #ifndef S_IRGRP
 #define S_IRGRP S_IRUSR
 #endif
@@ -1513,7 +1914,7 @@ void sortTreeInfo(TreeInfo **node_first)
 #ifndef S_ISGID
 #define S_ISGID 0
 #endif
-#endif // PLATFORM_WIN32
+#endif // PLATFORM_WINDOWS
 
 // file permissions for newly written files
 #define MODE_R_ALL             (S_IRUSR | S_IRGRP | S_IROTH)
@@ -1537,7 +1938,7 @@ char *getHomeDir(void)
 {
   static char *dir = NULL;
 
-#if defined(PLATFORM_WIN32)
+#if defined(PLATFORM_WINDOWS)
   if (dir == NULL)
   {
     dir = checked_malloc(MAX_PATH + 1);
@@ -1545,6 +1946,8 @@ char *getHomeDir(void)
     if (!SUCCEEDED(SHGetFolderPath(NULL, CSIDL_PERSONAL, NULL, 0, dir)))
       strcpy(dir, ".");
   }
+#elif defined(PLATFORM_EMSCRIPTEN)
+  dir = PERSISTENT_DIRECTORY;
 #elif defined(PLATFORM_UNIX)
   if (dir == NULL)
   {
@@ -1565,34 +1968,11 @@ char *getHomeDir(void)
   return dir;
 }
 
-char *getCommonDataDir(void)
-{
-  static char *common_data_dir = NULL;
-
-#if defined(PLATFORM_WIN32)
-  if (common_data_dir == NULL)
-  {
-    char *dir = checked_malloc(MAX_PATH + 1);
-
-    if (SUCCEEDED(SHGetFolderPath(NULL, CSIDL_COMMON_DOCUMENTS, NULL, 0, dir))
-       && !strEqual(dir, ""))          // empty for Windows 95/98
-      common_data_dir = getPath2(dir, program.userdata_subdir);
-    else
-      common_data_dir = options.rw_base_directory;
-  }
-#else
-  if (common_data_dir == NULL)
-    common_data_dir = options.rw_base_directory;
-#endif
-
-  return common_data_dir;
-}
-
 char *getPersonalDataDir(void)
 {
   static char *personal_data_dir = NULL;
 
-#if defined(PLATFORM_MACOSX)
+#if defined(PLATFORM_MAC)
   if (personal_data_dir == NULL)
     personal_data_dir = getPath2(getHomeDir(), "Documents");
 #else
@@ -1646,7 +2026,7 @@ static mode_t posix_umask(mode_t mask)
 
 static int posix_mkdir(const char *pathname, mode_t mode)
 {
-#if defined(PLATFORM_WIN32)
+#if defined(PLATFORM_WINDOWS)
   return mkdir(pathname);
 #else
   return mkdir(pathname, mode);
@@ -1662,13 +2042,14 @@ static boolean posix_process_running_setgid(void)
 #endif
 }
 
-void createDirectory(char *dir, char *text, int permission_class)
+void createDirectory(char *dir, char *text)
 {
   if (directoryExists(dir))
     return;
 
   // leave "other" permissions in umask untouched, but ensure group parts
   // of USERDATA_DIR_MODE are not masked
+  int permission_class = PERMS_PRIVATE;
   mode_t dir_mode = (permission_class == PERMS_PRIVATE ?
                     DIR_PERMS_PRIVATE : DIR_PERMS_PUBLIC);
   mode_t last_umask = posix_umask(0);
@@ -1697,17 +2078,17 @@ void createDirectory(char *dir, char *text, int permission_class)
 
 void InitMainUserDataDirectory(void)
 {
-  createDirectory(getMainUserGameDataDir(), "main user data", PERMS_PRIVATE);
+  createDirectory(getMainUserGameDataDir(), "main user data");
 }
 
 void InitUserDataDirectory(void)
 {
-  createDirectory(getMainUserGameDataDir(), "main user data", PERMS_PRIVATE);
+  createDirectory(getMainUserGameDataDir(), "main user data");
 
   if (user.nr != 0)
   {
-    createDirectory(getUserDir(-1), "users", PERMS_PRIVATE);
-    createDirectory(getUserDir(user.nr), "user data", PERMS_PRIVATE);
+    createDirectory(getUserDir(-1), "users");
+    createDirectory(getUserDir(user.nr), "user data");
   }
 }
 
@@ -1723,21 +2104,6 @@ void SetFilePermissions(char *filename, int permission_class)
   chmod(filename, perms);
 }
 
-char *getCookie(char *file_type)
-{
-  static char cookie[MAX_COOKIE_LEN + 1];
-
-  if (strlen(program.cookie_prefix) + 1 +
-      strlen(file_type) + strlen("_FILE_VERSION_x.x") > MAX_COOKIE_LEN)
-    return "[COOKIE ERROR]";   // should never happen
-
-  sprintf(cookie, "%s_%s_FILE_VERSION_%d.%d",
-         program.cookie_prefix, file_type,
-         program.version_super, program.version_major);
-
-  return cookie;
-}
-
 void fprintFileHeader(FILE *file, char *basename)
 {
   char *prefix = "# ";
@@ -1945,7 +2311,7 @@ unsigned int get_hash_from_key(void *key)
   return hash;
 }
 
-static int keys_are_equal(void *key1, void *key2)
+int hash_keys_are_equal(void *key1, void *key2)
 {
   return (strEqual((char *)key1, (char *)key2));
 }
@@ -1953,7 +2319,7 @@ static int keys_are_equal(void *key1, void *key2)
 SetupFileHash *newSetupFileHash(void)
 {
   SetupFileHash *new_hash =
-    create_hashtable(16, 0.75, get_hash_from_key, keys_are_equal);
+    create_hashtable(16, 0.75, get_hash_from_key, hash_keys_are_equal);
 
   if (new_hash == NULL)
     Fail("create_hashtable() failed -- out of memory");
@@ -2325,25 +2691,53 @@ static boolean loadSetupFileData(void *setup_file_data, char *filename,
   return TRUE;
 }
 
+static int compareSetupFileData(const void *object1, const void *object2)
+{
+  const struct ConfigInfo *entry1 = (struct ConfigInfo *)object1;
+  const struct ConfigInfo *entry2 = (struct ConfigInfo *)object2;
+
+  return strcmp(entry1->token, entry2->token);
+}
+
 static void saveSetupFileHash(SetupFileHash *hash, char *filename)
 {
+  int item_count = hashtable_count(hash);
+  int item_size = sizeof(struct ConfigInfo);
+  struct ConfigInfo *sort_array = checked_malloc(item_count * item_size);
   FILE *file;
+  int i = 0;
 
-  if (!(file = fopen(filename, MODE_WRITE)))
+  // copy string pointers from hash to array
+  BEGIN_HASH_ITERATION(hash, itr)
   {
-    Warn("cannot write configuration file '%s'", filename);
+    sort_array[i].token = HASH_ITERATION_TOKEN(itr);
+    sort_array[i].value = HASH_ITERATION_VALUE(itr);
 
-    return;
+    i++;
+
+    if (i > item_count)                // should never happen
+      break;
   }
+  END_HASH_ITERATION(hash, itr)
 
-  BEGIN_HASH_ITERATION(hash, itr)
+  // sort string pointers from hash in array
+  qsort(sort_array, item_count, item_size, compareSetupFileData);
+
+  if (!(file = fopen(filename, MODE_WRITE)))
   {
-    fprintf(file, "%s\n", getFormattedSetupEntry(HASH_ITERATION_TOKEN(itr),
-                                                HASH_ITERATION_VALUE(itr)));
+    Warn("cannot write configuration file '%s'", filename);
+
+    return;
   }
-  END_HASH_ITERATION(hash, itr)
 
+  fprintf(file, "%s\n\n", getFormattedSetupEntry("program.version",
+                                                program.version_string));
+  for (i = 0; i < item_count; i++)
+    fprintf(file, "%s\n", getFormattedSetupEntry(sort_array[i].token,
+                                                sort_array[i].value));
   fclose(file);
+
+  checked_free(sort_array);
 }
 
 SetupFileList *loadSetupFileList(char *filename)
@@ -2387,6 +2781,7 @@ SetupFileHash *loadSetupFileHash(char *filename)
 // ============================================================================
 
 #define TOKEN_STR_LAST_LEVEL_SERIES            "last_level_series"
+#define TOKEN_STR_LAST_PLAYED_MENU_USED                "last_played_menu_used"
 #define TOKEN_STR_LAST_PLAYED_LEVEL            "last_played_level"
 #define TOKEN_STR_HANDICAP_LEVEL               "handicap_level"
 #define TOKEN_STR_LAST_USER                    "last_user"
@@ -2419,11 +2814,15 @@ SetupFileHash *loadSetupFileHash(char *filename)
 #define LEVELINFO_TOKEN_FILENAME               24
 #define LEVELINFO_TOKEN_FILETYPE               25
 #define LEVELINFO_TOKEN_SPECIAL_FLAGS          26
-#define LEVELINFO_TOKEN_HANDICAP               27
-#define LEVELINFO_TOKEN_SKIP_LEVELS            28
-#define LEVELINFO_TOKEN_USE_EMC_TILES          29
+#define LEVELINFO_TOKEN_EMPTY_LEVEL_NAME       27
+#define LEVELINFO_TOKEN_FORCE_LEVEL_NAME       28
+#define LEVELINFO_TOKEN_HANDICAP               29
+#define LEVELINFO_TOKEN_TIME_LIMIT             30
+#define LEVELINFO_TOKEN_SKIP_LEVELS            31
+#define LEVELINFO_TOKEN_USE_EMC_TILES          32
+#define LEVELINFO_TOKEN_INFO_SCREENS_FROM_MAIN 33
 
-#define NUM_LEVELINFO_TOKENS                   30
+#define NUM_LEVELINFO_TOKENS                   34
 
 static LevelDirTree ldi;
 
@@ -2457,9 +2856,13 @@ static struct TokenInfo levelinfo_tokens[] =
   { TYPE_STRING,       &ldi.level_filename,    "filename"              },
   { TYPE_STRING,       &ldi.level_filetype,    "filetype"              },
   { TYPE_STRING,       &ldi.special_flags,     "special_flags"         },
+  { TYPE_STRING,       &ldi.empty_level_name,  "empty_level_name"      },
+  { TYPE_BOOLEAN,      &ldi.force_level_name,  "force_level_name"      },
   { TYPE_BOOLEAN,      &ldi.handicap,          "handicap"              },
+  { TYPE_BOOLEAN,      &ldi.time_limit,        "time_limit"            },
   { TYPE_BOOLEAN,      &ldi.skip_levels,       "skip_levels"           },
-  { TYPE_BOOLEAN,      &ldi.use_emc_tiles,     "use_emc_tiles"         }
+  { TYPE_BOOLEAN,      &ldi.use_emc_tiles,     "use_emc_tiles"         },
+  { TYPE_BOOLEAN,      &ldi.info_screens_from_main, "info_screens_from_main" }
 };
 
 static struct TokenInfo artworkinfo_tokens[] =
@@ -2477,7 +2880,6 @@ static struct TokenInfo artworkinfo_tokens[] =
   { TYPE_STRING,       &ldi.basepath,          "basepath"              },
   { TYPE_STRING,       &ldi.fullpath,          "fullpath"              },
   { TYPE_BOOLEAN,      &ldi.in_user_dir,       "in_user_dir"           },
-  { TYPE_INTEGER,      &ldi.color,             "color"                 },
   { TYPE_STRING,       &ldi.class_desc,        "class_desc"            },
 
   { -1,                        NULL,                   NULL                    },
@@ -2525,6 +2927,7 @@ static void setTreeInfoToDefaults(TreeInfo *ti, int type)
   ti->sort_priority = LEVELCLASS_UNDEFINED;    // default: least priority
   ti->latest_engine = FALSE;                   // default: get from level
   ti->parent_link = FALSE;
+  ti->is_copy = FALSE;
   ti->in_user_dir = FALSE;
   ti->user_defined = FALSE;
   ti->color = 0;
@@ -2554,6 +2957,9 @@ static void setTreeInfoToDefaults(TreeInfo *ti, int type)
 
     ti->special_flags = NULL;
 
+    ti->empty_level_name = NULL;
+    ti->force_level_name = FALSE;
+
     ti->levels = 0;
     ti->first_level = 0;
     ti->last_level = 0;
@@ -2561,9 +2967,11 @@ static void setTreeInfoToDefaults(TreeInfo *ti, int type)
     ti->handicap_level = 0;
     ti->readonly = TRUE;
     ti->handicap = TRUE;
+    ti->time_limit = TRUE;
     ti->skip_levels = FALSE;
 
     ti->use_emc_tiles = FALSE;
+    ti->info_screens_from_main = FALSE;
   }
 }
 
@@ -2606,6 +3014,7 @@ static void setTreeInfoToDefaultsFromParent(TreeInfo *ti, TreeInfo *parent)
   ti->sort_priority = parent->sort_priority;
   ti->latest_engine = parent->latest_engine;
   ti->parent_link = FALSE;
+  ti->is_copy = FALSE;
   ti->in_user_dir = parent->in_user_dir;
   ti->user_defined = parent->user_defined;
   ti->color = parent->color;
@@ -2635,6 +3044,9 @@ static void setTreeInfoToDefaultsFromParent(TreeInfo *ti, TreeInfo *parent)
 
     ti->special_flags = getStringCopy(parent->special_flags);
 
+    ti->empty_level_name = getStringCopy(parent->empty_level_name);
+    ti->force_level_name = parent->force_level_name;
+
     ti->levels = parent->levels;
     ti->first_level = parent->first_level;
     ti->last_level = parent->last_level;
@@ -2642,9 +3054,11 @@ static void setTreeInfoToDefaultsFromParent(TreeInfo *ti, TreeInfo *parent)
     ti->handicap_level = parent->handicap_level;
     ti->readonly = parent->readonly;
     ti->handicap = parent->handicap;
+    ti->time_limit = parent->time_limit;
     ti->skip_levels = parent->skip_levels;
 
     ti->use_emc_tiles = parent->use_emc_tiles;
+    ti->info_screens_from_main = parent->info_screens_from_main;
   }
 }
 
@@ -2697,6 +3111,9 @@ static TreeInfo *getTreeInfoCopy(TreeInfo *ti)
 
   ti_copy->special_flags       = getStringCopy(ti->special_flags);
 
+  ti_copy->empty_level_name    = getStringCopy(ti->empty_level_name);
+  ti_copy->force_level_name    = ti->force_level_name;
+
   ti_copy->levels              = ti->levels;
   ti_copy->first_level         = ti->first_level;
   ti_copy->last_level          = ti->last_level;
@@ -2706,13 +3123,16 @@ static TreeInfo *getTreeInfoCopy(TreeInfo *ti)
 
   ti_copy->level_group         = ti->level_group;
   ti_copy->parent_link         = ti->parent_link;
+  ti_copy->is_copy             = ti->is_copy;
   ti_copy->in_user_dir         = ti->in_user_dir;
   ti_copy->user_defined                = ti->user_defined;
   ti_copy->readonly            = ti->readonly;
   ti_copy->handicap            = ti->handicap;
+  ti_copy->time_limit          = ti->time_limit;
   ti_copy->skip_levels         = ti->skip_levels;
 
   ti_copy->use_emc_tiles       = ti->use_emc_tiles;
+  ti_copy->info_screens_from_main = ti->info_screens_from_main;
 
   ti_copy->color               = ti->color;
   ti_copy->class_desc          = getStringCopy(ti->class_desc);
@@ -2832,40 +3252,13 @@ static int compareTreeInfoEntries(const void *object1, const void *object2)
 {
   const TreeInfo *entry1 = *((TreeInfo **)object1);
   const TreeInfo *entry2 = *((TreeInfo **)object2);
-  int class_sorting1 = 0, class_sorting2 = 0;
-  int compare_result;
-
-  if (entry1->type == TREE_TYPE_LEVEL_DIR)
-  {
-    class_sorting1 = LEVELSORTING(entry1);
-    class_sorting2 = LEVELSORTING(entry2);
-  }
-  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);
-  }
+  int tree_sorting1 = TREE_SORTING(entry1);
+  int tree_sorting2 = TREE_SORTING(entry2);
 
-  if (entry1->parent_link || entry2->parent_link)
-    compare_result = (entry1->parent_link ? -1 : +1);
-  else if (entry1->sort_priority == entry2->sort_priority)
-  {
-    char *name1 = getStringToLower(entry1->name_sorting);
-    char *name2 = getStringToLower(entry2->name_sorting);
-
-    compare_result = strcmp(name1, name2);
-
-    free(name1);
-    free(name2);
-  }
-  else if (class_sorting1 == class_sorting2)
-    compare_result = entry1->sort_priority - entry2->sort_priority;
+  if (tree_sorting1 != tree_sorting2)
+    return (tree_sorting1 - tree_sorting2);
   else
-    compare_result = class_sorting1 - class_sorting2;
-
-  return compare_result;
+    return strcasecmp(entry1->name_sorting, entry2->name_sorting);
 }
 
 static TreeInfo *createParentTreeInfoNode(TreeInfo *node_parent)
@@ -2888,7 +3281,7 @@ static TreeInfo *createParentTreeInfoNode(TreeInfo *node_parent)
   setString(&ti_new->subdir, STRING_PARENT_DIRECTORY);
   setString(&ti_new->fullpath, node_parent->fullpath);
 
-  ti_new->sort_priority = node_parent->sort_priority;
+  ti_new->sort_priority = LEVELCLASS_PARENT;
   ti_new->latest_engine = node_parent->latest_engine;
 
   setString(&ti_new->class_desc, getLevelClassDescription(ti_new));
@@ -2911,14 +3304,14 @@ static TreeInfo *createTopTreeInfoNode(TreeInfo *node_first)
   ti_new->node_parent = NULL;
   ti_new->parent_link = FALSE;
 
-  setString(&ti_new->identifier, node_first->identifier);
+  setString(&ti_new->identifier, "top_tree_node");
   setString(&ti_new->name, TREE_INFOTEXT(type));
   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->sort_priority = LEVELCLASS_TOP;
   ti_new->latest_engine = node_first->latest_engine;
 
   setString(&ti_new->class_desc, TREE_INFOTEXT(type));
@@ -2928,12 +3321,36 @@ static TreeInfo *createTopTreeInfoNode(TreeInfo *node_first)
 
   TreeInfo *ti_new2 = createParentTreeInfoNode(ti_new);
 
-  setString(&ti_new2->name, BACKLINK_TEXT_MAIN);
+  setString(&ti_new2->name, TREE_BACKLINK_TEXT(type));
   setString(&ti_new2->name_sorting, ti_new2->name);
 
   return ti_new;
 }
 
+static void setTreeInfoParentNodes(TreeInfo *node, TreeInfo *node_parent)
+{
+  while (node)
+  {
+    if (node->node_group)
+      setTreeInfoParentNodes(node->node_group, node);
+
+    node->node_parent = node_parent;
+
+    node = node->next;
+  }
+}
+
+TreeInfo *addTopTreeInfoNode(TreeInfo *node_first)
+{
+  // add top tree node with back link node in previous tree
+  node_first = createTopTreeInfoNode(node_first);
+
+  // set all parent links (back links) in complete tree
+  setTreeInfoParentNodes(node_first, NULL);
+
+  return node_first;
+}
+
 
 // ----------------------------------------------------------------------------
 // functions for handling level and custom artwork info cache
@@ -2950,6 +3367,17 @@ static void LoadArtworkInfoCache(void)
     // try to load artwork info hash from already existing cache file
     artworkinfo_cache_old = loadSetupFileHash(filename);
 
+    // try to get program version that artwork info cache was written with
+    char *version = getHashEntry(artworkinfo_cache_old, "program.version");
+
+    // check program version of artwork info cache against current version
+    if (!strEqual(version, program.version_string))
+    {
+      freeSetupFileHash(artworkinfo_cache_old);
+
+      artworkinfo_cache_old = NULL;
+    }
+
     // if no artwork info cache file was found, start with empty hash
     if (artworkinfo_cache_old == NULL)
       artworkinfo_cache_old = newSetupFileHash();
@@ -2959,10 +3387,15 @@ static void LoadArtworkInfoCache(void)
 
   if (artworkinfo_cache_new == NULL)
     artworkinfo_cache_new = newSetupFileHash();
+
+  update_artworkinfo_cache = FALSE;
 }
 
 static void SaveArtworkInfoCache(void)
 {
+  if (!update_artworkinfo_cache)
+    return;
+
   char *filename = getPath2(getCacheDir(), ARTWORKINFO_CACHE_FILE);
 
   InitCacheDirectory();
@@ -3007,6 +3440,9 @@ static boolean modifiedFileTimestamp(char *filename, char *timestamp_string)
   if (timestamp_string == NULL)
     return TRUE;
 
+  if (!fileExists(filename))                   // file does not exist
+    return (atoi(timestamp_string) != 0);
+
   if (stat(filename, &file_status) != 0)       // cannot stat file
     return TRUE;
 
@@ -3461,15 +3897,13 @@ static boolean LoadLevelInfoFromLevelConf(TreeInfo **node_first,
   leveldir_new->user_defined =
     (leveldir_new->in_user_dir && IS_LEVELCLASS_PRIVATE(leveldir_new));
 
-  leveldir_new->color = LEVELCOLOR(leveldir_new);
-
   setString(&leveldir_new->class_desc, getLevelClassDescription(leveldir_new));
 
   leveldir_new->handicap_level =       // set handicap to default value
     (leveldir_new->user_defined || !leveldir_new->handicap ?
      leveldir_new->last_level : leveldir_new->first_level);
 
-  DrawInitText(leveldir_new->name, 150, FC_YELLOW);
+  DrawInitTextItem(leveldir_new->name);
 
   pushTreeInfo(node_first, leveldir_new);
 
@@ -3556,9 +3990,13 @@ static void LoadLevelInfoFromLevelDir(TreeInfo **node_first,
                                                    level_directory, ".");
   }
 
-  if (!valid_entry_found)
+  boolean valid_entry_expected =
+    (strEqual(level_directory, options.level_directory) ||
+     setup.internal.create_user_levelset);
+
+  if (valid_entry_expected && !valid_entry_found)
     Warn("cannot find any valid level series in directory '%s'",
-         level_directory);
+        level_directory);
 }
 
 boolean AdjustGraphicsForEMC(void)
@@ -3585,7 +4023,7 @@ void LoadLevelInfo(void)
 {
   InitUserLevelDirectory(getLoginName());
 
-  DrawInitText("Loading level series", 120, FC_GREEN);
+  DrawInitTextHead("Loading level series");
 
   LoadLevelInfoFromLevelDir(&leveldir_first, NULL, options.level_directory);
   LoadLevelInfoFromLevelDir(&leveldir_first, NULL, getUserLevelDir(NULL));
@@ -3706,9 +4144,6 @@ 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)
-  artwork_new->color = ARTWORKCOLOR(artwork_new);
-
   setString(&artwork_new->class_desc, getLevelClassDescription(artwork_new));
 
   if (setup_file_hash == NULL) // (after determining ".user_defined")
@@ -3726,9 +4161,6 @@ static boolean LoadArtworkInfoFromArtworkConf(TreeInfo **node_first,
        artwork_new->sort_priority = ARTWORKCLASS_CLASSICS;
       }
 
-      // set to new values after changing ".sort_priority"
-      artwork_new->color = ARTWORKCOLOR(artwork_new);
-
       setString(&artwork_new->class_desc,
                getLevelClassDescription(artwork_new));
     }
@@ -3864,7 +4296,7 @@ void LoadArtworkInfo(void)
 {
   LoadArtworkInfoCache();
 
-  DrawInitText("Looking for custom artwork", 120, FC_GREEN);
+  DrawInitTextHead("Looking for custom artwork");
 
   LoadArtworkInfoFromArtworkDir(&artwork.gfx_first, NULL,
                                options.graphics_directory,
@@ -3926,7 +4358,7 @@ void LoadArtworkInfo(void)
 static void MoveArtworkInfoIntoSubTree(ArtworkDirTree **artwork_node)
 {
   ArtworkDirTree *artwork_new = newTreeInfo();
-  char *top_node_name = "dedicated custom artwork";
+  char *top_node_name = "standalone artwork";
 
   setTreeInfoToDefaults(artwork_new, (*artwork_node)->type);
 
@@ -3986,7 +4418,9 @@ static void LoadArtworkInfoFromLevelInfoExt(ArtworkDirTree **artwork_node,
          setString(&artwork_new->name_sorting, level_node->name_sorting);
 
          artwork_new->sort_priority = level_node->sort_priority;
-         artwork_new->color = LEVELCOLOR(artwork_new);
+         artwork_new->in_user_dir = level_node->in_user_dir;
+
+         update_artworkinfo_cache = TRUE;
        }
 
        free(path);
@@ -3997,7 +4431,7 @@ static void LoadArtworkInfoFromLevelInfoExt(ArtworkDirTree **artwork_node,
        setArtworkInfoCacheEntry(artwork_new, level_node, type);
     }
 
-    DrawInitText(level_node->name, 150, FC_YELLOW);
+    DrawInitTextItem(level_node->name);
 
     if (level_node->node_group != NULL)
     {
@@ -4015,8 +4449,8 @@ static void LoadArtworkInfoFromLevelInfoExt(ArtworkDirTree **artwork_node,
       if (node_parent == NULL)         // check for top tree node
       {
        char *top_node_name = (empty_level_set_mode ?
-                              "artwork-only level sets" :
-                              "artwork from level sets");
+                              "artwork for certain level sets" :
+                              "artwork included in level sets");
 
        setString(&artwork_new->name,         top_node_name);
        setString(&artwork_new->name_sorting, top_node_name);
@@ -4048,17 +4482,22 @@ static void LoadArtworkInfoFromLevelInfoExt(ArtworkDirTree **artwork_node,
 
 static void LoadArtworkInfoFromLevelInfo(ArtworkDirTree **artwork_node)
 {
+  // move peviously loaded artwork tree into separate sub-tree
   MoveArtworkInfoIntoSubTree(artwork_node);
 
+  // load artwork from level sets into separate sub-trees
   LoadArtworkInfoFromLevelInfoExt(artwork_node, NULL, leveldir_first_all, TRUE);
   LoadArtworkInfoFromLevelInfoExt(artwork_node, NULL, leveldir_first_all, FALSE);
+
+  // add top tree node over all sub-trees and set parent links
+  *artwork_node = addTopTreeInfoNode(*artwork_node);
 }
 
 void LoadLevelArtworkInfo(void)
 {
   print_timestamp_init("LoadLevelArtworkInfo");
 
-  DrawInitText("Looking for custom level artwork", 120, FC_GREEN);
+  DrawInitTextHead("Looking for custom level artwork");
 
   print_timestamp_time("DrawTimeText");
 
@@ -4155,6 +4594,12 @@ static boolean AddTreeSetToTreeInfoExt(TreeInfo *tree_node_old, char *tree_dir,
   TreeInfo *tree_node_new = getTreeInfoFromIdentifier(*tree_node_first,
                                                      tree_subdir_new);
 
+  // if not found, check if added node is level group or artwork group
+  if (tree_node_new == NULL)
+    tree_node_new = getTreeInfoFromIdentifierExt(*tree_node_first,
+                                                tree_subdir_new,
+                                                TREE_NODE_TYPE_GROUP);
+
   if (tree_node_new == NULL)           // should not happen
     return FALSE;
 
@@ -4308,7 +4753,7 @@ boolean CreateUserLevelSet(char *level_subdir, char *level_name,
   int i;
 
   // create user level sub-directory, if needed
-  createDirectory(getUserLevelDir(level_subdir), "user level", PERMS_PRIVATE);
+  createDirectory(getUserLevelDir(level_subdir), "user level");
 
   filename = getPath2(getUserLevelDir(level_subdir), LEVELINFO_FILENAME);
 
@@ -4506,9 +4951,11 @@ static void InitLastPlayedLevels_ParentNode(void)
   setTreeInfoToDefaultsFromParent(leveldir_new, leveldir_first);
 
   leveldir_new->level_group = TRUE;
+  leveldir_new->sort_priority = LEVELCLASS_LAST_PLAYED_LEVEL;
 
   setString(&leveldir_new->identifier, TOKEN_STR_LAST_LEVEL_SERIES);
   setString(&leveldir_new->name, "<< (last played level sets)");
+  setString(&leveldir_new->name_sorting, leveldir_new->name);
 
   pushTreeInfo(leveldir_top, leveldir_new);
 
@@ -4519,7 +4966,6 @@ static void InitLastPlayedLevels_ParentNode(void)
 void UpdateLastPlayedLevels_TreeInfo(void)
 {
   char **last_level_series = setup.level_setup.last_level_series;
-  boolean reset_leveldir_current = FALSE;
   LevelDirTree *leveldir_last;
   TreeInfo **node_new = NULL;
   int i;
@@ -4529,13 +4975,9 @@ void UpdateLastPlayedLevels_TreeInfo(void)
 
   InitLastPlayedLevels_ParentNode();
 
-  // check if current level set is from "last played" sub-tree to be rebuilt
-  reset_leveldir_current = strEqual(leveldir_current->node_parent->identifier,
-                                   TOKEN_STR_LAST_LEVEL_SERIES);
-
   leveldir_last = getTreeInfoFromIdentifierExt(leveldir_first,
                                               TOKEN_STR_LAST_LEVEL_SERIES,
-                                              TRUE);
+                                              TREE_NODE_TYPE_GROUP);
   if (leveldir_last == NULL)
     return;
 
@@ -4543,16 +4985,22 @@ void UpdateLastPlayedLevels_TreeInfo(void)
 
   freeTreeInfo(*node_new);
 
+  *node_new = NULL;
+
   for (i = 0; last_level_series[i] != NULL; i++)
   {
     LevelDirTree *node_last = getTreeInfoFromIdentifier(leveldir_first,
                                                        last_level_series[i]);
+    if (node_last == NULL)
+      continue;
 
     *node_new = getTreeInfoCopy(node_last);    // copy complete node
 
     (*node_new)->node_top = &leveldir_first;   // correct top node link
     (*node_new)->node_parent = leveldir_last;  // correct parent node link
 
+    (*node_new)->is_copy = TRUE;               // mark entry as node copy
+
     (*node_new)->node_group = NULL;
     (*node_new)->next = NULL;
 
@@ -4560,10 +5008,6 @@ void UpdateLastPlayedLevels_TreeInfo(void)
 
     node_new = &((*node_new)->next);
   }
-
-  if (reset_leveldir_current)
-    leveldir_current = getTreeInfoFromIdentifier(leveldir_first,
-                                                 last_level_series[0]);
 }
 
 static void UpdateLastPlayedLevels_List(void)
@@ -4585,6 +5029,53 @@ static void UpdateLastPlayedLevels_List(void)
   setString(&last_level_series[0], leveldir_current->identifier);
 }
 
+#define LAST_PLAYED_MODE_SET                   1
+#define LAST_PLAYED_MODE_SET_FORCED            2
+#define LAST_PLAYED_MODE_GET                   3
+
+static TreeInfo *StoreOrRestoreLastPlayedLevels(TreeInfo *node, int mode)
+{
+  static char *identifier = NULL;
+
+  if (mode == LAST_PLAYED_MODE_SET)
+  {
+    setString(&identifier, (node && node->is_copy ? node->identifier : NULL));
+  }
+  else if (mode == LAST_PLAYED_MODE_SET_FORCED)
+  {
+    setString(&identifier, (node ? node->identifier : NULL));
+  }
+  else if (mode == LAST_PLAYED_MODE_GET)
+  {
+    TreeInfo *node_new = getTreeInfoFromIdentifierExt(leveldir_first,
+                                                     identifier,
+                                                     TREE_NODE_TYPE_COPY);
+    return (node_new != NULL ? node_new : node);
+  }
+
+  return NULL;         // not used
+}
+
+void StoreLastPlayedLevels(TreeInfo *node)
+{
+  StoreOrRestoreLastPlayedLevels(node, LAST_PLAYED_MODE_SET);
+}
+
+void ForcedStoreLastPlayedLevels(TreeInfo *node)
+{
+  StoreOrRestoreLastPlayedLevels(node, LAST_PLAYED_MODE_SET_FORCED);
+}
+
+void RestoreLastPlayedLevels(TreeInfo **node)
+{
+  *node = StoreOrRestoreLastPlayedLevels(*node, LAST_PLAYED_MODE_GET);
+}
+
+boolean CheckLastPlayedLevels(void)
+{
+  return (StoreOrRestoreLastPlayedLevels(NULL, LAST_PLAYED_MODE_GET) != NULL);
+}
+
 void LoadLevelSetup_LastSeries(void)
 {
   // --------------------------------------------------------------------------
@@ -4620,6 +5111,13 @@ void LoadLevelSetup_LastSeries(void)
     if (leveldir_current == NULL)
       leveldir_current = getFirstValidTreeInfoEntry(leveldir_first);
 
+    char *last_played_menu_used =
+      getHashEntry(level_setup_hash, TOKEN_STR_LAST_PLAYED_MENU_USED);
+
+    // store if last level set was selected from "last played" menu
+    if (strEqual(last_played_menu_used, "true"))
+      ForcedStoreLastPlayedLevels(leveldir_current);
+
     for (i = 0; i < MAX_LEVELDIR_HISTORY; i++)
     {
       char token[strlen(TOKEN_STR_LAST_LEVEL_SERIES) + 10];
@@ -4682,11 +5180,18 @@ static void SaveLevelSetup_LastSeries_Ext(boolean deactivate_last_level_series)
     fprintf(file, "# %s\n# ", "the following level set may have caused a problem and was deactivated");
 
   fprintf(file, "%s\n\n", getFormattedSetupEntry(TOKEN_STR_LAST_LEVEL_SERIES,
-                                              leveldir_current->identifier));
+                                                leveldir_current->identifier));
+
+  // store if last level set was selected from "last played" menu
+  boolean last_played_menu_used = CheckLastPlayedLevels();
+  char *setup_value = getSetupValue(TYPE_BOOLEAN, &last_played_menu_used);
+
+  fprintf(file, "%s\n\n", getFormattedSetupEntry(TOKEN_STR_LAST_PLAYED_MENU_USED,
+                                                setup_value));
 
   for (i = 0; last_level_series[i] != NULL; i++)
   {
-    char token[strlen(TOKEN_STR_LAST_LEVEL_SERIES) + 10];
+    char token[strlen(TOKEN_STR_LAST_LEVEL_SERIES) + 1 + 10 + 1];
 
     sprintf(token, "%s.%03d", TOKEN_STR_LAST_LEVEL_SERIES, i);