changed click events for global animations to ignore 'consumed' clicks
[rocksndiamonds.git] / src / events.c
index 2dc5a01623be8323f34e618098d016283adf66d0..93b135503b073c32e9a34ee79ccf5fbbcf13562a 100644 (file)
@@ -1,15 +1,13 @@
-/***********************************************************
-* Rocks'n'Diamonds -- McDuffin Strikes Back!               *
-*----------------------------------------------------------*
-* (c) 1995-2006 Artsoft Entertainment                      *
-*               Holger Schemel                             *
-*               Detmolder Strasse 189                      *
-*               33604 Bielefeld                            *
-*               Germany                                    *
-*               e-mail: info@artsoft.org                   *
-*----------------------------------------------------------*
-* events.c                                                 *
-***********************************************************/
+// ============================================================================
+// Rocks'n'Diamonds - McDuffin Strikes Back!
+// ----------------------------------------------------------------------------
+// (c) 1995-2014 by Artsoft Entertainment
+//                         Holger Schemel
+//                 info@artsoft.org
+//                 http://www.artsoft.org/
+// ----------------------------------------------------------------------------
+// events.c
+// ============================================================================
 
 #include "libgame/libgame.h"
 
 #include "editor.h"
 #include "files.h"
 #include "tape.h"
+#include "anim.h"
 #include "network.h"
 
 
 #define        DEBUG_EVENTS            0
 
+#define DEBUG_EVENTS_BUTTON    (DEBUG_EVENTS   * 0)
+#define DEBUG_EVENTS_MOTION    (DEBUG_EVENTS   * 0)
+#define DEBUG_EVENTS_WHEEL     (DEBUG_EVENTS   * 1)
+#define DEBUG_EVENTS_WINDOW    (DEBUG_EVENTS   * 0)
+#define DEBUG_EVENTS_FINGER    (DEBUG_EVENTS   * 0)
+#define DEBUG_EVENTS_TEXT      (DEBUG_EVENTS   * 1)
+#define DEBUG_EVENTS_KEY       (DEBUG_EVENTS   * 1)
 
-static boolean cursor_inside_playfield = FALSE;
-static boolean playfield_cursor_set = FALSE;
-static unsigned long playfield_cursor_delay = 0;
 
+static boolean cursor_inside_playfield = FALSE;
+static int cursor_mode_last = CURSOR_DEFAULT;
+static unsigned int special_cursor_delay = 0;
+static unsigned int special_cursor_delay_value = 1000;
 
 /* event filter especially needed for SDL event filtering due to
    delay problems with lots of mouse motion events when mouse button
    not pressed (X11 can handle this with 'PointerMotionHintMask') */
 
-int FilterMouseMotionEvents(const Event *event)
+/* event filter addition for SDL2: as SDL2 does not have a function to enable
+   or disable keyboard auto-repeat, filter repeated keyboard events instead */
+
+static int FilterEvents(const Event *event)
 {
   MotionEvent *motion;
 
+#if defined(TARGET_SDL2)
+  /* skip repeated key press events if keyboard auto-repeat is disabled */
+  if (event->type == EVENT_KEYPRESS &&
+      event->key.repeat &&
+      !keyrepeat_status)
+    return 0;
+#endif
+
+  if (event->type == EVENT_BUTTONPRESS ||
+      event->type == EVENT_BUTTONRELEASE)
+  {
+    ((ButtonEvent *)event)->x -= video.screen_xoffset;
+    ((ButtonEvent *)event)->y -= video.screen_yoffset;
+  }
+  else if (event->type == EVENT_MOTIONNOTIFY)
+  {
+    ((MotionEvent *)event)->x -= video.screen_xoffset;
+    ((MotionEvent *)event)->y -= video.screen_yoffset;
+  }
+
   /* non-motion events are directly passed to event handler functions */
   if (event->type != EVENT_MOTIONNOTIFY)
     return 1;
@@ -48,34 +78,40 @@ int FilterMouseMotionEvents(const Event *event)
   cursor_inside_playfield = (motion->x >= SX && motion->x < SX + SXSIZE &&
                             motion->y >= SY && motion->y < SY + SYSIZE);
 
-  if (game_status == GAME_MODE_PLAYING && playfield_cursor_set)
+  /* do no reset mouse cursor before all pending events have been processed */
+  if (gfx.cursor_mode == cursor_mode_last &&
+      ((game_status == GAME_MODE_TITLE &&
+       gfx.cursor_mode == CURSOR_NONE) ||
+       (game_status == GAME_MODE_PLAYING &&
+       gfx.cursor_mode == CURSOR_PLAYFIELD)))
   {
     SetMouseCursor(CURSOR_DEFAULT);
-    playfield_cursor_set = FALSE;
-    DelayReached(&playfield_cursor_delay, 0);
+
+    DelayReached(&special_cursor_delay, 0);
+
+    cursor_mode_last = CURSOR_DEFAULT;
   }
 
   /* skip mouse motion events without pressed button outside level editor */
   if (button_status == MB_RELEASED &&
       game_status != GAME_MODE_EDITOR && game_status != GAME_MODE_PLAYING)
     return 0;
-  else
-    return 1;
+
+  return 1;
 }
 
 /* to prevent delay problems, skip mouse motion events if the very next
    event is also a mouse motion event (and therefore effectively only
    handling the last of a row of mouse motion events in the event queue) */
 
-boolean SkipPressedMouseMotionEvent(const Event *event)
+static boolean SkipPressedMouseMotionEvent(const Event *event)
 {
   /* nothing to do if the current event is not a mouse motion event */
   if (event->type != EVENT_MOTIONNOTIFY)
     return FALSE;
 
-  /* only skip motion events with pressed button outside level editor */
-  if (button_status == MB_RELEASED ||
-      game_status == GAME_MODE_EDITOR || game_status == GAME_MODE_PLAYING)
+  /* only skip motion events with pressed button outside the game */
+  if (button_status == MB_RELEASED || game_status == GAME_MODE_PLAYING)
     return FALSE;
 
   if (PendingEvent())
@@ -92,11 +128,15 @@ boolean SkipPressedMouseMotionEvent(const Event *event)
   return FALSE;
 }
 
-/* this is only really needed for non-SDL targets to filter unwanted events;
-   when using SDL with properly installed event filter, this function can be
-   replaced with a simple "NextEvent()" call, but it doesn't hurt either */
+/* this is especially needed for event modifications for the Android target:
+   if mouse coordinates should be modified in the event filter function,
+   using a properly installed SDL event filter does not work, because in
+   the event filter, mouse coordinates in the event structure are still
+   physical pixel positions, not logical (scaled) screen positions, so this
+   has to be handled at a later stage in the event processing functions
+   (when device pixel positions are already converted to screen positions) */
 
-static boolean NextValidEvent(Event *event)
+boolean NextValidEvent(Event *event)
 {
   while (PendingEvent())
   {
@@ -104,7 +144,7 @@ static boolean NextValidEvent(Event *event)
 
     NextEvent(event);
 
-    if (FilterMouseMotionEvents(event))
+    if (FilterEvents(event))
       handle_this_event = TRUE;
 
     if (SkipPressedMouseMotionEvent(event))
@@ -117,78 +157,67 @@ static boolean NextValidEvent(Event *event)
   return FALSE;
 }
 
-void EventLoop(void)
+void HandleEvents()
 {
-  while (1)
+  Event event;
+  unsigned int event_frame_delay = 0;
+  unsigned int event_frame_delay_value = GAME_FRAME_DELAY;
+
+  ResetDelayCounter(&event_frame_delay);
+
+  while (NextValidEvent(&event))
   {
-    if (PendingEvent())                /* got event */
+    switch (event.type)
     {
-      Event event;
+      case EVENT_BUTTONPRESS:
+      case EVENT_BUTTONRELEASE:
+       HandleButtonEvent((ButtonEvent *) &event);
+       break;
 
-      while (NextValidEvent(&event))
-      {
-       switch (event.type)
-       {
-         case EVENT_BUTTONPRESS:
-         case EVENT_BUTTONRELEASE:
-           HandleButtonEvent((ButtonEvent *) &event);
-           break;
-  
-         case EVENT_MOTIONNOTIFY:
-           HandleMotionEvent((MotionEvent *) &event);
-           break;
-  
-         case EVENT_KEYPRESS:
-         case EVENT_KEYRELEASE:
-           HandleKeyEvent((KeyEvent *) &event);
-           break;
-  
-         default:
-           HandleOtherEvents(&event);
-           break;
-       }
-      }
-    }
-    else
-    {
-      /* when playing, display a special mouse pointer inside the playfield */
-      if (game_status == GAME_MODE_PLAYING && !tape.pausing)
-      {
-       if (!playfield_cursor_set && cursor_inside_playfield &&
-           DelayReached(&playfield_cursor_delay, 1000))
-       {
-         SetMouseCursor(CURSOR_PLAYFIELD);
-         playfield_cursor_set = TRUE;
-       }
-      }
-      else if (playfield_cursor_set)
-      {
-       SetMouseCursor(CURSOR_DEFAULT);
-       playfield_cursor_set = FALSE;
-      }
+      case EVENT_MOTIONNOTIFY:
+       HandleMotionEvent((MotionEvent *) &event);
+       break;
 
-      HandleNoEvent();
-    }
+#if defined(TARGET_SDL2)
+      case EVENT_WHEELMOTION:
+       HandleWheelEvent((WheelEvent *) &event);
+       break;
 
-    /* don't use all CPU time when idle; the main loop while playing
-       has its own synchronization and is CPU friendly, too */
+      case SDL_WINDOWEVENT:
+       HandleWindowEvent((WindowEvent *) &event);
+       break;
 
-    if (game_status == GAME_MODE_PLAYING)
-    {
-      HandleGameActions();
-    }
-    else
-    {
-      SyncDisplay();
-      if (!PendingEvent())     /* delay only if no pending events */
-       Delay(10);
-    }
+      case EVENT_FINGERPRESS:
+      case EVENT_FINGERRELEASE:
+      case EVENT_FINGERMOTION:
+       HandleFingerEvent((FingerEvent *) &event);
+       break;
 
-    /* refresh window contents from drawing buffer, if needed */
-    BackToFront();
+      case EVENT_TEXTINPUT:
+       HandleTextEvent((TextEvent *) &event);
+       break;
 
-    if (game_status == GAME_MODE_QUIT)
-      return;
+      case SDL_APP_WILLENTERBACKGROUND:
+      case SDL_APP_DIDENTERBACKGROUND:
+      case SDL_APP_WILLENTERFOREGROUND:
+      case SDL_APP_DIDENTERFOREGROUND:
+       HandlePauseResumeEvent((PauseResumeEvent *) &event);
+       break;
+#endif
+
+      case EVENT_KEYPRESS:
+      case EVENT_KEYRELEASE:
+       HandleKeyEvent((KeyEvent *) &event);
+       break;
+
+      default:
+       HandleOtherEvents(&event);
+       break;
+    }
+
+    // do not handle events for longer than standard frame delay period
+    if (DelayReached(&event_frame_delay, event_frame_delay_value))
+      break;
   }
 }
 
