added functions to get current player position for all game engines
[rocksndiamonds.git] / src / game.c
index ba066ed9bd6f740064244ccbbbf6d1f775ceded0..3d2f055f27c88415dccb499d83d65932ad8a5f1e 100644 (file)
 #include "init.h"
 #include "tools.h"
 #include "screens.h"
+#include "events.h"
 #include "files.h"
 #include "tape.h"
 #include "network.h"
+#include "anim.h"
 
 
 /* DEBUG SETTINGS */
@@ -970,13 +972,16 @@ static struct GamePanelControlInfo game_panel_controls[] =
 #define GAME_CTRL_ID_STOP              0
 #define GAME_CTRL_ID_PAUSE             1
 #define GAME_CTRL_ID_PLAY              2
-#define SOUND_CTRL_ID_MUSIC            3
-#define SOUND_CTRL_ID_LOOPS            4
-#define SOUND_CTRL_ID_SIMPLE           5
-#define GAME_CTRL_ID_SAVE              6
+#define GAME_CTRL_ID_UNDO              3
+#define GAME_CTRL_ID_REDO              4
+#define GAME_CTRL_ID_SAVE              5
+#define GAME_CTRL_ID_PAUSE2            6
 #define GAME_CTRL_ID_LOAD              7
+#define SOUND_CTRL_ID_MUSIC            8
+#define SOUND_CTRL_ID_LOOPS            9
+#define SOUND_CTRL_ID_SIMPLE           10
 
-#define NUM_GAME_BUTTONS               8
+#define NUM_GAME_BUTTONS               11
 
 
 /* forward declaration for internal use */
@@ -1945,7 +1950,7 @@ static void InitField(int x, int y, boolean init_game)
     CheckTriggeredElementChange(x, y, element, CE_CREATION_OF_X);
 }
 
-static inline void InitField_WithBug1(int x, int y, boolean init_game)
+inline static void InitField_WithBug1(int x, int y, boolean init_game)
 {
   InitField(x, y, init_game);
 
@@ -1955,7 +1960,7 @@ static inline void InitField_WithBug1(int x, int y, boolean init_game)
     InitMovDir(x, y);
 }
 
-static inline void InitField_WithBug2(int x, int y, boolean init_game)
+inline static void InitField_WithBug2(int x, int y, boolean init_game)
 {
   int old_element = Feld[x][y];
 
@@ -2442,7 +2447,7 @@ void DisplayGameControlValues()
   /* redraw game control buttons */
   RedrawGameButtons();
 
-  game_status = GAME_MODE_PSEUDO_PANEL;
+  SetGameStatus(GAME_MODE_PSEUDO_PANEL);
 
   for (i = 0; i < NUM_GAME_PANEL_CONTROLS; i++)
   {
@@ -2519,17 +2524,11 @@ void DisplayGameControlValues()
        height = graphic_info[graphic].height * size / TILESIZE;
 
        if (draw_masked)
-       {
-         SetClipOrigin(src_bitmap, src_bitmap->stored_clip_gc,
-                       dst_x - src_x, dst_y - src_y);
          BlitBitmapMasked(src_bitmap, drawto, src_x, src_y, width, height,
                           dst_x, dst_y);
-       }
        else
-       {
          BlitBitmap(src_bitmap, drawto, src_x, src_y, width, height,
                     dst_x, dst_y);
-       }
       }
     }
     else if (type == TYPE_STRING)
@@ -2576,12 +2575,12 @@ void DisplayGameControlValues()
     redraw_mask |= REDRAW_DOOR_1;
   }
 
-  game_status = GAME_MODE_PLAYING;
+  SetGameStatus(GAME_MODE_PLAYING);
 }
 
 void UpdateAndDisplayGameControlValues()
 {
-  if (tape.warp_forward)
+  if (tape.deactivate_display)
     return;
 
   UpdateGameControlValues();
@@ -3037,6 +3036,20 @@ static void InitGameEngine()
      setup.scroll_delay                   ? setup.scroll_delay_value       : 0);
   game.scroll_delay_value =
     MIN(MAX(MIN_SCROLL_DELAY, game.scroll_delay_value), MAX_SCROLL_DELAY);
+
+  /* ---------- initialize game engine snapshots ---------------------------- */
+  for (i = 0; i < MAX_PLAYERS; i++)
+    game.snapshot.last_action[i] = 0;
+  game.snapshot.changed_action = FALSE;
+  game.snapshot.collected_item = FALSE;
+  game.snapshot.mode =
+    (strEqual(setup.engine_snapshot_mode, STR_SNAPSHOT_MODE_EVERY_STEP) ?
+     SNAPSHOT_MODE_EVERY_STEP :
+     strEqual(setup.engine_snapshot_mode, STR_SNAPSHOT_MODE_EVERY_MOVE) ?
+     SNAPSHOT_MODE_EVERY_MOVE :
+     strEqual(setup.engine_snapshot_mode, STR_SNAPSHOT_MODE_EVERY_COLLECT) ?
+     SNAPSHOT_MODE_EVERY_COLLECT : SNAPSHOT_MODE_OFF);
+  game.snapshot.save_snapshot = FALSE;
 }
 
 int get_num_special_action(int element, int action_first, int action_last)
