re-enabled storing selection from "last played level set" menu
[rocksndiamonds.git] / src / libgame / setup.c
index 4b28476a40c18f86ef91754944d85ef13226b39a..32ae11215a1a87626d9acdac70e99d6ea62cafaf 100644 (file)
 
 #include "platform.h"
 
-#if !defined(PLATFORM_WIN32)
-#include <pwd.h>
-#include <sys/param.h>
-#endif
-
 #include "setup.h"
 #include "joystick.h"
 #include "text.h"
@@ -48,47 +43,22 @@ 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);
 static TreeInfo *getTreeInfoCopy(TreeInfo *ti);
@@ -101,6 +71,7 @@ static SetupFileHash *artworkinfo_cache_old = NULL;
 static SetupFileHash *artworkinfo_cache_new = NULL;
 static SetupFileHash *optional_tokens_hash = NULL;
 static boolean use_artworkinfo_cache = TRUE;
+static boolean update_artworkinfo_cache = FALSE;
 
 
 // ----------------------------------------------------------------------------
@@ -126,9 +97,9 @@ static char *getScoreDir(char *level_subdir)
   if (score_dir == NULL)
   {
     if (program.global_scores)
-      score_dir = getPath2(getCommonDataDir(),   score_subdir);
+      score_dir = getPath2(getCommonDataDir(),       score_subdir);
     else
-      score_dir = getPath2(getUserGameDataDir(), score_subdir);
+      score_dir = getPath2(getMainUserGameDataDir(), score_subdir);
   }
 
   if (level_subdir != NULL)
@@ -143,6 +114,32 @@ static char *getScoreDir(char *level_subdir)
   return score_dir;
 }
 
+static char *getUserSubdir(int nr)
+{
+  static char user_subdir[16] = { 0 };
+
+  sprintf(user_subdir, "%03d", nr);
+
+  return user_subdir;
+}
+
+static char *getUserDir(int nr)
+{
+  static char *user_dir = NULL;
+  char *main_data_dir = getMainUserGameDataDir();
+  char *users_subdir = USERS_DIRECTORY;
+  char *user_subdir = getUserSubdir(nr);
+
+  checked_free(user_dir);
+
+  if (nr != -1)
+    user_dir = getPath3(main_data_dir, users_subdir, user_subdir);
+  else
+    user_dir = getPath2(main_data_dir, users_subdir);
+
+  return user_dir;
+}
+
 static char *getLevelSetupDir(char *level_subdir)
 {
   static char *levelsetup_dir = NULL;
@@ -164,7 +161,7 @@ static char *getCacheDir(void)
   static char *cache_dir = NULL;
 
   if (cache_dir == NULL)
-    cache_dir = getPath2(getUserGameDataDir(), CACHE_DIRECTORY);
+    cache_dir = getPath2(getMainUserGameDataDir(), CACHE_DIRECTORY);
 
   return cache_dir;
 }
@@ -174,7 +171,7 @@ static char *getNetworkDir(void)
   static char *network_dir = NULL;
 
   if (network_dir == NULL)
-    network_dir = getPath2(getUserGameDataDir(), NETWORK_DIRECTORY);
+    network_dir = getPath2(getMainUserGameDataDir(), NETWORK_DIRECTORY);
 
   return network_dir;
 }
@@ -197,7 +194,7 @@ char *getLevelDirFromTreeInfo(TreeInfo *node)
 char *getUserLevelDir(char *level_subdir)
 {
   static char *userlevel_dir = NULL;
-  char *data_dir = getUserGameDataDir();
+  char *data_dir = getMainUserGameDataDir();
   char *userlevel_subdir = LEVELS_DIRECTORY;
 
   checked_free(userlevel_dir);
@@ -346,7 +343,7 @@ char *getUserGraphicsDir(void)
   static char *usergraphics_dir = NULL;
 
   if (usergraphics_dir == NULL)
-    usergraphics_dir = getPath2(getUserGameDataDir(), GRAPHICS_DIRECTORY);
+    usergraphics_dir = getPath2(getMainUserGameDataDir(), GRAPHICS_DIRECTORY);
 
   return usergraphics_dir;
 }
@@ -356,7 +353,7 @@ char *getUserSoundsDir(void)
   static char *usersounds_dir = NULL;
 
   if (usersounds_dir == NULL)