@@ -223,6 +252,10 @@ void HandleOtherEvents(Event *event)
     case SDL_JOYBUTTONUP:
       HandleJoystickEvent(event);
       break;
+
+    case SDL_SYSWMEVENT:
+      HandleWindowManagerEvent(event);
+      break;
 #endif
 
     default:
@@ -230,6 +263,68 @@ void HandleOtherEvents(Event *event)
   }
 }
 
+void HandleMouseCursor()
+{
+  if (game_status == GAME_MODE_TITLE)
+  {
+    /* when showing title screens, hide mouse pointer (if not moved) */
+
+    if (gfx.cursor_mode != CURSOR_NONE &&
+       DelayReached(&special_cursor_delay, special_cursor_delay_value))
+    {
+      SetMouseCursor(CURSOR_NONE);
+    }
+  }
+  else if (game_status == GAME_MODE_PLAYING && (!tape.pausing ||
+                                               tape.single_step))
+  {
+    /* when playing, display a special mouse pointer inside the playfield */
+
+    if (gfx.cursor_mode != CURSOR_PLAYFIELD &&
+       cursor_inside_playfield &&
+       DelayReached(&special_cursor_delay, special_cursor_delay_value))
+    {
+      SetMouseCursor(CURSOR_PLAYFIELD);
+    }
+  }
+  else if (gfx.cursor_mode != CURSOR_DEFAULT)
+  {
+    SetMouseCursor(CURSOR_DEFAULT);
+  }
+
+  /* this is set after all pending events have been processed */
+  cursor_mode_last = gfx.cursor_mode;
+}
+
+void EventLoop(void)
+{
+  while (1)
+  {
+    if (PendingEvent())
+      HandleEvents();
+    else
+      HandleMouseCursor();
+
+    /* also execute after pending events have been processed before */
+    HandleNoEvent();
+
+    /* don't use all CPU time when idle; the main loop while playing
+       has its own synchronization and is CPU friendly, too */
+
+    if (game_status == GAME_MODE_PLAYING)
+      HandleGameActions();
+
+    /* always copy backbuffer to visible screen for every video frame */
+    BackToFront();
+
+    /* reset video frame delay to default (may change again while playing) */
+    SetVideoFrameDelay(MenuFrameDelay);
+
+    if (game_status == GAME_MODE_QUIT)
+      return;
+  }
+}
+
 void ClearEventQueue()
 {
   while (PendingEvent())
@@ -245,11 +340,7 @@ void ClearEventQueue()
        break;
 
       case EVENT_KEYRELEASE:
-#if 1
        ClearPlayerAction();
-#else
-       key_joystick_mapping = 0;
-#endif
        break;
 
       default:
@@ -263,107 +354,791 @@ void ClearPlayerAction()
 {
   int i;
 
-  /* simulate key release events for still pressed keys */
-  key_joystick_mapping = 0;
-  for (i = 0; i < MAX_PLAYERS; i++)
-    stored_player[i].action = 0;
-}
+  /* simulate key release events for still pressed keys */
+  key_joystick_mapping = 0;
+  for (i = 0; i < MAX_PLAYERS; i++)
+    stored_player[i].action = 0;
+}
+
+void SleepWhileUnmapped()
+{
+  boolean window_unmapped = TRUE;
+
+  KeyboardAutoRepeatOn();
+
+  while (window_unmapped)
+  {
+    Event event;
+
+    NextEvent(&event);
+
+    switch (event.type)
+    {
+      case EVENT_BUTTONRELEASE:
+       button_status = MB_RELEASED;
+       break;
+
+      case EVENT_KEYRELEASE:
+       key_joystick_mapping = 0;
+       break;
+
+      case EVENT_MAPNOTIFY:
+       window_unmapped = FALSE;
+       break;
+
+      case EVENT_UNMAPNOTIFY:
+       /* this is only to surely prevent the 'should not happen' case
+        * of recursively looping between 'SleepWhileUnmapped()' and
+        * 'HandleOtherEvents()' which usually calls this funtion.
+        */
+       break;
+
+      default:
+       HandleOtherEvents(&event);
+       break;
+    }
+  }
+
+  if (game_status == GAME_MODE_PLAYING)
+    KeyboardAutoRepeatOffUnlessAutoplay();
+}
+
+void HandleExposeEvent(ExposeEvent *event)
+{
+}
+
+void HandleButtonEvent(ButtonEvent *event)
+{
+#if DEBUG_EVENTS_BUTTON
+  Error(ERR_DEBUG, "BUTTON EVENT: button %d %s, x/y %d/%d\n",
+       event->button,
+       event->type == EVENT_BUTTONPRESS ? "pressed" : "released",
+       event->x, event->y);
+#endif
+
+#if defined(HAS_SCREEN_KEYBOARD)
+  if (video.shifted_up)
+    event->y += video.shifted_up_pos;
+#endif
+
+  motion_status = FALSE;
+
+  if (event->type == EVENT_BUTTONPRESS)
+    button_status = event->button;
+  else
+    button_status = MB_RELEASED;
+
+  HandleButton(event->x, event->y, button_status, event->button);
+}
+
+void HandleMotionEvent(MotionEvent *event)
+{
+  if (button_status == MB_RELEASED && game_status != GAME_MODE_EDITOR)
+    return;
+
+  motion_status = TRUE;
+
+#if DEBUG_EVENTS_MOTION
+  Error(ERR_DEBUG, "MOTION EVENT: button %d moved, x/y %d/%d\n",
+       button_status, event->x, event->y);
+#endif
+
+  HandleButton(event->x, event->y, button_status, button_status);
+}
+
+#if defined(TARGET_SDL2)
+
+void HandleWheelEvent(WheelEvent *event)
+{
+  int button_nr;
+
+#if DEBUG_EVENTS_WHEEL
+#if 1
+  Error(ERR_DEBUG, "WHEEL EVENT: mouse == %d, x/y == %d/%d\n",
+       event->which, event->x, event->y);
+#else
+  // (SDL_MOUSEWHEEL_NORMAL/SDL_MOUSEWHEEL_FLIPPED needs SDL 2.0.4 or newer)
+  Error(ERR_DEBUG, "WHEEL EVENT: mouse == %d, x/y == %d/%d, direction == %s\n",
+       event->which, event->x, event->y,
+       (event->direction == SDL_MOUSEWHEEL_NORMAL ? "SDL_MOUSEWHEEL_NORMAL" :
+        "SDL_MOUSEWHEEL_FLIPPED"));
+#endif
+#endif
+
+  button_nr = (event->x < 0 ? MB_WHEEL_LEFT :
+              event->x > 0 ? MB_WHEEL_RIGHT :
+              event->y < 0 ? MB_WHEEL_DOWN :
+              event->y > 0 ? MB_WHEEL_UP : 0);
+
+#if defined(PLATFORM_WIN32) || defined(PLATFORM_MACOSX)
+  // accelerated mouse wheel available on Mac and Windows
+  wheel_steps = (event->x ? ABS(event->x) : ABS(event->y));
+#else
+  // no accelerated mouse wheel available on Unix/Linux
+  wheel_steps = DEFAULT_WHEEL_STEPS;
+#endif
+
+  motion_status = FALSE;
+
+  button_status = button_nr;
+  HandleButton(0, 0, button_status, -button_nr);
+
+  button_status = MB_RELEASED;
+  HandleButton(0, 0, button_status, -button_nr);
+}
+
+void HandleWindowEvent(WindowEvent *event)
+{
+#if DEBUG_EVENTS_WINDOW
+  int subtype = event->event;
+
+  char *event_name =
+    (subtype == SDL_WINDOWEVENT_SHOWN ? "SDL_WINDOWEVENT_SHOWN" :
+     subtype == SDL_WINDOWEVENT_HIDDEN ? "SDL_WINDOWEVENT_HIDDEN" :
+     subtype == SDL_WINDOWEVENT_EXPOSED ? "SDL_WINDOWEVENT_EXPOSED" :
+     subtype == SDL_WINDOWEVENT_MOVED ? "SDL_WINDOWEVENT_MOVED" :
+     subtype == SDL_WINDOWEVENT_SIZE_CHANGED ? "SDL_WINDOWEVENT_SIZE_CHANGED" :
+     subtype == SDL_WINDOWEVENT_RESIZED ? "SDL_WINDOWEVENT_RESIZED" :
+     subtype == SDL_WINDOWEVENT_MINIMIZED ? "SDL_WINDOWEVENT_MINIMIZED" :
+     subtype == SDL_WINDOWEVENT_MAXIMIZED ? "SDL_WINDOWEVENT_MAXIMIZED" :
+     subtype == SDL_WINDOWEVENT_RESTORED ? "SDL_WINDOWEVENT_RESTORED" :
+     subtype == SDL_WINDOWEVENT_ENTER ? "SDL_WINDOWEVENT_ENTER" :
+     subtype == SDL_WINDOWEVENT_LEAVE ? "SDL_WINDOWEVENT_LEAVE" :
+     subtype == SDL_WINDOWEVENT_FOCUS_GAINED ? "SDL_WINDOWEVENT_FOCUS_GAINED" :
+     subtype == SDL_WINDOWEVENT_FOCUS_LOST ? "SDL_WINDOWEVENT_FOCUS_LOST" :
+     subtype == SDL_WINDOWEVENT_CLOSE ? "SDL_WINDOWEVENT_CLOSE" :
+     "(UNKNOWN)");
+
+  Error(ERR_DEBUG, "WINDOW EVENT: '%s', %ld, %ld",
+       event_name, event->data1, event->data2);
+#endif
+
+#if 0
+  // (not needed, as the screen gets redrawn every 20 ms anyway)
+  if (event->event == SDL_WINDOWEVENT_SIZE_CHANGED ||
+      event->event == SDL_WINDOWEVENT_RESIZED ||
+      event->event == SDL_WINDOWEVENT_EXPOSED)
+    SDLRedrawWindow();
+#endif
+
+  if (event->event == SDL_WINDOWEVENT_RESIZED)
+  {
+    if (!video.fullscreen_enabled)
+    {
+      int new_window_width  = event->data1;
+      int new_window_height = event->data2;
+
+      // if window size has changed after resizing, calculate new scaling factor
+      if (new_window_width  != video.window_width ||
+         new_window_height != video.window_height)
+      {
+       int new_xpercent = 100.0 * new_window_width  / video.screen_width  + .5;
+       int new_ypercent = 100.0 * new_window_height / video.screen_height + .5;
+
+       // (extreme window scaling allowed, but cannot be saved permanently)
+       video.window_scaling_percent = MIN(new_xpercent, new_ypercent);
+       setup.window_scaling_percent =
+         MIN(MAX(MIN_WINDOW_SCALING_PERCENT, video.window_scaling_percent),
+             MAX_WINDOW_SCALING_PERCENT);
+
+       video.window_width  = new_window_width;
+       video.window_height = new_window_height;
+
+       if (game_status == GAME_MODE_SETUP)
+         RedrawSetupScreenAfterFullscreenToggle();
+
+       SetWindowTitle();
+      }
+    }
+#if defined(PLATFORM_ANDROID)
+    else
+    {
+      int new_display_width  = event->data1;
+      int new_display_height = event->data2;
+
+      // if fullscreen display size has changed, device has been rotated
+      if (new_display_width  != video.display_width ||
+         new_display_height != video.display_height)
+      {
+       video.display_width  = new_display_width;
+       video.display_height = new_display_height;
+
+       SDLSetScreenProperties();
+      }
+    }
+#endif
+  }
+}
+
+#define NUM_TOUCH_FINGERS              3
+
+static struct
+{
+  boolean touched;
+  SDL_FingerID finger_id;
+  int counter;
+  Key key;
+} touch_info[NUM_TOUCH_FINGERS];
+
+void HandleFingerEvent(FingerEvent *event)
+{
+  static Key motion_key_x = KSYM_UNDEFINED;
+  static Key motion_key_y = KSYM_UNDEFINED;
+  static Key button_key = KSYM_UNDEFINED;
+  static float motion_x1, motion_y1;
+  static float button_x1, button_y1;
+  static SDL_FingerID motion_id = -1;
+  static SDL_FingerID button_id = -1;
+  int move_trigger_distance_percent = setup.touch.move_distance;
+  int drop_trigger_distance_percent = setup.touch.drop_distance;
+  float move_trigger_distance = (float)move_trigger_distance_percent / 100;
+  float drop_trigger_distance = (float)drop_trigger_distance_percent / 100;
+  float event_x = event->x;
+  float event_y = event->y;
+
+#if DEBUG_EVENTS_FINGER
+  Error(ERR_DEBUG, "FINGER EVENT: finger was %s, touch ID %lld, finger ID %lld, x/y %f/%f, dx/dy %f/%f, pressure %f",
+       event->type == EVENT_FINGERPRESS ? "pressed" :
+       event->type == EVENT_FINGERRELEASE ? "released" : "moved",
+       event->touchId,
+       event->fingerId,
+       event->x, event->y,
+       event->dx, event->dy,
+       event->pressure);
+#endif
+
+  if (game_status != GAME_MODE_PLAYING)
+    return;
+
+  if (strEqual(setup.touch.control_type, TOUCH_CONTROL_FOLLOW_FINGER))
+    return;
+
+  if (strEqual(setup.touch.control_type, TOUCH_CONTROL_VIRTUAL_BUTTONS))
+  {
+    int key_status = (event->type == EVENT_FINGERRELEASE ? KEY_RELEASED :
+                     KEY_PRESSED);
+    float ypos = 1.0 - 1.0 / 3.0 * video.display_width / video.display_height;
+
+    event_y = (event_y - ypos) / (1 - ypos);
+
+    Key key = (event_x > 0         && event_x < 1.0 / 6.0 &&
+              event_y > 2.0 / 3.0 && event_y < 1 ?
+              setup.input[0].key.snap :
+              event_x > 1.0 / 6.0 && event_x < 1.0 / 3.0 &&
+              event_y > 2.0 / 3.0 && event_y < 1 ?
+              setup.input[0].key.drop :
+              event_x > 7.0 / 9.0 && event_x < 8.0 / 9.0 &&
+              event_y > 0         && event_y < 1.0 / 3.0 ?
+              setup.input[0].key.up :
+              event_x > 6.0 / 9.0 && event_x < 7.0 / 9.0 &&
+              event_y > 1.0 / 3.0 && event_y < 2.0 / 3.0 ?
+              setup.input[0].key.left :
+              event_x > 8.0 / 9.0 && event_x < 1 &&
+              event_y > 1.0 / 3.0 && event_y < 2.0 / 3.0 ?
+              setup.input[0].key.right :
+              event_x > 7.0 / 9.0 && event_x < 8.0 / 9.0 &&
+              event_y > 2.0 / 3.0 && event_y < 1 ?
+              setup.input[0].key.down :
+              KSYM_UNDEFINED);
+
+    char *key_status_name = (key_status == KEY_RELEASED ? "KEY_RELEASED" :
+                            "KEY_PRESSED");
+    int i;
+
+    Error(ERR_DEBUG, "::: key '%s' was '%s' [fingerId: %lld]",
+         getKeyNameFromKey(key), key_status_name, event->fingerId);
+
+    // check if we already know this touch event's finger id
+    for (i = 0; i < NUM_TOUCH_FINGERS; i++)
+    {
+      if (touch_info[i].touched &&
+         touch_info[i].finger_id == event->fingerId)
+      {
+       // Error(ERR_DEBUG, "MARK 1: %d", i);
+
+       break;
+      }
+    }
+
+    if (i >= NUM_TOUCH_FINGERS)
+    {
+      if (key_status == KEY_PRESSED)
+      {
+       int oldest_pos = 0, oldest_counter = touch_info[0].counter;
+
+       // unknown finger id -- get new, empty slot, if available
+       for (i = 0; i < NUM_TOUCH_FINGERS; i++)
+       {
+         if (touch_info[i].counter < oldest_counter)
+         {
+           oldest_pos = i;
+           oldest_counter = touch_info[i].counter;
+
+           // Error(ERR_DEBUG, "MARK 2: %d", i);
+         }
+
+         if (!touch_info[i].touched)
+         {
+           // Error(ERR_DEBUG, "MARK 3: %d", i);
+
+           break;
+         }
+       }
+
+       if (i >= NUM_TOUCH_FINGERS)
+       {
+         // all slots allocated -- use oldest slot
+         i = oldest_pos;
+
+         // Error(ERR_DEBUG, "MARK 4: %d", i);
+       }
+      }
+      else
+      {
+       // release of previously unknown key (should not happen)
+
+       if (key != KSYM_UNDEFINED)
+       {
+         HandleKey(key, KEY_RELEASED);
+
+         Error(ERR_DEBUG, "=> key == '%s', key_status == '%s' [slot %d] [1]",
+               getKeyNameFromKey(key), "KEY_RELEASED", i);
+       }
+      }
+    }
+
+    if (i < NUM_TOUCH_FINGERS)
+    {
+      if (key_status == KEY_PRESSED)
+      {
+       if (touch_info[i].key != key)
+       {
+         if (touch_info[i].key != KSYM_UNDEFINED)
+         {
+           HandleKey(touch_info[i].key, KEY_RELEASED);
+
+           Error(ERR_DEBUG, "=> key == '%s', key_status == '%s' [slot %d] [2]",
+                 getKeyNameFromKey(touch_info[i].key), "KEY_RELEASED", i);
+         }
+
+         if (key != KSYM_UNDEFINED)
+         {
+           HandleKey(key, KEY_PRESSED);
+
+           Error(ERR_DEBUG, "=> key == '%s', key_status == '%s' [slot %d] [3]",
+                 getKeyNameFromKey(key), "KEY_PRESSED", i);
+         }
+       }
+
+       touch_info[i].touched = TRUE;
+       touch_info[i].finger_id = event->fingerId;
+       touch_info[i].counter = Counter();
+       touch_info[i].key = key;
+      }
+      else
+      {
+       if (touch_info[i].key != KSYM_UNDEFINED)
+       {
+         HandleKey(touch_info[i].key, KEY_RELEASED);
+
+         Error(ERR_DEBUG, "=> key == '%s', key_status == '%s' [slot %d] [4]",
+               getKeyNameFromKey(touch_info[i].key), "KEY_RELEASED", i);
+       }
+
+       touch_info[i].touched = FALSE;
+       touch_info[i].finger_id = 0;
+       touch_info[i].counter = 0;
+       touch_info[i].key = 0;
+      }
+    }
+
+    return;
+  }
+
+  // use touch direction control
+
+  if (event->type == EVENT_FINGERPRESS)
+  {
+    if (event_x > 1.0 / 3.0)
+    {
+      // motion area
+
+      motion_id = event->fingerId;
+
+      motion_x1 = event_x;
+      motion_y1 = event_y;
+
+      motion_key_x = KSYM_UNDEFINED;
+      motion_key_y = KSYM_UNDEFINED;
+
+      Error(ERR_DEBUG, "---------- MOVE STARTED (WAIT) ----------");
+    }
+    else
+    {
+      // button area
+
+      button_id = event->fingerId;
+
+      button_x1 = event_x;
+      button_y1 = event_y;
+
+      button_key = setup.input[0].key.snap;
+
+      HandleKey(button_key, KEY_PRESSED);
+
+      Error(ERR_DEBUG, "---------- SNAP STARTED ----------");
+    }
+  }
+  else if (event->type == EVENT_FINGERRELEASE)
+  {
+    if (event->fingerId == motion_id)
+    {
+      motion_id = -1;
+
+      if (motion_key_x != KSYM_UNDEFINED)
+       HandleKey(motion_key_x, KEY_RELEASED);
+      if (motion_key_y != KSYM_UNDEFINED)
+       HandleKey(motion_key_y, KEY_RELEASED);
+
+      motion_key_x = KSYM_UNDEFINED;
+      motion_key_y = KSYM_UNDEFINED;
+
+      Error(ERR_DEBUG, "---------- MOVE STOPPED ----------");
+    }
+    else if (event->fingerId == button_id)
+    {
+      button_id = -1;
+
+      if (button_key != KSYM_UNDEFINED)
+       HandleKey(button_key, KEY_RELEASED);
+
+      button_key = KSYM_UNDEFINED;
+
+      Error(ERR_DEBUG, "---------- SNAP STOPPED ----------");
+    }
+  }
+  else if (event->type == EVENT_FINGERMOTION)
+  {
+    if (event->fingerId == motion_id)
+    {
+      float distance_x = ABS(event_x - motion_x1);
+      float distance_y = ABS(event_y - motion_y1);
+      Key new_motion_key_x = (event_x < motion_x1 ? setup.input[0].key.left :
+                             event_x > motion_x1 ? setup.input[0].key.right :
+                             KSYM_UNDEFINED);
+      Key new_motion_key_y = (event_y < motion_y1 ? setup.input[0].key.up :
+                             event_y > motion_y1 ? setup.input[0].key.down :
+                             KSYM_UNDEFINED);
+
+      if (distance_x < move_trigger_distance / 2 ||
+         distance_x < distance_y)
+       new_motion_key_x = KSYM_UNDEFINED;
+
+      if (distance_y < move_trigger_distance / 2 ||
+         distance_y < distance_x)
+       new_motion_key_y = KSYM_UNDEFINED;
+
+      if (distance_x > move_trigger_distance ||
+         distance_y > move_trigger_distance)
+      {
+       if (new_motion_key_x != motion_key_x)
+       {
+         if (motion_key_x != KSYM_UNDEFINED)
+           HandleKey(motion_key_x, KEY_RELEASED);
+         if (new_motion_key_x != KSYM_UNDEFINED)
+           HandleKey(new_motion_key_x, KEY_PRESSED);
+       }
+
+       if (new_motion_key_y != motion_key_y)
+       {
+         if (motion_key_y != KSYM_UNDEFINED)
+           HandleKey(motion_key_y, KEY_RELEASED);
+         if (new_motion_key_y != KSYM_UNDEFINED)
+           HandleKey(new_motion_key_y, KEY_PRESSED);
+       }
+
+       motion_x1 = event_x;
+       motion_y1 = event_y;
+
+       motion_key_x = new_motion_key_x;
+       motion_key_y = new_motion_key_y;
+
+       Error(ERR_DEBUG, "---------- MOVE STARTED (MOVE) ----------");
+      }
+    }
+    else if (event->fingerId == button_id)
+    {
+      float distance_x = ABS(event_x - button_x1);
+      float distance_y = ABS(event_y - button_y1);
+
+      if (distance_x < drop_trigger_distance / 2 &&
+         distance_y > drop_trigger_distance)
+      {
+       if (button_key == setup.input[0].key.snap)
+         HandleKey(button_key, KEY_RELEASED);
+
+       button_x1 = event_x;
+       button_y1 = event_y;
+
+       button_key = setup.input[0].key.drop;
+
+       HandleKey(button_key, KEY_PRESSED);
+
+       Error(ERR_DEBUG, "---------- DROP STARTED ----------");
+      }
+    }
+  }
+}
+
+static void HandleFollowFinger(int mx, int my, int button)
+{
+  static int old_mx = 0, old_my = 0;
+  static Key motion_key_x = KSYM_UNDEFINED;
+  static Key motion_key_y = KSYM_UNDEFINED;
+  static boolean started_on_player = FALSE;
+  static boolean player_is_dropping = FALSE;
+  static int player_drop_count = 0;
+  static int last_player_x = -1;
+  static int last_player_y = -1;
+
+  if (!strEqual(setup.touch.control_type, TOUCH_CONTROL_FOLLOW_FINGER))
+    return;
+
+  if (button == MB_PRESSED && IN_GFX_FIELD_PLAY(mx, my))
+  {
+    touch_info[0].touched = TRUE;
+    touch_info[0].key = 0;
+
+    old_mx = mx;
+    old_my = my;
+
+    if (!motion_status)
+    {
+      started_on_player = FALSE;
+      player_is_dropping = FALSE;
+      player_drop_count = 0;
+      last_player_x = -1;
+      last_player_y = -1;
+
+      motion_key_x = KSYM_UNDEFINED;
+      motion_key_y = KSYM_UNDEFINED;
+
+      Error(ERR_DEBUG, "---------- TOUCH ACTION STARTED ----------");
+    }
+  }
+  else if (button == MB_RELEASED && touch_info[0].touched)
+  {
+    touch_info[0].touched = FALSE;
+    touch_info[0].key = 0;
+
+    old_mx = 0;
+    old_my = 0;
+
+    if (motion_key_x != KSYM_UNDEFINED)
+      HandleKey(motion_key_x, KEY_RELEASED);
+    if (motion_key_y != KSYM_UNDEFINED)
+      HandleKey(motion_key_y, KEY_RELEASED);
+
+    if (started_on_player)
+    {
+      if (player_is_dropping)
+      {
+       Error(ERR_DEBUG, "---------- DROP STOPPED ----------");
+
+       HandleKey(setup.input[0].key.drop, KEY_RELEASED);
+      }
+      else
+      {
+       Error(ERR_DEBUG, "---------- SNAP STOPPED ----------");
+
+       HandleKey(setup.input[0].key.snap, KEY_RELEASED);
+      }
+    }
+
+    motion_key_x = KSYM_UNDEFINED;
+    motion_key_y = KSYM_UNDEFINED;
+
+    Error(ERR_DEBUG, "---------- TOUCH ACTION STOPPED ----------");
+  }
+
+  if (touch_info[0].touched)
+  {
+    int src_x = local_player->jx;
+    int src_y = local_player->jy;
+    int dst_x = getLevelFromScreenX(old_mx);
+    int dst_y = getLevelFromScreenY(old_my);
+    int dx = dst_x - src_x;
+    int dy = dst_y - src_y;
+    Key new_motion_key_x = (dx < 0 ? setup.input[0].key.left :
+                           dx > 0 ? setup.input[0].key.right :
+                           KSYM_UNDEFINED);
+    Key new_motion_key_y = (dy < 0 ? setup.input[0].key.up :
+                           dy > 0 ? setup.input[0].key.down :
+                           KSYM_UNDEFINED);
+
+    if (dx != 0 && dy != 0 && ABS(dx) != ABS(dy) &&
+       (last_player_x != local_player->jx ||
+        last_player_y != local_player->jy))
+    {
+      // in case of asymmetric diagonal movement, use "preferred" direction
+
+      int last_move_dir = (ABS(dx) > ABS(dy) ? MV_VERTICAL : MV_HORIZONTAL);
+
+      if (level.game_engine_type == GAME_ENGINE_TYPE_EM)
+       level.native_em_level->ply[0]->last_move_dir = last_move_dir;
+      else
+       local_player->last_move_dir = last_move_dir;
 
-void SleepWhileUnmapped()
-{
-  boolean window_unmapped = TRUE;
+      // (required to prevent accidentally forcing direction for next movement)
+      last_player_x = local_player->jx;
+      last_player_y = local_player->jy;
+    }
 
-  KeyboardAutoRepeatOn();
+    if (button == MB_PRESSED && !motion_status && dx == 0 && dy == 0)
+    {
+      started_on_player = TRUE;
+      player_drop_count = getPlayerInventorySize(0);
+      player_is_dropping = (player_drop_count > 0);
 
-  while (window_unmapped)
-  {
-    Event event;
+      if (player_is_dropping)
+      {
+       Error(ERR_DEBUG, "---------- DROP STARTED ----------");
 
-    NextEvent(&event);
+       HandleKey(setup.input[0].key.drop, KEY_PRESSED);
+      }
+      else
+      {
+       Error(ERR_DEBUG, "---------- SNAP STARTED ----------");
 
-    switch (event.type)
+       HandleKey(setup.input[0].key.snap, KEY_PRESSED);
+      }
+    }
+    else if (dx != 0 || dy != 0)
     {
-      case EVENT_BUTTONRELEASE:
-       button_status = MB_RELEASED;
-       break;
+      if (player_is_dropping &&
+         player_drop_count == getPlayerInventorySize(0))
+      {
+       Error(ERR_DEBUG, "---------- DROP -> SNAP ----------");
 
-      case EVENT_KEYRELEASE:
-       key_joystick_mapping = 0;
-       break;
+       HandleKey(setup.input[0].key.drop, KEY_RELEASED);
+       HandleKey(setup.input[0].key.snap, KEY_PRESSED);
 
-      case EVENT_MAPNOTIFY:
-       window_unmapped = FALSE;
-       break;
+       player_is_dropping = FALSE;
+      }
+    }
 
-      case EVENT_UNMAPNOTIFY:
-       /* this is only to surely prevent the 'should not happen' case
-        * of recursively looping between 'SleepWhileUnmapped()' and
-        * 'HandleOtherEvents()' which usually calls this funtion.
-        */
-       break;
+    if (new_motion_key_x != motion_key_x)
+    {
+      Error(ERR_DEBUG, "---------- %s %s ----------",
+           started_on_player && !player_is_dropping ? "SNAPPING" : "MOVING",
+           dx < 0 ? "LEFT" : dx > 0 ? "RIGHT" : "PAUSED");
+
+      if (motion_key_x != KSYM_UNDEFINED)
+       HandleKey(motion_key_x, KEY_RELEASED);
+      if (new_motion_key_x != KSYM_UNDEFINED)
+       HandleKey(new_motion_key_x, KEY_PRESSED);
+    }
 
-      default:
-       HandleOtherEvents(&event);
-       break;
+    if (new_motion_key_y != motion_key_y)
+    {
+      Error(ERR_DEBUG, "---------- %s %s ----------",
+           started_on_player && !player_is_dropping ? "SNAPPING" : "MOVING",
+           dy < 0 ? "UP" : dy > 0 ? "DOWN" : "PAUSED");
+
+      if (motion_key_y != KSYM_UNDEFINED)
+       HandleKey(motion_key_y, KEY_RELEASED);
+      if (new_motion_key_y != KSYM_UNDEFINED)
+       HandleKey(new_motion_key_y, KEY_PRESSED);
     }
-  }
 
-  if (game_status == GAME_MODE_PLAYING)
-    KeyboardAutoRepeatOffUnlessAutoplay();
+    motion_key_x = new_motion_key_x;
+    motion_key_y = new_motion_key_y;
+  }
 }
 
-void HandleExposeEvent(ExposeEvent *event)
+static boolean checkTextInputKeyModState()
 {
-#ifndef TARGET_SDL
-  RedrawPlayfield(FALSE, event->x, event->y, event->width, event->height);
-  FlushDisplay();
-#endif
+  // when playing, only handle raw key events and ignore text input
+  if (game_status == GAME_MODE_PLAYING)
+    return FALSE;
+
+  return ((GetKeyModState() & KMOD_TextInput) != KMOD_None);
 }
 
-void HandleButtonEvent(ButtonEvent *event)
+void HandleTextEvent(TextEvent *event)
 {
-#if DEBUG_EVENTS
-  printf("::: BUTTON EVENT: button %d %s\n", event->button,
-        event->type == EVENT_BUTTONPRESS ? "pressed" : "released");
+  char *text = event->text;
+  Key key = getKeyFromKeyName(text);
+
+#if DEBUG_EVENTS_TEXT
+  Error(ERR_DEBUG, "TEXT EVENT: text == '%s' [%d byte(s), '%c'/%d], resulting key == %d (%s) [%04x]",
+       text,
+       strlen(text),
+       text[0], (int)(text[0]),
+       key,
+       getKeyNameFromKey(key),
+       GetKeyModState());
 #endif
 
-  motion_status = FALSE;
-
-  if (event->type == EVENT_BUTTONPRESS)
-    button_status = event->button;
-  else
-    button_status = MB_RELEASED;
+#if !defined(HAS_SCREEN_KEYBOARD)
+  // non-mobile devices: only handle key input with modifier keys pressed here
+  // (every other key input is handled directly as physical key input event)
+  if (!checkTextInputKeyModState())
+    return;
+#endif
 
-  HandleButton(event->x, event->y, button_status, event->button);
+  // process text input as "classic" (with uppercase etc.) key input event
+  HandleKey(key, KEY_PRESSED);
+  HandleKey(key, KEY_RELEASED);
 }
 
-void HandleMotionEvent(MotionEvent *event)
+void HandlePauseResumeEvent(PauseResumeEvent *event)
 {
-  if (!PointerInWindow(window))
-    return;    /* window and pointer are on different screens */
-
-  if (button_status == MB_RELEASED && game_status != GAME_MODE_EDITOR)
-    return;
-
-  motion_status = TRUE;
-
-  HandleButton(event->x, event->y, button_status, button_status);
+  if (event->type == SDL_APP_WILLENTERBACKGROUND)
+  {
+    Mix_PauseMusic();
+  }
+  else if (event->type == SDL_APP_DIDENTERFOREGROUND)
+  {
+    Mix_ResumeMusic();
+  }
 }
 
+#endif
+
 void HandleKeyEvent(KeyEvent *event)
 {
-  int key_status = (event->type==EVENT_KEYPRESS ? KEY_PRESSED : KEY_RELEASED);
+  int key_status = (event->type == EVENT_KEYPRESS ? KEY_PRESSED : KEY_RELEASED);
   boolean with_modifiers = (game_status == GAME_MODE_PLAYING ? FALSE : TRUE);
   Key key = GetEventKey(event, with_modifiers);
   Key keymod = (with_modifiers ? GetEventKey(event, FALSE) : key);
 
-#if DEBUG_EVENTS
-  printf("::: KEY EVENT: %d %s\n", GetEventKey(event, TRUE),
-        event->type == EVENT_KEYPRESS ? "pressed" : "released");
+#if DEBUG_EVENTS_KEY
+  Error(ERR_DEBUG, "KEY EVENT: key was %s, keysym.scancode == %d, keysym.sym == %d, keymod = %d, GetKeyModState() = 0x%04x, resulting key == %d (%s)",
+       event->type == EVENT_KEYPRESS ? "pressed" : "released",
+       event->keysym.scancode,
+       event->keysym.sym,
+       keymod,
+       GetKeyModState(),
+       key,
+       getKeyNameFromKey(key));
+#endif
+
+#if defined(PLATFORM_ANDROID)
+  // always map the "back" button to the "escape" key on Android devices
+  if (key == KSYM_Back)
+    key = KSYM_Escape;
 #endif
 
   HandleKeyModState(keymod, key_status);
+
+#if defined(TARGET_SDL2)
+  // only handle raw key input without text modifier keys pressed
+  if (!checkTextInputKeyModState())
+    HandleKey(key, key_status);
+#else
   HandleKey(key, key_status);
+#endif
 }
 
 void HandleFocusEvent(FocusChangeEvent *event)
@@ -413,15 +1188,24 @@ void HandleClientMessageEvent(ClientMessageEvent *event)
     CloseAllAndExit(0);
 }
 
