
# this file contains code that wasn't yet converted

__EOF__

CHUNK_ID_LEN            = 4   # IFF style chunk id length 
CHUNK_SIZE_UNDEFINED    = 0   # undefined chunk size == 0 
CHUNK_SIZE_NONE         = -1  # do not write chunk size   
FILE_VERS_CHUNK_SIZE    = 8   # size of file version chunk
LEVEL_HEADER_SIZE       = 80  # size of level file header 
LEVEL_HEADER_UNUSED     = 0   # unused level header bytes 
LEVEL_CHUNK_CNT2_SIZE   = 160 # size of level CNT2 chunk  
LEVEL_CHUNK_CNT2_UNUSED = 11  # unused CNT2 chunk bytes   
LEVEL_CHUNK_CNT3_HEADER = 16  # size of level CNT3 header 
LEVEL_CHUNK_CNT3_UNUSED = 10  # unused CNT3 chunk bytes   
LEVEL_CPART_CUS3_SIZE   = 134 # size of CUS3 chunk part   
LEVEL_CPART_CUS3_UNUSED = 15  # unused CUS3 bytes / part  
LEVEL_CHUNK_GRP1_SIZE   = 74  # size of level GRP1 chunk  
TAPE_HEADER_SIZE        = 20  # size of tape file header  
TAPE_HEADER_UNUSED      = 3   # unused tape header bytes  

# =========================================================================
# tape file functions                                                      
# =========================================================================

static void setTapeInfoToDefaults()
{
  int i;

  # always start with reliable default values (empty tape)
  TapeErase();

  # default values (also for pre-1.2 tapes) with only the first player
  tape.player_participates[0] = true;
  for (i = 1; i < MAX_PLAYERS; i++)
    tape.player_participates[i] = false;

  # at least one (default: the first) player participates in every tape
  tape.num_participating_players = 1;

  tape.level_nr = level_nr;
  tape.counter = 0;
  tape.changed = false;

  tape.recording = false;
  tape.playing = false;
  tape.pausing = false;

  tape.no_valid_file = false;
}

static int LoadTape_VERS(FILE *file, int chunk_size, struct TapeInfo *tape)
{
  tape->file_version = getFileVersion(file);
  tape->game_version = getFileVersion(file);

  return chunk_size;
}

static int LoadTape_HEAD(FILE *file, int chunk_size, struct TapeInfo *tape)
{
  int i;

  tape->random_seed = file.get32be;
  tape->date        = file.get32be;
  tape->length      = file.get32be;

  # read header fields that are new since version 1.2
  if (tape->file_version >= FILE_VERSION_1_2)
  {
    byte store_participating_players = file.get8;
    int engine_version;

    # since version 1.2, tapes store which players participate in the tape
    tape->num_participating_players = 0;
    for (i = 0; i < MAX_PLAYERS; i++)
    {
      tape->player_participates[i] = false;

      if (store_participating_players & (1 << i))
      {
    tape->player_participates[i] = true;
    tape->num_participating_players++;
      }
    }

    ReadUnusedBytesFromFile(file, TAPE_HEADER_UNUSED);

    engine_version = getFileVersion(file);
    if (engine_version > 0)
      tape->engine_version = engine_version;
    else
      tape->engine_version = tape->game_version;
  }

  return chunk_size;
}

static int LoadTape_INFO(FILE *file, int chunk_size, struct TapeInfo *tape)
{
  int level_identifier_size;
  int i;

  level_identifier_size = file.get16be;

  tape->level_identifier =
    checked_realloc(tape->level_identifier, level_identifier_size);

  for (i = 0; i < level_identifier_size; i++)
    tape->level_identifier[i] = file.get8;

  tape->level_nr = file.get16be;

  chunk_size = 2 + level_identifier_size + 2;

  return chunk_size;
}

static int LoadTape_BODY(FILE *file, int chunk_size, struct TapeInfo *tape)
{
  int i, j;
  int chunk_size_expected =
    (tape->num_participating_players + 1) * tape->length;

  if (chunk_size_expected != chunk_size)
  {
    ReadUnusedBytesFromFile(file, chunk_size);
    return chunk_size_expected;
  }

  for (i = 0; i < tape->length; i++)
  {
    if (i >= MAX_TAPE_LEN)
      break;

    for (j = 0; j < MAX_PLAYERS; j++)
    {
      tape->pos[i].action[j] = MV_NO_MOVING;

      if (tape->player_participates[j])
    tape->pos[i].action[j] = file.get8;
    }

    tape->pos[i].delay = file.get8;

    if (tape->file_version == FILE_VERSION_1_0)
    {
      # eliminate possible diagonal moves in old tapes
      # this is only for backward compatibility

      byte joy_dir[4] = { JOY_LEFT, JOY_RIGHT, JOY_UP, JOY_DOWN };
      byte action = tape->pos[i].action[0];
      int k, num_moves = 0;

      for (k = 0; k<4; k++)
      {
    if (action & joy_dir[k])
    {
      tape->pos[i + num_moves].action[0] = joy_dir[k];
      if (num_moves > 0)
        tape->pos[i + num_moves].delay = 0;
      num_moves++;
    }
      }

      if (num_moves > 1)
      {
    num_moves--;
    i += num_moves;
    tape->length += num_moves;
      }
    }
    else if (tape->file_version < FILE_VERSION_2_0)
    {
      # convert pre-2.0 tapes to new tape format

      if (tape->pos[i].delay > 1)
      {
    # action part
    tape->pos[i + 1] = tape->pos[i];
    tape->pos[i + 1].delay = 1;

    # delay part
    for (j = 0; j < MAX_PLAYERS; j++)
      tape->pos[i].action[j] = MV_NO_MOVING;
    tape->pos[i].delay--;

    i++;
    tape->length++;
      }
    }

    if (feof(file))
      break;
  }

  if (i != tape->length)
    chunk_size = (tape->num_participating_players + 1) * i;

  return chunk_size;
}