-    usersounds_dir = getPath2(getUserGameDataDir(), SOUNDS_DIRECTORY);
+    usersounds_dir = getPath2(getMainUserGameDataDir(), SOUNDS_DIRECTORY);
 
   return usersounds_dir;
 }
@@ -366,7 +363,7 @@ char *getUserMusicDir(void)
   static char *usermusic_dir = NULL;
 
   if (usermusic_dir == NULL)
-    usermusic_dir = getPath2(getUserGameDataDir(), MUSIC_DIRECTORY);
+    usermusic_dir = getPath2(getMainUserGameDataDir(), MUSIC_DIRECTORY);
 
   return usermusic_dir;
 }
@@ -1072,7 +1069,7 @@ void InitScoreDirectory(char *level_subdir)
   if (program.global_scores)
     createDirectory(getCommonDataDir(), "common data", permissions);
   else
-    createDirectory(getUserGameDataDir(), "user data", permissions);
+    createDirectory(getMainUserGameDataDir(), "main user data", permissions);
 
   createDirectory(getScoreDir(NULL), "main score", permissions);
   createDirectory(getScoreDir(level_subdir), "level score", permissions);
@@ -1084,7 +1081,7 @@ void InitUserLevelDirectory(char *level_subdir)
 {
   if (!directoryExists(getUserLevelDir(level_subdir)))
   {
-    createDirectory(getUserGameDataDir(), "user data", PERMS_PRIVATE);
+    createDirectory(getMainUserGameDataDir(), "main user data", PERMS_PRIVATE);
     createDirectory(getUserLevelDir(NULL), "main user level", PERMS_PRIVATE);
     createDirectory(getUserLevelDir(level_subdir), "user level", PERMS_PRIVATE);
 
@@ -1097,7 +1094,7 @@ void InitNetworkLevelDirectory(char *level_subdir)
 {
   if (!directoryExists(getNetworkLevelDir(level_subdir)))
   {
-    createDirectory(getUserGameDataDir(), "user data", PERMS_PRIVATE);
+    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);
@@ -1113,7 +1110,7 @@ void InitLevelSetupDirectory(char *level_subdir)
 
 static void InitCacheDirectory(void)
 {
-  createDirectory(getUserGameDataDir(), "user data", PERMS_PRIVATE);
+  createDirectory(getMainUserGameDataDir(), "main user data", PERMS_PRIVATE);
   createDirectory(getCacheDir(), "cache data", PERMS_PRIVATE);
 }
 
@@ -1142,6 +1139,16 @@ void pushTreeInfo(TreeInfo **node_first, TreeInfo *node_new)
   *node_first = node_new;
 }
 
+void removeTreeInfo(TreeInfo **node_first)
+{
+  TreeInfo *node_old = *node_first;
+
+  *node_first = node_old->next;
+  node_old->next = NULL;
+
+  freeTreeInfo(node_old);
+}
+
 int numTreeInfo(TreeInfo *node)
 {
   int num = 0;
@@ -1157,7 +1164,21 @@ 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 *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);
 }
 
 TreeInfo *getFirstValidTreeInfoEntry(TreeInfo *node)
@@ -1194,7 +1215,7 @@ int numTreeInfoInGroup(TreeInfo *node)
   return numTreeInfo(getTreeInfoFirstGroupEntry(node));
 }
 
-int posTreeInfo(TreeInfo *node)
+int getPosFromTreeInfo(TreeInfo *node)
 {
   TreeInfo *node_cmp = getTreeInfoFirstGroupEntry(node);
   int pos = 0;
@@ -1228,27 +1249,26 @@ TreeInfo *getTreeInfoFromPos(TreeInfo *node, int pos)
   return node_default;
 }
 
-TreeInfo *getTreeInfoFromIdentifier(TreeInfo *node, char *identifier)
+static TreeInfo *getTreeInfoFromIdentifierExt(TreeInfo *node, char *identifier,
+                                             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)
     {
-      TreeInfo *node_group;
-
-      node_group = getTreeInfoFromIdentifier(node->node_group, identifier);
-
+      TreeInfo *node_group = getTreeInfoFromIdentifierExt(node->node_group,
+                                                         identifier,
+                                                         node_type_wanted);
       if (node_group)
        return node_group;
     }
-    else if (!node->parent_link)
-    {
-      if (strEqual(identifier, node->identifier))
-       return node;
-    }
 
     node = node->next;
   }
@@ -1256,6 +1276,11 @@ TreeInfo *getTreeInfoFromIdentifier(TreeInfo *node, char *identifier)
   return NULL;
 }
 