+void HandleWindowManagerEvent(Event *event)
+{
+#if defined(TARGET_SDL)
+  SDLHandleWindowManagerEvent(event);
+#endif
+}
+
 void HandleButton(int mx, int my, int button, int button_nr)
 {
   static int old_mx = 0, old_my = 0;
+  boolean button_hold = FALSE;
 
-  if (button < 0)
+  if (button_nr < 0)
   {
     mx = old_mx;
     my = old_my;
-    button = -button;
+    button_nr = -button_nr;
+    button_hold = TRUE;
   }
   else
   {
@@ -429,11 +1213,34 @@ void HandleButton(int mx, int my, int button, int button_nr)
     old_my = my;
   }
 
+#if defined(PLATFORM_ANDROID)
+  // when playing, only handle gadgets when using "follow finger" controls
+  boolean handle_gadgets =
+    (game_status != GAME_MODE_PLAYING ||
+     strEqual(setup.touch.control_type, TOUCH_CONTROL_FOLLOW_FINGER));
+
+  if (handle_gadgets &&
+      HandleGadgets(mx, my, button))
+  {
+    /* do not handle this button event anymore */
+    mx = my = -32;     /* force mouse event to be outside screen tiles */
+  }
+#else
   if (HandleGadgets(mx, my, button))
   {
     /* do not handle this button event anymore */
     mx = my = -32;     /* force mouse event to be outside screen tiles */
   }
+#endif
+
+  if (HandleGlobalAnimClicks(mx, my, button))
+  {
+    /* do not handle this button event anymore */
+    mx = my = -32;     /* force mouse event to be outside screen tiles */
+  }
+
+  if (button_hold && game_status == GAME_MODE_PLAYING && tape.pausing)
+    return;
 
   /* do not use scroll wheel button events for anything other than gadgets */
   if (IS_WHEEL_BUTTON(button_nr))
@@ -454,7 +1261,11 @@ void HandleButton(int mx, int my, int button, int button_nr)
       break;
 
     case GAME_MODE_LEVELS:
-      HandleChooseLevel(mx, my, 0, 0, button);
+      HandleChooseLevelSet(mx, my, 0, 0, button);
+      break;
+
+    case GAME_MODE_LEVELNR:
+      HandleChooseLevelNr(mx, my, 0, 0, button);
       break;
 
     case GAME_MODE_SCORES:
@@ -473,11 +1284,17 @@ void HandleButton(int mx, int my, int button, int button_nr)
       HandleSetupScreen(mx, my, 0, 0, button);
       break;
 
+#if defined(TARGET_SDL2)
     case GAME_MODE_PLAYING:
+      HandleFollowFinger(mx, my, button);
+#endif
+
 #ifdef DEBUG
-      if (button == MB_PRESSED && !motion_status && IN_GFX_SCREEN(mx, my))
-       DumpTile(LEVELX((mx - SX) / TILEX), LEVELY((my - SY) / TILEY));
+      if (button == MB_PRESSED && !motion_status && IN_GFX_FIELD_PLAY(mx, my) &&
+         GetKeyModState() & KMOD_Control)
+       DumpTileFromScreen(mx, my);
 #endif
+
       break;
 
     default:
@@ -519,8 +1336,8 @@ static void HandleKeysSpecial(Key key)
   cheat_input[cheat_input_len++] = letter;
   cheat_input[cheat_input_len] = '\0';
 
-#if DEBUG_EVENTS
-  printf("::: '%s' [%d]\n", cheat_input, cheat_input_len);
+#if DEBUG_EVENTS_KEY
+  Error(ERR_DEBUG, "SPECIAL KEY '%s' [%d]\n", cheat_input, cheat_input_len);
 #endif
 
   if (game_status == GAME_MODE_MAIN)
@@ -566,6 +1383,25 @@ static void HandleKeysSpecial(Key key)
     {
       DumpTape(&tape);
     }
+    else if (is_string_suffix(cheat_input, ":fix-tape") ||
+            is_string_suffix(cheat_input, ":ft"))
+    {
+      /* fix single-player tapes that contain player input for more than one
+        player (due to a bug in 3.3.1.2 and earlier versions), which results
+        in playing levels with more than one player in multi-player mode,
+        even though the tape was originally recorded in single-player mode */
+
+      /* remove player input actions for all players but the first one */
+      for (i = 1; i < MAX_PLAYERS; i++)
+       tape.player_participates[i] = FALSE;
+
+      tape.changed = TRUE;
+    }
+    else if (is_string_suffix(cheat_input, ":save-native-level") ||
+            is_string_suffix(cheat_input, ":snl"))
+    {
+      SaveNativeLevel(&level);
+    }
   }
   else if (game_status == GAME_MODE_PLAYING)
   {
@@ -588,23 +1424,80 @@ static void HandleKeysSpecial(Key key)
   }
 }
 