void LoadTapeFromFilename(char *filename)
{
  char cookie[MAX_LINE_LEN];
  char chunk_name[CHUNK_ID_LEN + 1];
  FILE *file;
  int chunk_size;

  # always start with reliable default values
  setTapeInfoToDefaults();

  if (!(file = fopen(filename, MODE_READ)))
  {
    tape.no_valid_file = true;

    return;
  }

  getFileChunkBE(file, chunk_name, NULL);
  if (strcmp(chunk_name, "RND1") == 0)
  {
    file.get32be;       # not used

    getFileChunkBE(file, chunk_name, NULL);
    if (strcmp(chunk_name, "TAPE") != 0)
    {
      tape.no_valid_file = true;

      error(ERR_WARN, "unknown format of tape file '%s'", filename);
      fclose(file);
      return;
    }
  }
  else  # check for pre-2.0 file format with cookie string
  {
    strcpy(cookie, chunk_name);
    fgets(&cookie[4], MAX_LINE_LEN - 4, file);
    if (strlen(cookie) > 0 && cookie[strlen(cookie) - 1] == '\n')
      cookie[strlen(cookie) - 1] = '\0';

    if (!checkCookieString(cookie, TAPE_COOKIE_TMPL))
    {
      tape.no_valid_file = true;

      error(ERR_WARN, "unknown format of tape file '%s'", filename);
      fclose(file);
      return;
    }

    if ((tape.file_version = getFileVersionFromCookieString(cookie)) == -1)
    {
      tape.no_valid_file = true;

      error(ERR_WARN, "unsupported version of tape file '%s'", filename);
      fclose(file);
      return;
    }

    # pre-2.0 tape files have no game version, so use file version here
    tape.game_version = tape.file_version;
  }

  if (tape.file_version < FILE_VERSION_1_2)
  {
    # tape files from versions before 1.2.0 without chunk structure
    LoadTape_HEAD(file, TAPE_HEADER_SIZE, &tape);
    LoadTape_BODY(file, 2 * tape.length,  &tape);
  }
  else
  {
    static struct
    {
      char *name;
      int size;
      int (*loader)(FILE *, int, struct TapeInfo *);
    }
    chunk_info[] =
    {
      { "VERS", FILE_VERS_CHUNK_SIZE,   LoadTape_VERS },
      { "HEAD", TAPE_HEADER_SIZE,   LoadTape_HEAD },
      { "INFO", -1,         LoadTape_INFO },
      { "BODY", -1,         LoadTape_BODY },
      {  NULL,  0,          NULL }
    };

    while (getFileChunkBE(file, chunk_name, &chunk_size))
    {
      int i = 0;

      while (chunk_info[i].name != NULL &&
         strcmp(chunk_name, chunk_info[i].name) != 0)
    i++;

      if (chunk_info[i].name == NULL)
      {
    error(ERR_WARN, "unknown chunk '%s' in tape file '%s'",
          chunk_name, filename);
    ReadUnusedBytesFromFile(file, chunk_size);
      }
      else if (chunk_info[i].size != -1 &&
           chunk_info[i].size != chunk_size)
      {
    error(ERR_WARN, "wrong size (%d) of chunk '%s' in tape file '%s'",
          chunk_size, chunk_name, filename);
    ReadUnusedBytesFromFile(file, chunk_size);
      }
      else
      {
    # call function to load this tape chunk
    int chunk_size_expected =
      (chunk_info[i].loader)(file, chunk_size, &tape);

    # the size of some chunks cannot be checked before reading other
    # chunks first (like "HEAD" and "BODY") that contain some header
    # information, so check them here
    if (chunk_size_expected != chunk_size)
    {
      error(ERR_WARN, "wrong size (%d) of chunk '%s' in tape file '%s'",
        chunk_size, chunk_name, filename);
    }
      }
    }
  }

  fclose(file);

  tape.length_seconds = GetTapeLength();
}

void LoadTape(int nr)
{
  char *filename = getTapeFilename(nr);

  LoadTapeFromFilename(filename);
}

void LoadSolutionTape(int nr)
{
  char *filename = getSolutionTapeFilename(nr);

  LoadTapeFromFilename(filename);
}

static void SaveTape_VERS(FILE *file, struct TapeInfo *tape)
{
  putFileVersion(file, tape->file_version);
  putFileVersion(file, tape->game_version);
}

static void SaveTape_HEAD(FILE *file, struct TapeInfo *tape)
{
  int i;
  byte store_participating_players = 0;

  # set bits for participating players for compact storage
  for (i = 0; i < MAX_PLAYERS; i++)
    if (tape->player_participates[i])
      store_participating_players |= (1 << i);

  putFile32BitBE(file, tape->random_seed);
  putFile32BitBE(file, tape->date);
  putFile32BitBE(file, tape->length);

  putFile8Bit(file, store_participating_players);

  # unused bytes not at the end here for 4-byte alignment of engine_version
  WriteUnusedBytesToFile(file, TAPE_HEADER_UNUSED);

  putFileVersion(file, tape->engine_version);
}

static void SaveTape_INFO(FILE *file, struct TapeInfo *tape)
{
  int level_identifier_size = strlen(tape->level_identifier) + 1;
  int i;

  putFile16BitBE(file, level_identifier_size);

  for (i = 0; i < level_identifier_size; i++)
    putFile8Bit(file, tape->level_identifier[i]);

  putFile16BitBE(file, tape->level_nr);
}

static void SaveTape_BODY(FILE *file, struct TapeInfo *tape)
{
  int i, j;

  for (i = 0; i < tape->length; i++)
  {
    for (j = 0; j < MAX_PLAYERS; j++)
      if (tape->player_participates[j])
    putFile8Bit(file, tape->pos[i].action[j]);

    putFile8Bit(file, tape->pos[i].delay);
  }
}

void SaveTape(int nr)
{
  char *filename = getTapeFilename(nr);
  FILE *file;
  boolean new_tape = true;
  int num_participating_players = 0;
  int info_chunk_size;
  int body_chunk_size;
  int i;

  InitTapeDirectory(leveldir_current->subdir);

  # if a tape still exists, ask to overwrite it
  if (fileExists(filename))
  {
    new_tape = false;
    if (!Request("Replace old tape ?", REQ_ASK))
      return;
  }

  if (!(file = fopen(filename, MODE_WRITE)))
  {
    error(ERR_WARN, "cannot save level recording file '%s'", filename);
    return;
  }

  tape.file_version = FILE_VERSION_ACTUAL;
  tape.game_version = GAME_VERSION_ACTUAL;

  # count number of participating players 
  for (i = 0; i < MAX_PLAYERS; i++)
    if (tape.player_participates[i])
      num_participating_players++;

  info_chunk_size = 2 + (strlen(tape.level_identifier) + 1) + 2;
  body_chunk_size = (num_participating_players + 1) * tape.length;

  putFileChunkBE(file, "RND1", CHUNK_SIZE_UNDEFINED);
  putFileChunkBE(file, "TAPE", CHUNK_SIZE_NONE);

  putFileChunkBE(file, "VERS", FILE_VERS_CHUNK_SIZE);
  SaveTape_VERS(file, &tape);

  putFileChunkBE(file, "HEAD", TAPE_HEADER_SIZE);
  SaveTape_HEAD(file, &tape);

  putFileChunkBE(file, "INFO", info_chunk_size);
  SaveTape_INFO(file, &tape);

  putFileChunkBE(file, "BODY", body_chunk_size);
  SaveTape_BODY(file, &tape);

  fclose(file);

  SetFilePermissions(filename, PERMS_PRIVATE);

  tape.changed = false;

  if (new_tape)
    Request("Tape saved !", REQ_CONFIRM);
}

void DumpTape(struct TapeInfo *tape)
{
  int i, j;

  if (tape->no_valid_file)
  {
    error(ERR_WARN, "cannot dump -- no valid tape file found");
    return;
  }

  printf_line("-", 79);
  printf("Tape of Level %03d (file version %08d, game version %08d)\n",
     tape->level_nr, tape->file_version, tape->game_version);
  printf("                  (effective engine version %08d)\n",
     tape->engine_version);
  printf("Level series identifier: '%s'\n", tape->level_identifier);
  printf_line("-", 79);

  for (i = 0; i < tape->length; i++)
  {
    if (i >= MAX_TAPE_LEN)
      break;

    printf("%03d: ", i);

    for (j = 0; j < MAX_PLAYERS; j++)
    {
      if (tape->player_participates[j])
      {
    int action = tape->pos[i].action[j];

    printf("%d:%02x ", j, action);
    printf("[%c%c%c%c|%c%c] - ",
           (action & JOY_LEFT ? '<' : ' '),
           (action & JOY_RIGHT ? '>' : ' '),
           (action & JOY_UP ? '^' : ' '),
           (action & JOY_DOWN ? 'v' : ' '),
           (action & JOY_BUTTON_1 ? '1' : ' '),
           (action & JOY_BUTTON_2 ? '2' : ' '));
      }
    }

    printf("(%03d)\n", tape->pos[i].delay);
  }

  printf_line("-", 79);
}