+TreeInfo *getTreeInfoFromIdentifier(TreeInfo *node, char *identifier)
+{
+  return getTreeInfoFromIdentifierExt(node, identifier, TREE_NODE_TYPE_DEFAULT);
+}
+
 static TreeInfo *cloneTreeNode(TreeInfo **node_top, TreeInfo *node_parent,
                               TreeInfo *node, boolean skip_sets_without_levels)
 {
@@ -1359,20 +1384,27 @@ static boolean adjustTreeSoundsForEMC(TreeInfo *node)
 
 void dumpTreeInfo(TreeInfo *node, int depth)
 {
+  char bullet_list[] = { '-', '*', 'o' };
   int i;
 
-  Print("Dumping TreeInfo:\n");
+  if (depth == 0)
+    Debug("tree", "Dumping TreeInfo:");
 
   while (node)
   {
-    for (i = 0; i < (depth + 1) * 3; i++)
-      Print(" ");
+    char bullet = bullet_list[depth % ARRAY_SIZE(bullet_list)];
 
-    Print("'%s' / '%s'\n", node->identifier, node->name);
+    for (i = 0; i < depth * 2; i++)
+      DebugContinued("", " ");
+
+    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]" : ""));
 
     /*
     // use for dumping artwork info tree
-    Print("subdir == '%s' ['%s', '%s'] [%d])\n",
+    Debug("tree", "subdir == '%s' ['%s', '%s'] [%d])",
          node->subdir, node->fullpath, node->basepath, node->in_user_dir);
     */
 
@@ -1499,15 +1531,17 @@ char *getHomeDir(void)
     if (!SUCCEEDED(SHGetFolderPath(NULL, CSIDL_PERSONAL, NULL, 0, dir)))
       strcpy(dir, ".");
   }
+#elif defined(PLATFORM_EMSCRIPTEN)
+  dir = "/persistent";
 #elif defined(PLATFORM_UNIX)
   if (dir == NULL)
   {
     if ((dir = getenv("HOME")) == NULL)
     {
-      struct passwd *pwd;
+      dir = getUnixHomeDir();
 
-      if ((pwd = getpwuid(getuid())) != NULL)
-       dir = getStringCopy(pwd->pw_dir);
+      if (dir != NULL)
+       dir = getStringCopy(dir);
       else
        dir = ".";
     }
@@ -1557,23 +1591,31 @@ char *getPersonalDataDir(void)
   return personal_data_dir;
 }
 
-char *getUserGameDataDir(void)
+char *getMainUserGameDataDir(void)
 {
-  static char *user_game_data_dir = NULL;
+  static char *main_user_data_dir = NULL;
 
 #if defined(PLATFORM_ANDROID)
-  if (user_game_data_dir == NULL)
-    user_game_data_dir = (char *)(SDL_AndroidGetExternalStorageState() &
+  if (main_user_data_dir == NULL)
+    main_user_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(),
+  if (main_user_data_dir == NULL)
+    main_user_data_dir = getPath2(getPersonalDataDir(),
                                  program.userdata_subdir);
 #endif
 
-  return user_game_data_dir;
+  return main_user_data_dir;
+}
+
+char *getUserGameDataDir(void)
+{
+  if (user.nr == 0)
+    return getMainUserGameDataDir();
+  else
+    return getUserDir(user.nr);
 }
 
 char *getSetupDir(void)
@@ -1641,9 +1683,20 @@ void createDirectory(char *dir, char *text, int permission_class)
   posix_umask(last_umask);             // restore previous umask
 }
 
+void InitMainUserDataDirectory(void)
+{
+  createDirectory(getMainUserGameDataDir(), "main user data", PERMS_PRIVATE);
+}
+
 void InitUserDataDirectory(void)
 {
-  createDirectory(getUserGameDataDir(), "user data", PERMS_PRIVATE);
+  createDirectory(getMainUserGameDataDir(), "main user data", PERMS_PRIVATE);
+
+  if (user.nr != 0)
+  {
+    createDirectory(getUserDir(-1), "users", PERMS_PRIVATE);
+    createDirectory(getUserDir(user.nr), "user data", PERMS_PRIVATE);
+  }
 }
 
 void SetFilePermissions(char *filename, int permission_class)
@@ -2260,25 +2313,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)
@@ -2324,6 +2405,7 @@ SetupFileHash *loadSetupFileHash(char *filename)
 #define TOKEN_STR_LAST_LEVEL_SERIES            "last_level_series"
 #define TOKEN_STR_LAST_PLAYED_LEVEL            "last_played_level"
 #define TOKEN_STR_HANDICAP_LEVEL               "handicap_level"