@@ -3075,6 +3088,7 @@ void InitGame()
 {
   int full_lev_fieldx = lev_fieldx + (BorderElement != EL_EMPTY ? 2 : 0);
   int full_lev_fieldy = lev_fieldy + (BorderElement != EL_EMPTY ? 2 : 0);
+  int fade_mask = REDRAW_FIELD;
 
   boolean emulate_bd = TRUE;   /* unless non-BOULDERDASH elements found */
   boolean emulate_sb = TRUE;   /* unless non-SOKOBAN     elements found */
@@ -3082,23 +3096,35 @@ void InitGame()
   int initial_move_dir = MV_DOWN;
   int i, j, x, y;
 
-  game_status = GAME_MODE_PLAYING;
-
-  StopAnimation();
+  // required here to update video display before fading (FIX THIS)
+  DrawMaskedBorder(REDRAW_DOOR_2);
 
   if (!game.restart_level)
     CloseDoor(DOOR_CLOSE_1);
 
+  SetGameStatus(GAME_MODE_PLAYING);
+
   if (level_editor_test_game)
     FadeSkipNextFadeIn();
   else
     FadeSetEnterScreen();
 
-  FadeOut(REDRAW_FIELD);
+  if (CheckIfGlobalBorderHasChanged())
+    fade_mask = REDRAW_ALL;
+
+  FadeSoundsAndMusic();
+
+  ExpireSoundLoops(TRUE);
+
+  FadeOut(fade_mask);
 
   /* needed if different viewport properties defined for playing */
   ChangeViewportPropertiesIfNeeded();
 
+  ClearField();
+
+  OpenDoor(GetDoorState() | DOOR_NO_DELAY | DOOR_FORCE_REDRAW);
+
   DrawCompleteVideoDisplay();
 
   InitGameEngine();
@@ -3187,6 +3213,11 @@ void InitGame()
     player->is_bored = FALSE;
     player->is_sleeping = FALSE;
 
+    player->was_waiting = TRUE;
+    player->was_moving = FALSE;
+    player->was_snapping = FALSE;
+    player->was_dropping = FALSE;
+
     player->frame_counter_bored = -1;
     player->frame_counter_sleeping = -1;
 
@@ -3909,11 +3940,19 @@ void InitGame()
 
   /* blit playfield from scroll buffer to normal back buffer for fading in */
   BlitScreenToBitmap(backbuffer);
-
-  redraw_mask |= REDRAW_FROM_BACKBUFFER;
   /* !!! FIX THIS (END) !!! */
 
-  FadeIn(REDRAW_FIELD);
+  DrawMaskedBorder(fade_mask);
+
+  FadeIn(fade_mask);
+
+#if 1
+  // full screen redraw is required at this point in the following cases:
+  // - special editor door undrawn when game was started from level editor
+  // - drawing area (playfield) was changed and has to be removed completely
+  redraw_mask = REDRAW_ALL;
+  BackToFront();
+#endif
 
   if (!game.restart_level)
   {
@@ -3934,9 +3973,14 @@ void InitGame()
   {
     UnmapGameButtons();
     UnmapTapeButtons();
+
+    FreeGameButtons();
+    CreateGameButtons();
+
     game_gadget[SOUND_CTRL_ID_MUSIC]->checked = setup.sound_music;
     game_gadget[SOUND_CTRL_ID_LOOPS]->checked = setup.sound_loops;
     game_gadget[SOUND_CTRL_ID_SIMPLE]->checked = setup.sound_simple;
+
     MapGameButtons();
     MapTapeButtons();
 
@@ -3989,15 +4033,28 @@ void InitGame()
   }
 
   game.restart_level = FALSE;
+
+  SaveEngineSnapshotToListInitial();
 }
 
-void UpdateEngineValues(int actual_scroll_x, int actual_scroll_y)
+void UpdateEngineValues(int actual_scroll_x, int actual_scroll_y,
+                       int actual_player_x, int actual_player_y)
 {
   /* this is used for non-R'n'D game engines to update certain engine values */
 
+  if (level.game_engine_type == GAME_ENGINE_TYPE_EM)
+  {
+    actual_player_x = correctLevelPosX_EM(actual_player_x);
+    actual_player_y = correctLevelPosY_EM(actual_player_y);
+  }
+
   /* needed to determine if sounds are played within the visible screen area */
   scroll_x = actual_scroll_x;
   scroll_y = actual_scroll_y;
+
+  /* needed to get player position for "follow finger" playing input method */
+  local_player->jx = actual_player_x;
+  local_player->jy = actual_player_y;
 }
 
 void InitMovDir(int x, int y)
@@ -4375,29 +4432,30 @@ void GameEnd()
 
   local_player->LevelSolved_GameEnd = TRUE;
 
-  CloseDoor(DOOR_CLOSE_1);
+  if (!global.use_envelope_request)
+    CloseDoor(DOOR_CLOSE_1);
 
   if (local_player->LevelSolved_SaveTape)
   {
     SaveTapeChecked(tape.level_nr);    /* ask to save tape */
   }
 
+  CloseDoor(DOOR_CLOSE_ALL);
+
   if (level_editor_test_game)
   {
-    game_status = GAME_MODE_MAIN;
+    SetGameStatus(GAME_MODE_MAIN);
 
-    DrawAndFadeInMainMenu(REDRAW_FIELD);
+    DrawMainMenu();
 
     return;
   }
 
   if (!local_player->LevelSolved_SaveScore)
   {
-    FadeOut(REDRAW_FIELD);
-
-    game_status = GAME_MODE_MAIN;
+    SetGameStatus(GAME_MODE_MAIN);
 
-    DrawAndFadeInMainMenu(REDRAW_FIELD);
+    DrawMainMenu();
 
     return;
   }
@@ -4414,7 +4472,7 @@ void GameEnd()
 
   if ((hi_pos = NewHiScore()) >= 0) 
   {
-    game_status = GAME_MODE_SCORES;
+    SetGameStatus(GAME_MODE_SCORES);
 
     DrawHallOfFame(hi_pos);
 
@@ -4426,9 +4484,7 @@ void GameEnd()
   }
   else
   {
-    FadeOut(REDRAW_FIELD);
-
-    game_status = GAME_MODE_MAIN;
+    SetGameStatus(GAME_MODE_MAIN);
 
     if (raise_level)
     {
@@ -4436,7 +4492,7 @@ void GameEnd()
       TapeErase();
     }
 
-    DrawAndFadeInMainMenu(REDRAW_FIELD);
+    DrawMainMenu();
   }
 }
 
