added support for more event types for global animations (yet to be added)
[rocksndiamonds.git] / src / events.c
1 // ============================================================================
2 // Rocks'n'Diamonds - McDuffin Strikes Back!
3 // ----------------------------------------------------------------------------
4 // (c) 1995-2014 by Artsoft Entertainment
5 //                  Holger Schemel
6 //                  info@artsoft.org
7 //                  http://www.artsoft.org/
8 // ----------------------------------------------------------------------------
9 // events.c
10 // ============================================================================
11
12 #include "libgame/libgame.h"
13
14 #include "events.h"
15 #include "init.h"
16 #include "screens.h"
17 #include "tools.h"
18 #include "game.h"
19 #include "editor.h"
20 #include "files.h"
21 #include "tape.h"
22 #include "anim.h"
23 #include "network.h"
24
25
26 #define DEBUG_EVENTS            0
27
28 #define DEBUG_EVENTS_BUTTON     (DEBUG_EVENTS   * 0)
29 #define DEBUG_EVENTS_MOTION     (DEBUG_EVENTS   * 0)
30 #define DEBUG_EVENTS_WHEEL      (DEBUG_EVENTS   * 1)
31 #define DEBUG_EVENTS_WINDOW     (DEBUG_EVENTS   * 0)
32 #define DEBUG_EVENTS_FINGER     (DEBUG_EVENTS   * 0)
33 #define DEBUG_EVENTS_TEXT       (DEBUG_EVENTS   * 1)
34 #define DEBUG_EVENTS_KEY        (DEBUG_EVENTS   * 1)
35
36
37 static boolean cursor_inside_playfield = FALSE;
38 static int cursor_mode_last = CURSOR_DEFAULT;
39 static unsigned int special_cursor_delay = 0;
40 static unsigned int special_cursor_delay_value = 1000;
41
42 static boolean virtual_button_pressed = FALSE;
43
44
45 // forward declarations for internal use
46 static void HandleNoEvent(void);
47 static void HandleEventActions(void);
48
49
50 // event filter especially needed for SDL event filtering due to
51 // delay problems with lots of mouse motion events when mouse button
52 // not pressed (X11 can handle this with 'PointerMotionHintMask')
53
54 // event filter addition for SDL2: as SDL2 does not have a function to enable
55 // or disable keyboard auto-repeat, filter repeated keyboard events instead
56
57 static int FilterEvents(const Event *event)
58 {
59   MotionEvent *motion;
60
61   // skip repeated key press events if keyboard auto-repeat is disabled
62   if (event->type == EVENT_KEYPRESS &&
63       event->key.repeat &&
64       !keyrepeat_status)
65     return 0;
66
67   if (event->type == EVENT_BUTTONPRESS ||
68       event->type == EVENT_BUTTONRELEASE)
69   {
70     ((ButtonEvent *)event)->x -= video.screen_xoffset;
71     ((ButtonEvent *)event)->y -= video.screen_yoffset;
72   }
73   else if (event->type == EVENT_MOTIONNOTIFY)
74   {
75     ((MotionEvent *)event)->x -= video.screen_xoffset;
76     ((MotionEvent *)event)->y -= video.screen_yoffset;
77   }
78
79   // non-motion events are directly passed to event handler functions
80   if (event->type != EVENT_MOTIONNOTIFY)
81     return 1;
82
83   motion = (MotionEvent *)event;
84   cursor_inside_playfield = (motion->x >= SX && motion->x < SX + SXSIZE &&
85                              motion->y >= SY && motion->y < SY + SYSIZE);
86
87   // do no reset mouse cursor before all pending events have been processed
88   if (gfx.cursor_mode == cursor_mode_last &&
89       ((game_status == GAME_MODE_TITLE &&
90         gfx.cursor_mode == CURSOR_NONE) ||
91        (game_status == GAME_MODE_PLAYING &&
92         gfx.cursor_mode == CURSOR_PLAYFIELD)))
93   {
94     SetMouseCursor(CURSOR_DEFAULT);
95
96     DelayReached(&special_cursor_delay, 0);
97
98     cursor_mode_last = CURSOR_DEFAULT;
99   }
100
101   // skip mouse motion events without pressed button outside level editor
102   if (button_status == MB_RELEASED &&
103       game_status != GAME_MODE_EDITOR && game_status != GAME_MODE_PLAYING)
104     return 0;
105
106   return 1;
107 }
108
109 // to prevent delay problems, skip mouse motion events if the very next
110 // event is also a mouse motion event (and therefore effectively only
111 // handling the last of a row of mouse motion events in the event queue)
112
113 static boolean SkipPressedMouseMotionEvent(const Event *event)
114 {
115   // nothing to do if the current event is not a mouse motion event
116   if (event->type != EVENT_MOTIONNOTIFY)
117     return FALSE;
118
119   // only skip motion events with pressed button outside the game
120   if (button_status == MB_RELEASED || game_status == GAME_MODE_PLAYING)
121     return FALSE;
122
123   if (PendingEvent())
124   {
125     Event next_event;
126
127     PeekEvent(&next_event);
128
129     // if next event is also a mouse motion event, skip the current one
130     if (next_event.type == EVENT_MOTIONNOTIFY)
131       return TRUE;
132   }
133
134   return FALSE;
135 }
136
137 static boolean WaitValidEvent(Event *event)
138 {
139   WaitEvent(event);
140
141   if (!FilterEvents(event))
142     return FALSE;
143
144   if (SkipPressedMouseMotionEvent(event))
145     return FALSE;
146
147   return TRUE;
148 }
149
150 /* this is especially needed for event modifications for the Android target:
151    if mouse coordinates should be modified in the event filter function,
152    using a properly installed SDL event filter does not work, because in
153    the event filter, mouse coordinates in the event structure are still
154    physical pixel positions, not logical (scaled) screen positions, so this
155    has to be handled at a later stage in the event processing functions
156    (when device pixel positions are already converted to screen positions) */
157
158 boolean NextValidEvent(Event *event)
159 {
160   while (PendingEvent())
161     if (WaitValidEvent(event))
162       return TRUE;
163
164   return FALSE;
165 }
166
167 static void HandleEvents(void)
168 {
169   Event event;
170   unsigned int event_frame_delay = 0;
171   unsigned int event_frame_delay_value = GAME_FRAME_DELAY;
172
173   ResetDelayCounter(&event_frame_delay);
174
175   while (NextValidEvent(&event))
176   {
177     switch (event.type)
178     {
179       case EVENT_BUTTONPRESS:
180       case EVENT_BUTTONRELEASE:
181         HandleButtonEvent((ButtonEvent *) &event);
182         break;
183
184       case EVENT_MOTIONNOTIFY:
185         HandleMotionEvent((MotionEvent *) &event);
186         break;
187
188       case EVENT_WHEELMOTION:
189         HandleWheelEvent((WheelEvent *) &event);
190         break;
191
192       case SDL_WINDOWEVENT:
193         HandleWindowEvent((WindowEvent *) &event);
194         break;
195
196       case EVENT_FINGERPRESS:
197       case EVENT_FINGERRELEASE:
198       case EVENT_FINGERMOTION:
199         HandleFingerEvent((FingerEvent *) &event);
200         break;
201
202       case EVENT_TEXTINPUT:
203         HandleTextEvent((TextEvent *) &event);
204         break;
205
206       case SDL_APP_WILLENTERBACKGROUND:
207       case SDL_APP_DIDENTERBACKGROUND:
208       case SDL_APP_WILLENTERFOREGROUND:
209       case SDL_APP_DIDENTERFOREGROUND:
210         HandlePauseResumeEvent((PauseResumeEvent *) &event);
211         break;
212
213       case EVENT_KEYPRESS:
214       case EVENT_KEYRELEASE:
215         HandleKeyEvent((KeyEvent *) &event);
216         break;
217
218       default:
219         HandleOtherEvents(&event);
220         break;
221     }
222
223     // do not handle events for longer than standard frame delay period
224     if (DelayReached(&event_frame_delay, event_frame_delay_value))
225       break;
226   }
227 }
228
229 void HandleOtherEvents(Event *event)
230 {
231   switch (event->type)
232   {
233     case EVENT_EXPOSE:
234       HandleExposeEvent((ExposeEvent *) event);
235       break;
236
237     case EVENT_UNMAPNOTIFY:
238 #if 0
239       // This causes the game to stop not only when iconified, but also
240       // when on another virtual desktop, which might be not desired.
241       SleepWhileUnmapped();
242 #endif
243       break;
244
245     case EVENT_FOCUSIN:
246     case EVENT_FOCUSOUT:
247       HandleFocusEvent((FocusChangeEvent *) event);
248       break;
249
250     case EVENT_CLIENTMESSAGE:
251       HandleClientMessageEvent((ClientMessageEvent *) event);
252       break;
253
254     case SDL_CONTROLLERBUTTONDOWN:
255     case SDL_CONTROLLERBUTTONUP:
256       // for any game controller button event, disable overlay buttons
257       SetOverlayEnabled(FALSE);
258
259       HandleSpecialGameControllerButtons(event);
260
261       // FALL THROUGH
262     case SDL_CONTROLLERDEVICEADDED:
263     case SDL_CONTROLLERDEVICEREMOVED:
264     case SDL_CONTROLLERAXISMOTION:
265     case SDL_JOYAXISMOTION:
266     case SDL_JOYBUTTONDOWN:
267     case SDL_JOYBUTTONUP:
268       HandleJoystickEvent(event);
269       break;
270
271     case SDL_DROPBEGIN:
272     case SDL_DROPCOMPLETE:
273     case SDL_DROPFILE:
274     case SDL_DROPTEXT:
275       HandleDropEvent(event);
276       break;
277
278     default:
279       break;
280   }
281 }
282
283 static void HandleMouseCursor(void)
284 {
285   if (game_status == GAME_MODE_TITLE)
286   {
287     // when showing title screens, hide mouse pointer (if not moved)
288
289     if (gfx.cursor_mode != CURSOR_NONE &&
290         DelayReached(&special_cursor_delay, special_cursor_delay_value))
291     {
292       SetMouseCursor(CURSOR_NONE);
293     }
294   }
295   else if (game_status == GAME_MODE_PLAYING && (!tape.pausing ||
296                                                 tape.single_step))
297   {
298     // when playing, display a special mouse pointer inside the playfield
299
300     if (gfx.cursor_mode != CURSOR_PLAYFIELD &&
301         cursor_inside_playfield &&
302         DelayReached(&special_cursor_delay, special_cursor_delay_value))
303     {
304       if (level.game_engine_type != GAME_ENGINE_TYPE_MM ||
305           tile_cursor.enabled)
306         SetMouseCursor(CURSOR_PLAYFIELD);
307     }
308   }
309   else if (gfx.cursor_mode != CURSOR_DEFAULT)
310   {
311     SetMouseCursor(CURSOR_DEFAULT);
312   }
313
314   // this is set after all pending events have been processed
315   cursor_mode_last = gfx.cursor_mode;
316 }
317
318 void EventLoop(void)
319 {
320   while (1)
321   {
322     if (PendingEvent())
323       HandleEvents();
324     else
325       HandleNoEvent();
326
327     // execute event related actions after pending events have been processed
328     HandleEventActions();
329
330     // don't use all CPU time when idle; the main loop while playing
331     // has its own synchronization and is CPU friendly, too
332
333     if (game_status == GAME_MODE_PLAYING)
334       HandleGameActions();
335
336     // always copy backbuffer to visible screen for every video frame
337     BackToFront();
338
339     // reset video frame delay to default (may change again while playing)
340     SetVideoFrameDelay(MenuFrameDelay);
341
342     if (game_status == GAME_MODE_QUIT)
343       return;
344   }
345 }
346
347 void ClearAutoRepeatKeyEvents(void)
348 {
349   while (PendingEvent())
350   {
351     Event next_event;
352
353     PeekEvent(&next_event);
354
355     // if event is repeated key press event, remove it from event queue
356     if (next_event.type == EVENT_KEYPRESS &&
357         next_event.key.repeat)
358       WaitEvent(&next_event);
359     else
360       break;
361   }
362 }
363
364 void ClearEventQueue(void)
365 {
366   Event event;
367
368   while (NextValidEvent(&event))
369   {
370     switch (event.type)
371     {
372       case EVENT_BUTTONRELEASE:
373         button_status = MB_RELEASED;
374         break;
375
376       case EVENT_KEYRELEASE:
377         ClearPlayerAction();
378         break;
379
380       case SDL_CONTROLLERBUTTONUP:
381         HandleJoystickEvent(&event);
382         ClearPlayerAction();
383         break;
384
385       default:
386         HandleOtherEvents(&event);
387         break;
388     }
389   }
390 }
391
392 static void ClearPlayerMouseAction(void)
393 {
394   local_player->mouse_action.lx = 0;
395   local_player->mouse_action.ly = 0;
396   local_player->mouse_action.button = 0;
397 }
398
399 void ClearPlayerAction(void)
400 {
401   int i;
402
403   // simulate key release events for still pressed keys
404   key_joystick_mapping = 0;
405   for (i = 0; i < MAX_PLAYERS; i++)
406   {
407     stored_player[i].action = 0;
408     stored_player[i].snap_action = 0;
409   }
410
411   ClearJoystickState();
412   ClearPlayerMouseAction();
413 }
414
415 static void SetPlayerMouseAction(int mx, int my, int button)
416 {
417   int lx = getLevelFromScreenX(mx);
418   int ly = getLevelFromScreenY(my);
419   int new_button = (!local_player->mouse_action.button && button);
420
421   if (local_player->mouse_action.button_hint)
422     button = local_player->mouse_action.button_hint;
423
424   ClearPlayerMouseAction();
425
426   if (!IN_GFX_FIELD_PLAY(mx, my) || !IN_LEV_FIELD(lx, ly))
427     return;
428
429   local_player->mouse_action.lx = lx;
430   local_player->mouse_action.ly = ly;
431   local_player->mouse_action.button = button;
432
433   if (tape.recording && tape.pausing && tape.use_mouse)
434   {
435     // un-pause a paused game only if mouse button was newly pressed down
436     if (new_button)
437       TapeTogglePause(TAPE_TOGGLE_AUTOMATIC);
438   }
439
440   SetTileCursorXY(lx, ly);
441 }
442
443 void SleepWhileUnmapped(void)
444 {
445   boolean window_unmapped = TRUE;
446
447   KeyboardAutoRepeatOn();
448
449   while (window_unmapped)
450   {
451     Event event;
452
453     if (!WaitValidEvent(&event))
454       continue;
455
456     switch (event.type)
457     {
458       case EVENT_BUTTONRELEASE:
459         button_status = MB_RELEASED;
460         break;
461
462       case EVENT_KEYRELEASE:
463         key_joystick_mapping = 0;
464         break;
465
466       case SDL_CONTROLLERBUTTONUP:
467         HandleJoystickEvent(&event);
468         key_joystick_mapping = 0;
469         break;
470
471       case EVENT_MAPNOTIFY:
472         window_unmapped = FALSE;
473         break;
474
475       case EVENT_UNMAPNOTIFY:
476         // this is only to surely prevent the 'should not happen' case
477         // of recursively looping between 'SleepWhileUnmapped()' and
478         // 'HandleOtherEvents()' which usually calls this funtion.
479         break;
480
481       default:
482         HandleOtherEvents(&event);
483         break;
484     }
485   }
486
487   if (game_status == GAME_MODE_PLAYING)
488     KeyboardAutoRepeatOffUnlessAutoplay();
489 }
490
491 void HandleExposeEvent(ExposeEvent *event)
492 {
493 }
494
495 void HandleButtonEvent(ButtonEvent *event)
496 {
497 #if DEBUG_EVENTS_BUTTON
498   Error(ERR_DEBUG, "BUTTON EVENT: button %d %s, x/y %d/%d\n",
499         event->button,
500         event->type == EVENT_BUTTONPRESS ? "pressed" : "released",
501         event->x, event->y);
502 #endif
503
504   // for any mouse button event, disable playfield tile cursor
505   SetTileCursorEnabled(FALSE);
506
507 #if defined(HAS_SCREEN_KEYBOARD)
508   if (video.shifted_up)
509     event->y += video.shifted_up_pos;
510 #endif
511
512   motion_status = FALSE;
513
514   if (event->type == EVENT_BUTTONPRESS)
515     button_status = event->button;
516   else
517     button_status = MB_RELEASED;
518
519   HandleButton(event->x, event->y, button_status, event->button);
520 }
521
522 void HandleMotionEvent(MotionEvent *event)
523 {
524   if (button_status == MB_RELEASED && game_status != GAME_MODE_EDITOR)
525     return;
526
527   motion_status = TRUE;
528
529 #if DEBUG_EVENTS_MOTION
530   Error(ERR_DEBUG, "MOTION EVENT: button %d moved, x/y %d/%d\n",
531         button_status, event->x, event->y);
532 #endif
533
534   HandleButton(event->x, event->y, button_status, button_status);
535 }
536
537 void HandleWheelEvent(WheelEvent *event)
538 {
539   int button_nr;
540
541 #if DEBUG_EVENTS_WHEEL
542 #if 1
543   Error(ERR_DEBUG, "WHEEL EVENT: mouse == %d, x/y == %d/%d\n",
544         event->which, event->x, event->y);
545 #else
546   // (SDL_MOUSEWHEEL_NORMAL/SDL_MOUSEWHEEL_FLIPPED needs SDL 2.0.4 or newer)
547   Error(ERR_DEBUG, "WHEEL EVENT: mouse == %d, x/y == %d/%d, direction == %s\n",
548         event->which, event->x, event->y,
549         (event->direction == SDL_MOUSEWHEEL_NORMAL ? "SDL_MOUSEWHEEL_NORMAL" :
550          "SDL_MOUSEWHEEL_FLIPPED"));
551 #endif
552 #endif
553
554   button_nr = (event->x < 0 ? MB_WHEEL_LEFT :
555                event->x > 0 ? MB_WHEEL_RIGHT :
556                event->y < 0 ? MB_WHEEL_DOWN :
557                event->y > 0 ? MB_WHEEL_UP : 0);
558
559 #if defined(PLATFORM_WIN32) || defined(PLATFORM_MACOSX)
560   // accelerated mouse wheel available on Mac and Windows
561   wheel_steps = (event->x ? ABS(event->x) : ABS(event->y));
562 #else
563   // no accelerated mouse wheel available on Unix/Linux
564   wheel_steps = DEFAULT_WHEEL_STEPS;
565 #endif
566
567   motion_status = FALSE;
568
569   button_status = button_nr;
570   HandleButton(0, 0, button_status, -button_nr);
571
572   button_status = MB_RELEASED;
573   HandleButton(0, 0, button_status, -button_nr);
574 }
575
576 void HandleWindowEvent(WindowEvent *event)
577 {
578 #if DEBUG_EVENTS_WINDOW
579   int subtype = event->event;
580
581   char *event_name =
582     (subtype == SDL_WINDOWEVENT_SHOWN ? "SDL_WINDOWEVENT_SHOWN" :
583      subtype == SDL_WINDOWEVENT_HIDDEN ? "SDL_WINDOWEVENT_HIDDEN" :
584      subtype == SDL_WINDOWEVENT_EXPOSED ? "SDL_WINDOWEVENT_EXPOSED" :
585      subtype == SDL_WINDOWEVENT_MOVED ? "SDL_WINDOWEVENT_MOVED" :
586      subtype == SDL_WINDOWEVENT_SIZE_CHANGED ? "SDL_WINDOWEVENT_SIZE_CHANGED" :
587      subtype == SDL_WINDOWEVENT_RESIZED ? "SDL_WINDOWEVENT_RESIZED" :
588      subtype == SDL_WINDOWEVENT_MINIMIZED ? "SDL_WINDOWEVENT_MINIMIZED" :
589      subtype == SDL_WINDOWEVENT_MAXIMIZED ? "SDL_WINDOWEVENT_MAXIMIZED" :
590      subtype == SDL_WINDOWEVENT_RESTORED ? "SDL_WINDOWEVENT_RESTORED" :
591      subtype == SDL_WINDOWEVENT_ENTER ? "SDL_WINDOWEVENT_ENTER" :
592      subtype == SDL_WINDOWEVENT_LEAVE ? "SDL_WINDOWEVENT_LEAVE" :
593      subtype == SDL_WINDOWEVENT_FOCUS_GAINED ? "SDL_WINDOWEVENT_FOCUS_GAINED" :
594      subtype == SDL_WINDOWEVENT_FOCUS_LOST ? "SDL_WINDOWEVENT_FOCUS_LOST" :
595      subtype == SDL_WINDOWEVENT_CLOSE ? "SDL_WINDOWEVENT_CLOSE" :
596      "(UNKNOWN)");
597
598   Error(ERR_DEBUG, "WINDOW EVENT: '%s', %ld, %ld",
599         event_name, event->data1, event->data2);
600 #endif
601
602 #if 0
603   // (not needed, as the screen gets redrawn every 20 ms anyway)
604   if (event->event == SDL_WINDOWEVENT_SIZE_CHANGED ||
605       event->event == SDL_WINDOWEVENT_RESIZED ||
606       event->event == SDL_WINDOWEVENT_EXPOSED)
607     SDLRedrawWindow();
608 #endif
609
610   if (event->event == SDL_WINDOWEVENT_RESIZED)
611   {
612     if (!video.fullscreen_enabled)
613     {
614       int new_window_width  = event->data1;
615       int new_window_height = event->data2;
616
617       // if window size has changed after resizing, calculate new scaling factor
618       if (new_window_width  != video.window_width ||
619           new_window_height != video.window_height)
620       {
621         int new_xpercent = 100.0 * new_window_width  / video.screen_width  + .5;
622         int new_ypercent = 100.0 * new_window_height / video.screen_height + .5;
623
624         // (extreme window scaling allowed, but cannot be saved permanently)
625         video.window_scaling_percent = MIN(new_xpercent, new_ypercent);
626         setup.window_scaling_percent =
627           MIN(MAX(MIN_WINDOW_SCALING_PERCENT, video.window_scaling_percent),
628               MAX_WINDOW_SCALING_PERCENT);
629
630         video.window_width  = new_window_width;
631         video.window_height = new_window_height;
632
633         if (game_status == GAME_MODE_SETUP)
634           RedrawSetupScreenAfterFullscreenToggle();
635
636         SetWindowTitle();
637       }
638     }
639 #if defined(PLATFORM_ANDROID)
640     else
641     {
642       int new_display_width  = event->data1;
643       int new_display_height = event->data2;
644
645       // if fullscreen display size has changed, device has been rotated
646       if (new_display_width  != video.display_width ||
647           new_display_height != video.display_height)
648       {
649         int nr = GRID_ACTIVE_NR();      // previous screen orientation
650
651         video.display_width  = new_display_width;
652         video.display_height = new_display_height;
653
654         SDLSetScreenProperties();
655
656         // check if screen orientation has changed (should always be true here)
657         if (nr != GRID_ACTIVE_NR())
658         {
659           int x, y;
660
661           if (game_status == GAME_MODE_SETUP)
662             RedrawSetupScreenAfterScreenRotation(nr);
663
664           nr = GRID_ACTIVE_NR();
665
666           overlay.grid_xsize = setup.touch.grid_xsize[nr];
667           overlay.grid_ysize = setup.touch.grid_ysize[nr];
668
669           for (x = 0; x < MAX_GRID_XSIZE; x++)
670             for (y = 0; y < MAX_GRID_YSIZE; y++)
671               overlay.grid_button[x][y] = setup.touch.grid_button[nr][x][y];
672         }
673       }
674     }
675 #endif
676   }
677 }
678
679 #define NUM_TOUCH_FINGERS               3
680
681 static struct
682 {
683   boolean touched;
684   SDL_FingerID finger_id;
685   int counter;
686   Key key;
687   byte action;
688 } touch_info[NUM_TOUCH_FINGERS];
689
690 static void HandleFingerEvent_VirtualButtons(FingerEvent *event)
691 {
692 #if 1
693   int x = event->x * overlay.grid_xsize;
694   int y = event->y * overlay.grid_ysize;
695   int grid_button = overlay.grid_button[x][y];
696   int grid_button_action = GET_ACTION_FROM_GRID_BUTTON(grid_button);
697   Key key = (grid_button == CHAR_GRID_BUTTON_LEFT  ? setup.input[0].key.left :
698              grid_button == CHAR_GRID_BUTTON_RIGHT ? setup.input[0].key.right :
699              grid_button == CHAR_GRID_BUTTON_UP    ? setup.input[0].key.up :
700              grid_button == CHAR_GRID_BUTTON_DOWN  ? setup.input[0].key.down :
701              grid_button == CHAR_GRID_BUTTON_SNAP  ? setup.input[0].key.snap :
702              grid_button == CHAR_GRID_BUTTON_DROP  ? setup.input[0].key.drop :
703              KSYM_UNDEFINED);
704 #else
705   float ypos = 1.0 - 1.0 / 3.0 * video.display_width / video.display_height;
706   float event_x = (event->x);
707   float event_y = (event->y - ypos) / (1 - ypos);
708   Key key = (event_x > 0         && event_x < 1.0 / 6.0 &&
709              event_y > 2.0 / 3.0 && event_y < 1 ?
710              setup.input[0].key.snap :
711              event_x > 1.0 / 6.0 && event_x < 1.0 / 3.0 &&
712              event_y > 2.0 / 3.0 && event_y < 1 ?
713              setup.input[0].key.drop :
714              event_x > 7.0 / 9.0 && event_x < 8.0 / 9.0 &&
715              event_y > 0         && event_y < 1.0 / 3.0 ?
716              setup.input[0].key.up :
717              event_x > 6.0 / 9.0 && event_x < 7.0 / 9.0 &&
718              event_y > 1.0 / 3.0 && event_y < 2.0 / 3.0 ?
719              setup.input[0].key.left :
720              event_x > 8.0 / 9.0 && event_x < 1 &&
721              event_y > 1.0 / 3.0 && event_y < 2.0 / 3.0 ?
722              setup.input[0].key.right :
723              event_x > 7.0 / 9.0 && event_x < 8.0 / 9.0 &&
724              event_y > 2.0 / 3.0 && event_y < 1 ?
725              setup.input[0].key.down :
726              KSYM_UNDEFINED);
727 #endif
728   int key_status = (event->type == EVENT_FINGERRELEASE ? KEY_RELEASED :
729                     KEY_PRESSED);
730   char *key_status_name = (key_status == KEY_RELEASED ? "KEY_RELEASED" :
731                            "KEY_PRESSED");
732   int i;
733
734   virtual_button_pressed = (key_status == KEY_PRESSED && key != KSYM_UNDEFINED);
735
736   // for any touch input event, enable overlay buttons (if activated)
737   SetOverlayEnabled(TRUE);
738
739   Error(ERR_DEBUG, "::: key '%s' was '%s' [fingerId: %lld]",
740         getKeyNameFromKey(key), key_status_name, event->fingerId);
741
742   if (key_status == KEY_PRESSED)
743     overlay.grid_button_action |= grid_button_action;
744   else
745     overlay.grid_button_action &= ~grid_button_action;
746
747   // check if we already know this touch event's finger id
748   for (i = 0; i < NUM_TOUCH_FINGERS; i++)
749   {
750     if (touch_info[i].touched &&
751         touch_info[i].finger_id == event->fingerId)
752     {
753       // Error(ERR_DEBUG, "MARK 1: %d", i);
754
755       break;
756     }
757   }
758
759   if (i >= NUM_TOUCH_FINGERS)
760   {
761     if (key_status == KEY_PRESSED)
762     {
763       int oldest_pos = 0, oldest_counter = touch_info[0].counter;
764
765       // unknown finger id -- get new, empty slot, if available
766       for (i = 0; i < NUM_TOUCH_FINGERS; i++)
767       {
768         if (touch_info[i].counter < oldest_counter)
769         {
770           oldest_pos = i;
771           oldest_counter = touch_info[i].counter;
772
773           // Error(ERR_DEBUG, "MARK 2: %d", i);
774         }
775
776         if (!touch_info[i].touched)
777         {
778           // Error(ERR_DEBUG, "MARK 3: %d", i);
779
780           break;
781         }
782       }
783
784       if (i >= NUM_TOUCH_FINGERS)
785       {
786         // all slots allocated -- use oldest slot
787         i = oldest_pos;
788
789         // Error(ERR_DEBUG, "MARK 4: %d", i);
790       }
791     }
792     else
793     {
794       // release of previously unknown key (should not happen)
795
796       if (key != KSYM_UNDEFINED)
797       {
798         HandleKey(key, KEY_RELEASED);
799
800         Error(ERR_DEBUG, "=> key == '%s', key_status == '%s' [slot %d] [1]",
801               getKeyNameFromKey(key), "KEY_RELEASED", i);
802       }
803     }
804   }
805
806   if (i < NUM_TOUCH_FINGERS)
807   {
808     if (key_status == KEY_PRESSED)
809     {
810       if (touch_info[i].key != key)
811       {
812         if (touch_info[i].key != KSYM_UNDEFINED)
813         {
814           HandleKey(touch_info[i].key, KEY_RELEASED);
815
816           // undraw previous grid button when moving finger away
817           overlay.grid_button_action &= ~touch_info[i].action;
818
819           Error(ERR_DEBUG, "=> key == '%s', key_status == '%s' [slot %d] [2]",
820                 getKeyNameFromKey(touch_info[i].key), "KEY_RELEASED", i);
821         }
822
823         if (key != KSYM_UNDEFINED)
824         {
825           HandleKey(key, KEY_PRESSED);
826
827           Error(ERR_DEBUG, "=> key == '%s', key_status == '%s' [slot %d] [3]",
828                 getKeyNameFromKey(key), "KEY_PRESSED", i);
829         }
830       }
831
832       touch_info[i].touched = TRUE;
833       touch_info[i].finger_id = event->fingerId;
834       touch_info[i].counter = Counter();
835       touch_info[i].key = key;
836       touch_info[i].action = grid_button_action;
837     }
838     else
839     {
840       if (touch_info[i].key != KSYM_UNDEFINED)
841       {
842         HandleKey(touch_info[i].key, KEY_RELEASED);
843
844         Error(ERR_DEBUG, "=> key == '%s', key_status == '%s' [slot %d] [4]",
845               getKeyNameFromKey(touch_info[i].key), "KEY_RELEASED", i);
846       }
847
848       touch_info[i].touched = FALSE;
849       touch_info[i].finger_id = 0;
850       touch_info[i].counter = 0;
851       touch_info[i].key = 0;
852       touch_info[i].action = JOY_NO_ACTION;
853     }
854   }
855 }
856
857 static void HandleFingerEvent_WipeGestures(FingerEvent *event)
858 {
859   static Key motion_key_x = KSYM_UNDEFINED;
860   static Key motion_key_y = KSYM_UNDEFINED;
861   static Key button_key = KSYM_UNDEFINED;
862   static float motion_x1, motion_y1;
863   static float button_x1, button_y1;
864   static SDL_FingerID motion_id = -1;
865   static SDL_FingerID button_id = -1;
866   int move_trigger_distance_percent = setup.touch.move_distance;
867   int drop_trigger_distance_percent = setup.touch.drop_distance;
868   float move_trigger_distance = (float)move_trigger_distance_percent / 100;
869   float drop_trigger_distance = (float)drop_trigger_distance_percent / 100;
870   float event_x = event->x;
871   float event_y = event->y;
872
873   if (event->type == EVENT_FINGERPRESS)
874   {
875     if (event_x > 1.0 / 3.0)
876     {
877       // motion area
878
879       motion_id = event->fingerId;
880
881       motion_x1 = event_x;
882       motion_y1 = event_y;
883
884       motion_key_x = KSYM_UNDEFINED;
885       motion_key_y = KSYM_UNDEFINED;
886
887       Error(ERR_DEBUG, "---------- MOVE STARTED (WAIT) ----------");
888     }
889     else
890     {
891       // button area
892
893       button_id = event->fingerId;
894
895       button_x1 = event_x;
896       button_y1 = event_y;
897
898       button_key = setup.input[0].key.snap;
899
900       HandleKey(button_key, KEY_PRESSED);
901
902       Error(ERR_DEBUG, "---------- SNAP STARTED ----------");
903     }
904   }
905   else if (event->type == EVENT_FINGERRELEASE)
906   {
907     if (event->fingerId == motion_id)
908     {
909       motion_id = -1;
910
911       if (motion_key_x != KSYM_UNDEFINED)
912         HandleKey(motion_key_x, KEY_RELEASED);
913       if (motion_key_y != KSYM_UNDEFINED)
914         HandleKey(motion_key_y, KEY_RELEASED);
915
916       motion_key_x = KSYM_UNDEFINED;
917       motion_key_y = KSYM_UNDEFINED;
918
919       Error(ERR_DEBUG, "---------- MOVE STOPPED ----------");
920     }
921     else if (event->fingerId == button_id)
922     {
923       button_id = -1;
924
925       if (button_key != KSYM_UNDEFINED)
926         HandleKey(button_key, KEY_RELEASED);
927
928       button_key = KSYM_UNDEFINED;
929
930       Error(ERR_DEBUG, "---------- SNAP STOPPED ----------");
931     }
932   }
933   else if (event->type == EVENT_FINGERMOTION)
934   {
935     if (event->fingerId == motion_id)
936     {
937       float distance_x = ABS(event_x - motion_x1);
938       float distance_y = ABS(event_y - motion_y1);
939       Key new_motion_key_x = (event_x < motion_x1 ? setup.input[0].key.left :
940                               event_x > motion_x1 ? setup.input[0].key.right :
941                               KSYM_UNDEFINED);
942       Key new_motion_key_y = (event_y < motion_y1 ? setup.input[0].key.up :
943                               event_y > motion_y1 ? setup.input[0].key.down :
944                               KSYM_UNDEFINED);
945
946       if (distance_x < move_trigger_distance / 2 ||
947           distance_x < distance_y)
948         new_motion_key_x = KSYM_UNDEFINED;
949
950       if (distance_y < move_trigger_distance / 2 ||
951           distance_y < distance_x)
952         new_motion_key_y = KSYM_UNDEFINED;
953
954       if (distance_x > move_trigger_distance ||
955           distance_y > move_trigger_distance)
956       {
957         if (new_motion_key_x != motion_key_x)
958         {
959           if (motion_key_x != KSYM_UNDEFINED)
960             HandleKey(motion_key_x, KEY_RELEASED);
961           if (new_motion_key_x != KSYM_UNDEFINED)
962             HandleKey(new_motion_key_x, KEY_PRESSED);
963         }
964
965         if (new_motion_key_y != motion_key_y)
966         {
967           if (motion_key_y != KSYM_UNDEFINED)
968             HandleKey(motion_key_y, KEY_RELEASED);
969           if (new_motion_key_y != KSYM_UNDEFINED)
970             HandleKey(new_motion_key_y, KEY_PRESSED);
971         }
972
973         motion_x1 = event_x;
974         motion_y1 = event_y;
975
976         motion_key_x = new_motion_key_x;
977         motion_key_y = new_motion_key_y;
978
979         Error(ERR_DEBUG, "---------- MOVE STARTED (MOVE) ----------");
980       }
981     }
982     else if (event->fingerId == button_id)
983     {
984       float distance_x = ABS(event_x - button_x1);
985       float distance_y = ABS(event_y - button_y1);
986
987       if (distance_x < drop_trigger_distance / 2 &&
988           distance_y > drop_trigger_distance)
989       {
990         if (button_key == setup.input[0].key.snap)
991           HandleKey(button_key, KEY_RELEASED);
992
993         button_x1 = event_x;
994         button_y1 = event_y;
995
996         button_key = setup.input[0].key.drop;
997
998         HandleKey(button_key, KEY_PRESSED);
999
1000         Error(ERR_DEBUG, "---------- DROP STARTED ----------");
1001       }
1002     }
1003   }
1004 }
1005
1006 void HandleFingerEvent(FingerEvent *event)
1007 {
1008 #if DEBUG_EVENTS_FINGER
1009   Error(ERR_DEBUG, "FINGER EVENT: finger was %s, touch ID %lld, finger ID %lld, x/y %f/%f, dx/dy %f/%f, pressure %f",
1010         event->type == EVENT_FINGERPRESS ? "pressed" :
1011         event->type == EVENT_FINGERRELEASE ? "released" : "moved",
1012         event->touchId,
1013         event->fingerId,
1014         event->x, event->y,
1015         event->dx, event->dy,
1016         event->pressure);
1017 #endif
1018
1019   runtime.uses_touch_device = TRUE;
1020
1021   if (game_status != GAME_MODE_PLAYING)
1022     return;
1023
1024   if (level.game_engine_type == GAME_ENGINE_TYPE_MM)
1025   {
1026     if (strEqual(setup.touch.control_type, TOUCH_CONTROL_OFF))
1027       local_player->mouse_action.button_hint =
1028         (event->type == EVENT_FINGERRELEASE ? MB_NOT_PRESSED :
1029          event->x < 0.5                     ? MB_LEFTBUTTON  :
1030          event->x > 0.5                     ? MB_RIGHTBUTTON :
1031          MB_NOT_PRESSED);
1032
1033     return;
1034   }
1035
1036   if (strEqual(setup.touch.control_type, TOUCH_CONTROL_VIRTUAL_BUTTONS))
1037     HandleFingerEvent_VirtualButtons(event);
1038   else if (strEqual(setup.touch.control_type, TOUCH_CONTROL_WIPE_GESTURES))
1039     HandleFingerEvent_WipeGestures(event);
1040 }
1041
1042 static void HandleButtonOrFinger_WipeGestures_MM(int mx, int my, int button)
1043 {
1044   static int old_mx = 0, old_my = 0;
1045   static int last_button = MB_LEFTBUTTON;
1046   static boolean touched = FALSE;
1047   static boolean tapped = FALSE;
1048
1049   // screen tile was tapped (but finger not touching the screen anymore)
1050   // (this point will also be reached without receiving a touch event)
1051   if (tapped && !touched)
1052   {
1053     SetPlayerMouseAction(old_mx, old_my, MB_RELEASED);
1054
1055     tapped = FALSE;
1056   }
1057
1058   // stop here if this function was not triggered by a touch event
1059   if (button == -1)
1060     return;
1061
1062   if (button == MB_PRESSED && IN_GFX_FIELD_PLAY(mx, my))
1063   {
1064     // finger started touching the screen
1065
1066     touched = TRUE;
1067     tapped = TRUE;
1068
1069     if (!motion_status)
1070     {
1071       old_mx = mx;
1072       old_my = my;
1073
1074       ClearPlayerMouseAction();
1075
1076       Error(ERR_DEBUG, "---------- TOUCH ACTION STARTED ----------");
1077     }
1078   }
1079   else if (button == MB_RELEASED && touched)
1080   {
1081     // finger stopped touching the screen
1082
1083     touched = FALSE;
1084
1085     if (tapped)
1086       SetPlayerMouseAction(old_mx, old_my, last_button);
1087     else
1088       SetPlayerMouseAction(old_mx, old_my, MB_RELEASED);
1089
1090     Error(ERR_DEBUG, "---------- TOUCH ACTION STOPPED ----------");
1091   }
1092
1093   if (touched)
1094   {
1095     // finger moved while touching the screen
1096
1097     int old_x = getLevelFromScreenX(old_mx);
1098     int old_y = getLevelFromScreenY(old_my);
1099     int new_x = getLevelFromScreenX(mx);
1100     int new_y = getLevelFromScreenY(my);
1101
1102     if (new_x != old_x || new_y != old_y)
1103       tapped = FALSE;
1104
1105     if (new_x != old_x)
1106     {
1107       // finger moved left or right from (horizontal) starting position
1108
1109       int button_nr = (new_x < old_x ? MB_LEFTBUTTON : MB_RIGHTBUTTON);
1110
1111       SetPlayerMouseAction(old_mx, old_my, button_nr);
1112
1113       last_button = button_nr;
1114
1115       Error(ERR_DEBUG, "---------- TOUCH ACTION: ROTATING ----------");
1116     }
1117     else
1118     {
1119       // finger stays at or returned to (horizontal) starting position
1120
1121       SetPlayerMouseAction(old_mx, old_my, MB_RELEASED);
1122
1123       Error(ERR_DEBUG, "---------- TOUCH ACTION PAUSED ----------");
1124     }
1125   }
1126 }
1127
1128 static void HandleButtonOrFinger_FollowFinger_MM(int mx, int my, int button)
1129 {
1130   static int old_mx = 0, old_my = 0;
1131   static int last_button = MB_LEFTBUTTON;
1132   static boolean touched = FALSE;
1133   static boolean tapped = FALSE;
1134
1135   // screen tile was tapped (but finger not touching the screen anymore)
1136   // (this point will also be reached without receiving a touch event)
1137   if (tapped && !touched)
1138   {
1139     SetPlayerMouseAction(old_mx, old_my, MB_RELEASED);
1140
1141     tapped = FALSE;
1142   }
1143
1144   // stop here if this function was not triggered by a touch event
1145   if (button == -1)
1146     return;
1147
1148   if (button == MB_PRESSED && IN_GFX_FIELD_PLAY(mx, my))
1149   {
1150     // finger started touching the screen
1151
1152     touched = TRUE;
1153     tapped = TRUE;
1154
1155     if (!motion_status)
1156     {
1157       old_mx = mx;
1158       old_my = my;
1159
1160       ClearPlayerMouseAction();
1161
1162       Error(ERR_DEBUG, "---------- TOUCH ACTION STARTED ----------");
1163     }
1164   }
1165   else if (button == MB_RELEASED && touched)
1166   {
1167     // finger stopped touching the screen
1168
1169     touched = FALSE;
1170
1171     if (tapped)
1172       SetPlayerMouseAction(old_mx, old_my, last_button);
1173     else
1174       SetPlayerMouseAction(old_mx, old_my, MB_RELEASED);
1175
1176     Error(ERR_DEBUG, "---------- TOUCH ACTION STOPPED ----------");
1177   }
1178
1179   if (touched)
1180   {
1181     // finger moved while touching the screen
1182
1183     int old_x = getLevelFromScreenX(old_mx);
1184     int old_y = getLevelFromScreenY(old_my);
1185     int new_x = getLevelFromScreenX(mx);
1186     int new_y = getLevelFromScreenY(my);
1187
1188     if (new_x != old_x || new_y != old_y)
1189     {
1190       // finger moved away from starting position
1191
1192       int button_nr = getButtonFromTouchPosition(old_x, old_y, mx, my);
1193
1194       // quickly alternate between clicking and releasing for maximum speed
1195       if (FrameCounter % 2 == 0)
1196         button_nr = MB_RELEASED;
1197
1198       SetPlayerMouseAction(old_mx, old_my, button_nr);
1199
1200       if (button_nr)
1201         last_button = button_nr;
1202
1203       tapped = FALSE;
1204
1205       Error(ERR_DEBUG, "---------- TOUCH ACTION: ROTATING ----------");
1206     }
1207     else
1208     {
1209       // finger stays at or returned to starting position
1210
1211       SetPlayerMouseAction(old_mx, old_my, MB_RELEASED);
1212
1213       Error(ERR_DEBUG, "---------- TOUCH ACTION PAUSED ----------");
1214     }
1215   }
1216 }
1217
1218 static void HandleButtonOrFinger_FollowFinger(int mx, int my, int button)
1219 {
1220   static int old_mx = 0, old_my = 0;
1221   static Key motion_key_x = KSYM_UNDEFINED;
1222   static Key motion_key_y = KSYM_UNDEFINED;
1223   static boolean touched = FALSE;
1224   static boolean started_on_player = FALSE;
1225   static boolean player_is_dropping = FALSE;
1226   static int player_drop_count = 0;
1227   static int last_player_x = -1;
1228   static int last_player_y = -1;
1229
1230   if (button == MB_PRESSED && IN_GFX_FIELD_PLAY(mx, my))
1231   {
1232     touched = TRUE;
1233
1234     old_mx = mx;
1235     old_my = my;
1236
1237     if (!motion_status)
1238     {
1239       started_on_player = FALSE;
1240       player_is_dropping = FALSE;
1241       player_drop_count = 0;
1242       last_player_x = -1;
1243       last_player_y = -1;
1244
1245       motion_key_x = KSYM_UNDEFINED;
1246       motion_key_y = KSYM_UNDEFINED;
1247
1248       Error(ERR_DEBUG, "---------- TOUCH ACTION STARTED ----------");
1249     }
1250   }
1251   else if (button == MB_RELEASED && touched)
1252   {
1253     touched = FALSE;
1254
1255     old_mx = 0;
1256     old_my = 0;
1257
1258     if (motion_key_x != KSYM_UNDEFINED)
1259       HandleKey(motion_key_x, KEY_RELEASED);
1260     if (motion_key_y != KSYM_UNDEFINED)
1261       HandleKey(motion_key_y, KEY_RELEASED);
1262
1263     if (started_on_player)
1264     {
1265       if (player_is_dropping)
1266       {
1267         Error(ERR_DEBUG, "---------- DROP STOPPED ----------");
1268
1269         HandleKey(setup.input[0].key.drop, KEY_RELEASED);
1270       }
1271       else
1272       {
1273         Error(ERR_DEBUG, "---------- SNAP STOPPED ----------");
1274
1275         HandleKey(setup.input[0].key.snap, KEY_RELEASED);
1276       }
1277     }
1278
1279     motion_key_x = KSYM_UNDEFINED;
1280     motion_key_y = KSYM_UNDEFINED;
1281
1282     Error(ERR_DEBUG, "---------- TOUCH ACTION STOPPED ----------");
1283   }
1284
1285   if (touched)
1286   {
1287     int src_x = local_player->jx;
1288     int src_y = local_player->jy;
1289     int dst_x = getLevelFromScreenX(old_mx);
1290     int dst_y = getLevelFromScreenY(old_my);
1291     int dx = dst_x - src_x;
1292     int dy = dst_y - src_y;
1293     Key new_motion_key_x = (dx < 0 ? setup.input[0].key.left :
1294                             dx > 0 ? setup.input[0].key.right :
1295                             KSYM_UNDEFINED);
1296     Key new_motion_key_y = (dy < 0 ? setup.input[0].key.up :
1297                             dy > 0 ? setup.input[0].key.down :
1298                             KSYM_UNDEFINED);
1299
1300     if (dx != 0 && dy != 0 && ABS(dx) != ABS(dy) &&
1301         (last_player_x != local_player->jx ||
1302          last_player_y != local_player->jy))
1303     {
1304       // in case of asymmetric diagonal movement, use "preferred" direction
1305
1306       int last_move_dir = (ABS(dx) > ABS(dy) ? MV_VERTICAL : MV_HORIZONTAL);
1307
1308       if (level.game_engine_type == GAME_ENGINE_TYPE_EM)
1309         level.native_em_level->ply[0]->last_move_dir = last_move_dir;
1310       else
1311         local_player->last_move_dir = last_move_dir;
1312
1313       // (required to prevent accidentally forcing direction for next movement)
1314       last_player_x = local_player->jx;
1315       last_player_y = local_player->jy;
1316     }
1317
1318     if (button == MB_PRESSED && !motion_status && dx == 0 && dy == 0)
1319     {
1320       started_on_player = TRUE;
1321       player_drop_count = getPlayerInventorySize(0);
1322       player_is_dropping = (player_drop_count > 0);
1323
1324       if (player_is_dropping)
1325       {
1326         Error(ERR_DEBUG, "---------- DROP STARTED ----------");
1327
1328         HandleKey(setup.input[0].key.drop, KEY_PRESSED);
1329       }
1330       else
1331       {
1332         Error(ERR_DEBUG, "---------- SNAP STARTED ----------");
1333
1334         HandleKey(setup.input[0].key.snap, KEY_PRESSED);
1335       }
1336     }
1337     else if (dx != 0 || dy != 0)
1338     {
1339       if (player_is_dropping &&
1340           player_drop_count == getPlayerInventorySize(0))
1341       {
1342         Error(ERR_DEBUG, "---------- DROP -> SNAP ----------");
1343
1344         HandleKey(setup.input[0].key.drop, KEY_RELEASED);
1345         HandleKey(setup.input[0].key.snap, KEY_PRESSED);
1346
1347         player_is_dropping = FALSE;
1348       }
1349     }
1350
1351     if (new_motion_key_x != motion_key_x)
1352     {
1353       Error(ERR_DEBUG, "---------- %s %s ----------",
1354             started_on_player && !player_is_dropping ? "SNAPPING" : "MOVING",
1355             dx < 0 ? "LEFT" : dx > 0 ? "RIGHT" : "PAUSED");
1356
1357       if (motion_key_x != KSYM_UNDEFINED)
1358         HandleKey(motion_key_x, KEY_RELEASED);
1359       if (new_motion_key_x != KSYM_UNDEFINED)
1360         HandleKey(new_motion_key_x, KEY_PRESSED);
1361     }
1362
1363     if (new_motion_key_y != motion_key_y)
1364     {
1365       Error(ERR_DEBUG, "---------- %s %s ----------",
1366             started_on_player && !player_is_dropping ? "SNAPPING" : "MOVING",
1367             dy < 0 ? "UP" : dy > 0 ? "DOWN" : "PAUSED");
1368
1369       if (motion_key_y != KSYM_UNDEFINED)
1370         HandleKey(motion_key_y, KEY_RELEASED);
1371       if (new_motion_key_y != KSYM_UNDEFINED)
1372         HandleKey(new_motion_key_y, KEY_PRESSED);
1373     }
1374
1375     motion_key_x = new_motion_key_x;
1376     motion_key_y = new_motion_key_y;
1377   }
1378 }
1379
1380 static void HandleButtonOrFinger(int mx, int my, int button)
1381 {
1382   if (game_status != GAME_MODE_PLAYING)
1383     return;
1384
1385   if (level.game_engine_type == GAME_ENGINE_TYPE_MM)
1386   {
1387     if (strEqual(setup.touch.control_type, TOUCH_CONTROL_WIPE_GESTURES))
1388       HandleButtonOrFinger_WipeGestures_MM(mx, my, button);
1389     else if (strEqual(setup.touch.control_type, TOUCH_CONTROL_FOLLOW_FINGER))
1390       HandleButtonOrFinger_FollowFinger_MM(mx, my, button);
1391     else if (strEqual(setup.touch.control_type, TOUCH_CONTROL_VIRTUAL_BUTTONS))
1392       SetPlayerMouseAction(mx, my, button);     // special case
1393   }
1394   else
1395   {
1396     if (strEqual(setup.touch.control_type, TOUCH_CONTROL_FOLLOW_FINGER))
1397       HandleButtonOrFinger_FollowFinger(mx, my, button);
1398   }
1399 }
1400
1401 static boolean checkTextInputKeyModState(void)
1402 {
1403   // when playing, only handle raw key events and ignore text input
1404   if (game_status == GAME_MODE_PLAYING)
1405     return FALSE;
1406
1407   return ((GetKeyModState() & KMOD_TextInput) != KMOD_None);
1408 }
1409
1410 void HandleTextEvent(TextEvent *event)
1411 {
1412   char *text = event->text;
1413   Key key = getKeyFromKeyName(text);
1414
1415 #if DEBUG_EVENTS_TEXT
1416   Error(ERR_DEBUG, "TEXT EVENT: text == '%s' [%d byte(s), '%c'/%d], resulting key == %d (%s) [%04x]",
1417         text,
1418         strlen(text),
1419         text[0], (int)(text[0]),
1420         key,
1421         getKeyNameFromKey(key),
1422         GetKeyModState());
1423 #endif
1424
1425 #if !defined(HAS_SCREEN_KEYBOARD)
1426   // non-mobile devices: only handle key input with modifier keys pressed here
1427   // (every other key input is handled directly as physical key input event)
1428   if (!checkTextInputKeyModState())
1429     return;
1430 #endif
1431
1432   // process text input as "classic" (with uppercase etc.) key input event
1433   HandleKey(key, KEY_PRESSED);
1434   HandleKey(key, KEY_RELEASED);
1435 }
1436
1437 void HandlePauseResumeEvent(PauseResumeEvent *event)
1438 {
1439   if (event->type == SDL_APP_WILLENTERBACKGROUND)
1440   {
1441     Mix_PauseMusic();
1442   }
1443   else if (event->type == SDL_APP_DIDENTERFOREGROUND)
1444   {
1445     Mix_ResumeMusic();
1446   }
1447 }
1448
1449 void HandleKeyEvent(KeyEvent *event)
1450 {
1451   int key_status = (event->type == EVENT_KEYPRESS ? KEY_PRESSED : KEY_RELEASED);
1452   boolean with_modifiers = (game_status == GAME_MODE_PLAYING ? FALSE : TRUE);
1453   Key key = GetEventKey(event, with_modifiers);
1454   Key keymod = (with_modifiers ? GetEventKey(event, FALSE) : key);
1455
1456 #if DEBUG_EVENTS_KEY
1457   Error(ERR_DEBUG, "KEY EVENT: key was %s, keysym.scancode == %d, keysym.sym == %d, keymod = %d, GetKeyModState() = 0x%04x, resulting key == %d (%s)",
1458         event->type == EVENT_KEYPRESS ? "pressed" : "released",
1459         event->keysym.scancode,
1460         event->keysym.sym,
1461         keymod,
1462         GetKeyModState(),
1463         key,
1464         getKeyNameFromKey(key));
1465 #endif
1466
1467 #if defined(PLATFORM_ANDROID)
1468   if (key == KSYM_Back)
1469   {
1470     // always map the "back" button to the "escape" key on Android devices
1471     key = KSYM_Escape;
1472   }
1473   else if (key == KSYM_Menu)
1474   {
1475     // the "menu" button can be used to toggle displaying virtual buttons
1476     if (key_status == KEY_PRESSED)
1477       SetOverlayEnabled(!GetOverlayEnabled());
1478   }
1479   else
1480   {
1481     // for any other "real" key event, disable virtual buttons
1482     SetOverlayEnabled(FALSE);
1483   }
1484 #endif
1485
1486   HandleKeyModState(keymod, key_status);
1487
1488   // only handle raw key input without text modifier keys pressed
1489   if (!checkTextInputKeyModState())
1490     HandleKey(key, key_status);
1491 }
1492
1493 void HandleFocusEvent(FocusChangeEvent *event)
1494 {
1495   static int old_joystick_status = -1;
1496
1497   if (event->type == EVENT_FOCUSOUT)
1498   {
1499     KeyboardAutoRepeatOn();
1500     old_joystick_status = joystick.status;
1501     joystick.status = JOYSTICK_NOT_AVAILABLE;
1502
1503     ClearPlayerAction();
1504   }
1505   else if (event->type == EVENT_FOCUSIN)
1506   {
1507     /* When there are two Rocks'n'Diamonds windows which overlap and
1508        the player moves the pointer from one game window to the other,
1509        a 'FocusOut' event is generated for the window the pointer is
1510        leaving and a 'FocusIn' event is generated for the window the
1511        pointer is entering. In some cases, it can happen that the
1512        'FocusIn' event is handled by the one game process before the
1513        'FocusOut' event by the other game process. In this case the
1514        X11 environment would end up with activated keyboard auto repeat,
1515        because unfortunately this is a global setting and not (which
1516        would be far better) set for each X11 window individually.
1517        The effect would be keyboard auto repeat while playing the game
1518        (game_status == GAME_MODE_PLAYING), which is not desired.
1519        To avoid this special case, we just wait 1/10 second before
1520        processing the 'FocusIn' event. */
1521
1522     if (game_status == GAME_MODE_PLAYING)
1523     {
1524       Delay(100);
1525       KeyboardAutoRepeatOffUnlessAutoplay();
1526     }
1527
1528     if (old_joystick_status != -1)
1529       joystick.status = old_joystick_status;
1530   }
1531 }
1532
1533 void HandleClientMessageEvent(ClientMessageEvent *event)
1534 {
1535   if (CheckCloseWindowEvent(event))
1536     CloseAllAndExit(0);
1537 }
1538
1539 static int HandleDropFileEvent(char *filename)
1540 {
1541   Error(ERR_DEBUG, "DROP FILE EVENT: '%s'", filename);
1542
1543   // check and extract dropped zip files into correct user data directory
1544   if (!strSuffixLower(filename, ".zip"))
1545   {
1546     Error(ERR_WARN, "file '%s' not supported", filename);
1547
1548     return TREE_TYPE_UNDEFINED;
1549   }
1550
1551   TreeInfo *tree_node = NULL;
1552   int tree_type = GetZipFileTreeType(filename);
1553   char *directory = TREE_USERDIR(tree_type);
1554
1555   if (directory == NULL)
1556   {
1557     Error(ERR_WARN, "zip file '%s' has invalid content!", filename);
1558
1559     return TREE_TYPE_UNDEFINED;
1560   }
1561
1562   if (tree_type == TREE_TYPE_LEVEL_DIR &&
1563       game_status == GAME_MODE_LEVELS &&
1564       leveldir_current->node_parent != NULL)
1565   {
1566     // extract new level set next to currently selected level set
1567     tree_node = leveldir_current;
1568
1569     // get parent directory of currently selected level set directory
1570     directory = getLevelDirFromTreeInfo(leveldir_current->node_parent);
1571
1572     // use private level directory instead of top-level package level directory
1573     if (strPrefix(directory, options.level_directory) &&
1574         strEqual(leveldir_current->node_parent->fullpath, "."))
1575       directory = getUserLevelDir(NULL);
1576   }
1577
1578   // extract level or artwork set from zip file to target directory
1579   char *top_dir = ExtractZipFileIntoDirectory(filename, directory, tree_type);
1580
1581   if (top_dir == NULL)
1582   {
1583     // error message already issued by "ExtractZipFileIntoDirectory()"
1584
1585     return TREE_TYPE_UNDEFINED;
1586   }
1587
1588   // add extracted level or artwork set to tree info structure
1589   AddTreeSetToTreeInfo(tree_node, directory, top_dir, tree_type);
1590
1591   // update menu screen (and possibly change current level set)
1592   DrawScreenAfterAddingSet(top_dir, tree_type);
1593
1594   return tree_type;
1595 }
1596
1597 static void HandleDropTextEvent(char *text)
1598 {
1599   Error(ERR_DEBUG, "DROP TEXT EVENT: '%s'", text);
1600 }
1601
1602 static void HandleDropCompleteEvent(int num_level_sets_succeeded,
1603                                     int num_artwork_sets_succeeded,
1604                                     int num_files_failed)
1605 {
1606   // only show request dialog if no other request dialog already active
1607   if (game.request_active)
1608     return;
1609
1610   // this case can happen with drag-and-drop with older SDL versions
1611   if (num_level_sets_succeeded == 0 &&
1612       num_artwork_sets_succeeded == 0 &&
1613       num_files_failed == 0)
1614     return;
1615
1616   char message[100];
1617
1618   if (num_level_sets_succeeded > 0 || num_artwork_sets_succeeded > 0)
1619   {
1620     char message_part1[50];
1621
1622     sprintf(message_part1, "New %s set%s added",
1623             (num_artwork_sets_succeeded == 0 ? "level" :
1624              num_level_sets_succeeded == 0 ? "artwork" : "level and artwork"),
1625             (num_level_sets_succeeded +
1626              num_artwork_sets_succeeded > 1 ? "s" : ""));
1627
1628     if (num_files_failed > 0)
1629       sprintf(message, "%s, but %d dropped file%s failed!",
1630               message_part1, num_files_failed, num_files_failed > 1 ? "s" : "");
1631     else
1632       sprintf(message, "%s!", message_part1);
1633   }
1634   else if (num_files_failed > 0)
1635   {
1636     sprintf(message, "Failed to process dropped file%s!",
1637             num_files_failed > 1 ? "s" : "");
1638   }
1639
1640   Request(message, REQ_CONFIRM);
1641 }
1642
1643 void HandleDropEvent(Event *event)
1644 {
1645   static boolean confirm_on_drop_complete = FALSE;
1646   static int num_level_sets_succeeded = 0;
1647   static int num_artwork_sets_succeeded = 0;
1648   static int num_files_failed = 0;
1649
1650   switch (event->type)
1651   {
1652     case SDL_DROPBEGIN:
1653     {
1654       confirm_on_drop_complete = TRUE;
1655       num_level_sets_succeeded = 0;
1656       num_artwork_sets_succeeded = 0;
1657       num_files_failed = 0;
1658
1659       break;
1660     }
1661
1662     case SDL_DROPFILE:
1663     {
1664       int tree_type = HandleDropFileEvent(event->drop.file);
1665
1666       if (tree_type == TREE_TYPE_LEVEL_DIR)
1667         num_level_sets_succeeded++;
1668       else if (tree_type == TREE_TYPE_GRAPHICS_DIR ||
1669                tree_type == TREE_TYPE_SOUNDS_DIR ||
1670                tree_type == TREE_TYPE_MUSIC_DIR)
1671         num_artwork_sets_succeeded++;
1672       else
1673         num_files_failed++;
1674
1675       // SDL_DROPBEGIN / SDL_DROPCOMPLETE did not exist in older SDL versions
1676       if (!confirm_on_drop_complete)
1677       {
1678         // process all remaining events, including further SDL_DROPFILE events
1679         ClearEventQueue();
1680
1681         HandleDropCompleteEvent(num_level_sets_succeeded,
1682                                 num_artwork_sets_succeeded,
1683                                 num_files_failed);
1684
1685         num_level_sets_succeeded = 0;
1686         num_artwork_sets_succeeded = 0;
1687         num_files_failed = 0;
1688       }
1689
1690       break;
1691     }
1692
1693     case SDL_DROPTEXT:
1694     {
1695       HandleDropTextEvent(event->drop.file);
1696
1697       break;
1698     }
1699
1700     case SDL_DROPCOMPLETE:
1701     {
1702       HandleDropCompleteEvent(num_level_sets_succeeded,
1703                               num_artwork_sets_succeeded,
1704                               num_files_failed);
1705
1706       break;
1707     }
1708   }
1709
1710   if (event->drop.file != NULL)
1711     SDL_free(event->drop.file);
1712 }
1713
1714 void HandleButton(int mx, int my, int button, int button_nr)
1715 {
1716   static int old_mx = 0, old_my = 0;
1717   boolean button_hold = FALSE;
1718   boolean handle_gadgets = TRUE;
1719
1720   if (button_nr < 0)
1721   {
1722     mx = old_mx;
1723     my = old_my;
1724     button_nr = -button_nr;
1725     button_hold = TRUE;
1726   }
1727   else
1728   {
1729     old_mx = mx;
1730     old_my = my;
1731   }
1732
1733 #if defined(PLATFORM_ANDROID)
1734   // when playing, only handle gadgets when using "follow finger" controls
1735   // or when using touch controls in combination with the MM game engine
1736   // or when using gadgets that do not overlap with virtual buttons
1737   handle_gadgets =
1738     (game_status != GAME_MODE_PLAYING ||
1739      level.game_engine_type == GAME_ENGINE_TYPE_MM ||
1740      strEqual(setup.touch.control_type, TOUCH_CONTROL_FOLLOW_FINGER) ||
1741      (strEqual(setup.touch.control_type, TOUCH_CONTROL_VIRTUAL_BUTTONS) &&
1742       !virtual_button_pressed));
1743 #endif
1744
1745   if (HandleGlobalAnimClicks(mx, my, button))
1746   {
1747     // do not handle this button event anymore
1748     return;             // force mouse event not to be handled at all
1749   }
1750
1751   if (handle_gadgets && HandleGadgets(mx, my, button))
1752   {
1753     // do not handle this button event anymore
1754     mx = my = -32;      // force mouse event to be outside screen tiles
1755   }
1756
1757   if (button_hold && game_status == GAME_MODE_PLAYING && tape.pausing)
1758     return;
1759
1760   // do not use scroll wheel button events for anything other than gadgets
1761   if (IS_WHEEL_BUTTON(button_nr))
1762     return;
1763
1764   switch (game_status)
1765   {
1766     case GAME_MODE_TITLE:
1767       HandleTitleScreen(mx, my, 0, 0, button);
1768       break;
1769
1770     case GAME_MODE_MAIN:
1771       HandleMainMenu(mx, my, 0, 0, button);
1772       break;
1773
1774     case GAME_MODE_PSEUDO_TYPENAME:
1775       HandleTypeName(0, KSYM_Return);
1776       break;
1777
1778     case GAME_MODE_LEVELS:
1779       HandleChooseLevelSet(mx, my, 0, 0, button);
1780       break;
1781
1782     case GAME_MODE_LEVELNR:
1783       HandleChooseLevelNr(mx, my, 0, 0, button);
1784       break;
1785
1786     case GAME_MODE_SCORES:
1787       HandleHallOfFame(0, 0, 0, 0, button);
1788       break;
1789
1790     case GAME_MODE_EDITOR:
1791       HandleLevelEditorIdle();
1792       break;
1793
1794     case GAME_MODE_INFO:
1795       HandleInfoScreen(mx, my, 0, 0, button);
1796       break;
1797
1798     case GAME_MODE_SETUP:
1799       HandleSetupScreen(mx, my, 0, 0, button);
1800       break;
1801
1802     case GAME_MODE_PLAYING:
1803       if (!strEqual(setup.touch.control_type, TOUCH_CONTROL_OFF))
1804         HandleButtonOrFinger(mx, my, button);
1805       else
1806         SetPlayerMouseAction(mx, my, button);
1807
1808 #ifdef DEBUG
1809       if (button == MB_PRESSED && !motion_status && !button_hold &&
1810           IN_GFX_FIELD_PLAY(mx, my) && GetKeyModState() & KMOD_Control)
1811         DumpTileFromScreen(mx, my);
1812 #endif
1813
1814       break;
1815
1816     default:
1817       break;
1818   }
1819 }
1820
1821 static boolean is_string_suffix(char *string, char *suffix)
1822 {
1823   int string_len = strlen(string);
1824   int suffix_len = strlen(suffix);
1825
1826   if (suffix_len > string_len)
1827     return FALSE;
1828
1829   return (strEqual(&string[string_len - suffix_len], suffix));
1830 }
1831
1832 #define MAX_CHEAT_INPUT_LEN     32
1833
1834 static void HandleKeysSpecial(Key key)
1835 {
1836   static char cheat_input[2 * MAX_CHEAT_INPUT_LEN + 1] = "";
1837   char letter = getCharFromKey(key);
1838   int cheat_input_len = strlen(cheat_input);
1839   int i;
1840
1841   if (letter == 0)
1842     return;
1843
1844   if (cheat_input_len >= 2 * MAX_CHEAT_INPUT_LEN)
1845   {
1846     for (i = 0; i < MAX_CHEAT_INPUT_LEN + 1; i++)
1847       cheat_input[i] = cheat_input[MAX_CHEAT_INPUT_LEN + i];
1848
1849     cheat_input_len = MAX_CHEAT_INPUT_LEN;
1850   }
1851
1852   cheat_input[cheat_input_len++] = letter;
1853   cheat_input[cheat_input_len] = '\0';
1854
1855 #if DEBUG_EVENTS_KEY
1856   Error(ERR_DEBUG, "SPECIAL KEY '%s' [%d]\n", cheat_input, cheat_input_len);
1857 #endif
1858
1859   if (game_status == GAME_MODE_MAIN)
1860   {
1861     if (is_string_suffix(cheat_input, ":insert-solution-tape") ||
1862         is_string_suffix(cheat_input, ":ist"))
1863     {
1864       InsertSolutionTape();
1865     }
1866     else if (is_string_suffix(cheat_input, ":play-solution-tape") ||
1867              is_string_suffix(cheat_input, ":pst"))
1868     {
1869       PlaySolutionTape();
1870     }
1871     else if (is_string_suffix(cheat_input, ":reload-graphics") ||
1872              is_string_suffix(cheat_input, ":rg"))
1873     {
1874       ReloadCustomArtwork(1 << ARTWORK_TYPE_GRAPHICS);
1875       DrawMainMenu();
1876     }
1877     else if (is_string_suffix(cheat_input, ":reload-sounds") ||
1878              is_string_suffix(cheat_input, ":rs"))
1879     {
1880       ReloadCustomArtwork(1 << ARTWORK_TYPE_SOUNDS);
1881       DrawMainMenu();
1882     }
1883     else if (is_string_suffix(cheat_input, ":reload-music") ||
1884              is_string_suffix(cheat_input, ":rm"))
1885     {
1886       ReloadCustomArtwork(1 << ARTWORK_TYPE_MUSIC);
1887       DrawMainMenu();
1888     }
1889     else if (is_string_suffix(cheat_input, ":reload-artwork") ||
1890              is_string_suffix(cheat_input, ":ra"))
1891     {
1892       ReloadCustomArtwork(1 << ARTWORK_TYPE_GRAPHICS |
1893                           1 << ARTWORK_TYPE_SOUNDS |
1894                           1 << ARTWORK_TYPE_MUSIC);
1895       DrawMainMenu();
1896     }
1897     else if (is_string_suffix(cheat_input, ":dump-level") ||
1898              is_string_suffix(cheat_input, ":dl"))
1899     {
1900       DumpLevel(&level);
1901     }
1902     else if (is_string_suffix(cheat_input, ":dump-tape") ||
1903              is_string_suffix(cheat_input, ":dt"))
1904     {
1905       DumpTape(&tape);
1906     }
1907     else if (is_string_suffix(cheat_input, ":fix-tape") ||
1908              is_string_suffix(cheat_input, ":ft"))
1909     {
1910       /* fix single-player tapes that contain player input for more than one
1911          player (due to a bug in 3.3.1.2 and earlier versions), which results
1912          in playing levels with more than one player in multi-player mode,
1913          even though the tape was originally recorded in single-player mode */
1914
1915       // remove player input actions for all players but the first one
1916       for (i = 1; i < MAX_PLAYERS; i++)
1917         tape.player_participates[i] = FALSE;
1918
1919       tape.changed = TRUE;
1920     }
1921     else if (is_string_suffix(cheat_input, ":save-native-level") ||
1922              is_string_suffix(cheat_input, ":snl"))
1923     {
1924       SaveNativeLevel(&level);
1925     }
1926     else if (is_string_suffix(cheat_input, ":frames-per-second") ||
1927              is_string_suffix(cheat_input, ":fps"))
1928     {
1929       global.show_frames_per_second = !global.show_frames_per_second;
1930     }
1931   }
1932   else if (game_status == GAME_MODE_PLAYING)
1933   {
1934 #ifdef DEBUG
1935     if (is_string_suffix(cheat_input, ".q"))
1936       DEBUG_SetMaximumDynamite();
1937 #endif
1938   }
1939   else if (game_status == GAME_MODE_EDITOR)
1940   {
1941     if (is_string_suffix(cheat_input, ":dump-brush") ||
1942         is_string_suffix(cheat_input, ":DB"))
1943     {
1944       DumpBrush();
1945     }
1946     else if (is_string_suffix(cheat_input, ":DDB"))
1947     {
1948       DumpBrush_Small();
1949     }
1950
1951     if (GetKeyModState() & (KMOD_Control | KMOD_Meta))
1952     {
1953       if (letter == 'x')        // copy brush to clipboard (small size)
1954       {
1955         CopyBrushToClipboard_Small();
1956       }
1957       else if (letter == 'c')   // copy brush to clipboard (normal size)
1958       {
1959         CopyBrushToClipboard();
1960       }
1961       else if (letter == 'v')   // paste brush from Clipboard
1962       {
1963         CopyClipboardToBrush();
1964       }
1965     }
1966   }
1967
1968   // special key shortcuts for all game modes
1969   if (is_string_suffix(cheat_input, ":dump-event-actions") ||
1970       is_string_suffix(cheat_input, ":dea") ||
1971       is_string_suffix(cheat_input, ":DEA"))
1972   {
1973     DumpGadgetIdentifiers();
1974     DumpScreenIdentifiers();
1975   }
1976 }
1977
1978 boolean HandleKeysDebug(Key key, int key_status)
1979 {
1980 #ifdef DEBUG
1981   int i;
1982
1983   if (key_status != KEY_PRESSED)
1984     return FALSE;
1985
1986   if (game_status == GAME_MODE_PLAYING || !setup.debug.frame_delay_game_only)
1987   {
1988     boolean mod_key_pressed = ((GetKeyModState() & KMOD_Valid) != KMOD_None);
1989
1990     for (i = 0; i < NUM_DEBUG_FRAME_DELAY_KEYS; i++)
1991     {
1992       if (key == setup.debug.frame_delay_key[i] &&
1993           (mod_key_pressed == setup.debug.frame_delay_use_mod_key))
1994       {
1995         GameFrameDelay = (GameFrameDelay != setup.debug.frame_delay[i] ?
1996                           setup.debug.frame_delay[i] : setup.game_frame_delay);
1997
1998         if (!setup.debug.frame_delay_game_only)
1999           MenuFrameDelay = GameFrameDelay;
2000
2001         SetVideoFrameDelay(GameFrameDelay);
2002
2003         if (GameFrameDelay > ONE_SECOND_DELAY)
2004           Error(ERR_INFO, "frame delay == %d ms", GameFrameDelay);
2005         else if (GameFrameDelay != 0)
2006           Error(ERR_INFO, "frame delay == %d ms (max. %d fps / %d %%)",
2007                 GameFrameDelay, ONE_SECOND_DELAY / GameFrameDelay,
2008                 GAME_FRAME_DELAY * 100 / GameFrameDelay);
2009         else
2010           Error(ERR_INFO, "frame delay == 0 ms (maximum speed)");
2011
2012         return TRUE;
2013       }
2014     }
2015   }
2016
2017   if (game_status == GAME_MODE_PLAYING)
2018   {
2019     if (key == KSYM_d)
2020     {
2021       options.debug = !options.debug;
2022
2023       Error(ERR_INFO, "debug mode %s",
2024             (options.debug ? "enabled" : "disabled"));
2025
2026       return TRUE;
2027     }
2028     else if (key == KSYM_v)
2029     {
2030       Error(ERR_INFO, "currently using game engine version %d",
2031             game.engine_version);
2032
2033       return TRUE;
2034     }
2035   }
2036 #endif
2037
2038   return FALSE;
2039 }
2040
2041 void HandleKey(Key key, int key_status)
2042 {
2043   boolean anyTextGadgetActiveOrJustFinished = anyTextGadgetActive();
2044   static boolean ignore_repeated_key = FALSE;
2045   static struct SetupKeyboardInfo ski;
2046   static struct SetupShortcutInfo ssi;
2047   static struct
2048   {
2049     Key *key_custom;
2050     Key *key_snap;
2051     Key key_default;
2052     byte action;
2053   } key_info[] =
2054   {
2055     { &ski.left,  &ssi.snap_left,  DEFAULT_KEY_LEFT,  JOY_LEFT        },
2056     { &ski.right, &ssi.snap_right, DEFAULT_KEY_RIGHT, JOY_RIGHT       },
2057     { &ski.up,    &ssi.snap_up,    DEFAULT_KEY_UP,    JOY_UP          },
2058     { &ski.down,  &ssi.snap_down,  DEFAULT_KEY_DOWN,  JOY_DOWN        },
2059     { &ski.snap,  NULL,            DEFAULT_KEY_SNAP,  JOY_BUTTON_SNAP },
2060     { &ski.drop,  NULL,            DEFAULT_KEY_DROP,  JOY_BUTTON_DROP }
2061   };
2062   int joy = 0;
2063   int i;
2064
2065   if (HandleKeysDebug(key, key_status))
2066     return;             // do not handle already processed keys again
2067
2068   // map special keys (media keys / remote control buttons) to default keys
2069   if (key == KSYM_PlayPause)
2070     key = KSYM_space;
2071   else if (key == KSYM_Select)
2072     key = KSYM_Return;
2073
2074   HandleSpecialGameControllerKeys(key, key_status);
2075
2076   if (game_status == GAME_MODE_PLAYING)
2077   {
2078     // only needed for single-step tape recording mode
2079     static boolean has_snapped[MAX_PLAYERS] = { FALSE, FALSE, FALSE, FALSE };
2080     int pnr;
2081
2082     for (pnr = 0; pnr < MAX_PLAYERS; pnr++)
2083     {
2084       byte key_action = 0;
2085       byte key_snap_action = 0;
2086
2087       if (setup.input[pnr].use_joystick)
2088         continue;
2089
2090       ski = setup.input[pnr].key;
2091
2092       for (i = 0; i < NUM_PLAYER_ACTIONS; i++)
2093         if (key == *key_info[i].key_custom)
2094           key_action |= key_info[i].action;
2095
2096       // use combined snap+direction keys for the first player only
2097       if (pnr == 0)
2098       {
2099         ssi = setup.shortcut;
2100
2101         // also remember normal snap key when handling snap+direction keys
2102         key_snap_action |= key_action & JOY_BUTTON_SNAP;
2103
2104         for (i = 0; i < NUM_DIRECTIONS; i++)
2105         {
2106           if (key == *key_info[i].key_snap)
2107           {
2108             key_action      |= key_info[i].action | JOY_BUTTON_SNAP;
2109             key_snap_action |= key_info[i].action;
2110           }
2111         }
2112       }
2113
2114       if (key_status == KEY_PRESSED)
2115       {
2116         stored_player[pnr].action      |= key_action;
2117         stored_player[pnr].snap_action |= key_snap_action;
2118       }
2119       else
2120       {
2121         stored_player[pnr].action      &= ~key_action;
2122         stored_player[pnr].snap_action &= ~key_snap_action;
2123       }
2124
2125       // restore snap action if one of several pressed snap keys was released
2126       if (stored_player[pnr].snap_action)
2127         stored_player[pnr].action |= JOY_BUTTON_SNAP;
2128
2129       if (tape.single_step && tape.recording && tape.pausing && !tape.use_mouse)
2130       {
2131         if (key_status == KEY_PRESSED && key_action & KEY_MOTION)
2132         {
2133           TapeTogglePause(TAPE_TOGGLE_AUTOMATIC);
2134
2135           // if snap key already pressed, keep pause mode when releasing
2136           if (stored_player[pnr].action & KEY_BUTTON_SNAP)
2137             has_snapped[pnr] = TRUE;
2138         }
2139         else if (key_status == KEY_PRESSED && key_action & KEY_BUTTON_DROP)
2140         {
2141           TapeTogglePause(TAPE_TOGGLE_AUTOMATIC);
2142
2143           if (level.game_engine_type == GAME_ENGINE_TYPE_SP &&
2144               getRedDiskReleaseFlag_SP() == 0)
2145           {
2146             // add a single inactive frame before dropping starts
2147             stored_player[pnr].action &= ~KEY_BUTTON_DROP;
2148             stored_player[pnr].force_dropping = TRUE;
2149           }
2150         }
2151         else if (key_status == KEY_RELEASED && key_action & KEY_BUTTON_SNAP)
2152         {
2153           // if snap key was pressed without direction, leave pause mode
2154           if (!has_snapped[pnr])
2155             TapeTogglePause(TAPE_TOGGLE_AUTOMATIC);
2156
2157           has_snapped[pnr] = FALSE;
2158         }
2159       }
2160       else if (tape.recording && tape.pausing && !tape.use_mouse)
2161       {
2162         // prevent key release events from un-pausing a paused game
2163         if (key_status == KEY_PRESSED && key_action & KEY_ACTION)
2164           TapeTogglePause(TAPE_TOGGLE_MANUAL);
2165       }
2166
2167       // for MM style levels, handle in-game keyboard input in HandleJoystick()
2168       if (level.game_engine_type == GAME_ENGINE_TYPE_MM)
2169         joy |= key_action;
2170     }
2171   }
2172   else
2173   {
2174     for (i = 0; i < NUM_PLAYER_ACTIONS; i++)
2175       if (key == key_info[i].key_default)
2176         joy |= key_info[i].action;
2177   }
2178
2179   if (joy)
2180   {
2181     if (key_status == KEY_PRESSED)
2182       key_joystick_mapping |= joy;
2183     else
2184       key_joystick_mapping &= ~joy;
2185
2186     HandleJoystick();
2187   }
2188
2189   if (game_status != GAME_MODE_PLAYING)
2190     key_joystick_mapping = 0;
2191
2192   if (key_status == KEY_RELEASED)
2193   {
2194     // reset flag to ignore repeated "key pressed" events after key release
2195     ignore_repeated_key = FALSE;
2196
2197     return;
2198   }
2199
2200   if ((key == KSYM_F11 ||
2201        ((key == KSYM_Return ||
2202          key == KSYM_KP_Enter) && (GetKeyModState() & KMOD_Alt))) &&
2203       video.fullscreen_available &&
2204       !ignore_repeated_key)
2205   {
2206     setup.fullscreen = !setup.fullscreen;
2207
2208     ToggleFullscreenOrChangeWindowScalingIfNeeded();
2209
2210     if (game_status == GAME_MODE_SETUP)
2211       RedrawSetupScreenAfterFullscreenToggle();
2212
2213     // set flag to ignore repeated "key pressed" events
2214     ignore_repeated_key = TRUE;
2215
2216     return;
2217   }
2218
2219   if ((key == KSYM_0     || key == KSYM_KP_0 ||
2220        key == KSYM_minus || key == KSYM_KP_Subtract ||
2221        key == KSYM_plus  || key == KSYM_KP_Add ||
2222        key == KSYM_equal) &&    // ("Shift-=" is "+" on US keyboards)
2223       (GetKeyModState() & (KMOD_Control | KMOD_Meta)) &&
2224       video.window_scaling_available &&
2225       !video.fullscreen_enabled)
2226   {
2227     if (key == KSYM_0 || key == KSYM_KP_0)
2228       setup.window_scaling_percent = STD_WINDOW_SCALING_PERCENT;
2229     else if (key == KSYM_minus || key == KSYM_KP_Subtract)
2230       setup.window_scaling_percent -= STEP_WINDOW_SCALING_PERCENT;
2231     else
2232       setup.window_scaling_percent += STEP_WINDOW_SCALING_PERCENT;
2233
2234     if (setup.window_scaling_percent < MIN_WINDOW_SCALING_PERCENT)
2235       setup.window_scaling_percent = MIN_WINDOW_SCALING_PERCENT;
2236     else if (setup.window_scaling_percent > MAX_WINDOW_SCALING_PERCENT)
2237       setup.window_scaling_percent = MAX_WINDOW_SCALING_PERCENT;
2238
2239     ToggleFullscreenOrChangeWindowScalingIfNeeded();
2240
2241     if (game_status == GAME_MODE_SETUP)
2242       RedrawSetupScreenAfterFullscreenToggle();
2243
2244     return;
2245   }
2246
2247   if (HandleGlobalAnimClicks(-1, -1, (key == KSYM_space ||
2248                                       key == KSYM_Return ||
2249                                       key == KSYM_Escape)))
2250   {
2251     // do not handle this key event anymore
2252     if (key != KSYM_Escape)     // always allow ESC key to be handled
2253       return;
2254   }
2255
2256   if (game_status == GAME_MODE_PLAYING && game.all_players_gone &&
2257       (key == KSYM_Return || key == setup.shortcut.toggle_pause))
2258   {
2259     GameEnd();
2260
2261     return;
2262   }
2263
2264   if (game_status == GAME_MODE_MAIN &&
2265       (key == setup.shortcut.toggle_pause || key == KSYM_space))
2266   {
2267     StartGameActions(network.enabled, setup.autorecord, level.random_seed);
2268
2269     return;
2270   }
2271
2272   if (game_status == GAME_MODE_MAIN || game_status == GAME_MODE_PLAYING)
2273   {
2274     if (key == setup.shortcut.save_game)
2275       TapeQuickSave();
2276     else if (key == setup.shortcut.load_game)
2277       TapeQuickLoad();
2278     else if (key == setup.shortcut.toggle_pause)
2279       TapeTogglePause(TAPE_TOGGLE_MANUAL | TAPE_TOGGLE_PLAY_PAUSE);
2280
2281     HandleTapeButtonKeys(key);
2282     HandleSoundButtonKeys(key);
2283   }
2284
2285   if (game_status == GAME_MODE_PLAYING && !network_playing)
2286   {
2287     int centered_player_nr_next = -999;
2288
2289     if (key == setup.shortcut.focus_player_all)
2290       centered_player_nr_next = -1;
2291     else
2292       for (i = 0; i < MAX_PLAYERS; i++)
2293         if (key == setup.shortcut.focus_player[i])
2294           centered_player_nr_next = i;
2295
2296     if (centered_player_nr_next != -999)
2297     {
2298       game.centered_player_nr_next = centered_player_nr_next;
2299       game.set_centered_player = TRUE;
2300
2301       if (tape.recording)
2302       {
2303         tape.centered_player_nr_next = game.centered_player_nr_next;
2304         tape.set_centered_player = TRUE;
2305       }
2306     }
2307   }
2308
2309   HandleKeysSpecial(key);
2310
2311   if (HandleGadgetsKeyInput(key))
2312     return;             // do not handle already processed keys again
2313
2314   switch (game_status)
2315   {
2316     case GAME_MODE_PSEUDO_TYPENAME:
2317       HandleTypeName(0, key);
2318       break;
2319
2320     case GAME_MODE_TITLE:
2321     case GAME_MODE_MAIN:
2322     case GAME_MODE_LEVELS:
2323     case GAME_MODE_LEVELNR:
2324     case GAME_MODE_SETUP:
2325     case GAME_MODE_INFO:
2326     case GAME_MODE_SCORES:
2327
2328       if (anyTextGadgetActiveOrJustFinished && key != KSYM_Escape)
2329         break;
2330
2331       switch (key)
2332       {
2333         case KSYM_space:
2334         case KSYM_Return:
2335           if (game_status == GAME_MODE_TITLE)
2336             HandleTitleScreen(0, 0, 0, 0, MB_MENU_CHOICE);
2337           else if (game_status == GAME_MODE_MAIN)
2338             HandleMainMenu(0, 0, 0, 0, MB_MENU_CHOICE);
2339           else if (game_status == GAME_MODE_LEVELS)
2340             HandleChooseLevelSet(0, 0, 0, 0, MB_MENU_CHOICE);
2341           else if (game_status == GAME_MODE_LEVELNR)
2342             HandleChooseLevelNr(0, 0, 0, 0, MB_MENU_CHOICE);
2343           else if (game_status == GAME_MODE_SETUP)
2344             HandleSetupScreen(0, 0, 0, 0, MB_MENU_CHOICE);
2345           else if (game_status == GAME_MODE_INFO)
2346             HandleInfoScreen(0, 0, 0, 0, MB_MENU_CHOICE);
2347           else if (game_status == GAME_MODE_SCORES)
2348             HandleHallOfFame(0, 0, 0, 0, MB_MENU_CHOICE);
2349           break;
2350
2351         case KSYM_Escape:
2352           if (game_status != GAME_MODE_MAIN)
2353             FadeSkipNextFadeIn();
2354
2355           if (game_status == GAME_MODE_TITLE)
2356             HandleTitleScreen(0, 0, 0, 0, MB_MENU_LEAVE);
2357           else if (game_status == GAME_MODE_LEVELS)
2358             HandleChooseLevelSet(0, 0, 0, 0, MB_MENU_LEAVE);
2359           else if (game_status == GAME_MODE_LEVELNR)
2360             HandleChooseLevelNr(0, 0, 0, 0, MB_MENU_LEAVE);
2361           else if (game_status == GAME_MODE_SETUP)
2362             HandleSetupScreen(0, 0, 0, 0, MB_MENU_LEAVE);
2363           else if (game_status == GAME_MODE_INFO)
2364             HandleInfoScreen(0, 0, 0, 0, MB_MENU_LEAVE);
2365           else if (game_status == GAME_MODE_SCORES)
2366             HandleHallOfFame(0, 0, 0, 0, MB_MENU_LEAVE);
2367           break;
2368
2369         case KSYM_Page_Up:
2370           if (game_status == GAME_MODE_LEVELS)
2371             HandleChooseLevelSet(0, 0, 0, -1 * SCROLL_PAGE, MB_MENU_MARK);
2372           else if (game_status == GAME_MODE_LEVELNR)
2373             HandleChooseLevelNr(0, 0, 0, -1 * SCROLL_PAGE, MB_MENU_MARK);
2374           else if (game_status == GAME_MODE_SETUP)
2375             HandleSetupScreen(0, 0, 0, -1 * SCROLL_PAGE, MB_MENU_MARK);
2376           else if (game_status == GAME_MODE_INFO)
2377             HandleInfoScreen(0, 0, 0, -1 * SCROLL_PAGE, MB_MENU_MARK);
2378           else if (game_status == GAME_MODE_SCORES)
2379             HandleHallOfFame(0, 0, 0, -1 * SCROLL_PAGE, MB_MENU_MARK);
2380           break;
2381
2382         case KSYM_Page_Down:
2383           if (game_status == GAME_MODE_LEVELS)
2384             HandleChooseLevelSet(0, 0, 0, +1 * SCROLL_PAGE, MB_MENU_MARK);
2385           else if (game_status == GAME_MODE_LEVELNR)
2386             HandleChooseLevelNr(0, 0, 0, +1 * SCROLL_PAGE, MB_MENU_MARK);
2387           else if (game_status == GAME_MODE_SETUP)
2388             HandleSetupScreen(0, 0, 0, +1 * SCROLL_PAGE, MB_MENU_MARK);
2389           else if (game_status == GAME_MODE_INFO)
2390             HandleInfoScreen(0, 0, 0, +1 * SCROLL_PAGE, MB_MENU_MARK);
2391           else if (game_status == GAME_MODE_SCORES)
2392             HandleHallOfFame(0, 0, 0, +1 * SCROLL_PAGE, MB_MENU_MARK);
2393           break;
2394
2395         default:
2396           break;
2397       }
2398       break;
2399
2400     case GAME_MODE_EDITOR:
2401       if (!anyTextGadgetActiveOrJustFinished || key == KSYM_Escape)
2402         HandleLevelEditorKeyInput(key);
2403       break;
2404
2405     case GAME_MODE_PLAYING:
2406     {
2407       switch (key)
2408       {
2409         case KSYM_Escape:
2410           RequestQuitGame(setup.ask_on_escape);
2411           break;
2412
2413         default:
2414           break;
2415       }
2416       break;
2417     }
2418
2419     default:
2420       if (key == KSYM_Escape)
2421       {
2422         SetGameStatus(GAME_MODE_MAIN);
2423
2424         DrawMainMenu();
2425
2426         return;
2427       }
2428   }
2429 }
2430
2431 void HandleNoEvent(void)
2432 {
2433   HandleMouseCursor();
2434
2435   switch (game_status)
2436   {
2437     case GAME_MODE_PLAYING:
2438       HandleButtonOrFinger(-1, -1, -1);
2439       break;
2440   }
2441 }
2442
2443 void HandleEventActions(void)
2444 {
2445   // if (button_status && game_status != GAME_MODE_PLAYING)
2446   if (button_status && (game_status != GAME_MODE_PLAYING ||
2447                         tape.pausing ||
2448                         level.game_engine_type == GAME_ENGINE_TYPE_MM))
2449   {
2450     HandleButton(0, 0, button_status, -button_status);
2451   }
2452   else
2453   {
2454     HandleJoystick();
2455   }
2456
2457   if (network.enabled)
2458     HandleNetworking();
2459
2460   switch (game_status)
2461   {
2462     case GAME_MODE_MAIN:
2463       DrawPreviewLevelAnimation();
2464       break;
2465
2466     case GAME_MODE_EDITOR:
2467       HandleLevelEditorIdle();
2468       break;
2469
2470     default:
2471       break;
2472   }
2473 }
2474
2475 static void HandleTileCursor(int dx, int dy, int button)
2476 {
2477   if (!dx || !button)
2478     ClearPlayerMouseAction();
2479
2480   if (!dx && !dy)
2481     return;
2482
2483   if (button)
2484   {
2485     SetPlayerMouseAction(tile_cursor.x, tile_cursor.y,
2486                          (dx < 0 ? MB_LEFTBUTTON :
2487                           dx > 0 ? MB_RIGHTBUTTON : MB_RELEASED));
2488   }
2489   else if (!tile_cursor.moving)
2490   {
2491     int old_xpos = tile_cursor.xpos;
2492     int old_ypos = tile_cursor.ypos;
2493     int new_xpos = old_xpos;
2494     int new_ypos = old_ypos;
2495
2496     if (IN_LEV_FIELD(old_xpos + dx, old_ypos))
2497       new_xpos = old_xpos + dx;
2498
2499     if (IN_LEV_FIELD(old_xpos, old_ypos + dy))
2500       new_ypos = old_ypos + dy;
2501
2502     SetTileCursorTargetXY(new_xpos, new_ypos);
2503   }
2504 }
2505
2506 static int HandleJoystickForAllPlayers(void)
2507 {
2508   int i;
2509   int result = 0;
2510   boolean no_joysticks_configured = TRUE;
2511   boolean use_as_joystick_nr = (game_status != GAME_MODE_PLAYING);
2512   static byte joy_action_last[MAX_PLAYERS];
2513
2514   for (i = 0; i < MAX_PLAYERS; i++)
2515     if (setup.input[i].use_joystick)
2516       no_joysticks_configured = FALSE;
2517
2518   // if no joysticks configured, map connected joysticks to players
2519   if (no_joysticks_configured)
2520     use_as_joystick_nr = TRUE;
2521
2522   for (i = 0; i < MAX_PLAYERS; i++)
2523   {
2524     byte joy_action = 0;
2525
2526     joy_action = JoystickExt(i, use_as_joystick_nr);
2527     result |= joy_action;
2528
2529     if ((setup.input[i].use_joystick || no_joysticks_configured) &&
2530         joy_action != joy_action_last[i])
2531       stored_player[i].action = joy_action;
2532
2533     joy_action_last[i] = joy_action;
2534   }
2535
2536   return result;
2537 }
2538
2539 void HandleJoystick(void)
2540 {
2541   static unsigned int joytest_delay = 0;
2542   static unsigned int joytest_delay_value = GADGET_FRAME_DELAY;
2543   static int joytest_last = 0;
2544   int delay_value_first = GADGET_FRAME_DELAY_FIRST;
2545   int delay_value       = GADGET_FRAME_DELAY;
2546   int joystick  = HandleJoystickForAllPlayers();
2547   int keyboard  = key_joystick_mapping;
2548   int joy       = (joystick | keyboard);
2549   int joytest   = joystick;
2550   int left      = joy & JOY_LEFT;
2551   int right     = joy & JOY_RIGHT;
2552   int up        = joy & JOY_UP;
2553   int down      = joy & JOY_DOWN;
2554   int button    = joy & JOY_BUTTON;
2555   int newbutton = (AnyJoystickButton() == JOY_BUTTON_NEW_PRESSED);
2556   int dx        = (left ? -1    : right ? 1     : 0);
2557   int dy        = (up   ? -1    : down  ? 1     : 0);
2558   boolean use_delay_value_first = (joytest != joytest_last);
2559
2560   if (HandleGlobalAnimClicks(-1, -1, newbutton))
2561   {
2562     // do not handle this button event anymore
2563     return;
2564   }
2565
2566   if (newbutton && (game_status == GAME_MODE_PSEUDO_TYPENAME ||
2567                     anyTextGadgetActive()))
2568   {
2569     // leave name input in main menu or text input gadget
2570     HandleKey(KSYM_Escape, KEY_PRESSED);
2571     HandleKey(KSYM_Escape, KEY_RELEASED);
2572
2573     return;
2574   }
2575
2576   if (level.game_engine_type == GAME_ENGINE_TYPE_MM)
2577   {
2578     if (game_status == GAME_MODE_PLAYING)
2579     {
2580       // when playing MM style levels, also use delay for keyboard events
2581       joytest |= keyboard;
2582
2583       // only use first delay value for new events, but not for changed events
2584       use_delay_value_first = (!joytest != !joytest_last);
2585
2586       // only use delay after the initial keyboard event
2587       delay_value = 0;
2588     }
2589
2590     // for any joystick or keyboard event, enable playfield tile cursor
2591     if (dx || dy || button)
2592       SetTileCursorEnabled(TRUE);
2593   }
2594
2595   if (joytest && !button && !DelayReached(&joytest_delay, joytest_delay_value))
2596   {
2597     // delay joystick/keyboard actions if axes/keys continually pressed
2598     newbutton = dx = dy = 0;
2599   }
2600   else
2601   {
2602     // first start with longer delay, then continue with shorter delay
2603     joytest_delay_value =
2604       (use_delay_value_first ? delay_value_first : delay_value);
2605   }
2606
2607   joytest_last = joytest;
2608
2609   switch (game_status)
2610   {
2611     case GAME_MODE_TITLE:
2612     case GAME_MODE_MAIN:
2613     case GAME_MODE_LEVELS:
2614     case GAME_MODE_LEVELNR:
2615     case GAME_MODE_SETUP:
2616     case GAME_MODE_INFO:
2617     case GAME_MODE_SCORES:
2618     {
2619       if (anyTextGadgetActive())
2620         break;
2621
2622       if (game_status == GAME_MODE_TITLE)
2623         HandleTitleScreen(0,0,dx,dy, newbutton ? MB_MENU_CHOICE : MB_MENU_MARK);
2624       else if (game_status == GAME_MODE_MAIN)
2625         HandleMainMenu(0,0,dx,dy, newbutton ? MB_MENU_CHOICE : MB_MENU_MARK);
2626       else if (game_status == GAME_MODE_LEVELS)
2627         HandleChooseLevelSet(0,0,dx,dy,newbutton?MB_MENU_CHOICE : MB_MENU_MARK);
2628       else if (game_status == GAME_MODE_LEVELNR)
2629         HandleChooseLevelNr(0,0,dx,dy,newbutton? MB_MENU_CHOICE : MB_MENU_MARK);
2630       else if (game_status == GAME_MODE_SETUP)
2631         HandleSetupScreen(0,0,dx,dy, newbutton ? MB_MENU_CHOICE : MB_MENU_MARK);
2632       else if (game_status == GAME_MODE_INFO)
2633         HandleInfoScreen(0,0,dx,dy, newbutton ? MB_MENU_CHOICE : MB_MENU_MARK);
2634       else if (game_status == GAME_MODE_SCORES)
2635         HandleHallOfFame(0,0,dx,dy, newbutton ? MB_MENU_CHOICE : MB_MENU_MARK);
2636
2637       break;
2638     }
2639
2640     case GAME_MODE_PLAYING:
2641 #if 0
2642       // !!! causes immediate GameEnd() when solving MM level with keyboard !!!
2643       if (tape.playing || keyboard)
2644         newbutton = ((joy & JOY_BUTTON) != 0);
2645 #endif
2646
2647       if (newbutton && game.all_players_gone)
2648       {
2649         GameEnd();
2650
2651         return;
2652       }
2653
2654       if (tape.single_step && tape.recording && tape.pausing && !tape.use_mouse)
2655       {
2656         if (joystick & JOY_ACTION)
2657           TapeTogglePause(TAPE_TOGGLE_AUTOMATIC);
2658       }
2659       else if (tape.recording && tape.pausing && !tape.use_mouse)
2660       {
2661         if (joystick & JOY_ACTION)
2662           TapeTogglePause(TAPE_TOGGLE_MANUAL);
2663       }
2664
2665       if (level.game_engine_type == GAME_ENGINE_TYPE_MM)
2666         HandleTileCursor(dx, dy, button);
2667
2668       break;
2669
2670     default:
2671       break;
2672   }
2673 }
2674
2675 void HandleSpecialGameControllerButtons(Event *event)
2676 {
2677   int key_status;
2678   Key key;
2679
2680   switch (event->type)
2681   {
2682     case SDL_CONTROLLERBUTTONDOWN:
2683       key_status = KEY_PRESSED;
2684       break;
2685
2686     case SDL_CONTROLLERBUTTONUP:
2687       key_status = KEY_RELEASED;
2688       break;
2689
2690     default:
2691       return;
2692   }
2693
2694   switch (event->cbutton.button)
2695   {
2696     case SDL_CONTROLLER_BUTTON_START:
2697       key = KSYM_space;
2698       break;
2699
2700     case SDL_CONTROLLER_BUTTON_BACK:
2701       key = KSYM_Escape;
2702       break;
2703
2704     default:
2705       return;
2706   }
2707
2708   HandleKey(key, key_status);
2709 }
2710
2711 void HandleSpecialGameControllerKeys(Key key, int key_status)
2712 {
2713 #if defined(KSYM_Rewind) && defined(KSYM_FastForward)
2714   int button = SDL_CONTROLLER_BUTTON_INVALID;
2715
2716   // map keys to joystick buttons (special hack for Amazon Fire TV remote)
2717   if (key == KSYM_Rewind)
2718     button = SDL_CONTROLLER_BUTTON_A;
2719   else if (key == KSYM_FastForward || key == KSYM_Menu)
2720     button = SDL_CONTROLLER_BUTTON_B;
2721
2722   if (button != SDL_CONTROLLER_BUTTON_INVALID)
2723   {
2724     Event event;
2725
2726     event.type = (key_status == KEY_PRESSED ? SDL_CONTROLLERBUTTONDOWN :
2727                   SDL_CONTROLLERBUTTONUP);
2728
2729     event.cbutton.which = 0;    // first joystick (Amazon Fire TV remote)
2730     event.cbutton.button = button;
2731     event.cbutton.state = (key_status == KEY_PRESSED ? SDL_PRESSED :
2732                            SDL_RELEASED);
2733
2734     HandleJoystickEvent(&event);
2735   }
2736 #endif
2737 }
2738
2739 boolean DoKeysymAction(int keysym)
2740 {
2741   if (keysym < 0)
2742   {
2743     Key key = (Key)(-keysym);
2744
2745     HandleKey(key, KEY_PRESSED);
2746     HandleKey(key, KEY_RELEASED);
2747
2748     return TRUE;
2749   }
2750
2751   return FALSE;
2752 }