moved API functions to separate source file
[rocksndiamonds.git] / src / api.c
diff --git a/src/api.c b/src/api.c
new file mode 100644 (file)
index 0000000..6fd07dc
--- /dev/null
+++ b/src/api.c
@@ -0,0 +1,1041 @@
+// ============================================================================
+// Rocks'n'Diamonds - McDuffin Strikes Back!
+// ----------------------------------------------------------------------------
+// (c) 1995-2022 by Artsoft Entertainment
+//                         Holger Schemel
+//                 info@artsoft.org
+//                 https://www.artsoft.org/
+// ----------------------------------------------------------------------------
+// api.c
+// ============================================================================
+
+#include "libgame/libgame.h"
+
+#include "api.h"
+#include "main.h"
+#include "files.h"
+#include "config.h"
+
+
+// ============================================================================
+// generic helper functions
+// ============================================================================
+
+static 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
+}
+
+static 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;
+}
+
+static char *getFileBase64(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;
+}
+
+
+// ============================================================================
+// add score API functions
+// ============================================================================
+
+struct ApiAddScoreThreadData
+{
+  int level_nr;
+  boolean tape_saved;
+  char *leveldir_subdir;
+  char *score_tape_filename;
+  struct ScoreEntry score_entry;
+};
+
+static void *CreateThreadData_ApiAddScore(int nr, boolean tape_saved,
+                                         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->tape_saved = tape_saved;
+  data->leveldir_subdir = getStringCopy(leveldir_current->subdir);
+  data->score_tape_filename = getStringCopy(score_tape_filename);
+  data->score_entry = *score_entry;
+
+  return data;
+}
+
+static void FreeThreadData_ApiAddScore(void *data_raw)
+{
+  struct ApiAddScoreThreadData *data = data_raw;
+
+  checked_free(data->leveldir_subdir);
+  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;
+  boolean tape_saved = data->tape_saved;
+  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 = getFileBase64(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"
+          "  \"use_step_counter\":     \"%d\",\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_saved\":           \"%d\",\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.use_step_counter,
+          level.rate_time_over_score,
+          player_name,
+          player_uuid,
+          score_entry->score,
+          score_entry->time,
+          score_entry->tape_basename,
+          tape_saved,
+          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;
+}
+
+static void HandleFailure_ApiAddScore(void *data_raw)
+{
+  struct ApiAddScoreThreadData *data = data_raw;
+
+  PrepareScoreTapesForUpload(data->leveldir_subdir);
+}
+
+#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);
+
+    HandleFailure_ApiAddScore(data_raw);
+  }
+
+  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);
+
+  HandleFailure_ApiAddScore(data_raw);
+
+  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());
+
+    HandleFailure_ApiAddScore(data_raw);
+
+    return;
+  }
+
+  if (!HTTP_SUCCESS(response->status_code))
+  {
+    Error("server failed to handle request: %d %s",
+         response->status_code,
+         response->status_text);
+
+    HandleFailure_ApiAddScore(data_raw);
+
+    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));
+
+  program.api_thread_count++;
+
+#if defined(PLATFORM_EMSCRIPTEN)
+  Emscripten_ApiAddScore_HttpRequest(request, data_raw);
+#else
+  ApiAddScore_HttpRequest(request, response, data_raw);
+#endif
+
+  program.api_thread_count--;
+
+  checked_free(request);
+  checked_free(response);
+
+  return 0;
+}
+
+void ApiAddScoreAsThread(int nr, boolean tape_saved, char *score_tape_filename)
+{
+  struct ApiAddScoreThreadData *data =
+    CreateThreadData_ApiAddScore(nr, tape_saved, score_tape_filename);
+
+  ExecuteAsThread(ApiAddScoreThread,
+                 "ApiAddScore", data,
+                 "upload score to server");
+}
+
+
+// ============================================================================
+// get score API functions
+// ============================================================================
+
+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;
+
+  char *levelset_identifier = getEscapedJSON(leveldir_current->identifier);
+  char *levelset_name       = getEscapedJSON(leveldir_current->name);
+
+  snprintf(request->body, MAX_HTTP_BODY_SIZE,
+          "{\n"
+          "%s"
+          "  \"game_version\":         \"%s\",\n"
+          "  \"game_platform\":        \"%s\",\n"
+          "  \"levelset_identifier\":  \"%s\",\n"
+          "  \"levelset_name\":        \"%s\",\n"
+          "  \"level_nr\":             \"%d\"\n"
+          "}\n",
+          getPasswordJSON(setup.api_server_password),
+          getProgramRealVersionString(),
+          getProgramPlatformString(),
+          levelset_identifier,
+          levelset_name,
+          level_nr);
+
+  checked_free(levelset_identifier);
+  checked_free(levelset_name);
+
+  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))
+  {
+    // do not show error message if no scores found for this level set
+    if (response->status_code == 404)
+      return;
+
+    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));
+
+  program.api_thread_count++;
+
+#if defined(PLATFORM_EMSCRIPTEN)
+  Emscripten_ApiGetScore_HttpRequest(request, data_raw);
+#else
+  ApiGetScore_HttpRequest(request, response, data_raw);
+#endif
+
+  program.api_thread_count--;
+
+  checked_free(request);
+  checked_free(response);
+
+  return 0;
+}
+
+void ApiGetScoreAsThread(int nr)
+{
+  struct ApiGetScoreThreadData *data = CreateThreadData_ApiGetScore(nr);
+
+  ExecuteAsThread(ApiGetScoreThread,
+                 "ApiGetScore", data,
+                 "download scores from server");
+}
+
+
+// ============================================================================
+// rename player API functions
+// ============================================================================
+
+struct ApiRenamePlayerThreadData
+{
+  char *player_name;
+  char *player_uuid;
+};
+
+static void *CreateThreadData_ApiRenamePlayer(void)
+{
+  struct ApiRenamePlayerThreadData *data =
+    checked_malloc(sizeof(struct ApiRenamePlayerThreadData));
+
+  data->player_name = getStringCopy(setup.player_name);
+  data->player_uuid = getStringCopy(setup.player_uuid);
+
+  return data;
+}
+
+static void FreeThreadData_ApiRenamePlayer(void *data_raw)
+{
+  struct ApiRenamePlayerThreadData *data = data_raw;
+
+  checked_free(data->player_name);
+  checked_free(data->player_uuid);
+  checked_free(data);
+}
+
+static boolean SetRequest_ApiRenamePlayer(struct HttpRequest *request,
+                                         void *data_raw)
+{
+  struct ApiRenamePlayerThreadData *data = data_raw;
+  char *player_name_raw = data->player_name;
+  char *player_uuid_raw = data->player_uuid;
+
+  request->hostname = setup.api_server_hostname;
+  request->port     = API_SERVER_PORT;
+  request->method   = API_SERVER_METHOD;
+  request->uri      = API_SERVER_URI_RENAME;
+
+  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"
+          "  \"name\":                 \"%s\",\n"
+          "  \"uuid\":                 \"%s\"\n"
+          "}\n",
+          getPasswordJSON(setup.api_server_password),
+          getProgramRealVersionString(),
+          getProgramPlatformString(),
+          player_name,
+          player_uuid);
+
+  checked_free(player_name);
+  checked_free(player_uuid);
+
+  ConvertHttpRequestBodyToServerEncoding(request);
+
+  return TRUE;
+}
+
+static void HandleResponse_ApiRenamePlayer(struct HttpResponse *response,
+                                          void *data_raw)
+{
+  // nothing to do here
+}
+
+#if defined(PLATFORM_EMSCRIPTEN)
+static void Emscripten_ApiRenamePlayer_Loaded(unsigned handle, void *data_raw,
+                                             void *buffer, unsigned int size)
+{
+  struct HttpResponse *response = GetHttpResponseFromBuffer(buffer, size);
+
+  if (response != NULL)
+  {
+    HandleResponse_ApiRenamePlayer(response, data_raw);
+
+    checked_free(response);
+  }
+  else
+  {
+    Error("server response too large to handle (%d bytes)", size);
+  }
+
+  FreeThreadData_ApiRenamePlayer(data_raw);
+}
+
+static void Emscripten_ApiRenamePlayer_Failed(unsigned handle, void *data_raw,
+                                             int code, const char *status)
+{
+  Error("server failed to handle request: %d %s", code, status);
+
+  FreeThreadData_ApiRenamePlayer(data_raw);
+}
+
+static void Emscripten_ApiRenamePlayer_Progress(unsigned handle, void *data_raw,
+                                               int bytes, int size)
+{
+  // nothing to do here
+}
+
+static void Emscripten_ApiRenamePlayer_HttpRequest(struct HttpRequest *request,
+                                                  void *data_raw)
+{
+  if (!SetRequest_ApiRenamePlayer(request, data_raw))
+  {
+    FreeThreadData_ApiRenamePlayer(data_raw);
+
+    return;
+  }
+
+  emscripten_async_wget2_data(request->uri,
+                             request->method,
+                             request->body,
+                             data_raw,
+                             TRUE,
+                             Emscripten_ApiRenamePlayer_Loaded,
+                             Emscripten_ApiRenamePlayer_Failed,
+                             Emscripten_ApiRenamePlayer_Progress);
+}
+
+#else
+
+static void ApiRenamePlayer_HttpRequestExt(struct HttpRequest *request,
+                                          struct HttpResponse *response,
+                                          void *data_raw)
+{
+  if (!SetRequest_ApiRenamePlayer(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_ApiRenamePlayer(response, data_raw);
+}
+
+static void ApiRenamePlayer_HttpRequest(struct HttpRequest *request,
+                                   struct HttpResponse *response,
+                                   void *data_raw)
+{
+  ApiRenamePlayer_HttpRequestExt(request, response, data_raw);
+
+  FreeThreadData_ApiRenamePlayer(data_raw);
+}
+#endif
+
+static int ApiRenamePlayerThread(void *data_raw)
+{
+  struct HttpRequest *request = checked_calloc(sizeof(struct HttpRequest));
+  struct HttpResponse *response = checked_calloc(sizeof(struct HttpResponse));
+
+  program.api_thread_count++;
+
+#if defined(PLATFORM_EMSCRIPTEN)
+  Emscripten_ApiRenamePlayer_HttpRequest(request, data_raw);
+#else
+  ApiRenamePlayer_HttpRequest(request, response, data_raw);
+#endif
+
+  program.api_thread_count--;
+
+  checked_free(request);
+  checked_free(response);
+
+  return 0;
+}
+
+void ApiRenamePlayerAsThread(void)
+{
+  struct ApiRenamePlayerThreadData *data = CreateThreadData_ApiRenamePlayer();
+
+  ExecuteAsThread(ApiRenamePlayerThread,
+                 "ApiRenamePlayer", data,
+                 "rename player on server");
+}
+
+
+// ============================================================================
+// reset player UUID API functions
+// ============================================================================
+
+struct ApiResetUUIDThreadData
+{
+  char *player_name;
+  char *player_uuid_old;
+  char *player_uuid_new;
+};
+
+static void *CreateThreadData_ApiResetUUID(char *uuid_new)
+{
+  struct ApiResetUUIDThreadData *data =
+    checked_malloc(sizeof(struct ApiResetUUIDThreadData));
+
+  data->player_name     = getStringCopy(setup.player_name);
+  data->player_uuid_old = getStringCopy(setup.player_uuid);
+  data->player_uuid_new = getStringCopy(uuid_new);
+
+  return data;
+}
+
+static void FreeThreadData_ApiResetUUID(void *data_raw)
+{
+  struct ApiResetUUIDThreadData *data = data_raw;
+
+  checked_free(data->player_name);
+  checked_free(data->player_uuid_old);
+  checked_free(data->player_uuid_new);
+  checked_free(data);
+}
+
+static boolean SetRequest_ApiResetUUID(struct HttpRequest *request,
+                                      void *data_raw)
+{
+  struct ApiResetUUIDThreadData *data = data_raw;
+  char *player_name_raw = data->player_name;
+  char *player_uuid_old_raw = data->player_uuid_old;
+  char *player_uuid_new_raw = data->player_uuid_new;
+
+  request->hostname = setup.api_server_hostname;
+  request->port     = API_SERVER_PORT;
+  request->method   = API_SERVER_METHOD;
+  request->uri      = API_SERVER_URI_RESETUUID;
+
+  char *player_name = getEscapedJSON(player_name_raw);
+  char *player_uuid_old = getEscapedJSON(player_uuid_old_raw);
+  char *player_uuid_new = getEscapedJSON(player_uuid_new_raw);
+
+  snprintf(request->body, MAX_HTTP_BODY_SIZE,
+          "{\n"
+          "%s"
+          "  \"game_version\":         \"%s\",\n"
+          "  \"game_platform\":        \"%s\",\n"
+          "  \"name\":                 \"%s\",\n"
+          "  \"uuid_old\":             \"%s\",\n"
+          "  \"uuid_new\":             \"%s\"\n"
+          "}\n",
+          getPasswordJSON(setup.api_server_password),
+          getProgramRealVersionString(),
+          getProgramPlatformString(),
+          player_name,
+          player_uuid_old,
+          player_uuid_new);
+
+  checked_free(player_name);
+  checked_free(player_uuid_old);
+  checked_free(player_uuid_new);
+
+  ConvertHttpRequestBodyToServerEncoding(request);
+
+  return TRUE;
+}
+
+static void HandleResponse_ApiResetUUID(struct HttpResponse *response,
+                                       void *data_raw)
+{
+  struct ApiResetUUIDThreadData *data = data_raw;
+
+  // upgrade player UUID in server setup file
+  setup.player_uuid = getStringCopy(data->player_uuid_new);
+  setup.player_version = 2;
+
+  SaveSetup_ServerSetup();
+}
+
+#if defined(PLATFORM_EMSCRIPTEN)
+static void Emscripten_ApiResetUUID_Loaded(unsigned handle, void *data_raw,
+                                          void *buffer, unsigned int size)
+{
+  struct HttpResponse *response = GetHttpResponseFromBuffer(buffer, size);
+
+  if (response != NULL)
+  {
+    HandleResponse_ApiResetUUID(response, data_raw);
+
+    checked_free(response);
+  }
+  else
+  {
+    Error("server response too large to handle (%d bytes)", size);
+  }
+
+  FreeThreadData_ApiResetUUID(data_raw);
+}
+
+static void Emscripten_ApiResetUUID_Failed(unsigned handle, void *data_raw,
+                                          int code, const char *status)
+{
+  Error("server failed to handle request: %d %s", code, status);
+
+  FreeThreadData_ApiResetUUID(data_raw);
+}
+
+static void Emscripten_ApiResetUUID_Progress(unsigned handle, void *data_raw,
+                                            int bytes, int size)
+{
+  // nothing to do here
+}
+
+static void Emscripten_ApiResetUUID_HttpRequest(struct HttpRequest *request,
+                                               void *data_raw)
+{
+  if (!SetRequest_ApiResetUUID(request, data_raw))
+  {
+    FreeThreadData_ApiResetUUID(data_raw);
+
+    return;
+  }
+
+  emscripten_async_wget2_data(request->uri,
+                             request->method,
+                             request->body,
+                             data_raw,
+                             TRUE,
+                             Emscripten_ApiResetUUID_Loaded,
+                             Emscripten_ApiResetUUID_Failed,
+                             Emscripten_ApiResetUUID_Progress);
+}
+
+#else
+
+static void ApiResetUUID_HttpRequestExt(struct HttpRequest *request,
+                                       struct HttpResponse *response,
+                                       void *data_raw)
+{
+  if (!SetRequest_ApiResetUUID(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_ApiResetUUID(response, data_raw);
+}
+
+static void ApiResetUUID_HttpRequest(struct HttpRequest *request,
+                                    struct HttpResponse *response,
+                                    void *data_raw)
+{
+  ApiResetUUID_HttpRequestExt(request, response, data_raw);
+
+  FreeThreadData_ApiResetUUID(data_raw);
+}
+#endif
+
+static int ApiResetUUIDThread(void *data_raw)
+{
+  struct HttpRequest *request = checked_calloc(sizeof(struct HttpRequest));
+  struct HttpResponse *response = checked_calloc(sizeof(struct HttpResponse));
+
+  program.api_thread_count++;
+
+#if defined(PLATFORM_EMSCRIPTEN)
+  Emscripten_ApiResetUUID_HttpRequest(request, data_raw);
+#else
+  ApiResetUUID_HttpRequest(request, response, data_raw);
+#endif
+
+  program.api_thread_count--;
+
+  checked_free(request);
+  checked_free(response);
+
+  return 0;
+}
+
+void ApiResetUUIDAsThread(char *uuid_new)
+{
+  struct ApiResetUUIDThreadData *data = CreateThreadData_ApiResetUUID(uuid_new);
+
+  ExecuteAsThread(ApiResetUUIDThread,
+                 "ApiResetUUID", data,
+                 "reset UUID on server");
+}