@@ -4856,169 +4912,107 @@ static void setScreenCenteredToAllPlayers(int *sx, int *sy)
 void DrawRelocateScreen(int old_x, int old_y, int x, int y, int move_dir,
                        boolean center_screen, boolean quick_relocation)
 {
+  unsigned int frame_delay_value_old = GetVideoFrameDelay();
   boolean ffwd_delay = (tape.playing && tape.fast_forward);
   boolean no_delay = (tape.warp_forward);
   int frame_delay_value = (ffwd_delay ? FfwdFrameDelay : GameFrameDelay);
   int wait_delay_value = (no_delay ? 0 : frame_delay_value);
+  int new_scroll_x, new_scroll_y;
 
-  if (quick_relocation)
+  if (level.lazy_relocation && IN_VIS_FIELD(SCREENX(x), SCREENY(y)))
   {
-    if (!IN_VIS_FIELD(SCREENX(x), SCREENY(y)) || center_screen)
-    {
-      if (!level.shifted_relocation || center_screen)
-      {
-       /* quick relocation (without scrolling), with centering of screen */
-
-       scroll_x = (x < SBX_Left  + MIDPOSX ? SBX_Left :
-                   x > SBX_Right + MIDPOSX ? SBX_Right :
-                   x - MIDPOSX);
-
-       scroll_y = (y < SBY_Upper + MIDPOSY ? SBY_Upper :
-                   y > SBY_Lower + MIDPOSY ? SBY_Lower :
-                   y - MIDPOSY);
-      }
-      else
-      {
-       /* quick relocation (without scrolling), but do not center screen */
-
-       int center_scroll_x = (old_x < SBX_Left  + MIDPOSX ? SBX_Left :
-                              old_x > SBX_Right + MIDPOSX ? SBX_Right :
-                              old_x - MIDPOSX);
+    /* case 1: quick relocation inside visible screen (without scrolling) */
 
-       int center_scroll_y = (old_y < SBY_Upper + MIDPOSY ? SBY_Upper :
-                              old_y > SBY_Lower + MIDPOSY ? SBY_Lower :
-                              old_y - MIDPOSY);
+    RedrawPlayfield();
 
-       int offset_x = x + (scroll_x - center_scroll_x);
-       int offset_y = y + (scroll_y - center_scroll_y);
-
-       scroll_x = (offset_x < SBX_Left  + MIDPOSX ? SBX_Left :
-                   offset_x > SBX_Right + MIDPOSX ? SBX_Right :
-                   offset_x - MIDPOSX);
+    return;
+  }
 
-       scroll_y = (offset_y < SBY_Upper + MIDPOSY ? SBY_Upper :
-                   offset_y > SBY_Lower + MIDPOSY ? SBY_Lower :
-                   offset_y - MIDPOSY);
-      }
-    }
-    else
-    {
-      if (!level.shifted_relocation || center_screen)
-      {
-       /* quick relocation (without scrolling), with centering of screen */
+  if (!level.shifted_relocation || center_screen)
+  {
+    /* relocation _with_ centering of screen */
 
-       scroll_x = (x < SBX_Left  + MIDPOSX ? SBX_Left :
+    new_scroll_x = (x < SBX_Left  + MIDPOSX ? SBX_Left :
                    x > SBX_Right + MIDPOSX ? SBX_Right :
                    x - MIDPOSX);
 
-       scroll_y = (y < SBY_Upper + MIDPOSY ? SBY_Upper :
+    new_scroll_y = (y < SBY_Upper + MIDPOSY ? SBY_Upper :
                    y > SBY_Lower + MIDPOSY ? SBY_Lower :
                    y - MIDPOSY);
-      }
-      else
-      {
-       /* quick relocation (without scrolling), but do not center screen */
+  }
+  else
+  {
+    /* relocation _without_ centering of screen */
 
-       int center_scroll_x = (old_x < SBX_Left  + MIDPOSX ? SBX_Left :
-                              old_x > SBX_Right + MIDPOSX ? SBX_Right :
-                              old_x - MIDPOSX);
+    int center_scroll_x = (old_x < SBX_Left  + MIDPOSX ? SBX_Left :
+                          old_x > SBX_Right + MIDPOSX ? SBX_Right :
+                          old_x - MIDPOSX);
 
-       int center_scroll_y = (old_y < SBY_Upper + MIDPOSY ? SBY_Upper :
-                              old_y > SBY_Lower + MIDPOSY ? SBY_Lower :
-                              old_y - MIDPOSY);
+    int center_scroll_y = (old_y < SBY_Upper + MIDPOSY ? SBY_Upper :
+                          old_y > SBY_Lower + MIDPOSY ? SBY_Lower :
+                          old_y - MIDPOSY);
 
-       int offset_x = x + (scroll_x - center_scroll_x);
-       int offset_y = y + (scroll_y - center_scroll_y);
+    int offset_x = x + (scroll_x - center_scroll_x);
+    int offset_y = y + (scroll_y - center_scroll_y);
 
-       scroll_x = (offset_x < SBX_Left  + MIDPOSX ? SBX_Left :
+    new_scroll_x = (offset_x < SBX_Left  + MIDPOSX ? SBX_Left :
                    offset_x > SBX_Right + MIDPOSX ? SBX_Right :
                    offset_x - MIDPOSX);
 
-       scroll_y = (offset_y < SBY_Upper + MIDPOSY ? SBY_Upper :
+    new_scroll_y = (offset_y < SBY_Upper + MIDPOSY ? SBY_Upper :
                    offset_y > SBY_Lower + MIDPOSY ? SBY_Lower :
                    offset_y - MIDPOSY);
-      }
-    }
-
-    RedrawPlayfield(TRUE, 0,0,0,0);
   }
-  else
-  {
-    int scroll_xx, scroll_yy;
-
-    if (!level.shifted_relocation || center_screen)
-    {
-      /* visible relocation (with scrolling), with centering of screen */
-
-      scroll_xx = (x < SBX_Left  + MIDPOSX ? SBX_Left :
-                  x > SBX_Right + MIDPOSX ? SBX_Right :
-                  x - MIDPOSX);
 
-      scroll_yy = (y < SBY_Upper + MIDPOSY ? SBY_Upper :
-                  y > SBY_Lower + MIDPOSY ? SBY_Lower :
-                  y - MIDPOSY);
-    }
-    else
-    {
-      /* visible relocation (with scrolling), but do not center screen */
+  if (quick_relocation)
+  {
+    /* case 2: quick relocation (redraw without visible scrolling) */
 
-      int center_scroll_x = (old_x < SBX_Left  + MIDPOSX ? SBX_Left :
-                            old_x > SBX_Right + MIDPOSX ? SBX_Right :
-                            old_x - MIDPOSX);
+    scroll_x = new_scroll_x;
+    scroll_y = new_scroll_y;
 
-      int center_scroll_y = (old_y < SBY_Upper + MIDPOSY ? SBY_Upper :
-                            old_y > SBY_Lower + MIDPOSY ? SBY_Lower :
-                            old_y - MIDPOSY);
+    RedrawPlayfield();
 
-      int offset_x = x + (scroll_x - center_scroll_x);
-      int offset_y = y + (scroll_y - center_scroll_y);
+    return;
+  }
 
-      scroll_xx = (offset_x < SBX_Left  + MIDPOSX ? SBX_Left :
-                  offset_x > SBX_Right + MIDPOSX ? SBX_Right :
-                  offset_x - MIDPOSX);
+  /* case 3: visible relocation (with scrolling to new position) */
 
-      scroll_yy = (offset_y < SBY_Upper + MIDPOSY ? SBY_Upper :
-                  offset_y > SBY_Lower + MIDPOSY ? SBY_Lower :
-                  offset_y - MIDPOSY);
-    }
+  ScrollScreen(NULL, SCROLL_GO_ON);    /* scroll last frame to full tile */
 
+  SetVideoFrameDelay(wait_delay_value);
 
-    ScrollScreen(NULL, SCROLL_GO_ON);  /* scroll last frame to full tile */
+  while (scroll_x != new_scroll_x || scroll_y != new_scroll_y)
+  {
+    int dx = 0, dy = 0;
+    int fx = FX, fy = FY;
 
-    while (scroll_x != scroll_xx || scroll_y != scroll_yy)
-    {
-      int dx = 0, dy = 0;
-      int fx = FX, fy = FY;
+    dx = (new_scroll_x < scroll_x ? +1 : new_scroll_x > scroll_x ? -1 : 0);
+    dy = (new_scroll_y < scroll_y ? +1 : new_scroll_y > scroll_y ? -1 : 0);
 
-      dx = (scroll_xx < scroll_x ? +1 : scroll_xx > scroll_x ? -1 : 0);
-      dy = (scroll_yy < scroll_y ? +1 : scroll_yy > scroll_y ? -1 : 0);
+    if (dx == 0 && dy == 0)            /* no scrolling needed at all */
+      break;
 
-      if (dx == 0 && dy == 0)          /* no scrolling needed at all */
-       break;
+    scroll_x -= dx;
+    scroll_y -= dy;
 
-      scroll_x -= dx;
-      scroll_y -= dy;
+    fx += dx * TILEX / 2;
+    fy += dy * TILEY / 2;
 
-      fx += dx * TILEX / 2;
-      fy += dy * TILEY / 2;
+    ScrollLevel(dx, dy);
+    DrawAllPlayers();
 
-      ScrollLevel(dx, dy);
-      DrawAllPlayers();
+    /* scroll in two steps of half tile size to make things smoother */
+    BlitBitmap(drawto_field, window, fx, fy, SXSIZE, SYSIZE, SX, SY);
 
-      /* scroll in two steps of half tile size to make things smoother */
-      BlitBitmap(drawto_field, window, fx, fy, SXSIZE, SYSIZE, SX, SY);
-      FlushDisplay();
-      Delay(wait_delay_value);
+    /* scroll second step to align at full tile size */
+    BlitScreenToBitmap(window);
+  }
 
-      /* scroll second step to align at full tile size */
-      BackToFront();
-      Delay(wait_delay_value);
-    }
+  DrawAllPlayers();
+  BackToFront();
 
-    DrawAllPlayers();
-    BackToFront();
-    Delay(wait_delay_value);
-  }
+  SetVideoFrameDelay(frame_delay_value_old);
 }
 
 void RelocatePlayer(int jx, int jy, int el_player_raw)
@@ -5068,8 +5062,7 @@ void RelocatePlayer(int jx, int jy, int el_player_raw)
 
       DrawPlayer(player);
 
-      BackToFront();
-      Delay(wait_delay_value);
+      BackToFront_WithFrameDelay(wait_delay_value);
     }
 
     DrawPlayer(player);                /* needed here only to cleanup last field */
@@ -9527,6 +9520,8 @@ static void ExecuteCustomElementAction(int x, int y, int element, int page)
     {
       local_player->gems_still_needed = action_arg_number_new;
 
+      game.snapshot.collected_item = TRUE;
+
       game_panel_controls[GAME_PANEL_GEMS].value =
        local_player->gems_still_needed;
 
@@ -10215,9 +10210,50 @@ static void HandleElementChange(int x, int y, int page)
     {
       /* !!! not clear why graphic animation should be reset at all here !!! */
       /* !!! UPDATE: but is needed for correct Snake Bite tail animation !!! */
+      /* !!! SOLUTION: do not reset if graphics engine set to 4 or above !!! */
+
+      /*
+       GRAPHICAL BUG ADDRESSED BY CHECKING GRAPHICS ENGINE VERSION:
+
+       When using an animation frame delay of 1 (this only happens with
+       "sp_zonk.moving.left/right" in the classic graphics), the default
+       (non-moving) animation shows wrong animation frames (while the
+       moving animation, like "sp_zonk.moving.left/right", is correct,
+       so this graphical bug never shows up with the classic graphics).
+       For an animation with 4 frames, this causes wrong frames 0,0,1,2
+       be drawn instead of the correct frames 0,1,2,3. This is caused by
+       "GfxFrame[][]" being reset *twice* (in two successive frames) after
+       an element change: First when the change delay ("ChangeDelay[][]")
+       counter has reached zero after decrementing, then a second time in
+       the next frame (after "GfxFrame[][]" was already incremented) when
+       "ChangeDelay[][]" is reset to the initial delay value again.
+
+       This causes frame 0 to be drawn twice, while the last frame won't
+       be drawn anymore, resulting in the wrong frame sequence 0,0,1,2.
+
+       As some animations may already be cleverly designed around this bug
+       (at least the "Snake Bite" snake tail animation does this), it cannot
+       simply be fixed here without breaking such existing animations.
+       Unfortunately, it cannot easily be detected if a graphics set was
+       designed "before" or "after" the bug was fixed. As a workaround,
+       a new graphics set option "game.graphics_engine_version" was added
+       to be able to specify the game's major release version for which the
+       graphics set was designed, which can then be used to decide if the
+       bugfix should be used (version 4 and above) or not (version 3 or
+       below, or if no version was specified at all, as with old sets).
+
+       (The wrong/fixed animation frames can be tested with the test level set
+       "test_gfxframe" and level "000", which contains a specially prepared
+       custom element at level position (x/y) == (11/9) which uses the zonk
+       animation mentioned above. Using "game.graphics_engine_version: 4"
+       fixes the wrong animation frames, showing the correct frames 0,1,2,3.
+       This can also be seen from the debug output for this test element.)
+      */
+
       /* when a custom element is about to change (for example by change delay),
         do not reset graphic animation when the custom element is moving */
-      if (!IS_MOVING(x, y))
+      if (game.graphics_engine_version < 4 &&
+         !IS_MOVING(x, y))
       {
        ResetGfxAnimation(x, y);
        ResetRandomAnimationValue(x, y);
@@ -10667,6 +10703,33 @@ static void SetPlayerWaiting(struct PlayerInfo *player, boolean is_waiting)
   }
 }
 
+static void CheckSaveEngineSnapshot(struct PlayerInfo *player)
+{
+  if ((!player->is_moving  && player->was_moving) ||
+      (player->MovPos == 0 && player->was_moving) ||
+      (player->is_snapping && !player->was_snapping) ||
+      (player->is_dropping && !player->was_dropping))
+  {
+    if (!CheckSaveEngineSnapshotToList())
+      return;
+
+    player->was_moving = FALSE;
+    player->was_snapping = TRUE;
+    player->was_dropping = TRUE;
+  }
+  else
+  {
+    if (player->is_moving)
+      player->was_moving = TRUE;
+
+    if (!player->is_snapping)
+      player->was_snapping = FALSE;
+
+    if (!player->is_dropping)
+      player->was_dropping = FALSE;
+  }
+}
+
 static void CheckSingleStepMode(struct PlayerInfo *player)
 {
   if (tape.single_step && tape.recording && !tape.pausing)
@@ -10679,6 +10742,8 @@ static void CheckSingleStepMode(struct PlayerInfo *player)
       SnapField(player, 0, 0);                 /* stop snapping */
     }
   }
+
+  CheckSaveEngineSnapshot(player);
 }
 
 static byte PlayerActions(struct PlayerInfo *player, byte player_action)
@@ -10831,6 +10896,9 @@ static void CheckLevelTime()
       DrawVideoDisplay(VIDEO_STATE_TIME_ON, TapeTime);
   }
 
+  if (tape.recording || tape.playing)
+    DrawVideoDisplay(VIDEO_STATE_FRAME_ON, FrameCounter);
+
   UpdateAndDisplayGameControlValues();
 }
 