# =========================================================================
# score file functions                                                     
# =========================================================================

void LoadScore(int nr)
{
  int i;
  char *filename = getScoreFilename(nr);
  char cookie[MAX_LINE_LEN];
  char line[MAX_LINE_LEN];
  char *line_ptr;
  FILE *file;

  # always start with reliable default values
  for (i = 0; i < MAX_SCORE_ENTRIES; i++)
  {
    strcpy(highscore[i].Name, EMPTY_PLAYER_NAME);
    highscore[i].Score = 0;
  }

  if (!(file = fopen(filename, MODE_READ)))
    return;

  # check file identifier
  fgets(cookie, MAX_LINE_LEN, file);
  if (strlen(cookie) > 0 && cookie[strlen(cookie) - 1] == '\n')
    cookie[strlen(cookie) - 1] = '\0';

  if (!checkCookieString(cookie, SCORE_COOKIE))
  {
    error(ERR_WARN, "unknown format of score file '%s'", filename);
    fclose(file);
    return;
  }

  for (i = 0; i < MAX_SCORE_ENTRIES; i++)
  {
    fscanf(file, "%d", &highscore[i].Score);
    fgets(line, MAX_LINE_LEN, file);

    if (line[strlen(line) - 1] == '\n')
      line[strlen(line) - 1] = '\0';

    for (line_ptr = line; *line_ptr; line_ptr++)
    {
      if (*line_ptr != ' ' && *line_ptr != '\t' && *line_ptr != '\0')
      {
    strncpy(highscore[i].Name, line_ptr, MAX_PLAYER_NAME_LEN);
    highscore[i].Name[MAX_PLAYER_NAME_LEN] = '\0';
    break;
      }
    }
  }

  fclose(file);
}

void SaveScore(int nr)
{
  int i;
  char *filename = getScoreFilename(nr);
  FILE *file;

  InitScoreDirectory(leveldir_current->subdir);

  if (!(file = fopen(filename, MODE_WRITE)))
  {
    error(ERR_WARN, "cannot save score for level %d", nr);
    return;
  }

  fprintf(file, "%s\n\n", SCORE_COOKIE);

  for (i = 0; i < MAX_SCORE_ENTRIES; i++)
    fprintf(file, "%d %s\n", highscore[i].Score, highscore[i].Name);

  fclose(file);

  SetFilePermissions(filename, PERMS_PUBLIC);
}


# =========================================================================
# setup file functions                                                     
# =========================================================================

#define TOKEN_STR_PLAYER_PREFIX         "player_"

# global setup
#define SETUP_TOKEN_PLAYER_NAME         0
#define SETUP_TOKEN_SOUND           1
#define SETUP_TOKEN_SOUND_LOOPS         2
#define SETUP_TOKEN_SOUND_MUSIC         3
#define SETUP_TOKEN_SOUND_SIMPLE        4
#define SETUP_TOKEN_TOONS           5
#define SETUP_TOKEN_SCROLL_DELAY        6
#define SETUP_TOKEN_SOFT_SCROLLING      7
#define SETUP_TOKEN_FADING          8
#define SETUP_TOKEN_AUTORECORD          9
#define SETUP_TOKEN_QUICK_DOORS         10
#define SETUP_TOKEN_TEAM_MODE           11
#define SETUP_TOKEN_HANDICAP            12
#define SETUP_TOKEN_SKIP_LEVELS         13
#define SETUP_TOKEN_TIME_LIMIT          14
#define SETUP_TOKEN_FULLSCREEN          15
#define SETUP_TOKEN_ASK_ON_ESCAPE       16
#define SETUP_TOKEN_GRAPHICS_SET        17
#define SETUP_TOKEN_SOUNDS_SET          18
#define SETUP_TOKEN_MUSIC_SET           19
#define SETUP_TOKEN_OVERRIDE_LEVEL_GRAPHICS 20
#define SETUP_TOKEN_OVERRIDE_LEVEL_SOUNDS   21
#define SETUP_TOKEN_OVERRIDE_LEVEL_MUSIC    22

#define NUM_GLOBAL_SETUP_TOKENS         23

# editor setup
#define SETUP_TOKEN_EDITOR_EL_BOULDERDASH   0
#define SETUP_TOKEN_EDITOR_EL_EMERALD_MINE  1
#define SETUP_TOKEN_EDITOR_EL_EMERALD_MINE_CLUB 2
#define SETUP_TOKEN_EDITOR_EL_MORE      3
#define SETUP_TOKEN_EDITOR_EL_SOKOBAN       4
#define SETUP_TOKEN_EDITOR_EL_SUPAPLEX      5
#define SETUP_TOKEN_EDITOR_EL_DIAMOND_CAVES 6
#define SETUP_TOKEN_EDITOR_EL_DX_BOULDERDASH    7
#define SETUP_TOKEN_EDITOR_EL_CHARS     8
#define SETUP_TOKEN_EDITOR_EL_CUSTOM        9
#define SETUP_TOKEN_EDITOR_EL_CUSTOM_MORE   10
#define SETUP_TOKEN_EDITOR_EL_HEADLINES     11
#define SETUP_TOKEN_EDITOR_EL_USER_DEFINED  12

#define NUM_EDITOR_SETUP_TOKENS         13

# shortcut setup
#define SETUP_TOKEN_SHORTCUT_SAVE_GAME      0
#define SETUP_TOKEN_SHORTCUT_LOAD_GAME      1
#define SETUP_TOKEN_SHORTCUT_TOGGLE_PAUSE   2

#define NUM_SHORTCUT_SETUP_TOKENS       3

# player setup
#define SETUP_TOKEN_PLAYER_USE_JOYSTICK     0
#define SETUP_TOKEN_PLAYER_JOY_DEVICE_NAME  1
#define SETUP_TOKEN_PLAYER_JOY_XLEFT        2
#define SETUP_TOKEN_PLAYER_JOY_XMIDDLE      3
#define SETUP_TOKEN_PLAYER_JOY_XRIGHT       4
#define SETUP_TOKEN_PLAYER_JOY_YUPPER       5
#define SETUP_TOKEN_PLAYER_JOY_YMIDDLE      6
#define SETUP_TOKEN_PLAYER_JOY_YLOWER       7
#define SETUP_TOKEN_PLAYER_JOY_SNAP     8
#define SETUP_TOKEN_PLAYER_JOY_DROP     9
#define SETUP_TOKEN_PLAYER_KEY_LEFT     10
#define SETUP_TOKEN_PLAYER_KEY_RIGHT        11
#define SETUP_TOKEN_PLAYER_KEY_UP       12
#define SETUP_TOKEN_PLAYER_KEY_DOWN     13
#define SETUP_TOKEN_PLAYER_KEY_SNAP     14
#define SETUP_TOKEN_PLAYER_KEY_DROP     15

#define NUM_PLAYER_SETUP_TOKENS         16

# system setup
#define SETUP_TOKEN_SYSTEM_SDL_AUDIODRIVER  0
#define SETUP_TOKEN_SYSTEM_AUDIO_FRAGMENT_SIZE  1

#define NUM_SYSTEM_SETUP_TOKENS         2

# options setup
#define SETUP_TOKEN_OPTIONS_VERBOSE     0

#define NUM_OPTIONS_SETUP_TOKENS        1


