added using setup option for displaying overlay touch buttons
[rocksndiamonds.git] / src / screens.c
index 30159ccf36bd7578f7e7c1641c8498de617deaea..b45e99119d3e7c5e163fd23cfff625fd5807db70 100644 (file)
@@ -22,6 +22,7 @@
 #include "network.h"
 #include "init.h"
 #include "config.h"
+#include "api.h"
 
 
 #define DEBUG_JOYSTICKS                0
@@ -301,7 +302,7 @@ static void UnmapScreenTreeGadgets(void);
 
 static void UpdateScreenMenuGadgets(int, boolean);
 static void AdjustScoreInfoButtons_SelectScore(int, int, int);
-static void AdjustScoreInfoButtons_PlayTape(int, int);
+static void AdjustScoreInfoButtons_PlayTape(int, int, boolean);
 
 static boolean OfferUploadTapes(void);
 static void execOfferUploadTapes(void);
@@ -311,6 +312,8 @@ static void HandleHallOfFame_SelectLevel(int, int);
 static char *getHallOfFameRankText(int, int);
 static char *getHallOfFameScoreText(int, int);
 
+static struct TokenInfo *getSetupInfoFinal(struct TokenInfo *);
+
 static struct GadgetInfo *screen_gadget[NUM_SCREEN_GADGETS];
 
 static int info_mode = INFO_MODE_MAIN;
@@ -1455,6 +1458,11 @@ static void clearMenuListArea(void)
   // clear menu list area, but not title or scrollbar
   DrawBackground(mSX, mSY + MENU_SCREEN_START_YPOS * 32,
                  scrollbar_xpos - mSX, NUM_MENU_ENTRIES_ON_SCREEN * 32);
+
+  // special compatibility handling for "Snake Bite" graphics set
+  if (strPrefix(leveldir_current->identifier, "snake_bite"))
+    ClearRectangle(drawto, mSX, mSY + MENU_SCREEN_START_YPOS * 32,
+                  scrollbar_xpos - mSX, NUM_MENU_ENTRIES_ON_SCREEN * 32);
 }
 
 static void drawCursorExt(int xpos, int ypos, boolean active, int graphic)
@@ -1523,6 +1531,15 @@ static int getChooseTreeEditYPos(int ypos_raw)
   return sy;
 }
 
+static int getChooseTreeEditXPosReal(int pos)
+{
+  int xpos = getChooseTreeEditXPos(pos);
+  int font_nr = getChooseTreeEditFont(FALSE);
+  int font_xoffset = getFontDrawOffsetX(font_nr);
+
+  return xpos + font_xoffset;
+}
+
 static void drawChooseTreeEdit(int ypos_raw, boolean active)
 {
   int sx = getChooseTreeEditXPos(POS_LEFT);
@@ -1692,6 +1709,21 @@ void DrawMainMenu(void)
     return;
   }
 
+  // needed if last screen was the playing screen, invoked from hall of fame
+  if (score_info_tape_play)
+  {
+    CloseDoor(DOOR_CLOSE_ALL);
+
+    SetGameStatus(GAME_MODE_SCOREINFO);
+
+    DrawScoreInfo(scores.last_entry_nr);
+
+    return;
+  }
+
+  // reset flag to continue playing next level from hall of fame
+  scores.continue_playing = FALSE;
+
   // leveldir_current may be invalid (level group, parent link, node copy)
   leveldir_current = getValidLevelSeries(leveldir_current, leveldir_last_valid);
 
@@ -2051,7 +2083,7 @@ static void HandleMainMenu_SelectLevel(int step, int direction,
   {
     // skipping levels is only allowed when trying to skip single level
     if (setup.skip_levels && new_level_nr == old_level_nr + 1 &&
-       Request("Level still unsolved! Skip despite handicap?", REQ_ASK))
+       Request("Level still unsolved! Skip it anyway?", REQ_ASK))
     {
       leveldir_current->handicap_level++;
       SaveLevelSetup_SeriesInfo();
@@ -2544,6 +2576,9 @@ static void DrawInfoScreen_Main(void)
 
   info_info = info_info_main;
 
+  // use modified info screen info without info screen entries marked as hidden
+  info_info = getSetupInfoFinal(info_info);
+
   // determine maximal number of info entries that can be displayed on screen
   num_info_info = 0;
   for (i = 0; info_info[i].type != 0 && i < NUM_MENU_ENTRIES_ON_SCREEN; i++)
@@ -3961,412 +3996,6 @@ void HandleInfoScreen(int mx, int my, int dx, int dy, int button)
 }
 
 
-// ============================================================================
-// 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;
-}
-
-static 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;
-}
-
-static void ApiResetUUIDAsThread(char *uuid_new)
-{
-  struct ApiResetUUIDThreadData *data = CreateThreadData_ApiResetUUID(uuid_new);
-
-  ExecuteAsThread(ApiResetUUIDThread,
-                 "ApiResetUUID", data,
-                 "reset UUID on server");
-}
-
-
 // ============================================================================
 // type name functions
 // ============================================================================