@@ -10903,9 +10971,11 @@ void StartGameActions(boolean init_network_game, boolean record_tape,
   InitGame();
 }
 
-void GameActions()
+void GameActionsExt()
 {
+#if 0
   static unsigned int game_frame_delay = 0;
+#endif
   unsigned int game_frame_delay_value;
   byte *recorded_player_action;
   byte summarized_player_action = 0;
@@ -10983,9 +11053,22 @@ void GameActions()
   if (tape.playing && tape.warp_forward && !tape.pausing)
     game_frame_delay_value = 0;
 
+  SetVideoFrameDelay(game_frame_delay_value);
+
+#if 0
+#if 0
+  /* ---------- main game synchronization point ---------- */
+
+  int skip = WaitUntilDelayReached(&game_frame_delay, game_frame_delay_value);
+
+  printf("::: skip == %d\n", skip);
+
+#else
   /* ---------- main game synchronization point ---------- */
 
   WaitUntilDelayReached(&game_frame_delay, game_frame_delay_value);
+#endif
+#endif
 
   if (network_playing && !network_player_action_received)
   {
@@ -11039,8 +11122,11 @@ void GameActions()
     SendToServer_MovePlayer(summarized_player_action);
 #endif
 
+  // summarize all actions at local players mapped input device position
+  // (this allows using different input devices in single player mode)
   if (!options.network && !game.team_mode)
-    local_player->effective_action = summarized_player_action;
+    stored_player[map_player_action[local_player->index_nr]].effective_action =
+      summarized_player_action;
 
   if (tape.recording &&
       setup.team_mode &&
@@ -11074,7 +11160,9 @@ void GameActions()
     TapeRecordAction(tape_action);
 
 #if USE_NEW_PLAYER_ASSIGNMENTS
-  if (game.team_mode)
+  // !!! also map player actions in single player mode !!!
+  // if (game.team_mode)
+  if (1)
   {
     byte mapped_action[MAX_PLAYERS];
 
@@ -11108,6 +11196,21 @@ void GameActions()
 #endif
 #endif
 
+  for (i = 0; i < MAX_PLAYERS; i++)
+  {
+    // allow engine snapshot in case of changed movement attempt
+    if ((game.snapshot.last_action[i] & KEY_MOTION) !=
+       (stored_player[i].effective_action & KEY_MOTION))
+      game.snapshot.changed_action = TRUE;
+
+    // allow engine snapshot in case of snapping/dropping attempt
+    if ((game.snapshot.last_action[i] & KEY_BUTTON) == 0 &&
+       (stored_player[i].effective_action & KEY_BUTTON) != 0)
+      game.snapshot.changed_action = TRUE;
+
+    game.snapshot.last_action[i] = stored_player[i].effective_action;
+  }
+
   if (level.game_engine_type == GAME_ENGINE_TYPE_EM)
   {
     GameActions_EM_Main();
@@ -11118,10 +11221,53 @@ void GameActions()
   }
   else
   {
-    GameActions_RND();
+    GameActions_RND_Main();
+  }
+
+  BlitScreenToBitmap(backbuffer);
+
+  CheckLevelTime();
+
+  AdvanceFrameAndPlayerCounters(-1);   /* advance counters for all players */
+
+  if (options.debug)                   /* calculate frames per second */
+  {
+    static unsigned int fps_counter = 0;
+    static int fps_frames = 0;
+    unsigned int fps_delay_ms = Counter() - fps_counter;
+
+    fps_frames++;
+
+    if (fps_delay_ms >= 500)   /* calculate fps every 0.5 seconds */
+    {
+      global.frames_per_second = 1000 * (float)fps_frames / fps_delay_ms;
+
+      fps_frames = 0;
+      fps_counter = Counter();
+    }
+
+    redraw_mask |= REDRAW_FPS;
   }
 }
 
+static void GameActions_CheckSaveEngineSnapshot()
+{
+  if (!game.snapshot.save_snapshot)
+    return;
+
+  // clear flag for saving snapshot _before_ saving snapshot
+  game.snapshot.save_snapshot = FALSE;
+
+  SaveEngineSnapshotToList();
+}
+
+void GameActions()
+{
+  GameActionsExt();
+
+  GameActions_CheckSaveEngineSnapshot();
+}
+
 void GameActions_EM_Main()
 {
   byte effective_action[MAX_PLAYERS];
@@ -11132,10 +11278,6 @@ void GameActions_EM_Main()
     effective_action[i] = stored_player[i].effective_action;
 
   GameActions_EM(effective_action, warp_mode);
-
-  CheckLevelTime();
-
-  AdvanceFrameAndPlayerCounters(-1);   /* advance counters for all players */
 }
 
 void GameActions_SP_Main()
@@ -11148,10 +11290,11 @@ void GameActions_SP_Main()
     effective_action[i] = stored_player[i].effective_action;
 
   GameActions_SP(effective_action, warp_mode);
+}
 
-  CheckLevelTime();
-
-  AdvanceFrameAndPlayerCounters(-1);   /* advance counters for all players */
+void GameActions_RND_Main()
+{
+  GameActions_RND();
 }
 
 void GameActions_RND()
@@ -11653,32 +11796,9 @@ void GameActions_RND()
   }
 #endif
 
-  CheckLevelTime();
-
   DrawAllPlayers();
   PlayAllPlayersSound();
 
-  if (options.debug)                   /* calculate frames per second */
-  {
-    static unsigned int fps_counter = 0;
-    static int fps_frames = 0;
-    unsigned int fps_delay_ms = Counter() - fps_counter;
-
-    fps_frames++;
-
-    if (fps_delay_ms >= 500)   /* calculate fps every 0.5 seconds */
-    {
-      global.frames_per_second = 1000 * (float)fps_frames / fps_delay_ms;
-
-      fps_frames = 0;
-      fps_counter = Counter();
-    }
-
-    redraw_mask |= REDRAW_FPS;
-  }
-
-  AdvanceFrameAndPlayerCounters(-1);   /* advance counters for all players */
-
   if (local_player->show_envelope != 0 && local_player->MovPos == 0)
   {
     ShowEnvelope(local_player->show_envelope - EL_ENVELOPE_1);
@@ -11732,16 +11852,16 @@ static boolean AllPlayersInVisibleScreen()
 
 void ScrollLevel(int dx, int dy)
 {
-  int softscroll_offset = (setup.soft_scrolling ? 2 * TILEX_VAR : 0);
+  int scroll_offset = 2 * TILEX_VAR;
   int x, y;
 
   BlitBitmap(drawto_field, drawto_field,
-            FX + TILEX_VAR * (dx == -1) - softscroll_offset,
-            FY + TILEY_VAR * (dy == -1) - softscroll_offset,
-            SXSIZE - TILEX_VAR * (dx != 0) + 2 * softscroll_offset,
-            SYSIZE - TILEY_VAR * (dy != 0) + 2 * softscroll_offset,
-            FX + TILEX_VAR * (dx == 1) - softscroll_offset,
-            FY + TILEY_VAR * (dy == 1) - softscroll_offset);
+            FX + TILEX_VAR * (dx == -1) - scroll_offset,
+            FY + TILEY_VAR * (dy == -1) - scroll_offset,
+            SXSIZE - TILEX_VAR * (dx != 0) + 2 * scroll_offset,
+            SYSIZE - TILEY_VAR * (dy != 0) + 2 * scroll_offset,
+            FX + TILEX_VAR * (dx == 1) - scroll_offset,
+            FY + TILEY_VAR * (dy == 1) - scroll_offset);
 
   if (dx != 0)
   {
@@ -11978,7 +12098,7 @@ boolean MovePlayer(struct PlayerInfo *player, int dx, int dy)
       AdvanceFrameAndPlayerCounters(player->index_nr);
 
       DrawAllPlayers();
-      BackToFront();
+      BackToFront_WithFrameDelay(0);
     }
 
     player->move_delay_value = original_move_delay_value;
@@ -12303,6 +12423,9 @@ void ScrollPlayer(struct PlayerInfo *player, int mode)
     if (tape.single_step && tape.recording && !tape.pausing &&
        !player->programmed_action)
       TapeTogglePause(TAPE_TOGGLE_AUTOMATIC);
+
+    if (!player->programmed_action)
+      CheckSaveEngineSnapshot(player);
   }
 }
 