+#define TOKEN_STR_LAST_USER                    "last_user"
 
 // level directory info
 #define LEVELINFO_TOKEN_IDENTIFIER             0
@@ -2411,7 +2493,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                    },
@@ -2459,6 +2540,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;
@@ -2540,6 +2622,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;
@@ -2640,6 +2723,7 @@ 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;
@@ -2766,40 +2850,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);
-  }
-
-  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);
+  int tree_sorting1 = TREE_SORTING(entry1);
+  int tree_sorting2 = TREE_SORTING(entry2);
 
-    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)
@@ -2816,13 +2873,13 @@ static TreeInfo *createParentTreeInfoNode(TreeInfo *node_parent)
   ti_new->parent_link = TRUE;
 
   setString(&ti_new->identifier, node_parent->identifier);
-  setString(&ti_new->name, ".. (parent directory)");
+  setString(&ti_new->name, BACKLINK_TEXT_PARENT);
   setString(&ti_new->name_sorting, ti_new->name);
 
   setString(&ti_new->subdir, STRING_PARENT_DIRECTORY);
   setString(&ti_new->fullpath, node_parent->fullpath);
 
-  ti_new->sort_priority = node_parent->sort_priority;
+  ti_new->sort_priority = LEVELCLASS_PARENT;
   ti_new->latest_engine = node_parent->latest_engine;
 
   setString(&ti_new->class_desc, getLevelClassDescription(ti_new));
@@ -2834,40 +2891,53 @@ static TreeInfo *createParentTreeInfoNode(TreeInfo *node_parent)
 
 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);
+  TreeInfo *ti_new = newTreeInfo();
+  int type = node_first->type;
+
+  setTreeInfoToDefaults(ti_new, type);
 
   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->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, "level sets");
+  setString(&ti_new->class_desc, TREE_INFOTEXT(type));
 
   ti_new->node_group = node_first;
   ti_new->level_group = TRUE;
 
-  ti_new2 = createParentTreeInfoNode(ti_new);
+  TreeInfo *ti_new2 = createParentTreeInfoNode(ti_new);
 
-  setString(&ti_new2->name, ".. (main menu)");
+  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;
+  }
+}
+
 
 // ----------------------------------------------------------------------------
 // functions for handling level and custom artwork info cache
@@ -2884,6 +2954,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();
@@ -2893,10 +2974,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();
@@ -2941,6 +3027,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;
 
@@ -3395,8 +3484,6 @@ 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
@@ -3640,9 +3727,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")
@@ -3660,9 +3744,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));
     }
@@ -3766,6 +3847,34 @@ static TreeInfo *getDummyArtworkInfo(int type)
   return artwork_new;
 }
 