static struct SetupInfo si;
static struct SetupEditorInfo sei;
static struct SetupShortcutInfo ssi;
static struct SetupInputInfo sii;
static struct SetupSystemInfo syi;
static struct OptionInfo soi;

static struct TokenInfo global_setup_tokens[] =
{
  { TYPE_STRING, &si.player_name,   "player_name"           },
  { TYPE_SWITCH, &si.sound,     "sound"             },
  { TYPE_SWITCH, &si.sound_loops,   "repeating_sound_loops"     },
  { TYPE_SWITCH, &si.sound_music,   "background_music"      },
  { TYPE_SWITCH, &si.sound_simple,  "simple_sound_effects"      },
  { TYPE_SWITCH, &si.toons,     "toons"             },
  { TYPE_SWITCH, &si.scroll_delay,  "scroll_delay"          },
  { TYPE_SWITCH, &si.soft_scrolling,    "soft_scrolling"        },
  { TYPE_SWITCH, &si.fading,        "screen_fading"         },
  { TYPE_SWITCH, &si.autorecord,    "automatic_tape_recording"  },
  { TYPE_SWITCH, &si.quick_doors,   "quick_doors"           },
  { TYPE_SWITCH, &si.team_mode,     "team_mode"         },
  { TYPE_SWITCH, &si.handicap,      "handicap"          },
  { TYPE_SWITCH, &si.skip_levels,   "skip_levels"           },
  { TYPE_SWITCH, &si.time_limit,    "time_limit"            },
  { TYPE_SWITCH, &si.fullscreen,    "fullscreen"            },
  { TYPE_SWITCH, &si.ask_on_escape, "ask_on_escape"         },
  { TYPE_STRING, &si.graphics_set,  "graphics_set"          },
  { TYPE_STRING, &si.sounds_set,    "sounds_set"            },
  { TYPE_STRING, &si.music_set,     "music_set"         },
  { TYPE_SWITCH, &si.override_level_graphics, "override_level_graphics" },
  { TYPE_SWITCH, &si.override_level_sounds,   "override_level_sounds"   },
  { TYPE_SWITCH, &si.override_level_music,    "override_level_music"    },
};

static struct TokenInfo editor_setup_tokens[] =
{
  { TYPE_SWITCH, &sei.el_boulderdash,   "editor.el_boulderdash"     },
  { TYPE_SWITCH, &sei.el_emerald_mine,  "editor.el_emerald_mine"    },
  { TYPE_SWITCH, &sei.el_emerald_mine_club,"editor.el_emerald_mine_club"},
  { TYPE_SWITCH, &sei.el_more,      "editor.el_more"        },
  { TYPE_SWITCH, &sei.el_sokoban,   "editor.el_sokoban"     },
  { TYPE_SWITCH, &sei.el_supaplex,  "editor.el_supaplex"        },
  { TYPE_SWITCH, &sei.el_diamond_caves, "editor.el_diamond_caves"   },
  { TYPE_SWITCH, &sei.el_dx_boulderdash,"editor.el_dx_boulderdash"  },
  { TYPE_SWITCH, &sei.el_chars,     "editor.el_chars"       },
  { TYPE_SWITCH, &sei.el_custom,    "editor.el_custom"      },
  { TYPE_SWITCH, &sei.el_custom_more,   "editor.el_custom_more"     },
  { TYPE_SWITCH, &sei.el_headlines, "editor.el_headlines"       },
  { TYPE_SWITCH, &sei.el_user_defined,  "editor.el_user_defined"    },
};

static struct TokenInfo shortcut_setup_tokens[] =
{
  { TYPE_KEY_X11, &ssi.save_game,   "shortcut.save_game"        },
  { TYPE_KEY_X11, &ssi.load_game,   "shortcut.load_game"        },
  { TYPE_KEY_X11, &ssi.toggle_pause,    "shortcut.toggle_pause"     }
};

static struct TokenInfo player_setup_tokens[] =
{
  { TYPE_BOOLEAN, &sii.use_joystick,    ".use_joystick"         },
  { TYPE_STRING,  &sii.joy.device_name, ".joy.device_name"      },
  { TYPE_INTEGER, &sii.joy.xleft,   ".joy.xleft"            },
  { TYPE_INTEGER, &sii.joy.xmiddle, ".joy.xmiddle"          },
  { TYPE_INTEGER, &sii.joy.xright,  ".joy.xright"           },
  { TYPE_INTEGER, &sii.joy.yupper,  ".joy.yupper"           },
  { TYPE_INTEGER, &sii.joy.ymiddle, ".joy.ymiddle"          },
  { TYPE_INTEGER, &sii.joy.ylower,  ".joy.ylower"           },
  { TYPE_INTEGER, &sii.joy.snap,    ".joy.snap_field"       },
  { TYPE_INTEGER, &sii.joy.drop,    ".joy.place_bomb"       },
  { TYPE_KEY_X11, &sii.key.left,    ".key.move_left"        },
  { TYPE_KEY_X11, &sii.key.right,   ".key.move_right"       },
  { TYPE_KEY_X11, &sii.key.up,      ".key.move_up"          },
  { TYPE_KEY_X11, &sii.key.down,    ".key.move_down"        },
  { TYPE_KEY_X11, &sii.key.snap,    ".key.snap_field"       },
  { TYPE_KEY_X11, &sii.key.drop,    ".key.place_bomb"       }
};

static struct TokenInfo system_setup_tokens[] =
{
  { TYPE_STRING,  &syi.sdl_audiodriver, "system.sdl_audiodriver"    },
  { TYPE_INTEGER, &syi.audio_fragment_size,"system.audio_fragment_size" }
};

static struct TokenInfo options_setup_tokens[] =
{
  { TYPE_BOOLEAN, &soi.verbose,     "options.verbose"       }
};

static char *get_corrected_login_name(char *login_name)
{
  # needed because player name must be a fixed length string
  char *login_name_new = checked_malloc(MAX_PLAYER_NAME_LEN + 1);

  strncpy(login_name_new, login_name, MAX_PLAYER_NAME_LEN);
  login_name_new[MAX_PLAYER_NAME_LEN] = '\0';

  if (strlen(login_name) > MAX_PLAYER_NAME_LEN)     # name has been cut
    if (strchr(login_name_new, ' '))
      *strchr(login_name_new, ' ') = '\0';

  return login_name_new;
}