@@ -13387,6 +13510,8 @@ static int DigField(struct PlayerInfo *player,
       if (local_player->gems_still_needed < 0)
        local_player->gems_still_needed = 0;
 
+      game.snapshot.collected_item = TRUE;
+
       game_panel_controls[GAME_PANEL_GEMS].value = local_player->gems_still_needed;
 
       DisplayGameControlValues();
@@ -13585,9 +13710,16 @@ static int DigField(struct PlayerInfo *player,
       SCAN_PLAYFIELD(xx, yy)
       {
        if (Feld[xx][yy] == EL_SP_DISK_YELLOW)
+       {
          Bang(xx, yy);
+       }
        else if (Feld[xx][yy] == EL_SP_TERMINAL)
+       {
          Feld[xx][yy] = EL_SP_TERMINAL_ACTIVE;
+
+         ResetGfxAnimation(xx, yy);
+         TEST_DrawLevelField(xx, yy);
+       }
       }
     }
     else if (IS_BELT_SWITCH(element))
@@ -14379,19 +14511,11 @@ void RequestQuitGameExt(boolean skip_request, boolean quick_quit, char *message)
 #endif
     {
       if (quick_quit)
-      {
        FadeSkipNextFadeIn();
 
-       game_status = GAME_MODE_MAIN;
+      SetGameStatus(GAME_MODE_MAIN);
 
-       DrawAndFadeInMainMenu(REDRAW_FIELD);
-      }
-      else
-      {
-       game_status = GAME_MODE_MAIN;
-
-       DrawAndFadeInMainMenu(REDRAW_FIELD);
-      }
+      DrawMainMenu();
     }
   }
   else         /* continue playing the game */
