X-Git-Url: https://git.artsoft.org/?a=blobdiff_plain;f=src%2Ffiles.c;h=afccc4a6a295f795d24171be61cf6ad705246e5d;hb=cb7f26a8df21868e53ba10fbf8815cf402c6603a;hp=66109470dfc7ab57007b5db46b0a43a23adc1fdb;hpb=ef10463b42e40a7a4327cf0b2e2cf926160eac9e;p=rocksndiamonds.git diff --git a/src/files.c b/src/files.c index 66109470..afccc4a6 100644 --- a/src/files.c +++ b/src/files.c @@ -59,6 +59,9 @@ #define TAPE_CHUNK_VERS_SIZE 8 // size of file version chunk #define TAPE_CHUNK_HEAD_SIZE 20 // size of tape file header #define TAPE_CHUNK_HEAD_UNUSED 1 // unused tape header bytes +#define TAPE_CHUNK_SCRN_SIZE 2 // size of screen size chunk + +#define SCORE_CHUNK_VERS_SIZE 8 // size of file version chunk #define LEVEL_CHUNK_CNT3_SIZE(x) (LEVEL_CHUNK_CNT3_HEADER + (x)) #define LEVEL_CHUNK_CUS3_SIZE(x) (2 + (x) * LEVEL_CPART_CUS3_SIZE) @@ -67,7 +70,7 @@ // file identifier strings #define LEVEL_COOKIE_TMPL "ROCKSNDIAMONDS_LEVEL_FILE_VERSION_x.x" #define TAPE_COOKIE_TMPL "ROCKSNDIAMONDS_TAPE_FILE_VERSION_x.x" -#define SCORE_COOKIE "ROCKSNDIAMONDS_SCORE_FILE_VERSION_1.2" +#define SCORE_COOKIE_TMPL "ROCKSNDIAMONDS_SCORE_FILE_VERSION_x.x" // values for deciding when (not) to save configuration data #define SAVE_CONF_NEVER 0 @@ -257,6 +260,18 @@ static struct LevelFileConfigInfo chunk_config_INFO[] = &li.solved_by_one_player, FALSE }, + { + -1, -1, + TYPE_INTEGER, CONF_VALUE_8_BIT(12), + &li.time_score_base, 1 + }, + + { + -1, -1, + TYPE_BOOLEAN, CONF_VALUE_8_BIT(13), + &li.rate_time_over_score, FALSE + }, + { -1, -1, -1, -1, @@ -307,6 +322,16 @@ static struct LevelFileConfigInfo chunk_config_ELEM[] = TYPE_BOOLEAN, CONF_VALUE_8_BIT(15), &li.lazy_relocation, FALSE }, + { + EL_PLAYER_1, -1, + TYPE_BOOLEAN, CONF_VALUE_8_BIT(16), + &li.finish_dig_collect, TRUE + }, + { + EL_PLAYER_1, -1, + TYPE_BOOLEAN, CONF_VALUE_8_BIT(17), + &li.keep_walkable_ce, FALSE + }, // (these values are different for each player) { @@ -1063,6 +1088,18 @@ static struct LevelFileConfigInfo chunk_config_CUSX_base[] = &xx_ei.move_delay_random, 0, &yy_ei.move_delay_random }, + { + -1, -1, + TYPE_INTEGER, CONF_VALUE_16_BIT(16), + &xx_ei.step_delay_fixed, 0, + &yy_ei.step_delay_fixed + }, + { + -1, -1, + TYPE_INTEGER, CONF_VALUE_16_BIT(17), + &xx_ei.step_delay_random, 0, + &yy_ei.step_delay_random + }, { -1, -1, @@ -3123,7 +3160,7 @@ static int LoadLevel_MicroChunk(File *file, struct LevelFileConfigInfo *conf, value = getMappedElement(value); if (data_type == TYPE_BOOLEAN) - *(boolean *)(conf[i].value) = value; + *(boolean *)(conf[i].value) = (value ? TRUE : FALSE); else *(int *) (conf[i].value) = value; @@ -3605,7 +3642,7 @@ static void CopyNativeLevel_RND_to_EM(struct LevelInfo *level) // initialize player positions and delete players from the playfield for (y = 0; y < cav->height; y++) for (x = 0; x < cav->width; x++) { - if (ELEM_IS_PLAYER(level->field[x][y])) + if (IS_PLAYER_ELEMENT(level->field[x][y])) { int player_nr = GET_PLAYER_NR(level->field[x][y]); @@ -3710,6 +3747,9 @@ static void CopyNativeLevel_EM_to_RND(struct LevelInfo *level) if (jx != -1 && jy != -1) level->field[jx][jy] = EL_PLAYER_1 + nr; } + + // time score is counted for each 10 seconds left in Emerald Mine levels + level->time_score_base = 10; } @@ -3903,12 +3943,11 @@ static void CopyNativeLevel_SP_to_RND(struct LevelInfo *level) level->time_wheel = 0; level->amoeba_content = EL_EMPTY; -#if 1 - // original Supaplex does not use score values -- use default values -#else + // original Supaplex does not use score values -- rate by playing time for (i = 0; i < LEVEL_SCORE_ELEMENTS; i++) level->score[i] = 0; -#endif + + level->rate_time_over_score = TRUE; // there are no yamyams in supaplex levels for (i = 0; i < level->num_yamyam_contents; i++) @@ -5727,6 +5766,9 @@ static void LoadLevelFromFileStream_DC(File *file, struct LevelInfo *level, // Diamond Caves has the same (strange) behaviour as Emerald Mine that gems // can slip down from flat walls, like normal walls and steel walls level->em_slippery_gems = TRUE; + + // time score is counted for each 10 seconds left in Diamond Caves levels + level->time_score_base = 10; } static void LoadLevelFromFileInfo_DC(struct LevelInfo *level, @@ -5853,6 +5895,21 @@ int getMappedElement_SB(int element_ascii, boolean use_ces) return EL_UNDEFINED; } +static void SetLevelSettings_SB(struct LevelInfo *level) +{ + // time settings + level->time = 0; + level->use_step_counter = TRUE; + + // score settings + level->score[SC_TIME_BONUS] = 0; + level->time_score_base = 1; + level->rate_time_over_score = TRUE; + + // game settings + level->auto_exit_sokoban = TRUE; +} + static void LoadLevelFromFileInfo_SB(struct LevelInfo *level, struct LevelFileInfo *level_file_info, boolean level_info_only) @@ -6086,14 +6143,11 @@ static void LoadLevelFromFileInfo_SB(struct LevelInfo *level, } // set special level settings for Sokoban levels - - level->time = 0; - level->use_step_counter = TRUE; + SetLevelSettings_SB(level); if (load_xsb_to_ces) { // special global settings can now be set in level template - level->use_custom_template = TRUE; } } @@ -6250,7 +6304,7 @@ static void LoadLevel_InitVersion(struct LevelInfo *level) if (level->game_version < VERSION_IDENT(3,2,0,5)) { // time bonus score was given for 10 s instead of 1 s before 3.2.0-5 - level->score[SC_TIME_BONUS] /= 10; + level->time_score_base = 10; } if (leveldir_current->latest_engine) @@ -6422,6 +6476,38 @@ static void LoadLevel_InitVersion(struct LevelInfo *level) // only Sokoban fields (but not objects) had to be solved before 4.1.1.1 if (level->game_version < VERSION_IDENT(4,1,1,1)) level->sb_objects_needed = FALSE; + + // CE actions were triggered by unfinished digging/collecting up to 4.2.2.0 + if (level->game_version <= VERSION_IDENT(4,2,2,0)) + level->finish_dig_collect = FALSE; + + // CE changing to player was kept under the player if walkable up to 4.2.3.1 + if (level->game_version <= VERSION_IDENT(4,2,3,1)) + level->keep_walkable_ce = TRUE; +} + +static void LoadLevel_InitSettings_SB(struct LevelInfo *level) +{ + boolean is_sokoban_level = TRUE; // unless non-Sokoban elements found + int x, y; + + // check if this level is (not) a Sokoban level + for (y = 0; y < level->fieldy; y++) + for (x = 0; x < level->fieldx; x++) + if (!IS_SB_ELEMENT(Tile[x][y])) + is_sokoban_level = FALSE; + + if (is_sokoban_level) + { + // set special level settings for Sokoban levels + SetLevelSettings_SB(level); + } +} + +static void LoadLevel_InitSettings(struct LevelInfo *level) +{ + // adjust level settings for (non-native) Sokoban-style levels + LoadLevel_InitSettings_SB(level); } static void LoadLevel_InitStandardElements(struct LevelInfo *level) @@ -6569,6 +6655,27 @@ static void LoadLevel_InitCustomElements(struct LevelInfo *level) element_info[element].ignition_delay = 8; } } + + // set mouse click change events to work for left/middle/right mouse button + if (level->game_version < VERSION_IDENT(4,2,3,0)) + { + for (i = 0; i < NUM_CUSTOM_ELEMENTS; i++) + { + int element = EL_CUSTOM_START + i; + struct ElementInfo *ei = &element_info[element]; + + for (j = 0; j < ei->num_change_pages; j++) + { + struct ElementChangeInfo *change = &ei->change_page[j]; + + if (change->has_event[CE_CLICKED_BY_MOUSE] || + change->has_event[CE_PRESSED_BY_MOUSE] || + change->has_event[CE_MOUSE_CLICKED_ON_X] || + change->has_event[CE_MOUSE_PRESSED_ON_X]) + change->trigger_side = CH_SIDE_ANY; + } + } + } } static void LoadLevel_InitElements(struct LevelInfo *level) @@ -6629,6 +6736,7 @@ static void LoadLevelTemplate_LoadAndInit(void) LoadLevel_InitVersion(&level_template); LoadLevel_InitElements(&level_template); + LoadLevel_InitSettings(&level_template); ActivateLevelTemplate(); } @@ -6669,6 +6777,7 @@ static void LoadLevel_LoadAndInit(struct NetworkLevelInfo *network_level) LoadLevel_InitVersion(&level); LoadLevel_InitElements(&level); LoadLevel_InitPlayfield(&level); + LoadLevel_InitSettings(&level); LoadLevel_InitNativeEngines(&level); } @@ -7594,10 +7703,33 @@ void DumpLevel(struct LevelInfo *level) Print("SP player blocks last field: %s\n", (level->sp_block_last_field ? "yes" : "no")); Print("use spring bug: %s\n", (level->use_spring_bug ? "yes" : "no")); Print("use step counter: %s\n", (level->use_step_counter ? "yes" : "no")); + Print("rate time over score: %s\n", (level->rate_time_over_score ? "yes" : "no")); PrintLine("-", 79); } +void DumpLevels(void) +{ + static LevelDirTree *dumplevel_leveldir = NULL; + + dumplevel_leveldir = getTreeInfoFromIdentifier(leveldir_first, + global.dumplevel_leveldir); + + if (dumplevel_leveldir == NULL) + Fail("no such level identifier: '%s'", global.dumplevel_leveldir); + + if (global.dumplevel_level_nr < dumplevel_leveldir->first_level || + global.dumplevel_level_nr > dumplevel_leveldir->last_level) + Fail("no such level number: %d", global.dumplevel_level_nr); + + leveldir_current = dumplevel_leveldir; + + LoadLevel(global.dumplevel_level_nr); + DumpLevel(&level); + + CloseAllAndExit(0); +} + // ============================================================================ // tape file functions @@ -7628,6 +7760,10 @@ static void setTapeInfoToDefaults(void) tape.playing = FALSE; tape.pausing = FALSE; + tape.scr_fieldx = SCR_FIELDX_DEFAULT; + tape.scr_fieldy = SCR_FIELDY_DEFAULT; + + tape.no_info_chunk = TRUE; tape.no_valid_file = FALSE; } @@ -7718,18 +7854,33 @@ static int LoadTape_HEAD(File *file, int chunk_size, struct TapeInfo *tape) return chunk_size; } +static int LoadTape_SCRN(File *file, int chunk_size, struct TapeInfo *tape) +{ + tape->scr_fieldx = getFile8Bit(file); + tape->scr_fieldy = getFile8Bit(file); + + return chunk_size; +} + static int LoadTape_INFO(File *file, int chunk_size, struct TapeInfo *tape) { + char *level_identifier = NULL; int level_identifier_size; int i; + tape->no_info_chunk = FALSE; + level_identifier_size = getFile16BitBE(file); - tape->level_identifier = - checked_realloc(tape->level_identifier, level_identifier_size); + level_identifier = checked_malloc(level_identifier_size); for (i = 0; i < level_identifier_size; i++) - tape->level_identifier[i] = getFile8Bit(file); + level_identifier[i] = getFile8Bit(file); + + strncpy(tape->level_identifier, level_identifier, MAX_FILENAME_LEN); + tape->level_identifier[MAX_FILENAME_LEN] = '\0'; + + checked_free(level_identifier); tape->level_nr = getFile16BitBE(file); @@ -8008,6 +8159,7 @@ void LoadTapeFromFilename(char *filename) { { "VERS", TAPE_CHUNK_VERS_SIZE, LoadTape_VERS }, { "HEAD", TAPE_CHUNK_HEAD_SIZE, LoadTape_HEAD }, + { "SCRN", TAPE_CHUNK_SCRN_SIZE, LoadTape_SCRN }, { "INFO", -1, LoadTape_INFO }, { "BODY", -1, LoadTape_BODY }, { NULL, 0, NULL } @@ -8088,6 +8240,14 @@ void LoadSolutionTape(int nr) CopyNativeTape_SP_to_RND(&level); } +static boolean checkSaveTape_SCRN(struct TapeInfo *tape) +{ + // chunk required for team mode tapes with non-default screen size + return (tape->num_participating_players > 1 && + (tape->scr_fieldx != SCR_FIELDX_DEFAULT || + tape->scr_fieldy != SCR_FIELDY_DEFAULT)); +} + static void SaveTape_VERS(FILE *file, struct TapeInfo *tape) { putFileVersion(file, tape->file_version); @@ -8120,6 +8280,12 @@ static void SaveTape_HEAD(FILE *file, struct TapeInfo *tape) putFileVersion(file, tape->engine_version); } +static void SaveTape_SCRN(FILE *file, struct TapeInfo *tape) +{ + putFile8Bit(file, tape->scr_fieldx); + putFile8Bit(file, tape->scr_fieldy); +} + static void SaveTape_INFO(FILE *file, struct TapeInfo *tape) { int level_identifier_size = strlen(tape->level_identifier) + 1; @@ -8185,6 +8351,12 @@ void SaveTapeToFilename(char *filename) putFileChunkBE(file, "HEAD", TAPE_CHUNK_HEAD_SIZE); SaveTape_HEAD(file, &tape); + if (checkSaveTape_SCRN(&tape)) + { + putFileChunkBE(file, "SCRN", TAPE_CHUNK_SCRN_SIZE); + SaveTape_SCRN(file, &tape); + } + putFileChunkBE(file, "INFO", info_chunk_size); SaveTape_INFO(file, &tape); @@ -8196,13 +8368,10 @@ void SaveTapeToFilename(char *filename) SetFilePermissions(filename, PERMS_PRIVATE); } -void SaveTape(int nr) +static void SaveTapeExt(char *filename) { - char *filename = getTapeFilename(nr); int i; - InitTapeDirectory(leveldir_current->subdir); - tape.file_version = FILE_VERSION_ACTUAL; tape.game_version = GAME_VERSION_ACTUAL; @@ -8218,6 +8387,25 @@ void SaveTape(int nr) tape.changed = FALSE; } +void SaveTape(int nr) +{ + char *filename = getTapeFilename(nr); + + InitTapeDirectory(leveldir_current->subdir); + + SaveTapeExt(filename); +} + +void SaveScoreTape(int nr) +{ + char *filename = getScoreTapeFilename(tape.score_tape_basename, nr); + + // used instead of "leveldir_current->subdir" (for network games) + InitScoreTapeDirectory(levelset.identifier, nr); + + SaveTapeExt(filename); +} + static boolean SaveTapeCheckedExt(int nr, char *msg_replace, char *msg_saved, unsigned int req_state_added) { @@ -8262,11 +8450,34 @@ void DumpTape(struct TapeInfo *tape) } 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("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"); PrintLine("-", 79); tape_frame_counter = 0; @@ -8304,12 +8515,69 @@ void DumpTape(struct TapeInfo *tape) 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 // ============================================================================ -void LoadScore(int nr) +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->num_entries = 0; + scores->last_added = -1; + scores->last_added_local = -1; + + scores->updated = FALSE; + scores->uploaded = FALSE; + scores->force_last_added = FALSE; +} + +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); @@ -8318,13 +8586,6 @@ void LoadScore(int nr) 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; @@ -8334,7 +8595,7 @@ void LoadScore(int nr) if (strlen(cookie) > 0 && cookie[strlen(cookie) - 1] == '\n') cookie[strlen(cookie) - 1] = '\0'; - if (!checkCookieString(cookie, SCORE_COOKIE)) + if (!checkCookieString(cookie, SCORE_COOKIE_TMPL)) { Warn("unknown format of score file '%s'", filename); @@ -8345,7 +8606,7 @@ void LoadScore(int nr) for (i = 0; i < MAX_SCORE_ENTRIES; i++) { - if (fscanf(file, "%d", &highscore[i].Score) == EOF) + if (fscanf(file, "%d", &scores.entry[i].score) == EOF) Warn("fscanf() failed; %s", strerror(errno)); if (fgets(line, MAX_LINE_LEN, file) == NULL) @@ -8358,8 +8619,8 @@ void LoadScore(int nr) { 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'; + strncpy(scores.entry[i].name, line_ptr, MAX_PLAYER_NAME_LEN); + scores.entry[i].name[MAX_PLAYER_NAME_LEN] = '\0'; break; } } @@ -8368,58 +8629,1113 @@ void LoadScore(int nr) fclose(file); } -void SaveScore(int nr) +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; - int permissions = (program.global_scores ? PERMS_PUBLIC : PERMS_PRIVATE); - char *filename = getScoreFilename(nr); - FILE *file; - // used instead of "leveldir_current->subdir" (for network games) - InitScoreDirectory(levelset.identifier); + for (i = 0; i < MAX_SCORE_ENTRIES; i++) + { + int score = scores.entry[i].score; - if (!(file = fopen(filename, MODE_WRITE))) + 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++) { - Warn("cannot save score for level %d", nr); + for (j = 0; j < MAX_PLAYER_NAME_LEN; j++) + scores->entry[i].name[j] = getFile8Bit(file); - return; + scores->entry[i].name[MAX_PLAYER_NAME_LEN] = '\0'; } - fprintf(file, "%s\n\n", SCORE_COOKIE); + chunk_size = scores->num_entries * MAX_PLAYER_NAME_LEN; - for (i = 0; i < MAX_SCORE_ENTRIES; i++) - fprintf(file, "%d %s\n", highscore[i].Score, highscore[i].Name); + return chunk_size; +} - fclose(file); +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; - SetFilePermissions(filename, permissions); + return chunk_size; } +static int LoadScore_TIME(File *file, int chunk_size, struct ScoreInfo *scores) +{ + int i; -// ============================================================================ -// setup file functions -// ============================================================================ + for (i = 0; i < scores->num_entries; i++) + scores->entry[i].time = getFile32BitBE(file); -#define TOKEN_STR_PLAYER_PREFIX "player_" + chunk_size = scores->num_entries * 4; + return chunk_size; +} -static struct TokenInfo global_setup_tokens[] = +static int LoadScore_TAPE(File *file, int chunk_size, struct ScoreInfo *scores) { + int i, j; + + for (i = 0; i < scores->num_entries; i++) { - TYPE_STRING, - &setup.player_name, "player_name" - }, - { - TYPE_SWITCH, - &setup.sound, "sound" - }, + 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")) { - TYPE_SWITCH, - &setup.sound_loops, "repeating_sound_loops" - }, + 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 { - TYPE_SWITCH, - &setup.sound_music, "background_music" + 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 }, + { "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_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 time_chunk_size; + int tape_chunk_size; + + 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; + time_chunk_size = scores.num_entries * 4; + tape_chunk_size = scores.num_entries * MAX_SCORE_TAPE_BASENAME_LEN; + + 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); + + 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); +} + +void ExecuteAsThread(SDL_ThreadFunction function, char *name, void *data, + char *error) +{ +#if defined(PLATFORM_EMSCRIPTEN) + // threads currently not fully supported by Emscripten/SDL and some browsers + function(data); +#else + SDL_Thread *thread = SDL_CreateThread(function, name, data); + + if (thread != NULL) + SDL_DetachThread(thread); + else + Error("Cannot create thread to %s!", error); + + // nasty kludge to lower probability of intermingled thread error messages + Delay(1); +#endif +} + +char *getPasswordJSON(char *password) +{ + static char password_json[MAX_FILENAME_LEN] = ""; + static boolean initialized = FALSE; + + if (!initialized) + { + if (password != NULL && + !strEqual(password, "") && + !strEqual(password, UNDEFINED_PASSWORD)) + snprintf(password_json, MAX_FILENAME_LEN, + " \"password\": \"%s\",\n", + setup.api_server_password); + + initialized = TRUE; + } + + return password_json; +} + +struct ApiGetScoreThreadData +{ + int level_nr; + char *score_cache_filename; +}; + +static void *CreateThreadData_ApiGetScore(int nr) +{ + struct ApiGetScoreThreadData *data = + checked_malloc(sizeof(struct ApiGetScoreThreadData)); + char *score_cache_filename = getScoreCacheFilename(nr); + + data->level_nr = nr; + data->score_cache_filename = getStringCopy(score_cache_filename); + + return data; +} + +static void FreeThreadData_ApiGetScore(void *data_raw) +{ + struct ApiGetScoreThreadData *data = data_raw; + + checked_free(data->score_cache_filename); + checked_free(data); +} + +static boolean SetRequest_ApiGetScore(struct HttpRequest *request, + void *data_raw) +{ + struct ApiGetScoreThreadData *data = data_raw; + int level_nr = data->level_nr; + + request->hostname = setup.api_server_hostname; + request->port = API_SERVER_PORT; + request->method = API_SERVER_METHOD; + request->uri = API_SERVER_URI_GET; + + snprintf(request->body, MAX_HTTP_BODY_SIZE, + "{\n" + "%s" + " \"game_version\": \"%s\",\n" + " \"game_platform\": \"%s\",\n" + " \"levelset_identifier\": \"%s\",\n" + " \"level_nr\": \"%d\"\n" + "}\n", + getPasswordJSON(setup.api_server_password), + getProgramRealVersionString(), + getProgramPlatformString(), + levelset.identifier, + level_nr); + + ConvertHttpRequestBodyToServerEncoding(request); + + return TRUE; +} + +static void HandleResponse_ApiGetScore(struct HttpResponse *response, + void *data_raw) +{ + struct ApiGetScoreThreadData *data = data_raw; + + if (response->body_size == 0) + { + // no scores available for this level + + return; + } + + ConvertHttpResponseBodyToClientEncoding(response); + + char *filename = data->score_cache_filename; + FILE *file; + int i; + + // used instead of "leveldir_current->subdir" (for network games) + InitScoreCacheDirectory(levelset.identifier); + + if (!(file = fopen(filename, MODE_WRITE))) + { + Warn("cannot save score cache file '%s'", filename); + + return; + } + + for (i = 0; i < response->body_size; i++) + fputc(response->body[i], file); + + fclose(file); + + SetFilePermissions(filename, PERMS_PRIVATE); + + server_scores.updated = TRUE; +} + +#if defined(PLATFORM_EMSCRIPTEN) +static void Emscripten_ApiGetScore_Loaded(unsigned handle, void *data_raw, + void *buffer, unsigned int size) +{ + struct HttpResponse *response = GetHttpResponseFromBuffer(buffer, size); + + if (response != NULL) + { + HandleResponse_ApiGetScore(response, data_raw); + + checked_free(response); + } + else + { + Error("server response too large to handle (%d bytes)", size); + } + + FreeThreadData_ApiGetScore(data_raw); +} + +static void Emscripten_ApiGetScore_Failed(unsigned handle, void *data_raw, + int code, const char *status) +{ + Error("server failed to handle request: %d %s", code, status); + + FreeThreadData_ApiGetScore(data_raw); +} + +static void Emscripten_ApiGetScore_Progress(unsigned handle, void *data_raw, + int bytes, int size) +{ + // nothing to do here +} + +static void Emscripten_ApiGetScore_HttpRequest(struct HttpRequest *request, + void *data_raw) +{ + if (!SetRequest_ApiGetScore(request, data_raw)) + { + FreeThreadData_ApiGetScore(data_raw); + + return; + } + + emscripten_async_wget2_data(request->uri, + request->method, + request->body, + data_raw, + TRUE, + Emscripten_ApiGetScore_Loaded, + Emscripten_ApiGetScore_Failed, + Emscripten_ApiGetScore_Progress); +} + +#else + +static void ApiGetScore_HttpRequestExt(struct HttpRequest *request, + struct HttpResponse *response, + void *data_raw) +{ + if (!SetRequest_ApiGetScore(request, data_raw)) + return; + + if (!DoHttpRequest(request, response)) + { + Error("HTTP request failed: %s", GetHttpError()); + + return; + } + + if (!HTTP_SUCCESS(response->status_code)) + { + Error("server failed to handle request: %d %s", + response->status_code, + response->status_text); + + return; + } + + HandleResponse_ApiGetScore(response, data_raw); +} + +static void ApiGetScore_HttpRequest(struct HttpRequest *request, + struct HttpResponse *response, + void *data_raw) +{ + ApiGetScore_HttpRequestExt(request, response, data_raw); + + FreeThreadData_ApiGetScore(data_raw); +} +#endif + +static int ApiGetScoreThread(void *data_raw) +{ + struct HttpRequest *request = checked_calloc(sizeof(struct HttpRequest)); + struct HttpResponse *response = checked_calloc(sizeof(struct HttpResponse)); + +#if defined(PLATFORM_EMSCRIPTEN) + Emscripten_ApiGetScore_HttpRequest(request, data_raw); +#else + ApiGetScore_HttpRequest(request, response, data_raw); +#endif + + checked_free(request); + checked_free(response); + + return 0; +} + +static void ApiGetScoreAsThread(int nr) +{ + struct ApiGetScoreThreadData *data = CreateThreadData_ApiGetScore(nr); + + ExecuteAsThread(ApiGetScoreThread, + "ApiGetScore", data, + "download scores from server"); +} + +static void LoadServerScoreFromCache(int nr) +{ + struct ScoreEntry score_entry; + struct + { + void *value; + boolean is_string; + int string_size; + } + score_mapping[] = + { + { &score_entry.score, FALSE, 0 }, + { &score_entry.time, FALSE, 0 }, + { score_entry.name, TRUE, MAX_PLAYER_NAME_LEN }, + { score_entry.tape_basename, TRUE, MAX_FILENAME_LEN }, + + { NULL, FALSE, 0 } + }; + char *filename = getScoreCacheFilename(nr); + SetupFileHash *score_hash = loadSetupFileHash(filename); + int i, j; + + server_scores.num_entries = 0; + + if (score_hash == NULL) + return; + + for (i = 0; i < MAX_SCORE_ENTRIES; i++) + { + score_entry = server_scores.entry[i]; + + for (j = 0; score_mapping[j].value != NULL; j++) + { + char token[10]; + + sprintf(token, "%02d.%d", i, j); + + char *value = getHashEntry(score_hash, token); + + if (value == NULL) + continue; + + if (score_mapping[j].is_string) + { + char *score_value = (char *)score_mapping[j].value; + int value_size = score_mapping[j].string_size; + + strncpy(score_value, value, value_size); + score_value[value_size] = '\0'; + } + else + { + int *score_value = (int *)score_mapping[j].value; + + *score_value = atoi(value); + } + + server_scores.num_entries = i + 1; + } + + server_scores.entry[i] = score_entry; + } + + freeSetupFileHash(score_hash); +} + +void LoadServerScore(int nr, boolean download_score) +{ + if (!setup.use_api_server) + return; + + // always start with reliable default values + setServerScoreInfoToDefaults(); + + // 1st step: load server scores from cache file (which may not exist) + // (this should prevent reading it while the thread is writing to it) + LoadServerScoreFromCache(nr); + + if (download_score && runtime.use_api_server) + { + // 2nd step: download server scores from score server to cache file + // (as thread, as it might time out if the server is not reachable) + ApiGetScoreAsThread(nr); + } +} + +static char *get_file_base64(char *filename) +{ + struct stat file_status; + + if (stat(filename, &file_status) != 0) + { + Error("cannot stat file '%s'", filename); + + return NULL; + } + + int buffer_size = file_status.st_size; + byte *buffer = checked_malloc(buffer_size); + FILE *file; + int i; + + if (!(file = fopen(filename, MODE_READ))) + { + Error("cannot open file '%s'", filename); + + checked_free(buffer); + + return NULL; + } + + for (i = 0; i < buffer_size; i++) + { + int c = fgetc(file); + + if (c == EOF) + { + Error("cannot read from input file '%s'", filename); + + fclose(file); + checked_free(buffer); + + return NULL; + } + + buffer[i] = (byte)c; + } + + fclose(file); + + int buffer_encoded_size = base64_encoded_size(buffer_size); + char *buffer_encoded = checked_malloc(buffer_encoded_size); + + base64_encode(buffer_encoded, buffer, buffer_size); + + checked_free(buffer); + + return buffer_encoded; +} + +struct ApiAddScoreThreadData +{ + int level_nr; + char *score_tape_filename; + struct ScoreEntry score_entry; +}; + +static void *CreateThreadData_ApiAddScore(int nr, char *score_tape_filename) +{ + struct ApiAddScoreThreadData *data = + checked_malloc(sizeof(struct ApiAddScoreThreadData)); + struct ScoreEntry *score_entry = &scores.entry[scores.last_added]; + + if (score_tape_filename == NULL) + score_tape_filename = getScoreTapeFilename(score_entry->tape_basename, nr); + + data->level_nr = nr; + data->score_entry = *score_entry; + data->score_tape_filename = getStringCopy(score_tape_filename); + + return data; +} + +static void FreeThreadData_ApiAddScore(void *data_raw) +{ + struct ApiAddScoreThreadData *data = data_raw; + + checked_free(data->score_tape_filename); + checked_free(data); +} + +static boolean SetRequest_ApiAddScore(struct HttpRequest *request, + void *data_raw) +{ + struct ApiAddScoreThreadData *data = data_raw; + struct ScoreEntry *score_entry = &data->score_entry; + char *score_tape_filename = data->score_tape_filename; + int level_nr = data->level_nr; + + request->hostname = setup.api_server_hostname; + request->port = API_SERVER_PORT; + request->method = API_SERVER_METHOD; + request->uri = API_SERVER_URI_ADD; + + char *tape_base64 = get_file_base64(score_tape_filename); + + if (tape_base64 == NULL) + { + Error("loading and base64 encoding score tape file failed"); + + return FALSE; + } + + char *player_name_raw = score_entry->name; + char *player_uuid_raw = setup.player_uuid; + + if (options.player_name != NULL && global.autoplay_leveldir != NULL) + { + player_name_raw = options.player_name; + player_uuid_raw = ""; + } + + char *levelset_identifier = getEscapedJSON(leveldir_current->identifier); + char *levelset_name = getEscapedJSON(leveldir_current->name); + char *levelset_author = getEscapedJSON(leveldir_current->author); + char *level_name = getEscapedJSON(level.name); + char *level_author = getEscapedJSON(level.author); + char *player_name = getEscapedJSON(player_name_raw); + char *player_uuid = getEscapedJSON(player_uuid_raw); + + snprintf(request->body, MAX_HTTP_BODY_SIZE, + "{\n" + "%s" + " \"game_version\": \"%s\",\n" + " \"game_platform\": \"%s\",\n" + " \"batch_time\": \"%d\",\n" + " \"levelset_identifier\": \"%s\",\n" + " \"levelset_name\": \"%s\",\n" + " \"levelset_author\": \"%s\",\n" + " \"levelset_num_levels\": \"%d\",\n" + " \"levelset_first_level\": \"%d\",\n" + " \"level_nr\": \"%d\",\n" + " \"level_name\": \"%s\",\n" + " \"level_author\": \"%s\",\n" + " \"rate_time_over_score\": \"%d\",\n" + " \"player_name\": \"%s\",\n" + " \"player_uuid\": \"%s\",\n" + " \"score\": \"%d\",\n" + " \"time\": \"%d\",\n" + " \"tape_basename\": \"%s\",\n" + " \"tape\": \"%s\"\n" + "}\n", + getPasswordJSON(setup.api_server_password), + getProgramRealVersionString(), + getProgramPlatformString(), + (int)global.autoplay_time, + levelset_identifier, + levelset_name, + levelset_author, + leveldir_current->levels, + leveldir_current->first_level, + level_nr, + level_name, + level_author, + level.rate_time_over_score, + player_name, + player_uuid, + score_entry->score, + score_entry->time, + score_entry->tape_basename, + tape_base64); + + checked_free(tape_base64); + + checked_free(levelset_identifier); + checked_free(levelset_name); + checked_free(levelset_author); + checked_free(level_name); + checked_free(level_author); + checked_free(player_name); + checked_free(player_uuid); + + ConvertHttpRequestBodyToServerEncoding(request); + + return TRUE; +} + +static void HandleResponse_ApiAddScore(struct HttpResponse *response, + void *data_raw) +{ + server_scores.uploaded = TRUE; +} + +#if defined(PLATFORM_EMSCRIPTEN) +static void Emscripten_ApiAddScore_Loaded(unsigned handle, void *data_raw, + void *buffer, unsigned int size) +{ + struct HttpResponse *response = GetHttpResponseFromBuffer(buffer, size); + + if (response != NULL) + { + HandleResponse_ApiAddScore(response, data_raw); + + checked_free(response); + } + else + { + Error("server response too large to handle (%d bytes)", size); + } + + FreeThreadData_ApiAddScore(data_raw); +} + +static void Emscripten_ApiAddScore_Failed(unsigned handle, void *data_raw, + int code, const char *status) +{ + Error("server failed to handle request: %d %s", code, status); + + FreeThreadData_ApiAddScore(data_raw); +} + +static void Emscripten_ApiAddScore_Progress(unsigned handle, void *data_raw, + int bytes, int size) +{ + // nothing to do here +} + +static void Emscripten_ApiAddScore_HttpRequest(struct HttpRequest *request, + void *data_raw) +{ + if (!SetRequest_ApiAddScore(request, data_raw)) + { + FreeThreadData_ApiAddScore(data_raw); + + return; + } + + emscripten_async_wget2_data(request->uri, + request->method, + request->body, + data_raw, + TRUE, + Emscripten_ApiAddScore_Loaded, + Emscripten_ApiAddScore_Failed, + Emscripten_ApiAddScore_Progress); +} + +#else + +static void ApiAddScore_HttpRequestExt(struct HttpRequest *request, + struct HttpResponse *response, + void *data_raw) +{ + if (!SetRequest_ApiAddScore(request, data_raw)) + return; + + if (!DoHttpRequest(request, response)) + { + Error("HTTP request failed: %s", GetHttpError()); + + return; + } + + if (!HTTP_SUCCESS(response->status_code)) + { + Error("server failed to handle request: %d %s", + response->status_code, + response->status_text); + + return; + } + + HandleResponse_ApiAddScore(response, data_raw); +} + +static void ApiAddScore_HttpRequest(struct HttpRequest *request, + struct HttpResponse *response, + void *data_raw) +{ + ApiAddScore_HttpRequestExt(request, response, data_raw); + + FreeThreadData_ApiAddScore(data_raw); +} +#endif + +static int ApiAddScoreThread(void *data_raw) +{ + struct HttpRequest *request = checked_calloc(sizeof(struct HttpRequest)); + struct HttpResponse *response = checked_calloc(sizeof(struct HttpResponse)); + +#if defined(PLATFORM_EMSCRIPTEN) + Emscripten_ApiAddScore_HttpRequest(request, data_raw); +#else + ApiAddScore_HttpRequest(request, response, data_raw); +#endif + + checked_free(request); + checked_free(response); + + return 0; +} + +static void ApiAddScoreAsThread(int nr, char *score_tape_filename) +{ + struct ApiAddScoreThreadData *data = + CreateThreadData_ApiAddScore(nr, score_tape_filename); + + ExecuteAsThread(ApiAddScoreThread, + "ApiAddScore", data, + "upload score to server"); +} + +void SaveServerScore(int nr) +{ + if (!runtime.use_api_server) + return; + + ApiAddScoreAsThread(nr, NULL); +} + +void SaveServerScoreFromFile(int nr, char *score_tape_filename) +{ + if (!runtime.use_api_server) + return; + + ApiAddScoreAsThread(nr, score_tape_filename); +} + +void LoadLocalAndServerScore(int nr, boolean download_score) +{ + int last_added_local = scores.last_added_local; + + LoadScore(nr); + + // restore last added local score entry (before merging server scores) + scores.last_added = scores.last_added_local = last_added_local; + + if (setup.use_api_server && !setup.only_show_local_scores) + { + // load server scores from cache file and trigger update from server + LoadServerScore(nr, download_score); + + // merge local scores with scores from server + MergeServerScore(); + } +} + + +// ============================================================================ +// setup file functions +// ============================================================================ + +#define TOKEN_STR_PLAYER_PREFIX "player_" + + +static struct TokenInfo global_setup_tokens[] = +{ + { + TYPE_STRING, + &setup.player_name, "player_name" + }, + { + TYPE_SWITCH, + &setup.multiple_users, "multiple_users" + }, + { + TYPE_SWITCH, + &setup.sound, "sound" + }, + { + TYPE_SWITCH, + &setup.sound_loops, "repeating_sound_loops" + }, + { + TYPE_SWITCH, + &setup.sound_music, "background_music" }, { TYPE_SWITCH, @@ -8487,7 +9803,11 @@ static struct TokenInfo global_setup_tokens[] = }, { TYPE_SWITCH, - &setup.skip_scores_after_game, "skip_scores_after_game" + &setup.count_score_after_game, "count_score_after_game" + }, + { + TYPE_SWITCH, + &setup.show_scores_after_game, "show_scores_after_game" }, { TYPE_SWITCH, @@ -8525,6 +9845,14 @@ static struct TokenInfo global_setup_tokens[] = TYPE_SWITCH, &setup.ask_on_game_over, "ask_on_game_over" }, + { + TYPE_SWITCH, + &setup.ask_on_quit_game, "ask_on_quit_game" + }, + { + TYPE_SWITCH, + &setup.ask_on_quit_program, "ask_on_quit_program" + }, { TYPE_SWITCH, &setup.quick_switch, "quick_player_switch" @@ -8541,6 +9869,10 @@ static struct TokenInfo global_setup_tokens[] = TYPE_SWITCH, &setup.prefer_lowpass_sounds, "prefer_lowpass_sounds" }, + { + TYPE_SWITCH, + &setup.prefer_extra_panel_items, "prefer_extra_panel_items" + }, { TYPE_SWITCH, &setup.game_speed_extended, "game_speed_extended" @@ -8559,7 +9891,15 @@ static struct TokenInfo global_setup_tokens[] = }, { TYPE_SWITCH, - &setup.show_snapshot_buttons, "show_snapshot_buttons" + &setup.show_load_save_buttons, "show_load_save_buttons" + }, + { + TYPE_SWITCH, + &setup.show_undo_redo_buttons, "show_undo_redo_buttons" + }, + { + TYPE_SWITCH, + &setup.only_show_local_scores, "only_show_local_scores" }, { TYPE_STRING, @@ -8659,6 +9999,34 @@ static struct TokenInfo auto_setup_tokens[] = }, }; +static struct TokenInfo server_setup_tokens[] = +{ + { + TYPE_STRING, + &setup.player_uuid, "player_uuid" + }, + { + TYPE_SWITCH, + &setup.use_api_server, TEST_PREFIX "use_api_server" + }, + { + TYPE_STRING, + &setup.api_server_hostname, TEST_PREFIX "api_server_hostname" + }, + { + TYPE_STRING, + &setup.api_server_password, TEST_PREFIX "api_server_password" + }, + { + TYPE_SWITCH, + &setup.ask_for_uploading_tapes, TEST_PREFIX "ask_for_uploading_tapes" + }, + { + TYPE_SWITCH, + &setup.provide_uploading_tapes, TEST_PREFIX "provide_uploading_tapes" + }, +}; + static struct TokenInfo editor_setup_tokens[] = { { @@ -8685,6 +10053,10 @@ static struct TokenInfo editor_setup_tokens[] = TYPE_SWITCH, &setup.editor.show_element_token, "editor.show_element_token" }, + { + TYPE_SWITCH, + &setup.editor.show_read_only_warning, "editor.show_read_only_warning" + }, }; static struct TokenInfo editor_cascade_setup_tokens[] = @@ -9171,26 +10543,13 @@ static struct TokenInfo options_setup_tokens[] = }, }; -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->player_name = getStringCopy(getDefaultUserName(user.nr)); + + si->multiple_users = TRUE; si->sound = TRUE; si->sound_loops = TRUE; @@ -9211,7 +10570,8 @@ static void setSetupInfoToDefaults(struct SetupInfo *si) si->skip_levels = TRUE; si->increment_levels = TRUE; si->auto_play_next_level = TRUE; - si->skip_scores_after_game = FALSE; + si->count_score_after_game = TRUE; + si->show_scores_after_game = TRUE; si->time_limit = TRUE; si->fullscreen = FALSE; si->window_scaling_percent = STD_WINDOW_SCALING_PERCENT; @@ -9221,15 +10581,20 @@ static void setSetupInfoToDefaults(struct SetupInfo *si) si->ask_on_escape = TRUE; si->ask_on_escape_editor = TRUE; si->ask_on_game_over = TRUE; + si->ask_on_quit_game = TRUE; + si->ask_on_quit_program = TRUE; si->quick_switch = FALSE; si->input_on_focus = FALSE; si->prefer_aga_graphics = TRUE; si->prefer_lowpass_sounds = FALSE; + si->prefer_extra_panel_items = TRUE; si->game_speed_extended = FALSE; si->game_frame_delay = GAME_FRAME_DELAY; si->sp_show_border_elements = FALSE; si->small_game_graphics = FALSE; - si->show_snapshot_buttons = FALSE; + si->show_load_save_buttons = FALSE; + si->show_undo_redo_buttons = FALSE; + si->only_show_local_scores = FALSE; si->graphics_set = getStringCopy(GFX_CLASSIC_SUBDIR); si->sounds_set = getStringCopy(SND_CLASSIC_SUBDIR); @@ -9328,6 +10693,8 @@ static void setSetupInfoToDefaults(struct SetupInfo *si) si->editor.show_element_token = FALSE; + si->editor.show_read_only_warning = TRUE; + si->editor.use_template_for_new_levels = TRUE; si->shortcut.save_game = DEFAULT_KEY_SAVE_GAME; @@ -9451,6 +10818,17 @@ static void setSetupInfoToDefaults_AutoSetup(struct SetupInfo *si) si->auto_setup.editor_zoom_tilesize = MINI_TILESIZE; } +static void setSetupInfoToDefaults_ServerSetup(struct SetupInfo *si) +{ + si->player_uuid = NULL; // (will be set later) + + si->use_api_server = TRUE; + si->api_server_hostname = getStringCopy(API_SERVER_HOSTNAME); + si->api_server_password = getStringCopy(UNDEFINED_PASSWORD); + si->ask_for_uploading_tapes = TRUE; + si->provide_uploading_tapes = TRUE; +} + static void setSetupInfoToDefaults_EditorCascade(struct SetupInfo *si) { si->editor_cascade.el_bd = TRUE; @@ -9538,7 +10916,7 @@ static void setSetupInfoFromTokenInfo(SetupFileHash *setup_file_hash, token_info[token_nr].text); } -static void decodeSetupFileHash(SetupFileHash *setup_file_hash) +static void decodeSetupFileHash_Default(SetupFileHash *setup_file_hash) { int i, pnr; @@ -9638,6 +11016,19 @@ static void decodeSetupFileHash_AutoSetup(SetupFileHash *setup_file_hash) auto_setup_tokens[i].text)); } +static void decodeSetupFileHash_ServerSetup(SetupFileHash *setup_file_hash) +{ + int i; + + if (!setup_file_hash) + return; + + for (i = 0; i < ARRAY_SIZE(server_setup_tokens); i++) + setSetupInfo(server_setup_tokens, i, + getHashEntry(setup_file_hash, + server_setup_tokens[i].text)); +} + static void decodeSetupFileHash_EditorCascade(SetupFileHash *setup_file_hash) { int i; @@ -9651,13 +11042,50 @@ static void decodeSetupFileHash_EditorCascade(SetupFileHash *setup_file_hash) editor_cascade_setup_tokens[i].text)); } +void LoadUserNames(void) +{ + int last_user_nr = user.nr; + int i; + + if (global.user_names != NULL) + { + for (i = 0; i < MAX_PLAYER_NAMES; i++) + checked_free(global.user_names[i]); + + checked_free(global.user_names); + } + + global.user_names = checked_calloc(MAX_PLAYER_NAMES * sizeof(char *)); + + for (i = 0; i < MAX_PLAYER_NAMES; i++) + { + user.nr = i; + + SetupFileHash *setup_file_hash = loadSetupFileHash(getSetupFilename()); + + if (setup_file_hash) + { + char *player_name = getHashEntry(setup_file_hash, "player_name"); + + global.user_names[i] = getFixedUserName(player_name); + + freeSetupFileHash(setup_file_hash); + } + + if (global.user_names[i] == NULL) + global.user_names[i] = getStringCopy(getDefaultUserName(i)); + } + + user.nr = last_user_nr; +} + void LoadSetupFromFilename(char *filename) { SetupFileHash *setup_file_hash = loadSetupFileHash(filename); if (setup_file_hash) { - decodeSetupFileHash(setup_file_hash); + decodeSetupFileHash_Default(setup_file_hash); freeSetupFileHash(setup_file_hash); } @@ -9672,7 +11100,7 @@ static void LoadSetup_SpecialPostProcessing(void) char *player_name_new; // needed to work around problems with fixed length strings - player_name_new = get_corrected_login_name(setup.player_name); + player_name_new = getFixedUserName(setup.player_name); free(setup.player_name); setup.player_name = player_name_new; @@ -9688,7 +11116,7 @@ static void LoadSetup_SpecialPostProcessing(void) MIN(MAX(MIN_SCROLL_DELAY, setup.scroll_delay_value), MAX_SCROLL_DELAY); } -void LoadSetup(void) +void LoadSetup_Default(void) { char *filename; @@ -9729,6 +11157,34 @@ void LoadSetup_AutoSetup(void) free(filename); } +void LoadSetup_ServerSetup(void) +{ + char *filename = getPath2(getSetupDir(), SERVERSETUP_FILENAME); + SetupFileHash *setup_file_hash = NULL; + + // always start with reliable default values + setSetupInfoToDefaults_ServerSetup(&setup); + + setup_file_hash = loadSetupFileHash(filename); + + if (setup_file_hash) + { + decodeSetupFileHash_ServerSetup(setup_file_hash); + + freeSetupFileHash(setup_file_hash); + } + + free(filename); + + if (setup.player_uuid == NULL) + { + // player UUID does not yet exist in setup file + setup.player_uuid = getStringCopy(getUUID()); + + SaveSetup_ServerSetup(); + } +} + void LoadSetup_EditorCascade(void) { char *filename = getPath2(getSetupDir(), EDITORCASCADE_FILENAME); @@ -9749,6 +11205,14 @@ void LoadSetup_EditorCascade(void) free(filename); } +void LoadSetup(void) +{ + LoadSetup_Default(); + LoadSetup_AutoSetup(); + LoadSetup_ServerSetup(); + LoadSetup_EditorCascade(); +} + static void addGameControllerMappingToHash(SetupFileHash *mappings_hash, char *mapping_line) { @@ -9798,7 +11262,7 @@ static void LoadSetup_ReadGameControllerMappings(SetupFileHash *mappings_hash, fclose(file); } -void SaveSetup(void) +void SaveSetup_Default(void) { char *filename = getSetupFilename(); FILE *file; @@ -9818,7 +11282,8 @@ void SaveSetup(void) for (i = 0; i < ARRAY_SIZE(global_setup_tokens); i++) { // just to make things nicer :) - if (global_setup_tokens[i].value == &setup.sound || + if (global_setup_tokens[i].value == &setup.multiple_users || + global_setup_tokens[i].value == &setup.sound || global_setup_tokens[i].value == &setup.graphics_set || global_setup_tokens[i].value == &setup.volume_simple || global_setup_tokens[i].value == &setup.network_mode || @@ -9928,6 +11393,41 @@ void SaveSetup_AutoSetup(void) free(filename); } +void SaveSetup_ServerSetup(void) +{ + char *filename = getPath2(getSetupDir(), SERVERSETUP_FILENAME); + FILE *file; + int i; + + InitUserDataDirectory(); + + if (!(file = fopen(filename, MODE_WRITE))) + { + Warn("cannot write server setup file '%s'", filename); + + free(filename); + + return; + } + + fprintFileHeader(file, SERVERSETUP_FILENAME); + + for (i = 0; i < ARRAY_SIZE(server_setup_tokens); i++) + { + // just to make things nicer :) + if (server_setup_tokens[i].value == &setup.use_api_server) + fprintf(file, "\n"); + + fprintf(file, "%s\n", getSetupLine(server_setup_tokens, "", i)); + } + + fclose(file); + + SetFilePermissions(filename, PERMS_PRIVATE); + + free(filename); +} + void SaveSetup_EditorCascade(void) { char *filename = getPath2(getSetupDir(), EDITORCASCADE_FILENAME); @@ -9957,6 +11457,14 @@ void SaveSetup_EditorCascade(void) free(filename); } +void SaveSetup(void) +{ + SaveSetup_Default(); + SaveSetup_AutoSetup(); + SaveSetup_ServerSetup(); + SaveSetup_EditorCascade(); +} + static void SaveSetup_WriteGameControllerMappings(SetupFileHash *mappings_hash, char *filename) { @@ -10421,6 +11929,9 @@ int get_parameter_value(char *value_raw, char *suffix, int type) if (string_has_parameter(value, "reverse")) result |= STYLE_REVERSE; + if (string_has_parameter(value, "leftmost_position")) + result |= STYLE_LEFTMOST_POSITION; + if (string_has_parameter(value, "block_clicks")) result |= STYLE_BLOCK; @@ -12003,6 +13514,11 @@ void ConvertLevels(void) Print("converting level ... "); +#if 0 + // special case: conversion of some EMC levels as requested by ACME + level.game_engine_type = GAME_ENGINE_TYPE_RND; +#endif + level_filename = getDefaultLevelFilename(level_nr); new_level = !fileExists(level_filename); @@ -12080,8 +13596,8 @@ void CreateLevelSketchImages(void) sprintf(basename1, "%04d.bmp", i); sprintf(basename2, "%04ds.bmp", i); - filename1 = getPath2(global.create_images_dir, basename1); - filename2 = getPath2(global.create_images_dir, basename2); + filename1 = getPath2(global.create_sketch_images_dir, basename1); + filename2 = getPath2(global.create_sketch_images_dir, basename2); DrawSizedElement(0, 0, element, TILESIZE); BlitBitmap(drawto, bitmap1, SX, SY, TILEX, TILEY, 0, 0); @@ -12125,6 +13641,104 @@ void CreateLevelSketchImages(void) } +// ---------------------------------------------------------------------------- +// create and save images for element collecting animations (raw BMP format) +// ---------------------------------------------------------------------------- + +static boolean createCollectImage(int element) +{ + return (IS_COLLECTIBLE(element) && !IS_SP_ELEMENT(element)); +} + +void CreateCollectElementImages(void) +{ + int i, j; + int num_steps = 8; + int anim_frames = num_steps - 1; + int tile_size = TILESIZE; + int anim_width = tile_size * anim_frames; + int anim_height = tile_size; + int num_collect_images = 0; + int pos_collect_images = 0; + + for (i = 0; i < MAX_NUM_ELEMENTS; i++) + if (createCollectImage(i)) + num_collect_images++; + + Info("Creating %d element collecting animation images ...", + num_collect_images); + + int dst_width = anim_width * 2; + int dst_height = anim_height * num_collect_images / 2; + Bitmap *dst_bitmap = CreateBitmap(dst_width, dst_height, DEFAULT_DEPTH); + char *basename = "RocksCollect.bmp"; + char *filename = getPath2(global.create_collect_images_dir, basename); + + for (i = 0; i < MAX_NUM_ELEMENTS; i++) + { + if (!createCollectImage(i)) + continue; + + int dst_x = (pos_collect_images / (num_collect_images / 2)) * anim_width; + int dst_y = (pos_collect_images % (num_collect_images / 2)) * anim_height; + int graphic = el2img(i); + char *token_name = element_info[i].token_name; + Bitmap *tmp_bitmap = CreateBitmap(tile_size, tile_size, DEFAULT_DEPTH); + Bitmap *src_bitmap; + int src_x, src_y; + + Info("- creating collecting image for '%s' ...", token_name); + + getGraphicSource(graphic, 0, &src_bitmap, &src_x, &src_y); + + BlitBitmap(src_bitmap, tmp_bitmap, src_x, src_y, + tile_size, tile_size, 0, 0); + + tmp_bitmap->surface_masked = tmp_bitmap->surface; + + for (j = 0; j < anim_frames; j++) + { + int frame_size_final = tile_size * (anim_frames - j) / num_steps; + int frame_size = frame_size_final * num_steps; + int offset = (tile_size - frame_size_final) / 2; + Bitmap *frame_bitmap = ZoomBitmap(tmp_bitmap, frame_size, frame_size); + + while (frame_size > frame_size_final) + { + frame_size /= 2; + + Bitmap *half_bitmap = ZoomBitmap(frame_bitmap, frame_size, frame_size); + + FreeBitmap(frame_bitmap); + + frame_bitmap = half_bitmap; + } + + BlitBitmap(frame_bitmap, dst_bitmap, 0, 0, + frame_size_final, frame_size_final, + dst_x + j * tile_size + offset, dst_y + offset); + + FreeBitmap(frame_bitmap); + } + + tmp_bitmap->surface_masked = NULL; + + FreeBitmap(tmp_bitmap); + + pos_collect_images++; + } + + if (SDL_SaveBMP(dst_bitmap->surface, filename) != 0) + Fail("cannot save element collecting image file '%s'", filename); + + FreeBitmap(dst_bitmap); + + Info("Done."); + + CloseAllAndExit(0); +} + + // ---------------------------------------------------------------------------- // create and save images for custom and group elements (raw BMP format) // ----------------------------------------------------------------------------