static void setSetupInfoToDefaults(struct SetupInfo *si)
{
  int i;

  si->player_name = get_corrected_login_name(getLoginName());

  si->sound = true;
  si->sound_loops = true;
  si->sound_music = true;
  si->sound_simple = true;
  si->toons = true;
  si->double_buffering = true;
  si->direct_draw = !si->double_buffering;
  si->scroll_delay = true;
  si->soft_scrolling = true;
  si->fading = false;
  si->autorecord = true;
  si->quick_doors = false;
  si->team_mode = false;
  si->handicap = true;
  si->skip_levels = true;
  si->time_limit = true;
  si->fullscreen = false;
  si->ask_on_escape = true;

  si->graphics_set = getStringCopy(GFX_CLASSIC_SUBDIR);
  si->sounds_set = getStringCopy(SND_CLASSIC_SUBDIR);
  si->music_set = getStringCopy(MUS_CLASSIC_SUBDIR);
  si->override_level_graphics = false;
  si->override_level_sounds = false;
  si->override_level_music = false;

  si->editor.el_boulderdash       = true;
  si->editor.el_emerald_mine      = true;
  si->editor.el_emerald_mine_club = true;
  si->editor.el_more              = true;
  si->editor.el_sokoban           = true;
  si->editor.el_supaplex          = true;
  si->editor.el_diamond_caves     = true;
  si->editor.el_dx_boulderdash    = true;
  si->editor.el_chars             = true;
  si->editor.el_custom            = true;
  si->editor.el_custom_more       = false;

  si->editor.el_headlines = true;
  si->editor.el_user_defined = false;

  si->shortcut.save_game = DEFAULT_KEY_SAVE_GAME;
  si->shortcut.load_game = DEFAULT_KEY_LOAD_GAME;
  si->shortcut.toggle_pause = DEFAULT_KEY_TOGGLE_PAUSE;

  for (i = 0; i < MAX_PLAYERS; i++)
  {
    si->input[i].use_joystick = false;
    si->input[i].joy.device_name=getStringCopy(getDeviceNameFromJoystickNr(i));
    si->input[i].joy.xleft   = JOYSTICK_XLEFT;
    si->input[i].joy.xmiddle = JOYSTICK_XMIDDLE;
    si->input[i].joy.xright  = JOYSTICK_XRIGHT;
    si->input[i].joy.yupper  = JOYSTICK_YUPPER;
    si->input[i].joy.ymiddle = JOYSTICK_YMIDDLE;
    si->input[i].joy.ylower  = JOYSTICK_YLOWER;
    si->input[i].joy.snap  = (i == 0 ? JOY_BUTTON_1 : 0);
    si->input[i].joy.drop  = (i == 0 ? JOY_BUTTON_2 : 0);
    si->input[i].key.left  = (i == 0 ? DEFAULT_KEY_LEFT  : KSYM_UNDEFINED);
    si->input[i].key.right = (i == 0 ? DEFAULT_KEY_RIGHT : KSYM_UNDEFINED);
    si->input[i].key.up    = (i == 0 ? DEFAULT_KEY_UP    : KSYM_UNDEFINED);
    si->input[i].key.down  = (i == 0 ? DEFAULT_KEY_DOWN  : KSYM_UNDEFINED);
    si->input[i].key.snap  = (i == 0 ? DEFAULT_KEY_SNAP  : KSYM_UNDEFINED);
    si->input[i].key.drop  = (i == 0 ? DEFAULT_KEY_DROP  : KSYM_UNDEFINED);
  }

  si->system.sdl_audiodriver = getStringCopy(ARG_DEFAULT);
  si->system.audio_fragment_size = DEFAULT_AUDIO_FRAGMENT_SIZE;

  si->options.verbose = false;
}

static void decodeSetupFileHash(SetupFileHash *setup_file_hash)
{
  int i, pnr;

  if (!setup_file_hash)
    return;

  # global setup
  si = setup;
  for (i = 0; i < NUM_GLOBAL_SETUP_TOKENS; i++)
    setSetupInfo(global_setup_tokens, i,
         getHashEntry(setup_file_hash, global_setup_tokens[i].text));
  setup = si;

  # editor setup
  sei = setup.editor;
  for (i = 0; i < NUM_EDITOR_SETUP_TOKENS; i++)
    setSetupInfo(editor_setup_tokens, i,
         getHashEntry(setup_file_hash,editor_setup_tokens[i].text));
  setup.editor = sei;

  # shortcut setup
  ssi = setup.shortcut;
  for (i = 0; i < NUM_SHORTCUT_SETUP_TOKENS; i++)
    setSetupInfo(shortcut_setup_tokens, i,
         getHashEntry(setup_file_hash,shortcut_setup_tokens[i].text));
  setup.shortcut = ssi;

  # player setup
  for (pnr = 0; pnr < MAX_PLAYERS; pnr++)
  {
    char prefix[30];

    sprintf(prefix, "%s%d", TOKEN_STR_PLAYER_PREFIX, pnr + 1);

    sii = setup.input[pnr];
    for (i = 0; i < NUM_PLAYER_SETUP_TOKENS; i++)
    {
      char full_token[100];

      sprintf(full_token, "%s%s", prefix, player_setup_tokens[i].text);
      setSetupInfo(player_setup_tokens, i,
           getHashEntry(setup_file_hash, full_token));
    }
    setup.input[pnr] = sii;
  }

  # system setup
  syi = setup.system;
  for (i = 0; i < NUM_SYSTEM_SETUP_TOKENS; i++)
    setSetupInfo(system_setup_tokens, i,
         getHashEntry(setup_file_hash, system_setup_tokens[i].text));
  setup.system = syi;

  # options setup
  soi = setup.options;
  for (i = 0; i < NUM_OPTIONS_SETUP_TOKENS; i++)
    setSetupInfo(options_setup_tokens, i,
         getHashEntry(setup_file_hash, options_setup_tokens[i].text));
  setup.options = soi;
}

void LoadSetup()
{
  char *filename = getSetupFilename();
  SetupFileHash *setup_file_hash = NULL;

  # always start with reliable default values
  setSetupInfoToDefaults(&setup);

  setup_file_hash = loadSetupFileHash(filename);

  if (setup_file_hash)
  {
    char *player_name_new;

    checkSetupFileHashIdentifier(setup_file_hash, getCookie("SETUP"));
    decodeSetupFileHash(setup_file_hash);

    setup.direct_draw = !setup.double_buffering;

    freeSetupFileHash(setup_file_hash);

    # needed to work around problems with fixed length strings
    player_name_new = get_corrected_login_name(setup.player_name);
    free(setup.player_name);
    setup.player_name = player_name_new;
  }
  else
    error(ERR_WARN, "using default setup values");
}

void SaveSetup()
{
  char *filename = getSetupFilename();
  FILE *file;
  int i, pnr;

  InitUserDataDirectory();

  if (!(file = fopen(filename, MODE_WRITE)))
  {
    error(ERR_WARN, "cannot write setup file '%s'", filename);
    return;
  }

  fprintf(file, "%s\n", getFormattedSetupEntry(TOKEN_STR_FILE_IDENTIFIER,
                           getCookie("SETUP")));
  fprintf(file, "\n");

  # global setup
  si = setup;
  for (i = 0; i < NUM_GLOBAL_SETUP_TOKENS; i++)
  {
    # just to make things nicer :)
    if (i == SETUP_TOKEN_PLAYER_NAME + 1 ||
    i == SETUP_TOKEN_GRAPHICS_SET)
      fprintf(file, "\n");

    fprintf(file, "%s\n", getSetupLine(global_setup_tokens, "", i));
  }

  # editor setup
  sei = setup.editor;
  fprintf(file, "\n");
  for (i = 0; i < NUM_EDITOR_SETUP_TOKENS; i++)
    fprintf(file, "%s\n", getSetupLine(editor_setup_tokens, "", i));

  # shortcut setup
  ssi = setup.shortcut;
  fprintf(file, "\n");
  for (i = 0; i < NUM_SHORTCUT_SETUP_TOKENS; i++)
    fprintf(file, "%s\n", getSetupLine(shortcut_setup_tokens, "", i));

  # player setup
  for (pnr = 0; pnr < MAX_PLAYERS; pnr++)
  {
    char prefix[30];

    sprintf(prefix, "%s%d", TOKEN_STR_PLAYER_PREFIX, pnr + 1);
    fprintf(file, "\n");

    sii = setup.input[pnr];
    for (i = 0; i < NUM_PLAYER_SETUP_TOKENS; i++)
      fprintf(file, "%s\n", getSetupLine(player_setup_tokens, prefix, i));
  }

  # system setup
  syi = setup.system;
  fprintf(file, "\n");
  for (i = 0; i < NUM_SYSTEM_SETUP_TOKENS; i++)
    fprintf(file, "%s\n", getSetupLine(system_setup_tokens, "", i));

  # options setup
  soi = setup.options;
  fprintf(file, "\n");
  for (i = 0; i < NUM_OPTIONS_SETUP_TOKENS; i++)
    fprintf(file, "%s\n", getSetupLine(options_setup_tokens, "", i));

  fclose(file);

  SetFilePermissions(filename, PERMS_PRIVATE);
}

