added optional button to restart game (door, panel and touch variants)
[rocksndiamonds.git] / src / api.c
1 // ============================================================================
2 // Rocks'n'Diamonds - McDuffin Strikes Back!
3 // ----------------------------------------------------------------------------
4 // (c) 1995-2022 by Artsoft Entertainment
5 //                  Holger Schemel
6 //                  info@artsoft.org
7 //                  https://www.artsoft.org/
8 // ----------------------------------------------------------------------------
9 // api.c
10 // ============================================================================
11
12 #include "libgame/libgame.h"
13
14 #include "api.h"
15 #include "main.h"
16 #include "files.h"
17 #include "config.h"
18
19
20 // ============================================================================
21 // generic helper functions
22 // ============================================================================
23
24 static void ExecuteAsThread(SDL_ThreadFunction function, char *name, void *data,
25                             char *error)
26 {
27 #if defined(PLATFORM_EMSCRIPTEN)
28   // threads currently not fully supported by Emscripten/SDL and some browsers
29   function(data);
30 #else
31   SDL_Thread *thread = SDL_CreateThread(function, name, data);
32
33   if (thread != NULL)
34     SDL_DetachThread(thread);
35   else
36     Error("Cannot create thread to %s!", error);
37
38   // nasty kludge to lower probability of intermingled thread error messages
39   Delay(1);
40 #endif
41 }
42
43 static char *getPasswordJSON(char *password)
44 {
45   static char password_json[MAX_FILENAME_LEN] = "";
46   static boolean initialized = FALSE;
47
48   if (!initialized)
49   {
50     if (password != NULL &&
51         !strEqual(password, "") &&
52         !strEqual(password, UNDEFINED_PASSWORD))
53       snprintf(password_json, MAX_FILENAME_LEN,
54                "  \"password\":             \"%s\",\n",
55                setup.api_server_password);
56
57     initialized = TRUE;
58   }
59
60   return password_json;
61 }
62
63 static char *getFileBase64(char *filename)
64 {
65   struct stat file_status;
66
67   if (stat(filename, &file_status) != 0)
68   {
69     Error("cannot stat file '%s'", filename);
70
71     return NULL;
72   }
73
74   int buffer_size = file_status.st_size;
75   byte *buffer = checked_malloc(buffer_size);
76   FILE *file;
77   int i;
78
79   if (!(file = fopen(filename, MODE_READ)))
80   {
81     Error("cannot open file '%s'", filename);
82
83     checked_free(buffer);
84
85     return NULL;
86   }
87
88   for (i = 0; i < buffer_size; i++)
89   {
90     int c = fgetc(file);
91
92     if (c == EOF)
93     {
94       Error("cannot read from input file '%s'", filename);
95
96       fclose(file);
97       checked_free(buffer);
98
99       return NULL;
100     }
101
102     buffer[i] = (byte)c;
103   }
104
105   fclose(file);
106
107   int buffer_encoded_size = base64_encoded_size(buffer_size);
108   char *buffer_encoded = checked_malloc(buffer_encoded_size);
109
110   base64_encode(buffer_encoded, buffer, buffer_size);
111
112   checked_free(buffer);
113
114   return buffer_encoded;
115 }
116
117
118 // ============================================================================
119 // add score API functions
120 // ============================================================================
121
122 struct ApiAddScoreThreadData
123 {
124   int level_nr;
125   boolean tape_saved;
126   char *leveldir_subdir;
127   char *score_tape_filename;
128   struct ScoreEntry score_entry;
129 };
130
131 static void *CreateThreadData_ApiAddScore(int nr, boolean tape_saved,
132                                           char *score_tape_filename)
133 {
134   struct ApiAddScoreThreadData *data =
135     checked_malloc(sizeof(struct ApiAddScoreThreadData));
136   struct ScoreEntry *score_entry = &scores.entry[scores.last_added];
137
138   if (score_tape_filename == NULL)
139     score_tape_filename = getScoreTapeFilename(score_entry->tape_basename, nr);
140
141   data->level_nr = nr;
142   data->tape_saved = tape_saved;
143   data->leveldir_subdir = getStringCopy(leveldir_current->subdir);
144   data->score_tape_filename = getStringCopy(score_tape_filename);
145   data->score_entry = *score_entry;
146
147   return data;
148 }
149
150 static void FreeThreadData_ApiAddScore(void *data_raw)
151 {
152   struct ApiAddScoreThreadData *data = data_raw;
153
154   checked_free(data->leveldir_subdir);
155   checked_free(data->score_tape_filename);
156   checked_free(data);
157 }
158
159 static boolean SetRequest_ApiAddScore(struct HttpRequest *request,
160                                       void *data_raw)
161 {
162   struct ApiAddScoreThreadData *data = data_raw;
163   struct ScoreEntry *score_entry = &data->score_entry;
164   char *score_tape_filename = data->score_tape_filename;
165   boolean tape_saved = data->tape_saved;
166   int level_nr = data->level_nr;
167
168   request->hostname = setup.api_server_hostname;
169   request->port     = API_SERVER_PORT;
170   request->method   = API_SERVER_METHOD;
171   request->uri      = API_SERVER_URI_ADD;
172
173   char *tape_base64 = getFileBase64(score_tape_filename);
174
175   if (tape_base64 == NULL)
176   {
177     Error("loading and base64 encoding score tape file failed");
178
179     return FALSE;
180   }
181
182   char *player_name_raw = score_entry->name;
183   char *player_uuid_raw = setup.player_uuid;
184
185   if (options.player_name != NULL && global.autoplay_leveldir != NULL)
186   {
187     player_name_raw = options.player_name;
188     player_uuid_raw = "";
189   }
190
191   char *levelset_identifier = getEscapedJSON(leveldir_current->identifier);
192   char *levelset_name       = getEscapedJSON(leveldir_current->name);
193   char *levelset_author     = getEscapedJSON(leveldir_current->author);
194   char *level_name          = getEscapedJSON(level.name);
195   char *level_author        = getEscapedJSON(level.author);
196   char *player_name         = getEscapedJSON(player_name_raw);
197   char *player_uuid         = getEscapedJSON(player_uuid_raw);
198
199   snprintf(request->body, MAX_HTTP_BODY_SIZE,
200            "{\n"
201            "%s"
202            "  \"game_version\":         \"%s\",\n"
203            "  \"game_platform\":        \"%s\",\n"
204            "  \"batch_time\":           \"%d\",\n"
205            "  \"levelset_identifier\":  \"%s\",\n"
206            "  \"levelset_name\":        \"%s\",\n"
207            "  \"levelset_author\":      \"%s\",\n"
208            "  \"levelset_num_levels\":  \"%d\",\n"
209            "  \"levelset_first_level\": \"%d\",\n"
210            "  \"level_nr\":             \"%d\",\n"
211            "  \"level_name\":           \"%s\",\n"
212            "  \"level_author\":         \"%s\",\n"
213            "  \"use_step_counter\":     \"%d\",\n"
214            "  \"rate_time_over_score\": \"%d\",\n"
215            "  \"player_name\":          \"%s\",\n"
216            "  \"player_uuid\":          \"%s\",\n"
217            "  \"score\":                \"%d\",\n"
218            "  \"time\":                 \"%d\",\n"
219            "  \"tape_basename\":        \"%s\",\n"
220            "  \"tape_saved\":           \"%d\",\n"
221            "  \"tape\":                 \"%s\"\n"
222            "}\n",
223            getPasswordJSON(setup.api_server_password),
224            getProgramRealVersionString(),
225            getProgramPlatformString(),
226            (int)global.autoplay_time,
227            levelset_identifier,
228            levelset_name,
229            levelset_author,
230            leveldir_current->levels,
231            leveldir_current->first_level,
232            level_nr,
233            level_name,
234            level_author,
235            level.use_step_counter,
236            level.rate_time_over_score,
237            player_name,
238            player_uuid,
239            score_entry->score,
240            score_entry->time,
241            score_entry->tape_basename,
242            tape_saved,
243            tape_base64);
244
245   checked_free(tape_base64);
246
247   checked_free(levelset_identifier);
248   checked_free(levelset_name);
249   checked_free(levelset_author);
250   checked_free(level_name);
251   checked_free(level_author);
252   checked_free(player_name);
253   checked_free(player_uuid);
254
255   ConvertHttpRequestBodyToServerEncoding(request);
256
257   return TRUE;
258 }
259
260 static void HandleResponse_ApiAddScore(struct HttpResponse *response,
261                                        void *data_raw)
262 {
263   server_scores.uploaded = TRUE;
264 }
265
266 static void HandleFailure_ApiAddScore(void *data_raw)
267 {
268   struct ApiAddScoreThreadData *data = data_raw;
269
270   PrepareScoreTapesForUpload(data->leveldir_subdir);
271 }
272
273 #if defined(PLATFORM_EMSCRIPTEN)
274 static void Emscripten_ApiAddScore_Loaded(unsigned handle, void *data_raw,
275                                           void *buffer, unsigned int size)
276 {
277   struct HttpResponse *response = GetHttpResponseFromBuffer(buffer, size);
278
279   if (response != NULL)
280   {
281     HandleResponse_ApiAddScore(response, data_raw);
282
283     checked_free(response);
284   }
285   else
286   {
287     Error("server response too large to handle (%d bytes)", size);
288
289     HandleFailure_ApiAddScore(data_raw);
290   }
291
292   FreeThreadData_ApiAddScore(data_raw);
293 }
294
295 static void Emscripten_ApiAddScore_Failed(unsigned handle, void *data_raw,
296                                           int code, const char *status)
297 {
298   Error("server failed to handle request: %d %s", code, status);
299
300   HandleFailure_ApiAddScore(data_raw);
301
302   FreeThreadData_ApiAddScore(data_raw);
303 }
304
305 static void Emscripten_ApiAddScore_Progress(unsigned handle, void *data_raw,
306                                             int bytes, int size)
307 {
308   // nothing to do here
309 }
310
311 static void Emscripten_ApiAddScore_HttpRequest(struct HttpRequest *request,
312                                                void *data_raw)
313 {
314   if (!SetRequest_ApiAddScore(request, data_raw))
315   {
316     FreeThreadData_ApiAddScore(data_raw);
317
318     return;
319   }
320
321   emscripten_async_wget2_data(request->uri,
322                               request->method,
323                               request->body,
324                               data_raw,
325                               TRUE,
326                               Emscripten_ApiAddScore_Loaded,
327                               Emscripten_ApiAddScore_Failed,
328                               Emscripten_ApiAddScore_Progress);
329 }
330
331 #else
332
333 static void ApiAddScore_HttpRequestExt(struct HttpRequest *request,
334                                        struct HttpResponse *response,
335                                        void *data_raw)
336 {
337   if (!SetRequest_ApiAddScore(request, data_raw))
338     return;
339
340   if (!DoHttpRequest(request, response))
341   {
342     Error("HTTP request failed: %s", GetHttpError());
343
344     HandleFailure_ApiAddScore(data_raw);
345
346     return;
347   }
348
349   if (!HTTP_SUCCESS(response->status_code))
350   {
351     Error("server failed to handle request: %d %s",
352           response->status_code,
353           response->status_text);
354
355     HandleFailure_ApiAddScore(data_raw);
356
357     return;
358   }
359
360   HandleResponse_ApiAddScore(response, data_raw);
361 }
362
363 static void ApiAddScore_HttpRequest(struct HttpRequest *request,
364                                     struct HttpResponse *response,
365                                     void *data_raw)
366 {
367   ApiAddScore_HttpRequestExt(request, response, data_raw);
368
369   FreeThreadData_ApiAddScore(data_raw);
370 }
371 #endif
372
373 static int ApiAddScoreThread(void *data_raw)
374 {
375   struct HttpRequest *request = checked_calloc(sizeof(struct HttpRequest));
376   struct HttpResponse *response = checked_calloc(sizeof(struct HttpResponse));
377
378   program.api_thread_count++;
379
380 #if defined(PLATFORM_EMSCRIPTEN)
381   Emscripten_ApiAddScore_HttpRequest(request, data_raw);
382 #else
383   ApiAddScore_HttpRequest(request, response, data_raw);
384 #endif
385
386   program.api_thread_count--;
387
388   checked_free(request);
389   checked_free(response);
390
391   return 0;
392 }
393
394 void ApiAddScoreAsThread(int nr, boolean tape_saved, char *score_tape_filename)
395 {
396   struct ApiAddScoreThreadData *data =
397     CreateThreadData_ApiAddScore(nr, tape_saved, score_tape_filename);
398
399   ExecuteAsThread(ApiAddScoreThread,
400                   "ApiAddScore", data,
401                   "upload score to server");
402 }
403
404
405 // ============================================================================
406 // get score API functions
407 // ============================================================================
408
409 struct ApiGetScoreThreadData
410 {
411   int level_nr;
412   char *score_cache_filename;
413 };
414
415 static void *CreateThreadData_ApiGetScore(int nr)
416 {
417   struct ApiGetScoreThreadData *data =
418     checked_malloc(sizeof(struct ApiGetScoreThreadData));
419   char *score_cache_filename = getScoreCacheFilename(nr);
420
421   data->level_nr = nr;
422   data->score_cache_filename = getStringCopy(score_cache_filename);
423
424   return data;
425 }
426
427 static void FreeThreadData_ApiGetScore(void *data_raw)
428 {
429   struct ApiGetScoreThreadData *data = data_raw;
430
431   checked_free(data->score_cache_filename);
432   checked_free(data);
433 }
434
435 static boolean SetRequest_ApiGetScore(struct HttpRequest *request,
436                                       void *data_raw)
437 {
438   struct ApiGetScoreThreadData *data = data_raw;
439   int level_nr = data->level_nr;
440
441   request->hostname = setup.api_server_hostname;
442   request->port     = API_SERVER_PORT;
443   request->method   = API_SERVER_METHOD;
444   request->uri      = API_SERVER_URI_GET;
445
446   char *levelset_identifier = getEscapedJSON(leveldir_current->identifier);
447   char *levelset_name       = getEscapedJSON(leveldir_current->name);
448
449   snprintf(request->body, MAX_HTTP_BODY_SIZE,
450            "{\n"
451            "%s"
452            "  \"game_version\":         \"%s\",\n"
453            "  \"game_platform\":        \"%s\",\n"
454            "  \"levelset_identifier\":  \"%s\",\n"
455            "  \"levelset_name\":        \"%s\",\n"
456            "  \"level_nr\":             \"%d\"\n"
457            "}\n",
458            getPasswordJSON(setup.api_server_password),
459            getProgramRealVersionString(),
460            getProgramPlatformString(),
461            levelset_identifier,
462            levelset_name,
463            level_nr);
464
465   checked_free(levelset_identifier);
466   checked_free(levelset_name);
467
468   ConvertHttpRequestBodyToServerEncoding(request);
469
470   return TRUE;
471 }
472
473 static void HandleResponse_ApiGetScore(struct HttpResponse *response,
474                                        void *data_raw)
475 {
476   struct ApiGetScoreThreadData *data = data_raw;
477
478   if (response->body_size == 0)
479   {
480     // no scores available for this level
481
482     return;
483   }
484
485   ConvertHttpResponseBodyToClientEncoding(response);
486
487   char *filename = data->score_cache_filename;
488   FILE *file;
489   int i;
490
491   // used instead of "leveldir_current->subdir" (for network games)
492   InitScoreCacheDirectory(levelset.identifier);
493
494   if (!(file = fopen(filename, MODE_WRITE)))
495   {
496     Warn("cannot save score cache file '%s'", filename);
497
498     return;
499   }
500
501   for (i = 0; i < response->body_size; i++)
502     fputc(response->body[i], file);
503
504   fclose(file);
505
506   SetFilePermissions(filename, PERMS_PRIVATE);
507
508   server_scores.updated = TRUE;
509 }
510
511 #if defined(PLATFORM_EMSCRIPTEN)
512 static void Emscripten_ApiGetScore_Loaded(unsigned handle, void *data_raw,
513                                           void *buffer, unsigned int size)
514 {
515   struct HttpResponse *response = GetHttpResponseFromBuffer(buffer, size);
516
517   if (response != NULL)
518   {
519     HandleResponse_ApiGetScore(response, data_raw);
520
521     checked_free(response);
522   }
523   else
524   {
525     Error("server response too large to handle (%d bytes)", size);
526   }
527
528   FreeThreadData_ApiGetScore(data_raw);
529 }
530
531 static void Emscripten_ApiGetScore_Failed(unsigned handle, void *data_raw,
532                                           int code, const char *status)
533 {
534   Error("server failed to handle request: %d %s", code, status);
535
536   FreeThreadData_ApiGetScore(data_raw);
537 }
538
539 static void Emscripten_ApiGetScore_Progress(unsigned handle, void *data_raw,
540                                             int bytes, int size)
541 {
542   // nothing to do here
543 }
544
545 static void Emscripten_ApiGetScore_HttpRequest(struct HttpRequest *request,
546                                                void *data_raw)
547 {
548   if (!SetRequest_ApiGetScore(request, data_raw))
549   {
550     FreeThreadData_ApiGetScore(data_raw);
551
552     return;
553   }
554
555   emscripten_async_wget2_data(request->uri,
556                               request->method,
557                               request->body,
558                               data_raw,
559                               TRUE,
560                               Emscripten_ApiGetScore_Loaded,
561                               Emscripten_ApiGetScore_Failed,
562                               Emscripten_ApiGetScore_Progress);
563 }
564
565 #else
566
567 static void ApiGetScore_HttpRequestExt(struct HttpRequest *request,
568                                        struct HttpResponse *response,
569                                        void *data_raw)
570 {
571   if (!SetRequest_ApiGetScore(request, data_raw))
572     return;
573
574   if (!DoHttpRequest(request, response))
575   {
576     Error("HTTP request failed: %s", GetHttpError());
577
578     return;
579   }
580
581   if (!HTTP_SUCCESS(response->status_code))
582   {
583     // do not show error message if no scores found for this level set
584     if (response->status_code == 404)
585       return;
586
587     Error("server failed to handle request: %d %s",
588           response->status_code,
589           response->status_text);
590
591     return;
592   }
593
594   HandleResponse_ApiGetScore(response, data_raw);
595 }
596
597 static void ApiGetScore_HttpRequest(struct HttpRequest *request,
598                                     struct HttpResponse *response,
599                                     void *data_raw)
600 {
601   ApiGetScore_HttpRequestExt(request, response, data_raw);
602
603   FreeThreadData_ApiGetScore(data_raw);
604 }
605 #endif
606
607 static int ApiGetScoreThread(void *data_raw)
608 {
609   struct HttpRequest *request = checked_calloc(sizeof(struct HttpRequest));
610   struct HttpResponse *response = checked_calloc(sizeof(struct HttpResponse));
611
612   program.api_thread_count++;
613
614 #if defined(PLATFORM_EMSCRIPTEN)
615   Emscripten_ApiGetScore_HttpRequest(request, data_raw);
616 #else
617   ApiGetScore_HttpRequest(request, response, data_raw);
618 #endif
619
620   program.api_thread_count--;
621
622   checked_free(request);
623   checked_free(response);
624
625   return 0;
626 }
627
628 void ApiGetScoreAsThread(int nr)
629 {
630   struct ApiGetScoreThreadData *data = CreateThreadData_ApiGetScore(nr);
631
632   ExecuteAsThread(ApiGetScoreThread,
633                   "ApiGetScore", data,
634                   "download scores from server");
635 }
636
637
638 // ============================================================================
639 // get score tape API functions
640 // ============================================================================
641
642 struct ApiGetScoreTapeThreadData
643 {
644   int level_nr;
645   int score_id;
646   char *score_tape_filename;
647 };
648
649 static void *CreateThreadData_ApiGetScoreTape(int nr, int id,
650                                               char *score_tape_basename)
651 {
652   struct ApiGetScoreTapeThreadData *data =
653     checked_malloc(sizeof(struct ApiGetScoreTapeThreadData));
654   char *score_tape_filename = getScoreCacheTapeFilename(score_tape_basename, nr);
655
656   data->level_nr = nr;
657   data->score_id = id;
658   data->score_tape_filename = getStringCopy(score_tape_filename);
659
660   return data;
661 }
662
663 static void FreeThreadData_ApiGetScoreTape(void *data_raw)
664 {
665   struct ApiGetScoreTapeThreadData *data = data_raw;
666
667   checked_free(data->score_tape_filename);
668   checked_free(data);
669 }
670
671 static boolean SetRequest_ApiGetScoreTape(struct HttpRequest *request,
672                                           void *data_raw)
673 {
674   struct ApiGetScoreTapeThreadData *data = data_raw;
675   int score_id = data->score_id;
676
677   request->hostname = setup.api_server_hostname;
678   request->port     = API_SERVER_PORT;
679   request->method   = API_SERVER_METHOD;
680   request->uri      = API_SERVER_URI_GETTAPE;
681
682   snprintf(request->body, MAX_HTTP_BODY_SIZE,
683            "{\n"
684            "%s"
685            "  \"game_version\":         \"%s\",\n"
686            "  \"game_platform\":        \"%s\",\n"
687            "  \"id\":                   \"%d\"\n"
688            "}\n",
689            getPasswordJSON(setup.api_server_password),
690            getProgramRealVersionString(),
691            getProgramPlatformString(),
692            score_id);
693
694   ConvertHttpRequestBodyToServerEncoding(request);
695
696   return TRUE;
697 }
698
699 static void HandleResponse_ApiGetScoreTape(struct HttpResponse *response,
700                                            void *data_raw)
701 {
702   struct ApiGetScoreTapeThreadData *data = data_raw;
703
704   if (response->body_size == 0)
705   {
706     // no score tape available for this level
707
708     return;
709   }
710
711   // (do not convert HTTP response body, as it contains binary data here)
712
713   int level_nr = data->level_nr;
714   char *filename = data->score_tape_filename;
715   FILE *file;
716   int i;
717
718   // used instead of "leveldir_current->subdir" (for network games)
719   InitScoreCacheTapeDirectory(levelset.identifier, level_nr);
720
721   if (!(file = fopen(filename, MODE_WRITE)))
722   {
723     Warn("cannot save score tape file '%s'", filename);
724
725     return;
726   }
727
728   for (i = 0; i < response->body_size; i++)
729     fputc(response->body[i], file);
730
731   fclose(file);
732
733   SetFilePermissions(filename, PERMS_PRIVATE);
734
735   server_scores.tape_downloaded = TRUE;
736 }
737
738 #if defined(PLATFORM_EMSCRIPTEN)
739 static void Emscripten_ApiGetScoreTape_Loaded(unsigned handle, void *data_raw,
740                                               void *buffer, unsigned int size)
741 {
742   struct HttpResponse *response = GetHttpResponseFromBuffer(buffer, size);
743
744   if (response != NULL)
745   {
746     HandleResponse_ApiGetScoreTape(response, data_raw);
747
748     checked_free(response);
749   }
750   else
751   {
752     Error("server response too large to handle (%d bytes)", size);
753   }
754
755   FreeThreadData_ApiGetScoreTape(data_raw);
756 }
757
758 static void Emscripten_ApiGetScoreTape_Failed(unsigned handle, void *data_raw,
759                                               int code, const char *status)
760 {
761   Error("server failed to handle request: %d %s", code, status);
762
763   FreeThreadData_ApiGetScoreTape(data_raw);
764 }
765
766 static void Emscripten_ApiGetScoreTape_Progress(unsigned handle, void *data_raw,
767                                                 int bytes, int size)
768 {
769   // nothing to do here
770 }
771
772 static void Emscripten_ApiGetScoreTape_HttpRequest(struct HttpRequest *request,
773                                                    void *data_raw)
774 {
775   if (!SetRequest_ApiGetScoreTape(request, data_raw))
776   {
777     FreeThreadData_ApiGetScoreTape(data_raw);
778
779     return;
780   }
781
782   emscripten_async_wget2_data(request->uri,
783                               request->method,
784                               request->body,
785                               data_raw,
786                               TRUE,
787                               Emscripten_ApiGetScoreTape_Loaded,
788                               Emscripten_ApiGetScoreTape_Failed,
789                               Emscripten_ApiGetScoreTape_Progress);
790 }
791
792 #else
793
794 static void ApiGetScoreTape_HttpRequestExt(struct HttpRequest *request,
795                                            struct HttpResponse *response,
796                                            void *data_raw)
797 {
798   if (!SetRequest_ApiGetScoreTape(request, data_raw))
799     return;
800
801   if (!DoHttpRequest(request, response))
802   {
803     Error("HTTP request failed: %s", GetHttpError());
804
805     return;
806   }
807
808   if (!HTTP_SUCCESS(response->status_code))
809   {
810     // do not show error message if no scores found for this level set
811     if (response->status_code == 404)
812       return;
813
814     Error("server failed to handle request: %d %s",
815           response->status_code,
816           response->status_text);
817
818     return;
819   }
820
821   HandleResponse_ApiGetScoreTape(response, data_raw);
822 }
823
824 static void ApiGetScoreTape_HttpRequest(struct HttpRequest *request,
825                                         struct HttpResponse *response,
826                                         void *data_raw)
827 {
828   ApiGetScoreTape_HttpRequestExt(request, response, data_raw);
829
830   FreeThreadData_ApiGetScoreTape(data_raw);
831 }
832 #endif
833
834 static int ApiGetScoreTapeThread(void *data_raw)
835 {
836   struct HttpRequest *request = checked_calloc(sizeof(struct HttpRequest));
837   struct HttpResponse *response = checked_calloc(sizeof(struct HttpResponse));
838
839   program.api_thread_count++;
840
841 #if defined(PLATFORM_EMSCRIPTEN)
842   Emscripten_ApiGetScoreTape_HttpRequest(request, data_raw);
843 #else
844   ApiGetScoreTape_HttpRequest(request, response, data_raw);
845 #endif
846
847   program.api_thread_count--;
848
849   checked_free(request);
850   checked_free(response);
851
852   return 0;
853 }
854
855 void ApiGetScoreTapeAsThread(int nr, int id, char *score_tape_basename)
856 {
857   struct ApiGetScoreTapeThreadData *data =
858     CreateThreadData_ApiGetScoreTape(nr, id, score_tape_basename);
859
860   ExecuteAsThread(ApiGetScoreTapeThread,
861                   "ApiGetScoreTape", data,
862                   "download score tape from server");
863 }
864
865
866 // ============================================================================
867 // rename player API functions
868 // ============================================================================
869
870 struct ApiRenamePlayerThreadData
871 {
872   char *player_name;
873   char *player_uuid;
874 };
875
876 static void *CreateThreadData_ApiRenamePlayer(void)
877 {
878   struct ApiRenamePlayerThreadData *data =
879     checked_malloc(sizeof(struct ApiRenamePlayerThreadData));
880
881   data->player_name = getStringCopy(setup.player_name);
882   data->player_uuid = getStringCopy(setup.player_uuid);
883
884   return data;
885 }
886
887 static void FreeThreadData_ApiRenamePlayer(void *data_raw)
888 {
889   struct ApiRenamePlayerThreadData *data = data_raw;
890
891   checked_free(data->player_name);
892   checked_free(data->player_uuid);
893   checked_free(data);
894 }
895
896 static boolean SetRequest_ApiRenamePlayer(struct HttpRequest *request,
897                                           void *data_raw)
898 {
899   struct ApiRenamePlayerThreadData *data = data_raw;
900   char *player_name_raw = data->player_name;
901   char *player_uuid_raw = data->player_uuid;
902
903   request->hostname = setup.api_server_hostname;
904   request->port     = API_SERVER_PORT;
905   request->method   = API_SERVER_METHOD;
906   request->uri      = API_SERVER_URI_RENAME;
907
908   char *player_name = getEscapedJSON(player_name_raw);
909   char *player_uuid = getEscapedJSON(player_uuid_raw);
910
911   snprintf(request->body, MAX_HTTP_BODY_SIZE,
912            "{\n"
913            "%s"
914            "  \"game_version\":         \"%s\",\n"
915            "  \"game_platform\":        \"%s\",\n"
916            "  \"name\":                 \"%s\",\n"
917            "  \"uuid\":                 \"%s\"\n"
918            "}\n",
919            getPasswordJSON(setup.api_server_password),
920            getProgramRealVersionString(),
921            getProgramPlatformString(),
922            player_name,
923            player_uuid);
924
925   checked_free(player_name);
926   checked_free(player_uuid);
927
928   ConvertHttpRequestBodyToServerEncoding(request);
929
930   return TRUE;
931 }
932
933 static void HandleResponse_ApiRenamePlayer(struct HttpResponse *response,
934                                            void *data_raw)
935 {
936   // nothing to do here
937 }
938
939 #if defined(PLATFORM_EMSCRIPTEN)
940 static void Emscripten_ApiRenamePlayer_Loaded(unsigned handle, void *data_raw,
941                                               void *buffer, unsigned int size)
942 {
943   struct HttpResponse *response = GetHttpResponseFromBuffer(buffer, size);
944
945   if (response != NULL)
946   {
947     HandleResponse_ApiRenamePlayer(response, data_raw);
948
949     checked_free(response);
950   }
951   else
952   {
953     Error("server response too large to handle (%d bytes)", size);
954   }
955
956   FreeThreadData_ApiRenamePlayer(data_raw);
957 }
958
959 static void Emscripten_ApiRenamePlayer_Failed(unsigned handle, void *data_raw,
960                                               int code, const char *status)
961 {
962   Error("server failed to handle request: %d %s", code, status);
963
964   FreeThreadData_ApiRenamePlayer(data_raw);
965 }
966
967 static void Emscripten_ApiRenamePlayer_Progress(unsigned handle, void *data_raw,
968                                                 int bytes, int size)
969 {
970   // nothing to do here
971 }
972
973 static void Emscripten_ApiRenamePlayer_HttpRequest(struct HttpRequest *request,
974                                                    void *data_raw)
975 {
976   if (!SetRequest_ApiRenamePlayer(request, data_raw))
977   {
978     FreeThreadData_ApiRenamePlayer(data_raw);
979
980     return;
981   }
982
983   emscripten_async_wget2_data(request->uri,
984                               request->method,
985                               request->body,
986                               data_raw,
987                               TRUE,
988                               Emscripten_ApiRenamePlayer_Loaded,
989                               Emscripten_ApiRenamePlayer_Failed,
990                               Emscripten_ApiRenamePlayer_Progress);
991 }
992
993 #else
994
995 static void ApiRenamePlayer_HttpRequestExt(struct HttpRequest *request,
996                                            struct HttpResponse *response,
997                                            void *data_raw)
998 {
999   if (!SetRequest_ApiRenamePlayer(request, data_raw))
1000     return;
1001
1002   if (!DoHttpRequest(request, response))
1003   {
1004     Error("HTTP request failed: %s", GetHttpError());
1005
1006     return;
1007   }
1008
1009   if (!HTTP_SUCCESS(response->status_code))
1010   {
1011     Error("server failed to handle request: %d %s",
1012           response->status_code,
1013           response->status_text);
1014
1015     return;
1016   }
1017
1018   HandleResponse_ApiRenamePlayer(response, data_raw);
1019 }
1020
1021 static void ApiRenamePlayer_HttpRequest(struct HttpRequest *request,
1022                                     struct HttpResponse *response,
1023                                     void *data_raw)
1024 {
1025   ApiRenamePlayer_HttpRequestExt(request, response, data_raw);
1026
1027   FreeThreadData_ApiRenamePlayer(data_raw);
1028 }
1029 #endif
1030
1031 static int ApiRenamePlayerThread(void *data_raw)
1032 {
1033   struct HttpRequest *request = checked_calloc(sizeof(struct HttpRequest));
1034   struct HttpResponse *response = checked_calloc(sizeof(struct HttpResponse));
1035
1036   program.api_thread_count++;
1037
1038 #if defined(PLATFORM_EMSCRIPTEN)
1039   Emscripten_ApiRenamePlayer_HttpRequest(request, data_raw);
1040 #else
1041   ApiRenamePlayer_HttpRequest(request, response, data_raw);
1042 #endif
1043
1044   program.api_thread_count--;
1045
1046   checked_free(request);
1047   checked_free(response);
1048
1049   return 0;
1050 }
1051
1052 void ApiRenamePlayerAsThread(void)
1053 {
1054   struct ApiRenamePlayerThreadData *data = CreateThreadData_ApiRenamePlayer();
1055
1056   ExecuteAsThread(ApiRenamePlayerThread,
1057                   "ApiRenamePlayer", data,
1058                   "rename player on server");
1059 }
1060
1061
1062 // ============================================================================
1063 // reset player UUID API functions
1064 // ============================================================================
1065
1066 struct ApiResetUUIDThreadData
1067 {
1068   char *player_name;
1069   char *player_uuid_old;
1070   char *player_uuid_new;
1071 };
1072
1073 static void *CreateThreadData_ApiResetUUID(char *uuid_new)
1074 {
1075   struct ApiResetUUIDThreadData *data =
1076     checked_malloc(sizeof(struct ApiResetUUIDThreadData));
1077
1078   data->player_name     = getStringCopy(setup.player_name);
1079   data->player_uuid_old = getStringCopy(setup.player_uuid);
1080   data->player_uuid_new = getStringCopy(uuid_new);
1081
1082   return data;
1083 }
1084
1085 static void FreeThreadData_ApiResetUUID(void *data_raw)
1086 {
1087   struct ApiResetUUIDThreadData *data = data_raw;
1088
1089   checked_free(data->player_name);
1090   checked_free(data->player_uuid_old);
1091   checked_free(data->player_uuid_new);
1092   checked_free(data);
1093 }
1094
1095 static boolean SetRequest_ApiResetUUID(struct HttpRequest *request,
1096                                        void *data_raw)
1097 {
1098   struct ApiResetUUIDThreadData *data = data_raw;
1099   char *player_name_raw = data->player_name;
1100   char *player_uuid_old_raw = data->player_uuid_old;
1101   char *player_uuid_new_raw = data->player_uuid_new;
1102
1103   request->hostname = setup.api_server_hostname;
1104   request->port     = API_SERVER_PORT;
1105   request->method   = API_SERVER_METHOD;
1106   request->uri      = API_SERVER_URI_RESETUUID;
1107
1108   char *player_name = getEscapedJSON(player_name_raw);
1109   char *player_uuid_old = getEscapedJSON(player_uuid_old_raw);
1110   char *player_uuid_new = getEscapedJSON(player_uuid_new_raw);
1111
1112   snprintf(request->body, MAX_HTTP_BODY_SIZE,
1113            "{\n"
1114            "%s"
1115            "  \"game_version\":         \"%s\",\n"
1116            "  \"game_platform\":        \"%s\",\n"
1117            "  \"name\":                 \"%s\",\n"
1118            "  \"uuid_old\":             \"%s\",\n"
1119            "  \"uuid_new\":             \"%s\"\n"
1120            "}\n",
1121            getPasswordJSON(setup.api_server_password),
1122            getProgramRealVersionString(),
1123            getProgramPlatformString(),
1124            player_name,
1125            player_uuid_old,
1126            player_uuid_new);
1127
1128   checked_free(player_name);
1129   checked_free(player_uuid_old);
1130   checked_free(player_uuid_new);
1131
1132   ConvertHttpRequestBodyToServerEncoding(request);
1133
1134   return TRUE;
1135 }
1136
1137 static void HandleResponse_ApiResetUUID(struct HttpResponse *response,
1138                                         void *data_raw)
1139 {
1140   struct ApiResetUUIDThreadData *data = data_raw;
1141
1142   // upgrade player UUID in server setup file
1143   setup.player_uuid = getStringCopy(data->player_uuid_new);
1144   setup.player_version = 2;
1145
1146   SaveSetup_ServerSetup();
1147 }
1148
1149 #if defined(PLATFORM_EMSCRIPTEN)
1150 static void Emscripten_ApiResetUUID_Loaded(unsigned handle, void *data_raw,
1151                                            void *buffer, unsigned int size)
1152 {
1153   struct HttpResponse *response = GetHttpResponseFromBuffer(buffer, size);
1154
1155   if (response != NULL)
1156   {
1157     HandleResponse_ApiResetUUID(response, data_raw);
1158
1159     checked_free(response);
1160   }
1161   else
1162   {
1163     Error("server response too large to handle (%d bytes)", size);
1164   }
1165
1166   FreeThreadData_ApiResetUUID(data_raw);
1167 }
1168
1169 static void Emscripten_ApiResetUUID_Failed(unsigned handle, void *data_raw,
1170                                            int code, const char *status)
1171 {
1172   Error("server failed to handle request: %d %s", code, status);
1173
1174   FreeThreadData_ApiResetUUID(data_raw);
1175 }
1176
1177 static void Emscripten_ApiResetUUID_Progress(unsigned handle, void *data_raw,
1178                                              int bytes, int size)
1179 {
1180   // nothing to do here
1181 }
1182
1183 static void Emscripten_ApiResetUUID_HttpRequest(struct HttpRequest *request,
1184                                                 void *data_raw)
1185 {
1186   if (!SetRequest_ApiResetUUID(request, data_raw))
1187   {
1188     FreeThreadData_ApiResetUUID(data_raw);
1189
1190     return;
1191   }
1192
1193   emscripten_async_wget2_data(request->uri,
1194                               request->method,
1195                               request->body,
1196                               data_raw,
1197                               TRUE,
1198                               Emscripten_ApiResetUUID_Loaded,
1199                               Emscripten_ApiResetUUID_Failed,
1200                               Emscripten_ApiResetUUID_Progress);
1201 }
1202
1203 #else
1204
1205 static void ApiResetUUID_HttpRequestExt(struct HttpRequest *request,
1206                                         struct HttpResponse *response,
1207                                         void *data_raw)
1208 {
1209   if (!SetRequest_ApiResetUUID(request, data_raw))
1210     return;
1211
1212   if (!DoHttpRequest(request, response))
1213   {
1214     Error("HTTP request failed: %s", GetHttpError());
1215
1216     return;
1217   }
1218
1219   if (!HTTP_SUCCESS(response->status_code))
1220   {
1221     Error("server failed to handle request: %d %s",
1222           response->status_code,
1223           response->status_text);
1224
1225     return;
1226   }
1227
1228   HandleResponse_ApiResetUUID(response, data_raw);
1229 }
1230
1231 static void ApiResetUUID_HttpRequest(struct HttpRequest *request,
1232                                      struct HttpResponse *response,
1233                                      void *data_raw)
1234 {
1235   ApiResetUUID_HttpRequestExt(request, response, data_raw);
1236
1237   FreeThreadData_ApiResetUUID(data_raw);
1238 }
1239 #endif
1240
1241 static int ApiResetUUIDThread(void *data_raw)
1242 {
1243   struct HttpRequest *request = checked_calloc(sizeof(struct HttpRequest));
1244   struct HttpResponse *response = checked_calloc(sizeof(struct HttpResponse));
1245
1246   program.api_thread_count++;
1247
1248 #if defined(PLATFORM_EMSCRIPTEN)
1249   Emscripten_ApiResetUUID_HttpRequest(request, data_raw);
1250 #else
1251   ApiResetUUID_HttpRequest(request, response, data_raw);
1252 #endif
1253
1254   program.api_thread_count--;
1255
1256   checked_free(request);
1257   checked_free(response);
1258
1259   return 0;
1260 }
1261
1262 void ApiResetUUIDAsThread(char *uuid_new)
1263 {
1264   struct ApiResetUUIDThreadData *data = CreateThreadData_ApiResetUUID(uuid_new);
1265
1266   ExecuteAsThread(ApiResetUUIDThread,
1267                   "ApiResetUUID", data,
1268                   "reset UUID on server");
1269 }