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
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;
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)
{
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 setScoreInfoToDefaults(void)
+static void setScoreInfoToDefaultsExt(struct ScoreInfo *scores)
{
int i;
for (i = 0; i < MAX_SCORE_ENTRIES; i++)
{
- strcpy(scores.entry[i].name, EMPTY_PLAYER_NAME);
- scores.entry[i].score = 0;
- scores.entry[i].time = 0;
+ 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->force_last_added = FALSE;
+}
+
+static void setScoreInfoToDefaults(void)
+{
+ setScoreInfoToDefaultsExt(&scores);
+}
+
+static void setServerScoreInfoToDefaults(void)
+{
+ setScoreInfoToDefaultsExt(&server_scores);
}
static void LoadScore_OLD(int nr)
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);
{ "NAME", -1, LoadScore_NAME },
{ "SCOR", -1, LoadScore_SCOR },
{ "TIME", -1, LoadScore_TIME },
+ { "TAPE", -1, LoadScore_TAPE },
{ NULL, 0, NULL }
};
void SaveScore_OLD(int nr)
{
int i;
- int permissions = (program.global_scores ? PERMS_PUBLIC : PERMS_PRIVATE);
char *filename = getScoreFilename(nr);
FILE *file;
fclose(file);
- SetFilePermissions(filename, permissions);
+ SetFilePermissions(filename, PERMS_PRIVATE);
}
#endif
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 permissions = (program.global_scores ? PERMS_PUBLIC : PERMS_PRIVATE);
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)))
{
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, "TIME", time_chunk_size);
SaveScore_TIME(file, &scores);
+ putFileChunkBE(file, "TAPE", tape_chunk_size);
+ SaveScore_TAPE(file, &scores);
+
fclose(file);
- SetFilePermissions(filename, permissions);
+ SetFilePermissions(filename, PERMS_PRIVATE);
}
void SaveScore(int nr)
SaveScoreToFilename(filename);
}
+static void ExecuteAsThread(SDL_ThreadFunction function, char *name, int data,
+ char *error)
+{
+ static int data_static;
+
+ data_static = data;
+
+ SDL_Thread *thread = SDL_CreateThread(function, name, &data_static);
+
+ 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);
+}
+
+static void DownloadServerScoreToCacheExt(struct HttpRequest *request,
+ struct HttpResponse *response,
+ int 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"
+ " \"levelset_identifier\": \"%s\",\n"
+ " \"level_nr\": \"%d\",\n"
+ " \"rate_time_over_score\": \"%d\"\n"
+ "}\n",
+ levelset.identifier, nr, level.rate_time_over_score);
+
+ 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;
+ }
+
+ if (response->body_size == 0)
+ {
+ // no scores available for this level
+
+ return;
+ }
+
+ ConvertHttpResponseBodyToClientEncoding(response);
+
+ char *filename = getScoreCacheFilename(nr);
+ 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;
+}
+
+static void DownloadServerScoreToCache(int nr)
+{
+ struct HttpRequest *request = checked_calloc(sizeof(struct HttpRequest));
+ struct HttpResponse *response = checked_calloc(sizeof(struct HttpResponse));
+
+ DownloadServerScoreToCacheExt(request, response, nr);
+
+ checked_free(request);
+ checked_free(response);
+}
+
+static int DownloadServerScoreToCacheThread(void *data)
+{
+ DownloadServerScoreToCache(*(int *)data);
+
+ return 0;
+}
+
+static void DownloadServerScoreToCacheAsThread(int nr)
+{
+ ExecuteAsThread(DownloadServerScoreToCacheThread,
+ "DownloadServerScoreToCache", nr,
+ "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.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.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)
+ DownloadServerScoreToCacheAsThread(nr);
+ }
+}
+
+static char *get_file_base64(char *filename)
+{
+ struct stat file_status;
+
+ if (stat(filename, &file_status) != 0)
+ {
+ Error("cannot stat file '%s'\n", 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'\n", 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'\n", 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;
+}
+
+static void UploadScoreToServerExt(struct HttpRequest *request,
+ struct HttpResponse *response,
+ int nr)
+{
+ struct ScoreEntry *score_entry = &scores.entry[scores.last_added];
+
+ request->hostname = setup.api_server_hostname;
+ request->port = API_SERVER_PORT;
+ request->method = API_SERVER_METHOD;
+ request->uri = API_SERVER_URI_ADD;
+
+ char *tape_filename = getScoreTapeFilename(score_entry->tape_basename, nr);
+ char *tape_base64 = get_file_base64(tape_filename);
+
+ if (tape_base64 == NULL)
+ {
+ Error("loading and base64 encoding score tape file failed");
+
+ return;
+ }
+
+ snprintf(request->body, MAX_HTTP_BODY_SIZE,
+ "{\n"
+ " \"game_version\": \"%s\",\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"
+ " \"player_name\": \"%s\",\n"
+ " \"score\": \"%d\",\n"
+ " \"time\": \"%d\",\n"
+ " \"tape_basename\": \"%s\",\n"
+ " \"tape\": \"%s\"\n"
+ "}\n",
+ getProgramRealVersionString(),
+ leveldir_current->identifier,
+ leveldir_current->name,
+ leveldir_current->author,
+ leveldir_current->levels,
+ leveldir_current->first_level,
+ level_nr,
+ score_entry->name,
+ score_entry->score,
+ score_entry->time,
+ score_entry->tape_basename,
+ tape_base64);
+
+ checked_free(tape_base64);
+
+ ConvertHttpRequestBodyToServerEncoding(request);
+
+ 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;
+ }
+}
+
+static void UploadScoreToServer(int nr)
+{
+ struct HttpRequest *request = checked_calloc(sizeof(struct HttpRequest));
+ struct HttpResponse *response = checked_calloc(sizeof(struct HttpResponse));
+
+ UploadScoreToServerExt(request, response, nr);
+
+ checked_free(request);
+ checked_free(response);
+}
+
+static int UploadScoreToServerThread(void *data)
+{
+ UploadScoreToServer(*(int *)data);
+
+ return 0;
+}
+
+static void UploadScoreToServerAsThread(int nr)
+{
+ ExecuteAsThread(UploadScoreToServerThread,
+ "UploadScoreToServer", nr,
+ "upload score to server");
+}
+
+void SaveServerScore(int nr)
+{
+ if (!runtime.api_server)
+ return;
+
+ UploadScoreToServerAsThread(nr);
+}
+
+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.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
TYPE_SWITCH,
&setup.show_snapshot_buttons, "show_snapshot_buttons"
},
+ {
+ TYPE_SWITCH,
+ &setup.only_show_local_scores, "only_show_local_scores"
+ },
{
TYPE_STRING,
&setup.graphics_set, "graphics_set"
TYPE_STRING,
&setup.network_server_hostname, "network_server_hostname"
},
+ {
+ TYPE_SWITCH,
+ &setup.api_server, "api_server"
+ },
+ {
+ TYPE_STRING,
+ &setup.api_server_hostname, "api_server_hostname"
+ },
{
TYPE_STRING,
&setup.touch.control_type, "touch.control_type"
si->sp_show_border_elements = FALSE;
si->small_game_graphics = FALSE;
si->show_snapshot_buttons = FALSE;
+ si->only_show_local_scores = FALSE;
si->graphics_set = getStringCopy(GFX_CLASSIC_SUBDIR);
si->sounds_set = getStringCopy(SND_CLASSIC_SUBDIR);
si->network_player_nr = 0; // first player
si->network_server_hostname = getStringCopy(STR_NETWORK_AUTO_DETECT);
+ si->api_server = TRUE;
+ si->api_server_hostname = getStringCopy(API_SERVER_HOSTNAME);
+
si->touch.control_type = getStringCopy(TOUCH_CONTROL_DEFAULT);
si->touch.move_distance = TOUCH_MOVE_DISTANCE_DEFAULT; // percent
si->touch.drop_distance = TOUCH_DROP_DISTANCE_DEFAULT; // percent
global_setup_tokens[i].value == &setup.graphics_set ||
global_setup_tokens[i].value == &setup.volume_simple ||
global_setup_tokens[i].value == &setup.network_mode ||
+ global_setup_tokens[i].value == &setup.api_server ||
global_setup_tokens[i].value == &setup.touch.control_type ||
global_setup_tokens[i].value == &setup.touch.grid_xsize[0] ||
global_setup_tokens[i].value == &setup.touch.grid_xsize[1])