void LoadCustomElementDescriptions()
{
  char *filename = getCustomArtworkConfigFilename(ARTWORK_TYPE_GRAPHICS);
  SetupFileHash *setup_file_hash;
  int i;

  for (i = 0; i < NUM_FILE_ELEMENTS; i++)
  {
    if (element_info[i].custom_description != NULL)
    {
      free(element_info[i].custom_description);
      element_info[i].custom_description = NULL;
    }
  }

  if ((setup_file_hash = loadSetupFileHash(filename)) == NULL)
    return;

  for (i = 0; i < NUM_FILE_ELEMENTS; i++)
  {
    char *token = getStringCat2(element_info[i].token_name, ".name");
    char *value = getHashEntry(setup_file_hash, token);

    if (value != NULL)
      element_info[i].custom_description = getStringCopy(value);

    free(token);
  }

  freeSetupFileHash(setup_file_hash);
}

void LoadSpecialMenuDesignSettings()
{
  char *filename = getCustomArtworkConfigFilename(ARTWORK_TYPE_GRAPHICS);
  SetupFileHash *setup_file_hash;
  int i, j;

  # always start with reliable default values from default config
  for (i = 0; image_config_vars[i].token != NULL; i++)
    for (j = 0; image_config[j].token != NULL; j++)
      if (strcmp(image_config_vars[i].token, image_config[j].token) == 0)
    *image_config_vars[i].value =
      get_auto_parameter_value(image_config_vars[i].token,
                   image_config[j].value);

  if ((setup_file_hash = loadSetupFileHash(filename)) == NULL)
    return;

  # special case: initialize with default values that may be overwritten
  for (i = 0; i < NUM_SPECIAL_GFX_ARGS; i++)
  {
    char *value_x = getHashEntry(setup_file_hash, "menu.draw_xoffset");
    char *value_y = getHashEntry(setup_file_hash, "menu.draw_yoffset");
    char *list_size = getHashEntry(setup_file_hash, "menu.list_size");

    if (value_x != NULL)
      menu.draw_xoffset[i] = get_integer_from_string(value_x);
    if (value_y != NULL)
      menu.draw_yoffset[i] = get_integer_from_string(value_y);
    if (list_size != NULL)
      menu.list_size[i] = get_integer_from_string(list_size);
  }

  # read (and overwrite with) values that may be specified in config file
  for (i = 0; image_config_vars[i].token != NULL; i++)
  {
    char *value = getHashEntry(setup_file_hash, image_config_vars[i].token);

    if (value != NULL)
      *image_config_vars[i].value =
    get_auto_parameter_value(image_config_vars[i].token, value);
  }

  freeSetupFileHash(setup_file_hash);
}

void LoadUserDefinedEditorElementList(int **elements, int *num_elements)
{
  char *filename = getEditorSetupFilename();
  SetupFileList *setup_file_list, *list;
  SetupFileHash *element_hash;
  int num_unknown_tokens = 0;
  int i;

  if ((setup_file_list = loadSetupFileList(filename)) == NULL)
    return;

  element_hash = newSetupFileHash();

  for (i = 0; i < NUM_FILE_ELEMENTS; i++)
    setHashEntry(element_hash, element_info[i].token_name, i_to_a(i));

  # determined size may be larger than needed (due to unknown elements)
  *num_elements = 0;
  for (list = setup_file_list; list != NULL; list = list->next)
    (*num_elements)++;

  # add space for up to 3 more elements for padding that may be needed
  *num_elements += 3;

  *elements = checked_malloc(*num_elements * sizeof(int));

  *num_elements = 0;
  for (list = setup_file_list; list != NULL; list = list->next)
  {
    char *value = getHashEntry(element_hash, list->token);

    if (value == NULL)      # try to find obsolete token mapping
    {
      char *mapped_token = get_mapped_token(list->token);

      if (mapped_token != NULL)
      {
    value = getHashEntry(element_hash, mapped_token);

    free(mapped_token);
      }
    }

    if (value != NULL)
    {
      (*elements)[(*num_elements)++] = atoi(value);
    }
    else
    {
      if (num_unknown_tokens == 0)
      {
    error(ERR_RETURN_LINE, "-");
    error(ERR_RETURN, "warning: unknown token(s) found in config file:");
    error(ERR_RETURN, "- config file: '%s'", filename);

    num_unknown_tokens++;
      }

      error(ERR_RETURN, "- token: '%s'", list->token);
    }
  }

  if (num_unknown_tokens > 0)
    error(ERR_RETURN_LINE, "-");

  while (*num_elements % 4) # pad with empty elements, if needed
    (*elements)[(*num_elements)++] = EL_EMPTY;

  freeSetupFileList(setup_file_list);
  freeSetupFileHash(element_hash);

}

static struct MusicFileInfo *get_music_file_info_ext(char *basename, int music,
                             boolean is_sound)
{
  SetupFileHash *setup_file_hash = NULL;
  struct MusicFileInfo tmp_music_file_info, *new_music_file_info;
  char *filename_music, *filename_prefix, *filename_info;
  struct
  {
    char *token;
    char **value_ptr;
  }
  token_to_value_ptr[] =
  {
    { "title_header",   &tmp_music_file_info.title_header   },
    { "artist_header",  &tmp_music_file_info.artist_header  },
    { "album_header",   &tmp_music_file_info.album_header   },
    { "year_header",    &tmp_music_file_info.year_header    },

    { "title",      &tmp_music_file_info.title      },
    { "artist",     &tmp_music_file_info.artist     },
    { "album",      &tmp_music_file_info.album      },
    { "year",       &tmp_music_file_info.year       },

    { NULL,     NULL                    },
  };
  int i;

  filename_music = (is_sound ? getCustomSoundFilename(basename) :
            getCustomMusicFilename(basename));

  if (filename_music == NULL)
    return NULL;

  # ---------- try to replace file extension ----------

  filename_prefix = getStringCopy(filename_music);
  if (strrchr(filename_prefix, '.') != NULL)
    *strrchr(filename_prefix, '.') = '\0';
  filename_info = getStringCat2(filename_prefix, ".txt");

  if (fileExists(filename_info))
    setup_file_hash = loadSetupFileHash(filename_info);

  free(filename_prefix);
  free(filename_info);

  if (setup_file_hash == NULL)
  {
    # ---------- try to add file extension ----------

    filename_prefix = getStringCopy(filename_music);
    filename_info = getStringCat2(filename_prefix, ".txt");

    if (fileExists(filename_info))
      setup_file_hash = loadSetupFileHash(filename_info);

    free(filename_prefix);
    free(filename_info);
  }

  if (setup_file_hash == NULL)
    return NULL;

  # ---------- music file info found ----------

  memset(&tmp_music_file_info, 0, sizeof(struct MusicFileInfo));

  for (i = 0; token_to_value_ptr[i].token != NULL; i++)
  {
    char *value = getHashEntry(setup_file_hash, token_to_value_ptr[i].token);

    *token_to_value_ptr[i].value_ptr =
      getStringCopy(value != NULL && *value != '\0' ? value : UNKNOWN_NAME);
  }

  tmp_music_file_info.basename = getStringCopy(basename);
  tmp_music_file_info.music = music;
  tmp_music_file_info.is_sound = is_sound;

  new_music_file_info = checked_malloc(sizeof(struct MusicFileInfo));
  *new_music_file_info = tmp_music_file_info;

  return new_music_file_info;
}