+void HandleKeysDebug(Key key)
+{
+#ifdef DEBUG
+  int i;
+
+  if (game_status == GAME_MODE_PLAYING || !setup.debug.frame_delay_game_only)
+  {
+    boolean mod_key_pressed = (GetKeyModState() != KMOD_None);
+
+    for (i = 0; i < NUM_DEBUG_FRAME_DELAY_KEYS; i++)
+    {
+      if (key == setup.debug.frame_delay_key[i] &&
+         (mod_key_pressed == setup.debug.frame_delay_use_mod_key))
+      {
+       GameFrameDelay = (GameFrameDelay != setup.debug.frame_delay[i] ?
+                         setup.debug.frame_delay[i] : setup.game_frame_delay);
+
+       if (!setup.debug.frame_delay_game_only)
+         MenuFrameDelay = GameFrameDelay;
+
+       SetVideoFrameDelay(GameFrameDelay);
+
+       if (GameFrameDelay > ONE_SECOND_DELAY)
+         Error(ERR_DEBUG, "frame delay == %d ms", GameFrameDelay);
+       else if (GameFrameDelay != 0)
+         Error(ERR_DEBUG, "frame delay == %d ms (max. %d fps / %d %%)",
+               GameFrameDelay, ONE_SECOND_DELAY / GameFrameDelay,
+               GAME_FRAME_DELAY * 100 / GameFrameDelay);
+       else
+         Error(ERR_DEBUG, "frame delay == 0 ms (maximum speed)");
+
+       break;
+      }
+    }
+  }
+
+  if (game_status == GAME_MODE_PLAYING)
+  {
+    if (key == KSYM_d)
+    {
+      options.debug = !options.debug;
+
+      Error(ERR_DEBUG, "debug mode %s",
+           (options.debug ? "enabled" : "disabled"));
+    }
+    else if (key == KSYM_v)
+    {
+      Error(ERR_DEBUG, "currently using game engine version %d",
+           game.engine_version);
+    }
+  }
+#endif
+}
+
 void HandleKey(Key key, int key_status)
 {
   boolean anyTextGadgetActiveOrJustFinished = anyTextGadgetActive();
-  static struct SetupKeyboardInfo custom_key;
+  static boolean ignore_repeated_key = FALSE;
+  static struct SetupKeyboardInfo ski;
+  static struct SetupShortcutInfo ssi;
   static struct
   {
     Key *key_custom;
+    Key *key_snap;
     Key key_default;
     byte action;
   } key_info[] =
   {
-    { &custom_key.left,  DEFAULT_KEY_LEFT,  JOY_LEFT     },
-    { &custom_key.right, DEFAULT_KEY_RIGHT, JOY_RIGHT    },
-    { &custom_key.up,    DEFAULT_KEY_UP,    JOY_UP       },
-    { &custom_key.down,  DEFAULT_KEY_DOWN,  JOY_DOWN     },
-    { &custom_key.snap,  DEFAULT_KEY_SNAP,  JOY_BUTTON_1 },
-    { &custom_key.drop,  DEFAULT_KEY_DROP,  JOY_BUTTON_2 }
+    { &ski.left,  &ssi.snap_left,  DEFAULT_KEY_LEFT,  JOY_LEFT        },
+    { &ski.right, &ssi.snap_right, DEFAULT_KEY_RIGHT, JOY_RIGHT       },
+    { &ski.up,    &ssi.snap_up,    DEFAULT_KEY_UP,    JOY_UP          },
+    { &ski.down,  &ssi.snap_down,  DEFAULT_KEY_DOWN,  JOY_DOWN        },
+    { &ski.snap,  NULL,            DEFAULT_KEY_SNAP,  JOY_BUTTON_SNAP },
+    { &ski.drop,  NULL,            DEFAULT_KEY_DROP,  JOY_BUTTON_DROP }
   };
   int joy = 0;
   int i;
@@ -612,7 +1505,9 @@ void HandleKey(Key key, int key_status)
   if (game_status == GAME_MODE_PLAYING)
   {
     /* only needed for single-step tape recording mode */
-    static boolean clear_button_2[MAX_PLAYERS] = { FALSE,FALSE,FALSE,FALSE };
+    static boolean clear_snap_button[MAX_PLAYERS] = { FALSE,FALSE,FALSE,FALSE };
+    static boolean clear_drop_button[MAX_PLAYERS] = { FALSE,FALSE,FALSE,FALSE };
+    static boolean element_snapped[MAX_PLAYERS] = { FALSE,FALSE,FALSE,FALSE };
     static boolean element_dropped[MAX_PLAYERS] = { FALSE,FALSE,FALSE,FALSE };
     int pnr;
 
@@ -623,16 +1518,36 @@ void HandleKey(Key key, int key_status)
       if (setup.input[pnr].use_joystick)
        continue;
 
-      custom_key = setup.input[pnr].key;
+      ski = setup.input[pnr].key;
 
-      for (i = 0; i < 6; i++)
+      for (i = 0; i < NUM_PLAYER_ACTIONS; i++)
        if (key == *key_info[i].key_custom)
          key_action |= key_info[i].action;
 
-      if (tape.single_step && clear_button_2[pnr])
+      /* use combined snap+direction keys for the first player only */
+      if (pnr == 0)
+      {
+       ssi = setup.shortcut;
+
+       for (i = 0; i < NUM_DIRECTIONS; i++)
+         if (key == *key_info[i].key_snap)
+           key_action |= key_info[i].action | JOY_BUTTON_SNAP;
+      }
+
+      /* clear delayed snap and drop actions in single step mode (see below) */
+      if (tape.single_step)
       {
-       stored_player[pnr].action &= ~KEY_BUTTON_2;
-       clear_button_2[pnr] = FALSE;
+       if (clear_snap_button[pnr])
+       {
+         stored_player[pnr].action &= ~KEY_BUTTON_SNAP;
+         clear_snap_button[pnr] = FALSE;
+       }
+
+       if (clear_drop_button[pnr])
+       {
+         stored_player[pnr].action &= ~KEY_BUTTON_DROP;
+         clear_drop_button[pnr] = FALSE;
+       }
       }
 
       if (key_status == KEY_PRESSED)
@@ -642,48 +1557,80 @@ void HandleKey(Key key, int key_status)
 
       if (tape.single_step && tape.recording && tape.pausing)
       {
-       if (key_status == KEY_PRESSED &&
-           (key_action & (KEY_MOTION | KEY_BUTTON_1)))
+       if (key_status == KEY_PRESSED && key_action & KEY_MOTION)
        {
          TapeTogglePause(TAPE_TOGGLE_AUTOMATIC);
 
-         if (key_action & KEY_MOTION)
+         /* if snap key already pressed, don't snap when releasing (below) */
+         if (stored_player[pnr].action & KEY_BUTTON_SNAP)
+           element_snapped[pnr] = TRUE;
+
+         /* if drop key already pressed, don't drop when releasing (below) */
+         if (stored_player[pnr].action & KEY_BUTTON_DROP)
+           element_dropped[pnr] = TRUE;
+       }
+       else if (key_status == KEY_PRESSED && key_action & KEY_BUTTON_DROP)
+       {
+         if (level.game_engine_type == GAME_ENGINE_TYPE_EM ||
+             level.game_engine_type == GAME_ENGINE_TYPE_SP)
          {
-           if (stored_player[pnr].action & KEY_BUTTON_2)
-             element_dropped[pnr] = TRUE;
+
+           if (level.game_engine_type == GAME_ENGINE_TYPE_SP &&
+               getRedDiskReleaseFlag_SP() == 0)
+             stored_player[pnr].action &= ~KEY_BUTTON_DROP;
+
+           TapeTogglePause(TAPE_TOGGLE_AUTOMATIC);
          }
        }
-       else if (key_status == KEY_RELEASED &&
-                (key_action & KEY_BUTTON_2))
+       else if (key_status == KEY_RELEASED && key_action & KEY_BUTTON)
        {
-         if (!element_dropped[pnr])
+         if (key_action & KEY_BUTTON_SNAP)
          {
-           TapeTogglePause(TAPE_TOGGLE_AUTOMATIC);
+           /* if snap key was released without moving (see above), snap now */
+           if (!element_snapped[pnr])
+           {
+             TapeTogglePause(TAPE_TOGGLE_AUTOMATIC);
+
+             stored_player[pnr].action |= KEY_BUTTON_SNAP;
 
-           stored_player[pnr].action |= KEY_BUTTON_2;
-           clear_button_2[pnr] = TRUE;
+             /* clear delayed snap button on next event */
+             clear_snap_button[pnr] = TRUE;
+           }
+
+           element_snapped[pnr] = FALSE;
          }
 
-         element_dropped[pnr] = FALSE;
+         if (key_action & KEY_BUTTON_DROP &&
+             level.game_engine_type == GAME_ENGINE_TYPE_RND)
+         {
+           /* if drop key was released without moving (see above), drop now */
+           if (!element_dropped[pnr])
+           {
+             TapeTogglePause(TAPE_TOGGLE_AUTOMATIC);
+
+             if (level.game_engine_type != GAME_ENGINE_TYPE_SP ||
+                 getRedDiskReleaseFlag_SP() != 0)
+               stored_player[pnr].action |= KEY_BUTTON_DROP;
+
+             /* clear delayed drop button on next event */
+             clear_drop_button[pnr] = TRUE;
+           }
+
+           element_dropped[pnr] = FALSE;
+         }
        }
       }
-#if 1
       else if (tape.recording && tape.pausing)
       {
        /* prevent key release events from un-pausing a paused game */
-       if (key_status == KEY_PRESSED &&
-           (key_action & KEY_ACTION))
+       if (key_status == KEY_PRESSED && key_action & KEY_ACTION)
          TapeTogglePause(TAPE_TOGGLE_MANUAL);
       }
-#else
-      else if (tape.recording && tape.pausing && (key_action & KEY_ACTION))
-       TapeTogglePause(TAPE_TOGGLE_MANUAL);
-#endif
     }
   }
   else
   {
-    for (i = 0; i < 6; i++)
+    for (i = 0; i < NUM_PLAYER_ACTIONS; i++)
       if (key == key_info[i].key_default)
        joy |= key_info[i].action;
   }
