added loading high scores from score server
[rocksndiamonds.git] / src / files.c
index 104ecb4d3560b2f29fa9ac41cfd59569036cf557..83cbc05947dbb181f793d4b7c2610d3154541b61 100644 (file)
@@ -6610,6 +6610,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)
@@ -8274,13 +8295,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;
 
@@ -8296,6 +8314,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)
 {
@@ -8387,16 +8424,30 @@ void DumpTape(struct TapeInfo *tape)
 // 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;
+}
+
+static void setScoreInfoToDefaults(void)
+{
+  setScoreInfoToDefaultsExt(&scores);
+}
+
+static void setServerScoreInfoToDefaults(void)
+{
+  setScoreInfoToDefaultsExt(&server_scores);
 }
 
 static void LoadScore_OLD(int nr)
@@ -8451,6 +8502,25 @@ static void LoadScore_OLD(int nr)
   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);
@@ -8526,6 +8596,23 @@ static int LoadScore_TIME(File *file, int chunk_size, struct ScoreInfo *scores)
   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);
@@ -8580,6 +8667,9 @@ void LoadScore(int nr)
   {
     // 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
   {
@@ -8596,6 +8686,7 @@ void LoadScore(int nr)
       { "NAME", -1,                    LoadScore_NAME },
       { "SCOR", -1,                    LoadScore_SCOR },
       { "TIME", -1,                    LoadScore_TIME },
+      { "TAPE", -1,                    LoadScore_TAPE },
 
       {  NULL,  0,                     NULL }
     };
@@ -8722,6 +8813,19 @@ static void SaveScore_TIME(FILE *file, struct ScoreInfo *scores)
     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;
@@ -8730,6 +8834,7 @@ static void SaveScoreToFilename(char *filename)
   int name_chunk_size;
   int scor_chunk_size;
   int time_chunk_size;
+  int tape_chunk_size;
 
   if (!(file = fopen(filename, MODE_WRITE)))
   {
@@ -8742,6 +8847,7 @@ static void SaveScoreToFilename(char *filename)
   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);
@@ -8761,6 +8867,9 @@ static void SaveScoreToFilename(char *filename)
   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);
@@ -8795,6 +8904,296 @@ void SaveScore(int nr)
   SaveScoreToFilename(filename);
 }
 
+static void DownloadServerScoreToCacheExt(struct HttpRequest *request,
+                                         struct HttpResponse *response,
+                                         int nr)
+{
+  request->hostname = 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);
+}
+
+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 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)
+{
+  // always start with reliable default values
+  setServerScoreInfoToDefaults();
+
+  DownloadServerScoreToCache(nr);
+  LoadServerScoreFromCache(nr);
+
+  MergeServerScore();
+}
+
+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 = 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);
+
+  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);
+}
+
+void SaveServerScore(int nr)
+{
+  UploadScoreToServer(nr);
+}
+
 
 // ============================================================================
 // setup file functions