+void SetCurrentArtwork(int type)
+{
+  ArtworkDirTree **current_ptr = ARTWORK_CURRENT_PTR(artwork, type);
+  ArtworkDirTree *first_node = ARTWORK_FIRST_NODE(artwork, type);
+  char *setup_set = SETUP_ARTWORK_SET(setup, type);
+  char *default_subdir = ARTWORK_DEFAULT_SUBDIR(type);
+
+  // set current artwork to artwork configured in setup menu
+  *current_ptr = getTreeInfoFromIdentifier(first_node, setup_set);
+
+  // if not found, set current artwork to default artwork
+  if (*current_ptr == NULL)
+    *current_ptr = getTreeInfoFromIdentifier(first_node, default_subdir);
+
+  // if not found, set current artwork to first artwork in tree
+  if (*current_ptr == NULL)
+    *current_ptr = getFirstValidTreeInfoEntry(first_node);
+}
+
+void ChangeCurrentArtworkIfNeeded(int type)
+{
+  char *current_identifier = ARTWORK_CURRENT_IDENTIFIER(artwork, type);
+  char *setup_set = SETUP_ARTWORK_SET(setup, type);
+
+  if (!strEqual(current_identifier, setup_set))
+    SetCurrentArtwork(type);
+}
+
 void LoadArtworkInfo(void)
 {
   LoadArtworkInfoCache();
@@ -3801,29 +3910,9 @@ void LoadArtworkInfo(void)
     artwork.mus_first = getDummyArtworkInfo(TREE_TYPE_MUSIC_DIR);
 
   // before sorting, the first entries will be from the user directory
-  artwork.gfx_current =
-    getTreeInfoFromIdentifier(artwork.gfx_first, setup.graphics_set);
-  if (artwork.gfx_current == NULL)
-    artwork.gfx_current =
-      getTreeInfoFromIdentifier(artwork.gfx_first, GFX_DEFAULT_SUBDIR);
-  if (artwork.gfx_current == NULL)
-    artwork.gfx_current = getFirstValidTreeInfoEntry(artwork.gfx_first);
-
-  artwork.snd_current =
-    getTreeInfoFromIdentifier(artwork.snd_first, setup.sounds_set);
-  if (artwork.snd_current == NULL)
-    artwork.snd_current =
-      getTreeInfoFromIdentifier(artwork.snd_first, SND_DEFAULT_SUBDIR);
-  if (artwork.snd_current == NULL)
-    artwork.snd_current = getFirstValidTreeInfoEntry(artwork.snd_first);
-
-  artwork.mus_current =
-    getTreeInfoFromIdentifier(artwork.mus_first, setup.music_set);
-  if (artwork.mus_current == NULL)
-    artwork.mus_current =
-      getTreeInfoFromIdentifier(artwork.mus_first, MUS_DEFAULT_SUBDIR);
-  if (artwork.mus_current == NULL)
-    artwork.mus_current = getFirstValidTreeInfoEntry(artwork.mus_first);
+  SetCurrentArtwork(ARTWORK_TYPE_GRAPHICS);
+  SetCurrentArtwork(ARTWORK_TYPE_SOUNDS);
+  SetCurrentArtwork(ARTWORK_TYPE_MUSIC);
 
   artwork.gfx_current_identifier = artwork.gfx_current->identifier;
   artwork.snd_current_identifier = artwork.snd_current->identifier;
@@ -3849,8 +3938,33 @@ void LoadArtworkInfo(void)
 #endif
 }
 