@@ -702,14 +1649,53 @@ void HandleKey(Key key, int key_status)
     key_joystick_mapping = 0;
 
   if (key_status == KEY_RELEASED)
+  {
+    // reset flag to ignore repeated "key pressed" events after key release
+    ignore_repeated_key = FALSE;
+
     return;
+  }
 
-  if ((key == KSYM_Return || key == KSYM_KP_Enter) &&
-      (GetKeyModState() & KMOD_Alt) && video.fullscreen_available)
+  if ((key == KSYM_F11 ||
+       ((key == KSYM_Return ||
+        key == KSYM_KP_Enter) && (GetKeyModState() & KMOD_Alt))) &&
+      video.fullscreen_available &&
+      !ignore_repeated_key)
   {
     setup.fullscreen = !setup.fullscreen;
 
-    ToggleFullscreenIfNeeded();
+    ToggleFullscreenOrChangeWindowScalingIfNeeded();
+
+    if (game_status == GAME_MODE_SETUP)
+      RedrawSetupScreenAfterFullscreenToggle();
+
+    // set flag to ignore repeated "key pressed" events
+    ignore_repeated_key = TRUE;
+
+    return;
+  }
+
+  if ((key == KSYM_0     || key == KSYM_KP_0 ||
+       key == KSYM_minus || key == KSYM_KP_Subtract ||
+       key == KSYM_plus  || key == KSYM_KP_Add ||
+       key == KSYM_equal) &&   // ("Shift-=" is "+" on US keyboards)
+      (GetKeyModState() & (KMOD_Control | KMOD_Meta)) &&
+      video.window_scaling_available &&
+      !video.fullscreen_enabled)
+  {
+    if (key == KSYM_0 || key == KSYM_KP_0)
+      setup.window_scaling_percent = STD_WINDOW_SCALING_PERCENT;
+    else if (key == KSYM_minus || key == KSYM_KP_Subtract)
+      setup.window_scaling_percent -= STEP_WINDOW_SCALING_PERCENT;
+    else
+      setup.window_scaling_percent += STEP_WINDOW_SCALING_PERCENT;
+
+    if (setup.window_scaling_percent < MIN_WINDOW_SCALING_PERCENT)
+      setup.window_scaling_percent = MIN_WINDOW_SCALING_PERCENT;
+    else if (setup.window_scaling_percent > MAX_WINDOW_SCALING_PERCENT)
+      setup.window_scaling_percent = MAX_WINDOW_SCALING_PERCENT;
+
+    ToggleFullscreenOrChangeWindowScalingIfNeeded();
 
     if (game_status == GAME_MODE_SETUP)
       RedrawSetupScreenAfterFullscreenToggle();
@@ -717,13 +1703,8 @@ void HandleKey(Key key, int key_status)
     return;
   }
 