@@ -4381,7 +4010,7 @@ static int getPlayerNameColor(char *name)
 }
 
 static void drawTypeNameText(char *name, struct TextPosInfo *pos,
-                             boolean active)
+                            boolean active)
 {
   char text[MAX_PLAYER_NAME_LEN + 2] = { 0 };
   boolean multiple_users = (game_status == GAME_MODE_PSEUDO_TYPENAMES);
@@ -4389,8 +4018,12 @@ static void drawTypeNameText(char *name, struct TextPosInfo *pos,
   int sy = (multiple_users ? amSY + pos->y : mSY + ALIGNED_TEXT_YPOS(pos));
   int font_nr = (active ? FONT_ACTIVE(pos->font) : pos->font);
   int font_width = getFontWidth(font_nr);
+  int font_xoffset = getFontDrawOffsetX(font_nr);
+  int font_yoffset = getFontDrawOffsetY(font_nr);
+  int font_sx = sx + font_xoffset;
+  int font_sy = sy + font_yoffset;
 
-  DrawBackgroundForFont(sx, sy, pos->width, pos->height, font_nr);
+  DrawBackgroundForFont(font_sx, font_sy, pos->width, pos->height, font_nr);
 
   sprintf(text, "%s%c", name, (active ? '_' : '\0'));
 
@@ -4706,12 +4339,23 @@ static int getAlignYOffsetFromTreeInfo(TreeInfo *ti)
   return align_yoffset;
 }
 
+static void StartPlayingFromHallOfFame(void)
+{
+  level_nr = scores.next_level_nr;
+  LoadLevel(level_nr);
+
+  StartGameActions(network.enabled, setup.autorecord, level.random_seed);
+}
+
 static void DrawChooseTree(TreeInfo **ti_ptr)
 {
   int fade_mask = REDRAW_FIELD;
   boolean restart_music = (game_status != game_status_last_screen &&
                           game_status_last_screen != GAME_MODE_SCOREINFO);
 
+  scores.continue_on_return = (game_status == GAME_MODE_SCORES &&
+                              game_status_last_screen == GAME_MODE_PLAYING);
+
   if (CheckFadeAll())
     fade_mask = REDRAW_ALL;
 
@@ -4721,7 +4365,11 @@ static void DrawChooseTree(TreeInfo **ti_ptr)
     {
       execSetupArtwork();
     }
-    else       // GAME_MODE_LEVELS
+    else if (game_status == GAME_MODE_SCORES && scores.continue_playing)
+    {
+      StartPlayingFromHallOfFame();
+    }
+    else
     {
       SetGameStatus(GAME_MODE_MAIN);
 
@@ -5021,8 +4669,8 @@ static void HandleChooseTree(int mx, int my, int dx, int dy, int button,
   boolean has_scrollbar = screen_gadget[SCREEN_CTRL_ID_SCROLL_VERTICAL]->mapped;
   int mx_scrollbar = screen_gadget[SCREEN_CTRL_ID_SCROLL_VERTICAL]->x;
   int mx_right_border = (has_scrollbar ? mx_scrollbar : SX + SXSIZE);
-  int sx1_edit_name = getChooseTreeEditXPos(POS_LEFT);
-  int sx2_edit_name = getChooseTreeEditXPos(POS_RIGHT);
+  int sx1_edit_name = getChooseTreeEditXPosReal(POS_LEFT);
+  int sx2_edit_name = getChooseTreeEditXPosReal(POS_RIGHT);
   int x = 0;
   int y = (ti != NULL ? ti->cl_cursor : 0);
   int step = (button == 1 ? 1 : button == 2 ? 5 : 10);
@@ -5164,6 +4812,23 @@ static void HandleChooseTree(int mx, int my, int dx, int dy, int button,
     return;
   }
 
+#if defined(PLATFORM_ANDROID)
+  // directly continue when touching the screen after playing
+  if ((mx || my) && scores.continue_on_return)
+  {
+    // ignore touch events until released
+    mx = my = 0;
+  }
+#endif
+
+  // any mouse click or cursor key stops leaving scores by "Return" key
+  if ((mx || my || dx || dy) && scores.continue_on_return)
+  {
+    scores.continue_on_return = FALSE;
+    level_nr = scores.last_level_nr;
+    LoadLevel(level_nr);
+  }
+
   if (mx || my)                // mouse input
   {
     x = (mx - amSX) / 32;
@@ -5243,15 +4908,17 @@ static void HandleChooseTree(int mx, int my, int dx, int dy, int button,
 
       node_cursor->cl_first = ti->cl_first;
       node_cursor->cl_cursor = ti->cl_cursor;
+
       *ti_ptr = node_cursor->node_group;
       DrawChooseTree(ti_ptr);
 
       return;
     }
   }
-  else if (dx == -1 && ti->node_parent)
+  else if ((dx == -1 || button == MB_MENU_CONTINUE) && ti->node_parent)
   {
-    FadeSetLeaveMenu();
+    if (game_status != GAME_MODE_SCORES)
+      FadeSetLeaveMenu();
 
     PlaySound(SND_MENU_ITEM_SELECTING);
 
@@ -5334,6 +5001,7 @@ static void HandleChooseTree(int mx, int my, int dx, int dy, int button,
 
        node_cursor->cl_first = ti->cl_first;
        node_cursor->cl_cursor = ti->cl_cursor;
+
        *ti_ptr = node_cursor->node_group;
        DrawChooseTree(ti_ptr);
       }
@@ -5352,6 +5020,7 @@ static void HandleChooseTree(int mx, int my, int dx, int dy, int button,
 
        node_cursor->cl_first = ti->cl_first;
        node_cursor->cl_cursor = ti->cl_cursor;
+
        *ti_ptr = node_cursor;
 
        if (ti->type == TREE_TYPE_LEVEL_DIR)
@@ -5442,16 +5111,13 @@ static void HandleChooseTree(int mx, int my, int dx, int dy, int button,
          }
          else if (game_status == GAME_MODE_SCORES)
          {
-           if (game_status_last_screen == GAME_MODE_PLAYING &&
-               setup.auto_play_next_level && setup.increment_levels &&
-               scores.last_level_nr < leveldir_current->last_level &&
-               !network_playing)
+           if (scores.continue_playing && scores.continue_on_return)
            {
-             StartGameActions(network.enabled, setup.autorecord,
-                              level.random_seed);
+             StartPlayingFromHallOfFame();
+
              return;
            }
-           else
+           else if (!scores.continue_on_return)
            {
              SetGameStatus(GAME_MODE_SCOREINFO);
 
@@ -5515,6 +5181,9 @@ void DrawChoosePlayerName(void)
   if (player_name_current == NULL)
     player_name_current = player_name;
 
+  // set text size for main name input (also used on name selection screen)
+  InitializeMainControls();
+
   DrawChooseTree(&player_name_current);
 }
 
@@ -5645,12 +5314,15 @@ static void DrawHallOfFame_setScoreEntries(void)
   if (score_entry_current == NULL)
     score_entry_current = getFirstValidTreeInfoEntry(score_entries);
 
+  if (score_entries != NULL && scores.continue_playing)
+    setString(&score_entries->node_group->name, BACKLINK_TEXT_NEXT);
+
   // ("score_entries" and "score_entry_current" may be NULL here)
 }
 
-void DrawHallOfFame(int level_nr)
+void DrawHallOfFame(int nr)
 {
-  scores.last_level_nr = level_nr;
+  scores.last_level_nr = nr;
 
   // (this is needed when called from GameEnd() after winning a game)
   KeyboardAutoRepeatOn();
@@ -5659,7 +5331,7 @@ void DrawHallOfFame(int level_nr)
   SetDrawDeactivationMask(REDRAW_NONE);
   SetDrawBackgroundMask(REDRAW_FIELD);
 
-  LoadLocalAndServerScore(level_nr, TRUE);
+  LoadLocalAndServerScore(scores.last_level_nr, TRUE);
 
   DrawHallOfFame_setScoreEntries();
 
@@ -5704,6 +5376,28 @@ static char *getHallOfFameScoreText(int nr, int size)
     return getHallOfFameTimeText(nr);                  // show playing time
 }
 
+static char *getHallOfFameTapeDateText(struct ScoreEntry *entry)
+{
+  static char tape_date[MAX_ISO_DATE_LEN + 1];
+  int i, j;
+
+  if (!strEqual(entry->tape_date, UNKNOWN_NAME) ||
+      strEqual(entry->tape_basename, UNDEFINED_FILENAME))
+    return entry->tape_date;
+
+  for (i = 0, j = 0; i < 8; i++, j++)
+  {
+    tape_date[j] = entry->tape_basename[i];
+
+    if (i == 3 || i == 5)
+      tape_date[++j] = '-';
+  }
+
+  tape_date[MAX_ISO_DATE_LEN] = '\0';
+
+  return tape_date;
+}
+
 static void HandleHallOfFame_SelectLevel(int step, int direction)
 {
   int old_level_nr = scores.last_level_nr;
@@ -5760,6 +5454,7 @@ static void DrawScoreInfo_Content(int entry_nr)
 {
   struct ScoreEntry *entry = &scores.entry[entry_nr];
   char *pos_text = getHallOfFameRankText(entry_nr, 0);
+  char *tape_date = getHallOfFameTapeDateText(entry);
   int font_title = MENU_INFO_FONT_TITLE;
   int font_head  = MENU_INFO_FONT_HEAD;
   int font_text  = MENU_INFO_FONT_TEXT;
@@ -5771,26 +5466,28 @@ static void DrawScoreInfo_Content(int entry_nr)
   int ystep_title = getMenuTextStep(spacing_title, font_title);
   int ystep_para  = getMenuTextStep(spacing_para,  font_text);
   int ystep_line  = getMenuTextStep(spacing_line,  font_text);
+  int xstart  = mSX - SX + menu.left_spacing[GAME_MODE_SCOREINFO];
   int ystart  = mSY - SY + menu.top_spacing[GAME_MODE_SCOREINFO];
   int ybottom = mSY - SY + SYSIZE - menu.bottom_spacing[GAME_MODE_SCOREINFO];
-  int xstart1 = mSX - SX + 2 * xstep;
-  int xstart2 = mSX - SX + 13 * xstep;
+  int xstart1 = xstart + xstep;
+  int xstart2 = xstart + xstep * 12;
   int select_x = SX + xstart1;
   int select_y1, select_y2;
   int play_x, play_y;
   int play_height = screen_gadget[SCREEN_CTRL_ID_PLAY_TAPE]->height;
+  boolean play_visible = !strEqual(tape_date, UNKNOWN_NAME);
   int font_width = getFontWidth(font_text);
   int font_height = getFontHeight(font_text);
-  int tape_date_width  = getTextWidth(entry->tape_date, font_text);
+  int tape_date_width  = getTextWidth(tape_date, font_text);
   int pad_left = xstart2;
-  int pad_right = MENU_SCREEN_INFO_SPACE_RIGHT;
+  int pad_right = menu.right_spacing[GAME_MODE_SCOREINFO];
   int max_chars_per_line = (SXSIZE - pad_left - pad_right) / font_width;
   int max_lines_per_text = 5;
   int lines;
 
   ClearField();
 
-  // redraw score selection buttons (which have just been erased)
+  // redraw level selection buttons (which have just been erased)
   RedrawScreenMenuGadgets(SCREEN_MASK_SCORES);
 
   if (score_entries == NULL)
@@ -5855,7 +5552,7 @@ static void DrawScoreInfo_Content(int entry_nr)
   play_y = SY + ystart + (font_height - play_height) / 2;
 
   DrawTextF(xstart1, ystart, font_head, "Tape Date");
-  DrawTextF(xstart2, ystart, font_text, entry->tape_date);
+  DrawTextF(xstart2, ystart, font_text, tape_date);
   ystart += ystep_line;
 
   DrawTextF(xstart1, ystart, font_head, "Platform");
@@ -5877,19 +5574,37 @@ static void DrawScoreInfo_Content(int entry_nr)
   DrawTextSCentered(ybottom, font_foot, "Press any key or button to go back");
 
   AdjustScoreInfoButtons_SelectScore(select_x, select_y1, select_y2);
-  AdjustScoreInfoButtons_PlayTape(play_x, play_y);
+  AdjustScoreInfoButtons_PlayTape(play_x, play_y, play_visible);
 }
 
 static void DrawScoreInfo(int entry_nr)
 {
   scores.last_entry_nr = entry_nr;
-
-  SetMainBackgroundImageIfDefined(IMG_BACKGROUND_SCOREINFO);
+  score_info_tape_play = FALSE;
 
   UnmapAllGadgets();
 
+  FreeScreenGadgets();
+  CreateScreenGadgets();
+
   FadeOut(REDRAW_FIELD);
 
+  // needed if different viewport properties defined after playing score tape
+  ChangeViewportPropertiesIfNeeded();
+
+  // set this after "ChangeViewportPropertiesIfNeeded()" (which may reset it)
+  SetDrawDeactivationMask(REDRAW_NONE);
+  SetDrawBackgroundMask(REDRAW_FIELD);
+
+  // needed if different background image defined after playing score tape
+  SetMainBackgroundImage(IMG_BACKGROUND_SCORES);
+  SetMainBackgroundImageIfDefined(IMG_BACKGROUND_SCOREINFO);
+
+  // special compatibility handling for "Snake Bite" graphics set
+  if (strPrefix(leveldir_current->identifier, "snake_bite"))
+    ClearRectangle(gfx.background_bitmap, gfx.real_sx, gfx.real_sy + 64,
+                  gfx.full_sxsize, gfx.full_sysize - 64);
+
   DrawScoreInfo_Content(entry_nr);
 
   // map gadgets for score info screen
@@ -5920,6 +5635,12 @@ static void HandleScoreInfo_SelectScore(int step, int direction)
 
 static void HandleScoreInfo_PlayTape(void)
 {
+  if (!PlayScoreTape(scores.last_entry_nr))
+  {
+    DrawScoreInfo_Content(scores.last_entry_nr);
+
+    FadeIn(REDRAW_FIELD);
+  }
 }
 
 void HandleScoreInfo(int mx, int my, int dx, int dy, int button)
@@ -5946,7 +5667,7 @@ void HandleScoreInfo(int mx, int my, int dx, int dy, int button)
 
     SetGameStatus(GAME_MODE_SCORES);
 
-    DrawHallOfFame(level_nr);
+    DrawHallOfFame(scores.last_level_nr);
   }
   else if (dx)
   {
@@ -7538,6 +7259,15 @@ static struct
   { &setup.internal.menu_exit,         execExitSetup                   },
   { &setup.internal.menu_save_and_exit,        execSaveAndExitSetup            },
 
+  { &setup.internal.info_title,                execInfoTitleScreen             },
+  { &setup.internal.info_elements,     execInfoElements                },
+  { &setup.internal.info_music,                execInfoMusic                   },
+  { &setup.internal.info_credits,      execInfoCredits                 },
+  { &setup.internal.info_program,      execInfoProgram                 },
+  { &setup.internal.info_version,      execInfoVersion                 },
+  { &setup.internal.info_levelset,     execInfoLevelSet                },
+  { &setup.internal.info_exit,         execExitInfo                    },
+
   { NULL,                              NULL                            }
 };
 
@@ -7582,8 +7312,8 @@ static struct TokenInfo setup_info_game[] =
   { TYPE_SWITCH,       &setup.multiple_users,  "Multiple Users/Teams:" },
   { TYPE_YES_NO,       &setup.input_on_focus,  "Only Move Focussed Player:" },
   { TYPE_SWITCH,       &setup.time_limit,      "Time Limit:"           },
-  { TYPE_SWITCH,       &setup.handicap,        "Handicap:"             },
-  { TYPE_SWITCH,       &setup.skip_levels,     "Skip Unsolved Levels:" },
+  { TYPE_SWITCH,       &setup.handicap,        "Force Solving Levels:" },
+  { TYPE_SWITCH,       &setup.skip_levels,     "Allow Skipping Levels:" },
   { TYPE_SWITCH,       &setup.increment_levels,"Increment Solved Levels:" },
   { TYPE_SWITCH,       &setup.auto_play_next_level,"Auto-play Next Level:" },
   { TYPE_SWITCH,       &setup.count_score_after_game,"Count Score After Game:" },
@@ -7664,7 +7394,7 @@ static struct TokenInfo setup_info_editor[] =
 
 static struct TokenInfo setup_info_graphics[] =
 {
-#if !defined(PLATFORM_ANDROID)
+#if !defined(PLATFORM_ANDROID) && !defined(PLATFORM_EMSCRIPTEN)
   { TYPE_SWITCH,       &setup.fullscreen,      "Fullscreen:"           },
   { TYPE_ENTER_LIST,   execSetupChooseWindowSize, "Window Scaling:"    },
   { TYPE_STRING,       &window_size_text,      ""                      },
@@ -7677,8 +7407,10 @@ static struct TokenInfo setup_info_graphics[] =
   { TYPE_ENTER_LIST,   execSetupChooseScrollDelay, "Scroll Delay:"     },
   { TYPE_STRING,       &scroll_delay_text,     ""                      },
 #endif
+#if !defined(PLATFORM_EMSCRIPTEN)
   { TYPE_ENTER_LIST,   execSetupChooseVsyncMode, "Vertical Sync (VSync):" },
   { TYPE_STRING,       &vsync_mode_text,       ""                      },
+#endif
   { TYPE_SWITCH,       &setup.fade_screens,    "Fade Screens:"         },
   { TYPE_SWITCH,       &setup.quick_switch,    "Quick Player Focus Switch:" },
   { TYPE_SWITCH,       &setup.quick_doors,     "Quick Menu Doors:"     },
@@ -10013,6 +9745,10 @@ static void CreateScreenMenubuttons(void)
     int type = GD_TYPE_NORMAL_BUTTON;
     boolean checked = FALSE;
 
+    // do not use touch buttons if overlay touch buttons are disabled
+    if (is_touch_button && !setup.touch.overlay_buttons)
+      continue;
+
     event_mask = menubutton_info[i].event_mask;
 
     x = (is_touch_button ? pos->x : mSX + GDI_ACTIVE_POS(pos->x));
@@ -10056,12 +9792,18 @@ static void CreateScreenMenubuttons(void)
       // if x/y set to -1, dynamically place buttons next to title text
       int title_width = getTextWidth(INFOTEXT_SCORE_ENTRY, FONT_TITLE_1);
 
+      // special compatibility handling for "Snake Bite" graphics set
+      if (strPrefix(leveldir_current->identifier, "snake_bite"))
+       title_width = strlen(INFOTEXT_SCORE_ENTRY) * 32;
+
+      // use "SX" here to center buttons (ignore horizontal draw offset)
       if (pos->x == -1)
        x = (id == SCREEN_CTRL_ID_PREV_LEVEL2 ?
             SX + (SXSIZE - title_width) / 2 - width * 3 / 2 :
             id == SCREEN_CTRL_ID_NEXT_LEVEL2 ?
             SX + (SXSIZE + title_width) / 2 + width / 2 : 0);
 
+      // use "mSY" here to place buttons (respect vertical draw offset)
       if (pos->y == -1)
        y = (id == SCREEN_CTRL_ID_PREV_LEVEL2 ||
             id == SCREEN_CTRL_ID_NEXT_LEVEL2 ? mSY + MENU_TITLE1_YPOS : 0);
@@ -10402,13 +10144,17 @@ static void AdjustScoreInfoButtons_SelectScore(int x, int y1, int y2)
     ModifyGadget(gi_2, GDI_X, x, GDI_Y, y2, GDI_END);
 }
 
-static void AdjustScoreInfoButtons_PlayTape(int x, int y)
+static void AdjustScoreInfoButtons_PlayTape(int x, int y, boolean visible)
 {
   struct GadgetInfo *gi = screen_gadget[SCREEN_CTRL_ID_PLAY_TAPE];
   struct MenuPosInfo *pos = menubutton_info[SCREEN_CTRL_ID_PLAY_TAPE].pos;
 
-  if (pos->x == -1 && pos->y == -1)
-    ModifyGadget(gi, GDI_X, x, GDI_Y, y, GDI_END);
+  // set gadget position dynamically, pre-defined or off-screen
+  int xx = (visible ? (pos->x == -1 ? x : pos->x) : POS_OFFSCREEN);
+  int yy = (visible ? (pos->y == -1 ? y : pos->y) : POS_OFFSCREEN);
+
+  ModifyGadget(gi, GDI_X, xx, GDI_Y, yy, GDI_END);
+  MapGadget(gi);       // (needed if deactivated on last score page)
 }
 
 static void HandleScreenGadgets(struct GadgetInfo *gi)
@@ -10566,6 +10312,12 @@ static void HandleScreenGadgets(struct GadgetInfo *gi)
   }
 }
 
+void HandleScreenGadgetKeys(Key key)
+{
+  if (key == setup.shortcut.tape_play)
+    HandleScreenGadgets(screen_gadget[SCREEN_CTRL_ID_PLAY_TAPE]);
+}
+
 void DumpScreenIdentifiers(void)
 {
   int i;