static struct MusicFileInfo *get_music_file_info(char *basename, int music)
{
  return get_music_file_info_ext(basename, music, false);
}

static struct MusicFileInfo *get_sound_file_info(char *basename, int sound)
{
  return get_music_file_info_ext(basename, sound, true);
}

static boolean music_info_listed_ext(struct MusicFileInfo *list,
                     char *basename, boolean is_sound)
{
  for (; list != NULL; list = list->next)
    if (list->is_sound == is_sound && strcmp(list->basename, basename) == 0)
      return true;

  return false;
}

static boolean music_info_listed(struct MusicFileInfo *list, char *basename)
{
  return music_info_listed_ext(list, basename, false);
}

static boolean sound_info_listed(struct MusicFileInfo *list, char *basename)
{
  return music_info_listed_ext(list, basename, true);
}

void LoadMusicInfo()
{
  char *music_directory = getCustomMusicDirectory();
  int num_music = getMusicListSize();
  int num_music_noconf = 0;
  int num_sounds = getSoundListSize();
  DIR *dir;
  struct dirent *dir_entry;
  struct FileInfo *music, *sound;
  struct MusicFileInfo *next, **new;
  int i;

  while (music_file_info != NULL)
  {
    next = music_file_info->next;

    checked_free(music_file_info->basename);

    checked_free(music_file_info->title_header);
    checked_free(music_file_info->artist_header);
    checked_free(music_file_info->album_header);
    checked_free(music_file_info->year_header);

    checked_free(music_file_info->title);
    checked_free(music_file_info->artist);
    checked_free(music_file_info->album);
    checked_free(music_file_info->year);

    free(music_file_info);

    music_file_info = next;
  }

  new = &music_file_info;

  for (i = 0; i < num_music; i++)
  {
    music = getMusicListEntry(i);

    if (music->filename == NULL)
      continue;

    if (strcmp(music->filename, UNDEFINED_FILENAME) == 0)
      continue;

    # a configured file may be not recognized as music
    if (!FileIsMusic(music->filename))
      continue;

    if (!music_info_listed(music_file_info, music->filename))
    {
      *new = get_music_file_info(music->filename, i);
      if (*new != NULL)
    new = &(*new)->next;
    }
  }

  if ((dir = opendir(music_directory)) == NULL)
  {
    error(ERR_WARN, "cannot read music directory '%s'", music_directory);
    return;
  }

  while ((dir_entry = readdir(dir)) != NULL)    # loop until last dir entry
  {
    char *basename = dir_entry->d_name;
    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++)
    {
      music = getMusicListEntry(i);

      if (music->filename == NULL)
    continue;

      if (strcmp(basename, music->filename) == 0)
      {
    music_already_used = true;
    break;
      }
    }

    if (music_already_used)
      continue;

    if (!FileIsMusic(basename))
      continue;

    if (!music_info_listed(music_file_info, basename))
    {
      *new = get_music_file_info(basename, MAP_NOCONF_MUSIC(num_music_noconf));
      if (*new != NULL)
    new = &(*new)->next;
    }

    num_music_noconf++;
  }

  closedir(dir);

  for (i = 0; i < num_sounds; i++)
  {
    sound = getSoundListEntry(i);

    if (sound->filename == NULL)
      continue;

    if (strcmp(sound->filename, UNDEFINED_FILENAME) == 0)
      continue;

    # a configured file may be not recognized as sound
    if (!FileIsSound(sound->filename))
      continue;

    if (!sound_info_listed(music_file_info, sound->filename))
    {
      *new = get_sound_file_info(sound->filename, i);
      if (*new != NULL)
    new = &(*new)->next;
    }
  }

}

void add_helpanim_entry(int element, int action, int direction, int delay,
            int *num_list_entries)
{
  struct HelpAnimInfo *new_list_entry;
  (*num_list_entries)++;

  helpanim_info =
    checked_realloc(helpanim_info,
            *num_list_entries * sizeof(struct HelpAnimInfo));
  new_list_entry = &helpanim_info[*num_list_entries - 1];

  new_list_entry->element = element;
  new_list_entry->action = action;
  new_list_entry->direction = direction;
  new_list_entry->delay = delay;
}

void print_unknown_token(char *filename, char *token, int token_nr)
{
  if (token_nr == 0)
  {
    error(ERR_RETURN_LINE, "-");
    error(ERR_RETURN, "warning: unknown token(s) found in config file:");
    error(ERR_RETURN, "- config file: '%s'", filename);
  }

  error(ERR_RETURN, "- token: '%s'", token);
}

void print_unknown_token_end(int token_nr)
{
  if (token_nr > 0)
    error(ERR_RETURN_LINE, "-");
}