-#if 0
-  if (game_status == GAME_MODE_PLAYING && local_player->LevelSolved_GameEnd &&
-      (key == KSYM_Return || key == setup.shortcut.toggle_pause))
-#else
   if (game_status == GAME_MODE_PLAYING && AllPlayersGone &&
       (key == KSYM_Return || key == setup.shortcut.toggle_pause))
-#endif
   {
     GameEnd();
 
@@ -733,7 +1714,7 @@ void HandleKey(Key key, int key_status)
   if (game_status == GAME_MODE_MAIN &&
       (key == setup.shortcut.toggle_pause || key == KSYM_space))
   {
-    StartGameActions(options.network, setup.autorecord, NEW_RANDOMIZE);
+    StartGameActions(options.network, setup.autorecord, level.random_seed);
 
     return;
   }
@@ -745,7 +1726,10 @@ void HandleKey(Key key, int key_status)
     else if (key == setup.shortcut.load_game)
       TapeQuickLoad();
     else if (key == setup.shortcut.toggle_pause)
-      TapeTogglePause(TAPE_TOGGLE_MANUAL);
+      TapeTogglePause(TAPE_TOGGLE_MANUAL | TAPE_TOGGLE_PLAY_PAUSE);
+
+    HandleTapeButtonKeys(key);
+    HandleSoundButtonKeys(key);
   }
 
   if (game_status == GAME_MODE_PLAYING && !network_playing)