-static void LoadArtworkInfoFromLevelInfo(ArtworkDirTree **artwork_node,
-                                        LevelDirTree *level_node)
+static void MoveArtworkInfoIntoSubTree(ArtworkDirTree **artwork_node)
+{
+  ArtworkDirTree *artwork_new = newTreeInfo();
+  char *top_node_name = "standalone artwork";
+
+  setTreeInfoToDefaults(artwork_new, (*artwork_node)->type);
+
+  artwork_new->level_group = TRUE;
+
+  setString(&artwork_new->identifier,   top_node_name);
+  setString(&artwork_new->name,         top_node_name);
+  setString(&artwork_new->name_sorting, top_node_name);
+
+  // create node to link back to current custom artwork directory
+  createParentTreeInfoNode(artwork_new);
+
+  // move existing custom artwork tree into newly created sub-tree
+  artwork_new->node_group->next = *artwork_node;
+
+  // change custom artwork tree to contain only newly created node
+  *artwork_node = artwork_new;
+}
+
+static void LoadArtworkInfoFromLevelInfoExt(ArtworkDirTree **artwork_node,
+                                           ArtworkDirTree *node_parent,
+                                           LevelDirTree *level_node,
+                                           boolean empty_level_set_mode)
 {
   int type = (*artwork_node)->type;
 
@@ -3858,8 +3972,10 @@ static void LoadArtworkInfoFromLevelInfo(ArtworkDirTree **artwork_node,
 
   while (level_node)
   {
+    boolean empty_level_set = (level_node->levels == 0);
+
     // check all tree entries for artwork, but skip parent link entries
-    if (!level_node->parent_link)
+    if (!level_node->parent_link && empty_level_set == empty_level_set_mode)
     {
       TreeInfo *artwork_new = getArtworkInfoCacheEntry(level_node, type);
       boolean cached = (artwork_new != NULL);
@@ -3885,7 +4001,9 @@ static void LoadArtworkInfoFromLevelInfo(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);
@@ -3899,12 +4017,68 @@ static void LoadArtworkInfoFromLevelInfo(ArtworkDirTree **artwork_node,
     DrawInitText(level_node->name, 150, FC_YELLOW);
 
     if (level_node->node_group != NULL)
-      LoadArtworkInfoFromLevelInfo(artwork_node, level_node->node_group);
+    {
+      TreeInfo *artwork_new = newTreeInfo();
+
+      if (node_parent)
+       setTreeInfoToDefaultsFromParent(artwork_new, node_parent);
+      else
+       setTreeInfoToDefaults(artwork_new, type);
+
+      artwork_new->level_group = TRUE;
+
+      setString(&artwork_new->identifier,   level_node->subdir);
+
+      if (node_parent == NULL)         // check for top tree node
+      {
+       char *top_node_name = (empty_level_set_mode ?
+                              "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);
+      }
+      else
+      {
+       setString(&artwork_new->name,         level_node->name);
+       setString(&artwork_new->name_sorting, level_node->name_sorting);
+      }
+
+      pushTreeInfo(artwork_node, artwork_new);
+
+      // create node to link back to current custom artwork directory
+      createParentTreeInfoNode(artwork_new);
+
+      // recursively step into sub-directory and look for more custom artwork
+      LoadArtworkInfoFromLevelInfoExt(&artwork_new->node_group, artwork_new,
+                                     level_node->node_group,
+                                     empty_level_set_mode);
+
+      // if sub-tree has no custom artwork at all, remove it
+      if (artwork_new->node_group->next == NULL)
+       removeTreeInfo(artwork_node);
+    }
 
     level_node = level_node->next;
   }
 }
 
+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 three separate sub-trees
+  *artwork_node = createTopTreeInfoNode(*artwork_node);
+
+  // set all parent links (back links) in complete artwork tree
+  setTreeInfoParentNodes(*artwork_node, NULL);
+}
+
 void LoadLevelArtworkInfo(void)
 {
   print_timestamp_init("LoadLevelArtworkInfo");
@@ -3913,11 +4087,11 @@ void LoadLevelArtworkInfo(void)
 
   print_timestamp_time("DrawTimeText");
 
-  LoadArtworkInfoFromLevelInfo(&artwork.gfx_first, leveldir_first_all);
+  LoadArtworkInfoFromLevelInfo(&artwork.gfx_first);
   print_timestamp_time("LoadArtworkInfoFromLevelInfo (gfx)");
-  LoadArtworkInfoFromLevelInfo(&artwork.snd_first, leveldir_first_all);
+  LoadArtworkInfoFromLevelInfo(&artwork.snd_first);
   print_timestamp_time("LoadArtworkInfoFromLevelInfo (snd)");
-  LoadArtworkInfoFromLevelInfo(&artwork.mus_first, leveldir_first_all);
+  LoadArtworkInfoFromLevelInfo(&artwork.mus_first);
   print_timestamp_time("LoadArtworkInfoFromLevelInfo (mus)");
 
   SaveArtworkInfoCache();
@@ -3925,39 +4099,9 @@ void LoadLevelArtworkInfo(void)
   print_timestamp_time("SaveArtworkInfoCache");
 
   // needed for reloading level artwork not known at ealier stage
-
-  if (!strEqual(artwork.gfx_current_identifier, setup.graphics_set))
-  {
-    artwork.gfx_current =
-      getTreeInfoFromIdentifier(artwork.gfx_first, setup.graphics_set);
-    if (artwork.gfx_current == NULL)
-      artwork.gfx_current =
-       getTreeInfoFromIdentifier(artwork.gfx_first, GFX_DEFAULT_SUBDIR);
-    if (artwork.gfx_current == NULL)
-      artwork.gfx_current = getFirstValidTreeInfoEntry(artwork.gfx_first);
-  }
-
-  if (!strEqual(artwork.snd_current_identifier, setup.sounds_set))
-  {
-    artwork.snd_current =
-      getTreeInfoFromIdentifier(artwork.snd_first, setup.sounds_set);
-    if (artwork.snd_current == NULL)
-      artwork.snd_current =
-       getTreeInfoFromIdentifier(artwork.snd_first, SND_DEFAULT_SUBDIR);
-    if (artwork.snd_current == NULL)
-      artwork.snd_current = getFirstValidTreeInfoEntry(artwork.snd_first);
-  }
-
-  if (!strEqual(artwork.mus_current_identifier, setup.music_set))
-  {
-    artwork.mus_current =
-      getTreeInfoFromIdentifier(artwork.mus_first, setup.music_set);
-    if (artwork.mus_current == NULL)
-      artwork.mus_current =
-       getTreeInfoFromIdentifier(artwork.mus_first, MUS_DEFAULT_SUBDIR);
-    if (artwork.mus_current == NULL)
-      artwork.mus_current = getFirstValidTreeInfoEntry(artwork.mus_first);
-  }
+  ChangeCurrentArtworkIfNeeded(ARTWORK_TYPE_GRAPHICS);
+  ChangeCurrentArtworkIfNeeded(ARTWORK_TYPE_SOUNDS);
+  ChangeCurrentArtworkIfNeeded(ARTWORK_TYPE_MUSIC);
 
   print_timestamp_time("getTreeInfoFromIdentifier");
 
@@ -4373,6 +4517,127 @@ char *getSetupLine(struct TokenInfo *token_info, char *prefix, int token_nr)
   return line;
 }
 
+static void InitLastPlayedLevels_ParentNode(void)
+{
+  LevelDirTree **leveldir_top = &leveldir_first->node_group->next;
+  LevelDirTree *leveldir_new = NULL;
+
+  // check if parent node for last played levels already exists
+  if (strEqual((*leveldir_top)->identifier, TOKEN_STR_LAST_LEVEL_SERIES))
+    return;
+
+  leveldir_new = newTreeInfo();
+
+  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);
+
+  // create node to link back to current level directory
+  createParentTreeInfoNode(leveldir_new);
+}
+
+void UpdateLastPlayedLevels_TreeInfo(void)
+{
+  char **last_level_series = setup.level_setup.last_level_series;
+  LevelDirTree *leveldir_last;
+  TreeInfo **node_new = NULL;
+  int i;
+
+  if (last_level_series[0] == NULL)
+    return;
+
+  InitLastPlayedLevels_ParentNode();
+
+  leveldir_last = getTreeInfoFromIdentifierExt(leveldir_first,
+                                              TOKEN_STR_LAST_LEVEL_SERIES,
+                                              TREE_NODE_TYPE_GROUP);
+  if (leveldir_last == NULL)
+    return;
+
+  node_new = &leveldir_last->node_group->next;
+
+  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;
+
+    (*node_new)->cl_first = -1;                        // force setting tree cursor
+
+    node_new = &((*node_new)->next);
+  }
+}
+
+static void UpdateLastPlayedLevels_List(void)
+{
+  char **last_level_series = setup.level_setup.last_level_series;
+  int pos = MAX_LEVELDIR_HISTORY - 1;
+  int i;
+
+  // search for potentially already existing entry in list of level sets
+  for (i = 0; last_level_series[i] != NULL; i++)
+    if (strEqual(last_level_series[i], leveldir_current->identifier))
+      pos = i;
+
+  // move list of level sets one entry down (using potentially free entry)
+  for (i = pos; i > 0; i--)
+    setString(&last_level_series[i], last_level_series[i - 1]);
+
+  // put last played level set at top position
+  setString(&last_level_series[0], leveldir_current->identifier);
+}
+
+static TreeInfo *StoreOrRestoreLastPlayedLevels(TreeInfo *node, boolean store)
+{
+  static char *identifier = NULL;
+
+  if (store)
+  {
+    setString(&identifier, (node && node->is_copy ? node->identifier : NULL));
+
+    return NULL;       // not used
+  }
+  else
+  {
+    TreeInfo *node_new = getTreeInfoFromIdentifierExt(leveldir_first,
+                                                     identifier,
+                                                     TREE_NODE_TYPE_COPY);
+    return (node_new != NULL ? node_new : node);
+  }
+}
+
+void StoreLastPlayedLevels(TreeInfo *node)
+{
+  StoreOrRestoreLastPlayedLevels(node, TRUE);
+}
+
+void RestoreLastPlayedLevels(TreeInfo **node)
+{
+  *node = StoreOrRestoreLastPlayedLevels(*node, FALSE);
+}
+
 void LoadLevelSetup_LastSeries(void)
 {
   // --------------------------------------------------------------------------
@@ -4381,10 +4646,15 @@ void LoadLevelSetup_LastSeries(void)
 
   char *filename = getPath2(getSetupDir(), LEVELSETUP_FILENAME);
   SetupFileHash *level_setup_hash = NULL;
+  int pos = 0;
+  int i;
 
   // always start with reliable default values
   leveldir_current = getFirstValidTreeInfoEntry(leveldir_first);
 
+  // start with empty history of last played level sets
+  setString(&setup.level_setup.last_level_series[0], NULL);
+
   if (!strEqual(DEFAULT_LEVELSET, UNDEFINED_LEVELSET))
   {
     leveldir_current = getTreeInfoFromIdentifier(leveldir_first,
@@ -4403,6 +4673,24 @@ void LoadLevelSetup_LastSeries(void)
     if (leveldir_current == NULL)
       leveldir_current = getFirstValidTreeInfoEntry(leveldir_first);
 
+    for (i = 0; i < MAX_LEVELDIR_HISTORY; i++)
+    {
+      char token[strlen(TOKEN_STR_LAST_LEVEL_SERIES) + 10];
+      LevelDirTree *leveldir_last;
+
+      sprintf(token, "%s.%03d", TOKEN_STR_LAST_LEVEL_SERIES, i);
+
+      last_level_series = getHashEntry(level_setup_hash, token);
+
+      leveldir_last = getTreeInfoFromIdentifier(leveldir_first,
+                                               last_level_series);
+      if (leveldir_last != NULL)
+       setString(&setup.level_setup.last_level_series[pos++],
+                 last_level_series);
+    }
+
+    setString(&setup.level_setup.last_level_series[pos], NULL);
+
     freeSetupFileHash(level_setup_hash);
   }
   else
@@ -4423,12 +4711,15 @@ static void SaveLevelSetup_LastSeries_Ext(boolean deactivate_last_level_series)
   if (leveldir_current == NULL)
     return;
 
+  char **last_level_series = setup.level_setup.last_level_series;
   char *filename = getPath2(getSetupDir(), LEVELSETUP_FILENAME);
-  char *level_subdir = leveldir_current->subdir;
   FILE *file;
+  int i;
 
   InitUserDataDirectory();
 
+  UpdateLastPlayedLevels_List();
+
   if (!(file = fopen(filename, MODE_WRITE)))
   {
     Warn("cannot write setup file '%s'", filename);
@@ -4443,8 +4734,17 @@ static void SaveLevelSetup_LastSeries_Ext(boolean deactivate_last_level_series)
   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));
+  fprintf(file, "%s\n\n", getFormattedSetupEntry(TOKEN_STR_LAST_LEVEL_SERIES,
+                                              leveldir_current->identifier));
+
+  for (i = 0; last_level_series[i] != NULL; i++)
+  {
+    char token[strlen(TOKEN_STR_LAST_LEVEL_SERIES) + 10];
+
+    sprintf(token, "%s.%03d", TOKEN_STR_LAST_LEVEL_SERIES, i);
+
+    fprintf(file, "%s\n", getFormattedSetupEntry(token, last_level_series[i]));
+  }
 
   fclose(file);
 
@@ -4708,3 +5008,66 @@ void LevelStats_incSolved(int nr)
   if (nr >= 0 && nr < MAX_LEVELS)
     level_stats[nr].solved++;
 }
+
+void LoadUserSetup(void)
+{
+  // --------------------------------------------------------------------------
+  // ~/.<program>/usersetup.conf
+  // --------------------------------------------------------------------------
+
+  char *filename = getPath2(getMainUserGameDataDir(), USERSETUP_FILENAME);
+  SetupFileHash *user_setup_hash = NULL;
+
+  // always start with reliable default values
+  user.nr = 0;
+
+  if ((user_setup_hash = loadSetupFileHash(filename)))
+  {
+    char *token_value;
+
+    // get last selected user number
+    token_value = getHashEntry(user_setup_hash, TOKEN_STR_LAST_USER);
+
+    if (token_value)
+      user.nr = MIN(MAX(0, atoi(token_value)), MAX_PLAYER_NAMES - 1);
+
+    freeSetupFileHash(user_setup_hash);
+  }
+  else
+  {
+    Debug("setup", "using default setup values");
+  }
+
+  free(filename);
+}
+
+void SaveUserSetup(void)
+{
+  // --------------------------------------------------------------------------
+  // ~/.<program>/usersetup.conf
+  // --------------------------------------------------------------------------
+
+  char *filename = getPath2(getMainUserGameDataDir(), USERSETUP_FILENAME);
+  FILE *file;
+
+  InitMainUserDataDirectory();
+
+  if (!(file = fopen(filename, MODE_WRITE)))
+  {
+    Warn("cannot write setup file '%s'", filename);
+
+    free(filename);
+
+    return;
+  }
+
+  fprintFileHeader(file, USERSETUP_FILENAME);
+
+  fprintf(file, "%s\n", getFormattedSetupEntry(TOKEN_STR_LAST_USER,
+                                              i_to_a(user.nr)));
+  fclose(file);
+
+  SetFilePermissions(filename, PERMS_PRIVATE);
+
+  free(filename);
+}