void LoadHelpAnimInfo()
{
  char *filename = getHelpAnimFilename();
  SetupFileList *setup_file_list = NULL, *list;
  SetupFileHash *element_hash, *action_hash, *direction_hash;
  int num_list_entries = 0;
  int num_unknown_tokens = 0;
  int i;

  if (fileExists(filename))
    setup_file_list = loadSetupFileList(filename);

  if (setup_file_list == NULL)
  {
    # use reliable default values from static configuration
    SetupFileList *insert_ptr;

    insert_ptr = setup_file_list =
      newSetupFileList(helpanim_config[0].token,
               helpanim_config[0].value);

    for (i = 1; helpanim_config[i].token; i++)
      insert_ptr = addListEntry(insert_ptr,
                helpanim_config[i].token,
                helpanim_config[i].value);
  }

  element_hash   = newSetupFileHash();
  action_hash    = newSetupFileHash();
  direction_hash = newSetupFileHash();

  for (i = 0; i < MAX_NUM_ELEMENTS; i++)
    setHashEntry(element_hash, element_info[i].token_name, i_to_a(i));

  for (i = 0; i < NUM_ACTIONS; i++)
    setHashEntry(action_hash, element_action_info[i].suffix,
         i_to_a(element_action_info[i].value));

  # do not store direction index (bit) here, but direction value!
  for (i = 0; i < NUM_DIRECTIONS; i++)
    setHashEntry(direction_hash, element_direction_info[i].suffix,
         i_to_a(1 << element_direction_info[i].value));

  for (list = setup_file_list; list != NULL; list = list->next)
  {
    char *element_token, *action_token, *direction_token;
    char *element_value, *action_value, *direction_value;
    int delay = atoi(list->value);

    if (strcmp(list->token, "end") == 0)
    {
      add_helpanim_entry(HELPANIM_LIST_NEXT, -1, -1, -1, &num_list_entries);

      continue;
    }

    # first try to break element into element/action/direction parts;
    # if this does not work, also accept combined "element[.act][.dir]"
    # elements (like "dynamite.active"), which are unique elements

    if (strchr(list->token, '.') == NULL)   # token contains no '.'
    {
      element_value = getHashEntry(element_hash, list->token);
      if (element_value != NULL)    # element found
    add_helpanim_entry(atoi(element_value), -1, -1, delay,
               &num_list_entries);
      else
      {
    # no further suffixes found -- this is not an element
    print_unknown_token(filename, list->token, num_unknown_tokens++);
      }

      continue;
    }

    # token has format "<prefix>.<something>"

    action_token = strchr(list->token, '.');    # suffix may be action ...
    direction_token = action_token;     # ... or direction

    element_token = getStringCopy(list->token);
    *strchr(element_token, '.') = '\0';

    element_value = getHashEntry(element_hash, element_token);

    if (element_value == NULL)      # this is no element
    {
      element_value = getHashEntry(element_hash, list->token);
      if (element_value != NULL)    # combined element found
    add_helpanim_entry(atoi(element_value), -1, -1, delay,
               &num_list_entries);
      else
    print_unknown_token(filename, list->token, num_unknown_tokens++);

      free(element_token);

      continue;
    }

    action_value = getHashEntry(action_hash, action_token);

    if (action_value != NULL)       # action found
    {
      add_helpanim_entry(atoi(element_value), atoi(action_value), -1, delay,
            &num_list_entries);

      free(element_token);

      continue;
    }

    direction_value = getHashEntry(direction_hash, direction_token);

    if (direction_value != NULL)    # direction found
    {
      add_helpanim_entry(atoi(element_value), -1, atoi(direction_value), delay,
             &num_list_entries);

      free(element_token);

      continue;
    }

    if (strchr(action_token + 1, '.') == NULL)
    {
      # no further suffixes found -- this is not an action nor direction

      element_value = getHashEntry(element_hash, list->token);
      if (element_value != NULL)    # combined element found
    add_helpanim_entry(atoi(element_value), -1, -1, delay,
               &num_list_entries);
      else
    print_unknown_token(filename, list->token, num_unknown_tokens++);

      free(element_token);

      continue;
    }

    # token has format "<prefix>.<suffix>.<something>"

    direction_token = strchr(action_token + 1, '.');

    action_token = getStringCopy(action_token);
    *strchr(action_token + 1, '.') = '\0';

    action_value = getHashEntry(action_hash, action_token);

    if (action_value == NULL)       # this is no action
    {
      element_value = getHashEntry(element_hash, list->token);
      if (element_value != NULL)    # combined element found
    add_helpanim_entry(atoi(element_value), -1, -1, delay,
               &num_list_entries);
      else
    print_unknown_token(filename, list->token, num_unknown_tokens++);

      free(element_token);
      free(action_token);

      continue;
    }

    direction_value = getHashEntry(direction_hash, direction_token);

    if (direction_value != NULL)    # direction found
    {
      add_helpanim_entry(atoi(element_value), atoi(action_value),
             atoi(direction_value), delay, &num_list_entries);

      free(element_token);
      free(action_token);

      continue;
    }

    # this is no direction

    element_value = getHashEntry(element_hash, list->token);
    if (element_value != NULL)      # combined element found
      add_helpanim_entry(atoi(element_value), -1, -1, delay,
             &num_list_entries);
    else
      print_unknown_token(filename, list->token, num_unknown_tokens++);

    free(element_token);
    free(action_token);
  }

  print_unknown_token_end(num_unknown_tokens);

  add_helpanim_entry(HELPANIM_LIST_NEXT, -1, -1, -1, &num_list_entries);
  add_helpanim_entry(HELPANIM_LIST_END,  -1, -1, -1, &num_list_entries);

  freeSetupFileList(setup_file_list);
  freeSetupFileHash(element_hash);
  freeSetupFileHash(action_hash);
  freeSetupFileHash(direction_hash);

}

void LoadHelpTextInfo()
{
  char *filename = getHelpTextFilename();
  int i;

  if (helptext_info != NULL)
  {
    freeSetupFileHash(helptext_info);
    helptext_info = NULL;
  }

  if (fileExists(filename))
    helptext_info = loadSetupFileHash(filename);

  if (helptext_info == NULL)
  {
    # use reliable default values from static configuration
    helptext_info = newSetupFileHash();

    for (i = 0; helptext_config[i].token; i++)
      setHashEntry(helptext_info,
           helptext_config[i].token,
           helptext_config[i].value);
  }

}


# =========================================================================
# convert levels
# =========================================================================

#define MAX_NUM_CONVERT_LEVELS      1000

void ConvertLevels()
{
  static LevelDirTree *convert_leveldir = NULL;
  static int convert_level_nr = -1;
  static int num_levels_handled = 0;
  static int num_levels_converted = 0;
  static boolean levels_failed[MAX_NUM_CONVERT_LEVELS];
  int i;

  convert_leveldir = getTreeInfoFromIdentifier(leveldir_first,
                           global.convert_leveldir);

  if (convert_leveldir == NULL)
    error(ERR_EXIT, "no such level identifier: '%s'",
      global.convert_leveldir);

  leveldir_current = convert_leveldir;

  if (global.convert_level_nr != -1)
  {
    convert_leveldir->first_level = global.convert_level_nr;
    convert_leveldir->last_level  = global.convert_level_nr;
  }

  convert_level_nr = convert_leveldir->first_level;

  printf_line("=", 79);
  printf("Converting levels\n");
  printf_line("-", 79);
  printf("Level series identifier: '%s'\n", convert_leveldir->identifier);
  printf("Level series name:       '%s'\n", convert_leveldir->name);
  printf("Level series author:     '%s'\n", convert_leveldir->author);
  printf("Number of levels:        %d\n",   convert_leveldir->levels);
  printf_line("=", 79);
  printf("\n");

  for (i = 0; i < MAX_NUM_CONVERT_LEVELS; i++)
    levels_failed[i] = false;

  while (convert_level_nr <= convert_leveldir->last_level)
  {
    char *level_filename;
    boolean new_level;

    level_nr = convert_level_nr++;

    printf("Level %03d: ", level_nr);

    LoadLevel(level_nr);
    if (level.no_valid_file)
    {
      printf("(no level)\n");
      continue;
    }

    printf("converting level ... ");

    level_filename = getDefaultLevelFilename(level_nr);
    new_level = !fileExists(level_filename);

    if (new_level)
    {
      SaveLevel(level_nr);

      num_levels_converted++;

      printf("converted.\n");
    }
    else
    {
      if (level_nr >= 0 && level_nr < MAX_NUM_CONVERT_LEVELS)
    levels_failed[level_nr] = true;

      printf("NOT CONVERTED -- LEVEL ALREADY EXISTS.\n");
    }

    num_levels_handled++;
  }

  printf("\n");
  printf_line("=", 79);
  printf("Number of levels handled: %d\n", num_levels_handled);
  printf("Number of levels converted: %d (%d%%)\n", num_levels_converted,
     (num_levels_handled ?
      num_levels_converted * 100 / num_levels_handled : 0));
  printf_line("-", 79);
  printf("Summary (for automatic parsing by scripts):\n");
  printf("LEVELDIR '%s', CONVERTED %d/%d (%d%%)",
     convert_leveldir->identifier, num_levels_converted,
     num_levels_handled,
     (num_levels_handled ?
      num_levels_converted * 100 / num_levels_handled : 0));

  if (num_levels_handled != num_levels_converted)
  {
    printf(", FAILED:");
    for (i = 0; i < MAX_NUM_CONVERT_LEVELS; i++)
      if (levels_failed[i])
    printf(" %03d", i);
  }

  printf("\n");
  printf_line("=", 79);

  CloseAllAndExit(0);
}

