+boolean SaveTapeChecked_LevelSolved(int nr)
+{
+ return SaveTapeCheckedExt(nr, "Level solved! Replace old tape?",
+ "Level solved! Tape saved!", REQ_STAY_OPEN);
+}
+
+void DumpTape(struct TapeInfo *tape)
+{
+ int tape_frame_counter;
+ int i, j;
+
+ if (tape->no_valid_file)
+ {
+ Warn("cannot dump -- no valid tape file found");
+
+ return;
+ }
+
+ PrintLine("-", 79);
+
+ Print("Tape of Level %03d (file version %08d, game version %08d)\n",
+ tape->level_nr, tape->file_version, tape->game_version);
+ Print(" (effective engine version %08d)\n",
+ tape->engine_version);
+ Print("Level series identifier: '%s'\n", tape->level_identifier);
+
+ Print("Solution tape: %s\n",
+ tape->solved ? "yes" :
+ tape->game_version < VERSION_IDENT(4,3,2,3) ? "unknown" : "no");
+
+ Print("Special tape properties: ");
+ if (tape->property_bits == TAPE_PROPERTY_NONE)
+ Print("[none]");
+ if (tape->property_bits & TAPE_PROPERTY_EM_RANDOM_BUG)
+ Print("[em_random_bug]");
+ if (tape->property_bits & TAPE_PROPERTY_GAME_SPEED)
+ Print("[game_speed]");
+ if (tape->property_bits & TAPE_PROPERTY_PAUSE_MODE)
+ Print("[pause]");
+ if (tape->property_bits & TAPE_PROPERTY_SINGLE_STEP)
+ Print("[single_step]");
+ if (tape->property_bits & TAPE_PROPERTY_SNAPSHOT)
+ Print("[snapshot]");
+ if (tape->property_bits & TAPE_PROPERTY_REPLAYED)
+ Print("[replayed]");
+ if (tape->property_bits & TAPE_PROPERTY_TAS_KEYS)
+ Print("[tas_keys]");
+ if (tape->property_bits & TAPE_PROPERTY_SMALL_GRAPHICS)
+ Print("[small_graphics]");
+ Print("\n");
+
+ int year2 = tape->date / 10000;
+ int year4 = (year2 < 70 ? 2000 + year2 : 1900 + year2);
+ int month_index_raw = (tape->date / 100) % 100;
+ int month_index = month_index_raw % 12; // prevent invalid index
+ int month = month_index + 1;
+ int day = tape->date % 100;
+
+ Print("Tape date: %04d-%02d-%02d\n", year4, month, day);
+
+ PrintLine("-", 79);
+
+ tape_frame_counter = 0;
+
+ for (i = 0; i < tape->length; i++)
+ {
+ if (i >= MAX_TAPE_LEN)
+ break;
+
+ Print("%04d: ", i);
+
+ for (j = 0; j < MAX_PLAYERS; j++)
+ {
+ if (tape->player_participates[j])
+ {
+ int action = tape->pos[i].action[j];
+
+ Print("%d:%02x ", j, action);
+ Print("[%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' : ' '));
+ }
+ }
+
+ Print("(%03d) ", tape->pos[i].delay);
+ Print("[%05d]\n", tape_frame_counter);
+
+ tape_frame_counter += tape->pos[i].delay;
+ }
+
+ PrintLine("-", 79);
+}
+
+void DumpTapes(void)
+{
+ static LevelDirTree *dumptape_leveldir = NULL;
+
+ dumptape_leveldir = getTreeInfoFromIdentifier(leveldir_first,
+ global.dumptape_leveldir);
+
+ if (dumptape_leveldir == NULL)
+ Fail("no such level identifier: '%s'", global.dumptape_leveldir);
+
+ if (global.dumptape_level_nr < dumptape_leveldir->first_level ||
+ global.dumptape_level_nr > dumptape_leveldir->last_level)
+ Fail("no such level number: %d", global.dumptape_level_nr);
+
+ leveldir_current = dumptape_leveldir;
+
+ if (options.mytapes)
+ LoadTape(global.dumptape_level_nr);
+ else
+ LoadSolutionTape(global.dumptape_level_nr);
+
+ DumpTape(&tape);
+
+ CloseAllAndExit(0);
+}
+
+
+// ============================================================================
+// score file functions
+// ============================================================================
+
+static void setScoreInfoToDefaultsExt(struct ScoreInfo *scores)
+{
+ int i;
+
+ for (i = 0; i < MAX_SCORE_ENTRIES; i++)
+ {
+ strcpy(scores->entry[i].tape_basename, UNDEFINED_FILENAME);
+ strcpy(scores->entry[i].name, EMPTY_PLAYER_NAME);
+ scores->entry[i].score = 0;
+ scores->entry[i].time = 0;
+
+ scores->entry[i].id = -1;
+ strcpy(scores->entry[i].tape_date, UNKNOWN_NAME);
+ strcpy(scores->entry[i].platform, UNKNOWN_NAME);
+ strcpy(scores->entry[i].version, UNKNOWN_NAME);
+ strcpy(scores->entry[i].country_name, UNKNOWN_NAME);
+ strcpy(scores->entry[i].country_code, "??");
+ }
+
+ scores->num_entries = 0;
+ scores->last_added = -1;
+ scores->last_added_local = -1;
+
+ scores->updated = FALSE;
+ scores->uploaded = FALSE;
+ scores->tape_downloaded = FALSE;
+ scores->force_last_added = FALSE;
+
+ // The following values are intentionally not reset here:
+ // - last_level_nr
+ // - last_entry_nr
+ // - next_level_nr
+ // - continue_playing
+ // - continue_on_return
+}
+
+static void setScoreInfoToDefaults(void)
+{
+ setScoreInfoToDefaultsExt(&scores);
+}
+
+static void setServerScoreInfoToDefaults(void)
+{
+ setScoreInfoToDefaultsExt(&server_scores);
+}
+
+static void LoadScore_OLD(int nr)
+{
+ int i;
+ char *filename = getScoreFilename(nr);
+ char cookie[MAX_LINE_LEN];
+ char line[MAX_LINE_LEN];
+ char *line_ptr;
+ FILE *file;
+
+ if (!(file = fopen(filename, MODE_READ)))
+ return;
+
+ // check file identifier
+ if (fgets(cookie, MAX_LINE_LEN, file) == NULL)
+ cookie[0] = '\0';
+ if (strlen(cookie) > 0 && cookie[strlen(cookie) - 1] == '\n')
+ cookie[strlen(cookie) - 1] = '\0';
+
+ if (!checkCookieString(cookie, SCORE_COOKIE_TMPL))
+ {
+ Warn("unknown format of score file '%s'", filename);
+
+ fclose(file);
+
+ return;
+ }
+
+ for (i = 0; i < MAX_SCORE_ENTRIES; i++)
+ {
+ if (fscanf(file, "%d", &scores.entry[i].score) == EOF)
+ Warn("fscanf() failed; %s", strerror(errno));
+
+ if (fgets(line, MAX_LINE_LEN, file) == NULL)
+ line[0] = '\0';
+
+ if (strlen(line) > 0 && 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(scores.entry[i].name, line_ptr, MAX_PLAYER_NAME_LEN);
+ scores.entry[i].name[MAX_PLAYER_NAME_LEN] = '\0';
+ break;
+ }
+ }
+ }
+
+ fclose(file);
+}
+
+static void ConvertScore_OLD(void)
+{
+ // only convert score to time for levels that rate playing time over score
+ if (!level.rate_time_over_score)
+ return;
+
+ // convert old score to playing time for score-less levels (like Supaplex)
+ int time_final_max = 999;
+ int i;
+
+ for (i = 0; i < MAX_SCORE_ENTRIES; i++)
+ {
+ int score = scores.entry[i].score;
+
+ if (score > 0 && score < time_final_max)
+ scores.entry[i].time = (time_final_max - score - 1) * FRAMES_PER_SECOND;
+ }
+}
+
+static int LoadScore_VERS(File *file, int chunk_size, struct ScoreInfo *scores)
+{
+ scores->file_version = getFileVersion(file);
+ scores->game_version = getFileVersion(file);
+
+ return chunk_size;
+}
+
+static int LoadScore_INFO(File *file, int chunk_size, struct ScoreInfo *scores)
+{
+ char *level_identifier = NULL;
+ int level_identifier_size;
+ int i;
+
+ level_identifier_size = getFile16BitBE(file);
+
+ level_identifier = checked_malloc(level_identifier_size);
+
+ for (i = 0; i < level_identifier_size; i++)
+ level_identifier[i] = getFile8Bit(file);
+
+ strncpy(scores->level_identifier, level_identifier, MAX_FILENAME_LEN);
+ scores->level_identifier[MAX_FILENAME_LEN] = '\0';
+
+ checked_free(level_identifier);
+
+ scores->level_nr = getFile16BitBE(file);
+ scores->num_entries = getFile16BitBE(file);
+
+ chunk_size = 2 + level_identifier_size + 2 + 2;
+
+ return chunk_size;
+}
+
+static int LoadScore_NAME(File *file, int chunk_size, struct ScoreInfo *scores)
+{
+ int i, j;
+
+ for (i = 0; i < scores->num_entries; i++)
+ {
+ for (j = 0; j < MAX_PLAYER_NAME_LEN; j++)
+ scores->entry[i].name[j] = getFile8Bit(file);
+
+ scores->entry[i].name[MAX_PLAYER_NAME_LEN] = '\0';
+ }
+
+ chunk_size = scores->num_entries * MAX_PLAYER_NAME_LEN;
+
+ return chunk_size;
+}
+
+static int LoadScore_SCOR(File *file, int chunk_size, struct ScoreInfo *scores)
+{
+ int i;
+
+ for (i = 0; i < scores->num_entries; i++)
+ scores->entry[i].score = getFile16BitBE(file);
+
+ chunk_size = scores->num_entries * 2;
+
+ return chunk_size;
+}
+
+static int LoadScore_SC4R(File *file, int chunk_size, struct ScoreInfo *scores)
+{
+ int i;
+
+ for (i = 0; i < scores->num_entries; i++)
+ scores->entry[i].score = getFile32BitBE(file);
+
+ chunk_size = scores->num_entries * 4;
+
+ return chunk_size;
+}
+
+static int LoadScore_TIME(File *file, int chunk_size, struct ScoreInfo *scores)
+{
+ int i;
+
+ for (i = 0; i < scores->num_entries; i++)
+ scores->entry[i].time = getFile32BitBE(file);
+
+ chunk_size = scores->num_entries * 4;
+
+ return chunk_size;
+}
+
+static int LoadScore_TAPE(File *file, int chunk_size, struct ScoreInfo *scores)
+{
+ int i, j;
+
+ for (i = 0; i < scores->num_entries; i++)
+ {
+ for (j = 0; j < MAX_SCORE_TAPE_BASENAME_LEN; j++)
+ scores->entry[i].tape_basename[j] = getFile8Bit(file);
+
+ scores->entry[i].tape_basename[MAX_SCORE_TAPE_BASENAME_LEN] = '\0';
+ }
+
+ chunk_size = scores->num_entries * MAX_SCORE_TAPE_BASENAME_LEN;
+
+ return chunk_size;
+}
+
+void LoadScore(int nr)
+{
+ char *filename = getScoreFilename(nr);
+ char cookie[MAX_LINE_LEN];
+ char chunk_name[CHUNK_ID_LEN + 1];
+ int chunk_size;
+ boolean old_score_file_format = FALSE;
+ File *file;
+
+ // always start with reliable default values
+ setScoreInfoToDefaults();
+
+ if (!(file = openFile(filename, MODE_READ)))
+ return;
+
+ getFileChunkBE(file, chunk_name, NULL);
+ if (strEqual(chunk_name, "RND1"))
+ {
+ getFile32BitBE(file); // not used
+
+ getFileChunkBE(file, chunk_name, NULL);
+ if (!strEqual(chunk_name, "SCOR"))
+ {
+ Warn("unknown format of score file '%s'", filename);
+
+ closeFile(file);
+
+ return;
+ }
+ }
+ else // check for old file format with cookie string
+ {
+ strcpy(cookie, chunk_name);
+ if (getStringFromFile(file, &cookie[4], MAX_LINE_LEN - 4) == NULL)
+ cookie[4] = '\0';
+ if (strlen(cookie) > 0 && cookie[strlen(cookie) - 1] == '\n')
+ cookie[strlen(cookie) - 1] = '\0';
+
+ if (!checkCookieString(cookie, SCORE_COOKIE_TMPL))
+ {
+ Warn("unknown format of score file '%s'", filename);
+
+ closeFile(file);
+
+ return;
+ }
+
+ old_score_file_format = TRUE;
+ }
+
+ if (old_score_file_format)
+ {
+ // score files from versions before 4.2.4.0 without chunk structure
+ LoadScore_OLD(nr);
+
+ // convert score to time, if possible (mainly for Supaplex levels)
+ ConvertScore_OLD();
+ }
+ else
+ {
+ static struct
+ {
+ char *name;
+ int size;
+ int (*loader)(File *, int, struct ScoreInfo *);
+ }
+ chunk_info[] =
+ {
+ { "VERS", SCORE_CHUNK_VERS_SIZE, LoadScore_VERS },
+ { "INFO", -1, LoadScore_INFO },
+ { "NAME", -1, LoadScore_NAME },
+ { "SCOR", -1, LoadScore_SCOR },
+ { "SC4R", -1, LoadScore_SC4R },
+ { "TIME", -1, LoadScore_TIME },
+ { "TAPE", -1, LoadScore_TAPE },
+
+ { NULL, 0, NULL }
+ };
+
+ while (getFileChunkBE(file, chunk_name, &chunk_size))
+ {
+ int i = 0;
+
+ while (chunk_info[i].name != NULL &&
+ !strEqual(chunk_name, chunk_info[i].name))
+ i++;
+
+ if (chunk_info[i].name == NULL)
+ {
+ Warn("unknown chunk '%s' in score file '%s'",
+ chunk_name, filename);
+
+ ReadUnusedBytesFromFile(file, chunk_size);
+ }
+ else if (chunk_info[i].size != -1 &&
+ chunk_info[i].size != chunk_size)
+ {
+ Warn("wrong size (%d) of chunk '%s' in score file '%s'",
+ chunk_size, chunk_name, filename);
+
+ ReadUnusedBytesFromFile(file, chunk_size);
+ }
+ else
+ {
+ // call function to load this score chunk
+ int chunk_size_expected =
+ (chunk_info[i].loader)(file, chunk_size, &scores);
+
+ // 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)
+ {
+ Warn("wrong size (%d) of chunk '%s' in score file '%s'",
+ chunk_size, chunk_name, filename);
+ }
+ }
+ }
+ }
+
+ closeFile(file);
+}
+
+#if ENABLE_HISTORIC_CHUNKS
+void SaveScore_OLD(int nr)
+{
+ int i;
+ char *filename = getScoreFilename(nr);
+ FILE *file;
+
+ // used instead of "leveldir_current->subdir" (for network games)
+ InitScoreDirectory(levelset.identifier);
+
+ if (!(file = fopen(filename, MODE_WRITE)))
+ {
+ 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", scores.entry[i].score, scores.entry[i].name);
+
+ fclose(file);
+
+ SetFilePermissions(filename, PERMS_PRIVATE);
+}
+#endif
+
+static void SaveScore_VERS(FILE *file, struct ScoreInfo *scores)
+{
+ putFileVersion(file, scores->file_version);
+ putFileVersion(file, scores->game_version);
+}
+
+static void SaveScore_INFO(FILE *file, struct ScoreInfo *scores)
+{
+ int level_identifier_size = strlen(scores->level_identifier) + 1;
+ int i;
+
+ putFile16BitBE(file, level_identifier_size);
+
+ for (i = 0; i < level_identifier_size; i++)
+ putFile8Bit(file, scores->level_identifier[i]);
+
+ putFile16BitBE(file, scores->level_nr);
+ putFile16BitBE(file, scores->num_entries);
+}
+
+static void SaveScore_NAME(FILE *file, struct ScoreInfo *scores)
+{
+ int i, j;
+
+ for (i = 0; i < scores->num_entries; i++)
+ {
+ int name_size = strlen(scores->entry[i].name);
+
+ for (j = 0; j < MAX_PLAYER_NAME_LEN; j++)
+ putFile8Bit(file, (j < name_size ? scores->entry[i].name[j] : 0));
+ }
+}
+
+static void SaveScore_SCOR(FILE *file, struct ScoreInfo *scores)
+{
+ int i;
+
+ for (i = 0; i < scores->num_entries; i++)
+ putFile16BitBE(file, scores->entry[i].score);
+}
+
+static void SaveScore_SC4R(FILE *file, struct ScoreInfo *scores)
+{
+ int i;
+
+ for (i = 0; i < scores->num_entries; i++)
+ putFile32BitBE(file, scores->entry[i].score);
+}
+
+static void SaveScore_TIME(FILE *file, struct ScoreInfo *scores)
+{
+ int i;
+
+ for (i = 0; i < scores->num_entries; i++)
+ putFile32BitBE(file, scores->entry[i].time);
+}
+
+static void SaveScore_TAPE(FILE *file, struct ScoreInfo *scores)
+{
+ int i, j;
+
+ for (i = 0; i < scores->num_entries; i++)
+ {
+ int size = strlen(scores->entry[i].tape_basename);
+
+ for (j = 0; j < MAX_SCORE_TAPE_BASENAME_LEN; j++)
+ putFile8Bit(file, (j < size ? scores->entry[i].tape_basename[j] : 0));
+ }
+}
+
+static void SaveScoreToFilename(char *filename)
+{
+ FILE *file;
+ int info_chunk_size;
+ int name_chunk_size;
+ int scor_chunk_size;
+ int sc4r_chunk_size;
+ int time_chunk_size;
+ int tape_chunk_size;
+ boolean has_large_score_values;
+ int i;
+
+ if (!(file = fopen(filename, MODE_WRITE)))
+ {
+ Warn("cannot save score file '%s'", filename);
+
+ return;
+ }
+
+ info_chunk_size = 2 + (strlen(scores.level_identifier) + 1) + 2 + 2;
+ name_chunk_size = scores.num_entries * MAX_PLAYER_NAME_LEN;
+ scor_chunk_size = scores.num_entries * 2;
+ sc4r_chunk_size = scores.num_entries * 4;
+ time_chunk_size = scores.num_entries * 4;
+ tape_chunk_size = scores.num_entries * MAX_SCORE_TAPE_BASENAME_LEN;
+
+ has_large_score_values = FALSE;
+ for (i = 0; i < scores.num_entries; i++)
+ if (scores.entry[i].score > 0xffff)
+ has_large_score_values = TRUE;
+
+ putFileChunkBE(file, "RND1", CHUNK_SIZE_UNDEFINED);
+ putFileChunkBE(file, "SCOR", CHUNK_SIZE_NONE);
+
+ putFileChunkBE(file, "VERS", SCORE_CHUNK_VERS_SIZE);
+ SaveScore_VERS(file, &scores);
+
+ putFileChunkBE(file, "INFO", info_chunk_size);
+ SaveScore_INFO(file, &scores);
+
+ putFileChunkBE(file, "NAME", name_chunk_size);
+ SaveScore_NAME(file, &scores);
+
+ if (has_large_score_values)
+ {
+ putFileChunkBE(file, "SC4R", sc4r_chunk_size);
+ SaveScore_SC4R(file, &scores);
+ }
+ else
+ {
+ putFileChunkBE(file, "SCOR", scor_chunk_size);
+ SaveScore_SCOR(file, &scores);
+ }
+
+ putFileChunkBE(file, "TIME", time_chunk_size);
+ SaveScore_TIME(file, &scores);
+
+ putFileChunkBE(file, "TAPE", tape_chunk_size);
+ SaveScore_TAPE(file, &scores);
+
+ fclose(file);
+
+ SetFilePermissions(filename, PERMS_PRIVATE);
+}
+
+void SaveScore(int nr)
+{
+ char *filename = getScoreFilename(nr);
+ int i;
+
+ // used instead of "leveldir_current->subdir" (for network games)
+ InitScoreDirectory(levelset.identifier);
+
+ scores.file_version = FILE_VERSION_ACTUAL;
+ scores.game_version = GAME_VERSION_ACTUAL;
+
+ strncpy(scores.level_identifier, levelset.identifier, MAX_FILENAME_LEN);
+ scores.level_identifier[MAX_FILENAME_LEN] = '\0';
+ scores.level_nr = level_nr;
+
+ for (i = 0; i < MAX_SCORE_ENTRIES; i++)
+ if (scores.entry[i].score == 0 &&
+ scores.entry[i].time == 0 &&
+ strEqual(scores.entry[i].name, EMPTY_PLAYER_NAME))
+ break;
+
+ scores.num_entries = i;
+
+ if (scores.num_entries == 0)
+ return;
+
+ SaveScoreToFilename(filename);