@@ -14546,22 +14670,22 @@ static void LoadEngineSnapshotValues_RND()
   }
 }
 
-void FreeEngineSnapshot()
+void FreeEngineSnapshotSingle()
 {
-  FreeEngineSnapshotBuffers();
+  FreeSnapshotSingle();
 
   setString(&snapshot_level_identifier, NULL);
   snapshot_level_nr = -1;
 }
 
-void SaveEngineSnapshot()
+void FreeEngineSnapshotList()
 {
-  /* do not save snapshots from editor */
-  if (level_editor_test_game)
-    return;
+  FreeSnapshotList();
+}
 
-  /* free previous snapshot buffers, if needed */
-  FreeEngineSnapshotBuffers();
+ListNode *SaveEngineSnapshotBuffers()
+{
+  ListNode *buffers = NULL;
 
   /* copy some special values to a structure better suited for the snapshot */
 
@@ -14570,87 +14694,82 @@ void SaveEngineSnapshot()
   if (level.game_engine_type == GAME_ENGINE_TYPE_EM)
     SaveEngineSnapshotValues_EM();
   if (level.game_engine_type == GAME_ENGINE_TYPE_SP)
-    SaveEngineSnapshotValues_SP();
+    SaveEngineSnapshotValues_SP(&buffers);
 
   /* save values stored in special snapshot structure */
 
   if (level.game_engine_type == GAME_ENGINE_TYPE_RND)
-    SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(engine_snapshot_rnd));
+    SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(engine_snapshot_rnd));
   if (level.game_engine_type == GAME_ENGINE_TYPE_EM)
-    SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(engine_snapshot_em));
+    SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(engine_snapshot_em));
   if (level.game_engine_type == GAME_ENGINE_TYPE_SP)
-    SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(engine_snapshot_sp));
+    SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(engine_snapshot_sp));
 
   /* save further RND engine values */
 
-  SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(stored_player));
-  SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(game));
-  SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(tape));
-
-  SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(ZX));
-  SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(ZY));
-  SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(ExitX));
-  SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(ExitY));
-
-  SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(FrameCounter));
-  SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(TimeFrames));
-  SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(TimePlayed));
-  SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(TimeLeft));
-  SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(TapeTime));
-
-  SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(ScreenMovDir));
-  SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(ScreenMovPos));
-  SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(ScreenGfxPos));
-
-  SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(ScrollStepSize));
-
-  SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(AllPlayersGone));
-
-  SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(AmoebaCnt));
-  SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(AmoebaCnt2));
-
-  SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(Feld));
-  SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(MovPos));
-  SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(MovDir));
-  SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(MovDelay));
-  SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(ChangeDelay));
-  SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(ChangePage));
-  SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(CustomValue));
-  SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(Store));
-  SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(Store2));
-  SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(StorePlayer));
-  SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(Back));
-  SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(AmoebaNr));
-  SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(WasJustMoving));
-  SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(WasJustFalling));
-  SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(CheckCollision));
-  SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(CheckImpact));
-  SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(Stop));
-  SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(Pushed));
-
-  SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(ChangeCount));
-  SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(ChangeEvent));
-
-  SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(ExplodePhase));
-  SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(ExplodeDelay));
-  SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(ExplodeField));
-
-  SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(RunnerVisit));
-  SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(PlayerVisit));
-
-  SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(GfxFrame));
-  SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(GfxRandom));
-  SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(GfxElement));
-  SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(GfxAction));
-  SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(GfxDir));
-
-  SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(scroll_x));
-  SaveEngineSnapshotBuffer(ARGS_ADDRESS_AND_SIZEOF(scroll_y));
-
-  /* save level identification information */
-
-  setString(&snapshot_level_identifier, leveldir_current->identifier);
-  snapshot_level_nr = level_nr;
+  SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(stored_player));
+  SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(game));
+  SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(tape));
+
+  SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(ZX));
+  SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(ZY));
+  SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(ExitX));
+  SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(ExitY));
+
+  SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(FrameCounter));
+  SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(TimeFrames));
+  SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(TimePlayed));
+  SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(TimeLeft));
+  SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(TapeTime));
+
+  SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(ScreenMovDir));
+  SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(ScreenMovPos));
+  SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(ScreenGfxPos));
+
+  SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(ScrollStepSize));
+
+  SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(AllPlayersGone));
+
+  SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(AmoebaCnt));
+  SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(AmoebaCnt2));
+
+  SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(Feld));
+  SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(MovPos));
+  SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(MovDir));
+  SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(MovDelay));
+  SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(ChangeDelay));
+  SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(ChangePage));
+  SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(CustomValue));
+  SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(Store));
+  SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(Store2));
+  SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(StorePlayer));
+  SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(Back));
+  SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(AmoebaNr));
+  SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(WasJustMoving));
+  SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(WasJustFalling));
+  SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(CheckCollision));
+  SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(CheckImpact));
+  SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(Stop));
+  SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(Pushed));
+
+  SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(ChangeCount));
+  SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(ChangeEvent));
+
+  SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(ExplodePhase));
+  SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(ExplodeDelay));
+  SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(ExplodeField));
+
+  SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(RunnerVisit));
+  SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(PlayerVisit));
+
+  SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(GfxFrame));
+  SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(GfxRandom));
+  SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(GfxElement));
+  SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(GfxAction));
+  SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(GfxDir));
+
+  SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(scroll_x));
+  SaveSnapshotBuffer(&buffers, ARGS_ADDRESS_AND_SIZEOF(scroll_y));
 
 #if 0
   ListNode *node = engine_snapshot_list_rnd;
@@ -14665,14 +14784,59 @@ void SaveEngineSnapshot()
 
   printf("::: size of engine snapshot: %d bytes\n", num_bytes);
 #endif