@@ -789,6 +1773,7 @@ void HandleKey(Key key, int key_status)
     case GAME_MODE_TITLE:
     case GAME_MODE_MAIN:
     case GAME_MODE_LEVELS:
+    case GAME_MODE_LEVELNR:
     case GAME_MODE_SETUP:
     case GAME_MODE_INFO:
     case GAME_MODE_SCORES:
@@ -801,7 +1786,9 @@ void HandleKey(Key key, int key_status)
          else if (game_status == GAME_MODE_MAIN)
            HandleMainMenu(0, 0, 0, 0, MB_MENU_CHOICE);
           else if (game_status == GAME_MODE_LEVELS)
-            HandleChooseLevel(0, 0, 0, 0, MB_MENU_CHOICE);
+            HandleChooseLevelSet(0, 0, 0, 0, MB_MENU_CHOICE);
+          else if (game_status == GAME_MODE_LEVELNR)
+            HandleChooseLevelNr(0, 0, 0, 0, MB_MENU_CHOICE);
          else if (game_status == GAME_MODE_SETUP)
            HandleSetupScreen(0, 0, 0, 0, MB_MENU_CHOICE);
          else if (game_status == GAME_MODE_INFO)
@@ -811,12 +1798,15 @@ void HandleKey(Key key, int key_status)
          break;
 
        case KSYM_Escape:
-         FadeSkipNextFadeIn();
+         if (game_status != GAME_MODE_MAIN)
+           FadeSkipNextFadeIn();
 
          if (game_status == GAME_MODE_TITLE)
            HandleTitleScreen(0, 0, 0, 0, MB_MENU_LEAVE);
           else if (game_status == GAME_MODE_LEVELS)
-            HandleChooseLevel(0, 0, 0, 0, MB_MENU_LEAVE);
+            HandleChooseLevelSet(0, 0, 0, 0, MB_MENU_LEAVE);
+          else if (game_status == GAME_MODE_LEVELNR)
+            HandleChooseLevelNr(0, 0, 0, 0, MB_MENU_LEAVE);
          else if (game_status == GAME_MODE_SETUP)
            HandleSetupScreen(0, 0, 0, 0, MB_MENU_LEAVE);
          else if (game_status == GAME_MODE_INFO)
@@ -827,7 +1817,9 @@ void HandleKey(Key key, int key_status)
 
         case KSYM_Page_Up:
           if (game_status == GAME_MODE_LEVELS)
-            HandleChooseLevel(0, 0, 0, -1 * SCROLL_PAGE, MB_MENU_MARK);
+            HandleChooseLevelSet(0, 0, 0, -1 * SCROLL_PAGE, MB_MENU_MARK);
+          else if (game_status == GAME_MODE_LEVELNR)
+            HandleChooseLevelNr(0, 0, 0, -1 * SCROLL_PAGE, MB_MENU_MARK);
          else if (game_status == GAME_MODE_SETUP)
            HandleSetupScreen(0, 0, 0, -1 * SCROLL_PAGE, MB_MENU_MARK);
          else if (game_status == GAME_MODE_INFO)
@@ -838,7 +1830,9 @@ void HandleKey(Key key, int key_status)
 
         case KSYM_Page_Down:
           if (game_status == GAME_MODE_LEVELS)
-            HandleChooseLevel(0, 0, 0, +1 * SCROLL_PAGE, MB_MENU_MARK);
+            HandleChooseLevelSet(0, 0, 0, +1 * SCROLL_PAGE, MB_MENU_MARK);
+          else if (game_status == GAME_MODE_LEVELNR)
+            HandleChooseLevelNr(0, 0, 0, +1 * SCROLL_PAGE, MB_MENU_MARK);
          else if (game_status == GAME_MODE_SETUP)
            HandleSetupScreen(0, 0, 0, +1 * SCROLL_PAGE, MB_MENU_MARK);
          else if (game_status == GAME_MODE_INFO)
@@ -847,12 +1841,6 @@ void HandleKey(Key key, int key_status)
            HandleHallOfFame(0, 0, 0, +1 * SCROLL_PAGE, MB_MENU_MARK);
          break;
 
-#ifdef DEBUG
-       case KSYM_0:
-         GameFrameDelay = (GameFrameDelay == 500 ? GAME_FRAME_DELAY : 500);
-         break;
-#endif
-
        default:
          break;
       }
@@ -871,91 +1859,6 @@ void HandleKey(Key key, int key_status)
          RequestQuitGame(setup.ask_on_escape);
          break;
 
-#ifdef DEBUG
-       case KSYM_0:
-#if 0
-       case KSYM_1:
-       case KSYM_2:
-       case KSYM_3:
-       case KSYM_4:
-       case KSYM_5:
-       case KSYM_6:
-       case KSYM_7:
-       case KSYM_8:
-       case KSYM_9:
-#endif
-         if (key == KSYM_0)
-         {
-           if (GameFrameDelay == 500)
-             GameFrameDelay = GAME_FRAME_DELAY;
-           else
-             GameFrameDelay = 500;
-         }
-         else
-           GameFrameDelay = (key - KSYM_0) * 10;
-         printf("Game speed == %d%% (%d ms delay between two frames)\n",
-                GAME_FRAME_DELAY * 100 / GameFrameDelay, GameFrameDelay);
-         break;
-
-       case KSYM_d:
-         if (options.debug)
-         {
-           options.debug = FALSE;
-           printf("debug mode disabled\n");
-         }
-         else
-         {
-           options.debug = TRUE;
-           printf("debug mode enabled\n");
-         }
-         break;
-
-       case KSYM_S:
-         if (!global.fps_slowdown)
-         {
-           global.fps_slowdown = TRUE;
-           global.fps_slowdown_factor = 2;
-           printf("fps slowdown enabled -- display only every 2nd frame\n");
-         }
-         else if (global.fps_slowdown_factor == 2)
-         {
-           global.fps_slowdown_factor = 4;
-           printf("fps slowdown enabled -- display only every 4th frame\n");
-         }
-         else
-         {
-           global.fps_slowdown = FALSE;
-           global.fps_slowdown_factor = 1;
-           printf("fps slowdown disabled\n");
-         }
-         break;
-
-       case KSYM_f:
-         ScrollStepSize = TILEX/8;
-         printf("ScrollStepSize == %d (1/8)\n", ScrollStepSize);
-         break;
-
-       case KSYM_g:
-         ScrollStepSize = TILEX/4;
-         printf("ScrollStepSize == %d (1/4)\n", ScrollStepSize);
-         break;
-
-       case KSYM_h:
-         ScrollStepSize = TILEX/2;
-         printf("ScrollStepSize == %d (1/2)\n", ScrollStepSize);
-         break;
-
-       case KSYM_l:
-         ScrollStepSize = TILEX;
-         printf("ScrollStepSize == %d (1/1)\n", ScrollStepSize);
-         break;
-
-       case KSYM_v:
-         printf("::: currently using game engine version %d\n",
-                game.engine_version);
-         break;
-#endif
-
        default:
          break;
       }
@@ -965,21 +1868,27 @@ void HandleKey(Key key, int key_status)
     default:
       if (key == KSYM_Escape)
       {
-       game_status = GAME_MODE_MAIN;
+       SetGameStatus(GAME_MODE_MAIN);
+
        DrawMainMenu();
 
        return;
       }
   }
+
+  HandleKeysDebug(key);
 }
 
 void HandleNoEvent()
 {
-  if (button_status && game_status != GAME_MODE_PLAYING)
+  // if (button_status && game_status != GAME_MODE_PLAYING)
+  if (button_status && (game_status != GAME_MODE_PLAYING || tape.pausing))
   {
-    HandleButton(0, 0, -button_status, button_status);
-
-    return;
+    HandleButton(0, 0, button_status, -button_status);
+  }
+  else
+  {
+    HandleJoystick();
   }
 
 #if defined(NETWORK_AVALIABLE)
@@ -987,7 +1896,25 @@ void HandleNoEvent()
     HandleNetworking();
 #endif
 
-  HandleJoystick();
+  switch (game_status)
+  {
+    case GAME_MODE_MAIN:
+      DrawPreviewLevelAnimation();
+      break;
+
+    case GAME_MODE_EDITOR:
+      HandleLevelEditorIdle();
+      break;
+
+#if defined(TARGET_SDL2)
+    case GAME_MODE_PLAYING:
+      HandleFollowFinger(-1, -1, -1);
+      break;
+#endif
+
+    default:
+      break;
+  }
 }
 
 static int HandleJoystickForAllPlayers()
@@ -1035,10 +1962,11 @@ void HandleJoystick()
     case GAME_MODE_TITLE:
     case GAME_MODE_MAIN:
     case GAME_MODE_LEVELS:
+    case GAME_MODE_LEVELNR:
     case GAME_MODE_SETUP:
     case GAME_MODE_INFO:
     {
-      static unsigned long joystickmove_delay = 0;
+      static unsigned int joystickmove_delay = 0;
 
       if (joystick && !button &&
          !DelayReached(&joystickmove_delay, GADGET_FRAME_DELAY))
@@ -1049,7 +1977,9 @@ void HandleJoystick()
       else if (game_status == GAME_MODE_MAIN)
        HandleMainMenu(0,0,dx,dy, newbutton ? MB_MENU_CHOICE : MB_MENU_MARK);
       else if (game_status == GAME_MODE_LEVELS)
-        HandleChooseLevel(0,0,dx,dy, newbutton ? MB_MENU_CHOICE : MB_MENU_MARK);
+        HandleChooseLevelSet(0,0,dx,dy,newbutton?MB_MENU_CHOICE : MB_MENU_MARK);
+      else if (game_status == GAME_MODE_LEVELNR)
+        HandleChooseLevelNr(0,0,dx,dy,newbutton? MB_MENU_CHOICE : MB_MENU_MARK);
       else if (game_status == GAME_MODE_SETUP)
        HandleSetupScreen(0,0,dx,dy, newbutton ? MB_MENU_CHOICE : MB_MENU_MARK);
       else if (game_status == GAME_MODE_INFO)
@@ -1061,19 +1991,11 @@ void HandleJoystick()
       HandleHallOfFame(0, 0, dx, dy, !newbutton);
       break;
 
-    case GAME_MODE_EDITOR:
-      HandleLevelEditorIdle();
-      break;
-
     case GAME_MODE_PLAYING:
       if (tape.playing || keyboard)
        newbutton = ((joy & JOY_BUTTON) != 0);
 
-#if 0
-      if (local_player->LevelSolved_GameEnd && newbutton)
-#else
-      if (AllPlayersGone && newbutton)
-#endif
+      if (newbutton && AllPlayersGone)
       {
        GameEnd();