+
+  return buffers;
+}
+
+void SaveEngineSnapshotSingle()
+{
+  ListNode *buffers = SaveEngineSnapshotBuffers();
+
+  /* finally save all snapshot buffers to single snapshot */
+  SaveSnapshotSingle(buffers);
+
+  /* save level identification information */
+  setString(&snapshot_level_identifier, leveldir_current->identifier);
+  snapshot_level_nr = level_nr;
+}
+
+boolean CheckSaveEngineSnapshotToList()
+{
+  boolean save_snapshot =
+    ((game.snapshot.mode == SNAPSHOT_MODE_EVERY_STEP) ||
+     (game.snapshot.mode == SNAPSHOT_MODE_EVERY_MOVE &&
+      game.snapshot.changed_action) ||
+     (game.snapshot.mode == SNAPSHOT_MODE_EVERY_COLLECT &&
+      game.snapshot.collected_item));
+
+  game.snapshot.changed_action = FALSE;
+  game.snapshot.collected_item = FALSE;
+  game.snapshot.save_snapshot = save_snapshot;
+
+  return save_snapshot;
+}
+
+void SaveEngineSnapshotToList()
+{
+  if (game.snapshot.mode == SNAPSHOT_MODE_OFF ||
+      tape.quick_resume)
+    return;
+
+  ListNode *buffers = SaveEngineSnapshotBuffers();
+
+  /* finally save all snapshot buffers to snapshot list */
+  SaveSnapshotToList(buffers);
 }
 
-void LoadEngineSnapshot()
+void SaveEngineSnapshotToListInitial()
 {
-  /* restore generically stored snapshot buffers */
+  FreeEngineSnapshotList();
 
-  LoadEngineSnapshotBuffers();
+  SaveEngineSnapshotToList();
+}
 
+void LoadEngineSnapshotValues()
+{
   /* restore special values from snapshot structure */
 
   if (level.game_engine_type == GAME_ENGINE_TYPE_RND)
@@ -14683,54 +14847,92 @@ void LoadEngineSnapshot()
     LoadEngineSnapshotValues_SP();
 }
 
-boolean CheckEngineSnapshot()
+void LoadEngineSnapshotSingle()
+{
+  LoadSnapshotSingle();
+
+  LoadEngineSnapshotValues();
+}
+
+void LoadEngineSnapshot_Undo(int steps)
+{
+  LoadSnapshotFromList_Older(steps);
+
+  LoadEngineSnapshotValues();
+}
+
+void LoadEngineSnapshot_Redo(int steps)
+{
+  LoadSnapshotFromList_Newer(steps);
+
+  LoadEngineSnapshotValues();
+}
+
+boolean CheckEngineSnapshotSingle()
 {
   return (strEqual(snapshot_level_identifier, leveldir_current->identifier) &&
          snapshot_level_nr == level_nr);
 }
 
+boolean CheckEngineSnapshotList()
+{
+  return CheckSnapshotList();
+}
+
 
 /* ---------- new game button stuff ---------------------------------------- */
 
 static struct
 {
   int graphic;
-  struct Rect *pos;
+  struct XY *pos;
   int gadget_id;
   char *infotext;
 } gamebutton_info[NUM_GAME_BUTTONS] =
 {
   {
-    IMG_GAME_BUTTON_GFX_STOP,          &game.button.stop,
+    IMG_GFX_GAME_BUTTON_STOP,          &game.button.stop,
     GAME_CTRL_ID_STOP,                 "stop game"
   },
   {
-    IMG_GAME_BUTTON_GFX_PAUSE,         &game.button.pause,
+    IMG_GFX_GAME_BUTTON_PAUSE,         &game.button.pause,
     GAME_CTRL_ID_PAUSE,                        "pause game"
   },
   {
-    IMG_GAME_BUTTON_GFX_PLAY,          &game.button.play,
+    IMG_GFX_GAME_BUTTON_PLAY,          &game.button.play,
     GAME_CTRL_ID_PLAY,                 "play game"
   },
   {
-    IMG_GAME_BUTTON_GFX_SOUND_MUSIC,   &game.button.sound_music,
-    SOUND_CTRL_ID_MUSIC,               "background music on/off"
+    IMG_GFX_GAME_BUTTON_UNDO,          &game.button.undo,
+    GAME_CTRL_ID_UNDO,                 "undo step"
   },
   {
-    IMG_GAME_BUTTON_GFX_SOUND_LOOPS,   &game.button.sound_loops,
-    SOUND_CTRL_ID_LOOPS,               "sound loops on/off"
+    IMG_GFX_GAME_BUTTON_REDO,          &game.button.redo,
+    GAME_CTRL_ID_REDO,                 "redo step"
   },
   {
-    IMG_GAME_BUTTON_GFX_SOUND_SIMPLE,  &game.button.sound_simple,
-    SOUND_CTRL_ID_SIMPLE,              "normal sounds on/off"
+    IMG_GFX_GAME_BUTTON_SAVE,          &game.button.save,
+    GAME_CTRL_ID_SAVE,                 "save game"
   },
   {
-    IMG_GAME_BUTTON_GFX_SAVE,          &game.button.save,
-    GAME_CTRL_ID_SAVE,                 "save game"
+    IMG_GFX_GAME_BUTTON_PAUSE2,                &game.button.pause2,
+    GAME_CTRL_ID_PAUSE2,               "pause game"
   },
   {
-    IMG_GAME_BUTTON_GFX_LOAD,          &game.button.load,
+    IMG_GFX_GAME_BUTTON_LOAD,          &game.button.load,
     GAME_CTRL_ID_LOAD,                 "load game"
+  },
+  {
+    IMG_GFX_GAME_BUTTON_SOUND_MUSIC,   &game.button.sound_music,
+    SOUND_CTRL_ID_MUSIC,               "background music on/off"
+  },
+  {
+    IMG_GFX_GAME_BUTTON_SOUND_LOOPS,   &game.button.sound_loops,
+    SOUND_CTRL_ID_LOOPS,               "sound loops on/off"
+  },
+  {
+    IMG_GFX_GAME_BUTTON_SOUND_SIMPLE,  &game.button.sound_simple,
+    SOUND_CTRL_ID_SIMPLE,              "normal sounds on/off"
   }
 };
 
@@ -14741,7 +14943,7 @@ void CreateGameButtons()
   for (i = 0; i < NUM_GAME_BUTTONS; i++)
   {
     struct GraphicInfo *gfx = &graphic_info[gamebutton_info[i].graphic];
-    struct Rect *pos = gamebutton_info[i].pos;
+    struct XY *pos = gamebutton_info[i].pos;
     struct GadgetInfo *gi;
     int button_type;
     boolean checked;
@@ -14766,7 +14968,6 @@ void CreateGameButtons()
     }
 
     if (id == GAME_CTRL_ID_STOP ||
-       id == GAME_CTRL_ID_PAUSE ||
        id == GAME_CTRL_ID_PLAY ||
        id == GAME_CTRL_ID_SAVE ||
        id == GAME_CTRL_ID_LOAD)
@@ -14775,6 +14976,13 @@ void CreateGameButtons()
       checked = FALSE;
       event_mask = GD_EVENT_RELEASED;
     }
+    else if (id == GAME_CTRL_ID_UNDO ||
+            id == GAME_CTRL_ID_REDO)
+    {
+      button_type = GD_TYPE_NORMAL_BUTTON;
+      checked = FALSE;
+      event_mask = GD_EVENT_PRESSED | GD_EVENT_REPEATED;
+    }
     else
     {
       button_type = GD_TYPE_CHECK_BUTTON;
@@ -14818,12 +15026,80 @@ void FreeGameButtons()
     FreeGadget(game_gadget[i]);
 }
 
+static void UnmapGameButtonsAtSamePosition(int id)
+{
+  int i;
+
+  for (i = 0; i < NUM_GAME_BUTTONS; i++)
+    if (i != id &&
+       gamebutton_info[i].pos->x == gamebutton_info[id].pos->x &&
+       gamebutton_info[i].pos->y == gamebutton_info[id].pos->y)
+      UnmapGadget(game_gadget[i]);
+}
+
+static void UnmapGameButtonsAtSamePosition_All()
+{
+  if (setup.show_snapshot_buttons)
+  {
+    UnmapGameButtonsAtSamePosition(GAME_CTRL_ID_SAVE);
+    UnmapGameButtonsAtSamePosition(GAME_CTRL_ID_PAUSE2);
+    UnmapGameButtonsAtSamePosition(GAME_CTRL_ID_LOAD);
+  }
+  else
+  {
+    UnmapGameButtonsAtSamePosition(GAME_CTRL_ID_STOP);
+    UnmapGameButtonsAtSamePosition(GAME_CTRL_ID_PAUSE);
+    UnmapGameButtonsAtSamePosition(GAME_CTRL_ID_PLAY);
+  }
+}
+
+static void MapGameButtonsAtSamePosition(int id)
+{
+  int i;
+
+  for (i = 0; i < NUM_GAME_BUTTONS; i++)
+    if (i != id &&
+       gamebutton_info[i].pos->x == gamebutton_info[id].pos->x &&
+       gamebutton_info[i].pos->y == gamebutton_info[id].pos->y)
+      MapGadget(game_gadget[i]);
+
+  UnmapGameButtonsAtSamePosition_All();
+}
+
+void MapUndoRedoButtons()
+{
+  UnmapGameButtonsAtSamePosition(GAME_CTRL_ID_UNDO);
+  UnmapGameButtonsAtSamePosition(GAME_CTRL_ID_REDO);
+
+  MapGadget(game_gadget[GAME_CTRL_ID_UNDO]);
+  MapGadget(game_gadget[GAME_CTRL_ID_REDO]);
+
+  ModifyGadget(game_gadget[GAME_CTRL_ID_PAUSE2], GDI_CHECKED, TRUE, GDI_END);
+}
+
+void UnmapUndoRedoButtons()
+{
+  UnmapGadget(game_gadget[GAME_CTRL_ID_UNDO]);
+  UnmapGadget(game_gadget[GAME_CTRL_ID_REDO]);
+
+  MapGameButtonsAtSamePosition(GAME_CTRL_ID_UNDO);
+  MapGameButtonsAtSamePosition(GAME_CTRL_ID_REDO);
+
+  ModifyGadget(game_gadget[GAME_CTRL_ID_PAUSE2], GDI_CHECKED, FALSE, GDI_END);
+}
+
 void MapGameButtons()
 {
   int i;
 
   for (i = 0; i < NUM_GAME_BUTTONS; i++)
-    MapGadget(game_gadget[i]);
+    if (i != GAME_CTRL_ID_UNDO &&
+       i != GAME_CTRL_ID_REDO)
+      MapGadget(game_gadget[i]);
+
+  UnmapGameButtonsAtSamePosition_All();
+
+  RedrawGameButtons();
 }
 
 void UnmapGameButtons()
@@ -14845,8 +15121,47 @@ void RedrawGameButtons()
   redraw_mask &= ~REDRAW_ALL;
 }
 
-static void HandleGameButtonsExt(int id)
+void GameUndoRedoExt()
+{
+  ClearPlayerAction();
+
+  tape.pausing = TRUE;
+
+  RedrawPlayfield();
+  UpdateAndDisplayGameControlValues();
+
+  DrawCompleteVideoDisplay();
+  DrawVideoDisplay(VIDEO_STATE_TIME_ON, TapeTime);
+  DrawVideoDisplay(VIDEO_STATE_FRAME_ON, FrameCounter);
+  DrawVideoDisplay(VIDEO_STATE_1STEP(tape.single_step), 0);
+
+  BackToFront();
+}
+
+void GameUndo(int steps)
+{
+  if (!CheckEngineSnapshotList())
+    return;
+
+  LoadEngineSnapshot_Undo(steps);
+
+  GameUndoRedoExt();
+}
+
+void GameRedo(int steps)
 {
+  if (!CheckEngineSnapshotList())
+    return;
+
+  LoadEngineSnapshot_Redo(steps);
+
+  GameUndoRedoExt();
+}
+
+static void HandleGameButtonsExt(int id, int button)
+{
+  static boolean game_undo_executed = FALSE;
+  int steps = BUTTON_STEPSIZE(button);
   boolean handle_game_buttons =
     (game_status == GAME_MODE_PLAYING ||
      (game_status == GAME_MODE_MAIN && tape.show_game_buttons));
@@ -14868,6 +15183,7 @@ static void HandleGameButtonsExt(int id)
       break;
 
     case GAME_CTRL_ID_PAUSE:
+    case GAME_CTRL_ID_PAUSE2:
       if (options.network && game_status == GAME_MODE_PLAYING)
       {
 #if defined(NETWORK_AVALIABLE)
@@ -14879,6 +15195,9 @@ static void HandleGameButtonsExt(int id)
       }
       else
        TapeTogglePause(TAPE_TOGGLE_MANUAL);
+
+      game_undo_executed = FALSE;
+
       break;
 
     case GAME_CTRL_ID_PLAY:
@@ -14893,13 +15212,37 @@ static void HandleGameButtonsExt(int id)
          SendToServer_ContinuePlaying();
        else
 #endif
-       {
-         tape.pausing = FALSE;
-         DrawVideoDisplay(VIDEO_STATE_PAUSE_OFF, 0);
-       }
+         TapeTogglePause(TAPE_TOGGLE_MANUAL | TAPE_TOGGLE_PLAY_PAUSE);
       }
       break;
 
+    case GAME_CTRL_ID_UNDO:
+      // Important: When using "save snapshot when collecting an item" mode,
+      // load last (current) snapshot for first "undo" after pressing "pause"
+      // (else the last-but-one snapshot would be loaded, because the snapshot
+      // pointer already points to the last snapshot when pressing "pause",
+      // which is fine for "every step/move" mode, but not for "every collect")
+      if (game.snapshot.mode == SNAPSHOT_MODE_EVERY_COLLECT &&
+         !game_undo_executed)
+       steps--;
+
+      game_undo_executed = TRUE;
+
+      GameUndo(steps);
+      break;
+
+    case GAME_CTRL_ID_REDO:
+      GameRedo(steps);
+      break;
+
+    case GAME_CTRL_ID_SAVE:
+      TapeQuickSave();
+      break;
+
+    case GAME_CTRL_ID_LOAD:
+      TapeQuickLoad();
+      break;
+
     case SOUND_CTRL_ID_MUSIC:
       if (setup.sound_music)
       { 
@@ -14939,14 +15282,6 @@ static void HandleGameButtonsExt(int id)
       }
       break;
 
-    case GAME_CTRL_ID_SAVE:
-      TapeQuickSave();
-      break;
-
-    case GAME_CTRL_ID_LOAD:
-      TapeQuickLoad();
-      break;
-
     default:
       break;
   }
@@ -14954,7 +15289,7 @@ static void HandleGameButtonsExt(int id)
 
 static void HandleGameButtons(struct GadgetInfo *gi)
 {
-  HandleGameButtonsExt(gi->custom_id);
+  HandleGameButtonsExt(gi->custom_id, gi->event.button);
 }
 
 void HandleSoundButtonKeys(Key key)