+ 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);
+
+ // undraw previous grid button when moving finger away
+ overlay.grid_button_action &= ~touch_info[i].action;
+
+ Debug("event:finger", "key == '%s', key_status == '%s' [slot %d] [2]",
+ getKeyNameFromKey(touch_info[i].key), "KEY_RELEASED", i);
+ }
+
+ if (key != KSYM_UNDEFINED)
+ {
+ HandleKey(key, KEY_PRESSED);
+
+ Debug("event:finger", "key == '%s', key_status == '%s' [slot %d] [3]",
+ getKeyNameFromKey(key), "KEY_PRESSED", i);
+ }
+ }
+
+ SetTouchInfo(i, event->fingerId, Counter(), key, grid_button_action);
+ }
+ else
+ {
+ if (touch_info[i].key != KSYM_UNDEFINED)
+ {
+ HandleKey(touch_info[i].key, KEY_RELEASED);
+
+ Debug("event:finger", "key == '%s', key_status == '%s' [slot %d] [4]",
+ getKeyNameFromKey(touch_info[i].key), "KEY_RELEASED", i);
+ }
+
+ SetTouchInfo(i, 0, 0, 0, JOY_NO_ACTION);
+ }
+ }
+}
+
+static void HandleFingerEvent_WipeGestures(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 (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;
+
+ Debug("event:finger", "---------- 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);
+
+ Debug("event:finger", "---------- 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;
+
+ Debug("event:finger", "---------- 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;
+
+ Debug("event:finger", "---------- 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;
+
+ Debug("event:finger", "---------- 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);
+
+ Debug("event:finger", "---------- DROP STARTED ----------");
+ }
+ }
+ }
+}
+
+void HandleFingerEvent(FingerEvent *event)
+{
+#if DEBUG_EVENTS_FINGER
+ Debug("event:finger", "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
+
+ runtime.uses_touch_device = TRUE;
+
+ if (game_status != GAME_MODE_PLAYING)
+ return;
+
+ if (level.game_engine_type == GAME_ENGINE_TYPE_MM)
+ {
+ if (strEqual(setup.touch.control_type, TOUCH_CONTROL_OFF))
+ local_player->mouse_action.button_hint =
+ (event->type == EVENT_FINGERRELEASE ? MB_NOT_PRESSED :
+ event->x < 0.5 ? MB_LEFTBUTTON :
+ event->x > 0.5 ? MB_RIGHTBUTTON :
+ MB_NOT_PRESSED);
+
+ return;
+ }
+
+ if (strEqual(setup.touch.control_type, TOUCH_CONTROL_VIRTUAL_BUTTONS))
+ HandleFingerEvent_VirtualButtons(event);
+ else if (strEqual(setup.touch.control_type, TOUCH_CONTROL_WIPE_GESTURES))
+ HandleFingerEvent_WipeGestures(event);
+}
+
+static void HandleButtonOrFinger_WipeGestures_MM(int mx, int my, int button)
+{
+ static int old_mx = 0, old_my = 0;
+ static int last_button = MB_LEFTBUTTON;
+ static boolean touched = FALSE;
+ static boolean tapped = FALSE;
+
+ // screen tile was tapped (but finger not touching the screen anymore)
+ // (this point will also be reached without receiving a touch event)
+ if (tapped && !touched)
+ {
+ SetPlayerMouseAction(old_mx, old_my, MB_RELEASED);
+
+ tapped = FALSE;
+ }
+
+ // stop here if this function was not triggered by a touch event
+ if (button == -1)
+ return;
+
+ if (button == MB_PRESSED && IN_GFX_FIELD_PLAY(mx, my))
+ {
+ // finger started touching the screen
+
+ touched = TRUE;
+ tapped = TRUE;
+
+ if (!motion_status)
+ {
+ old_mx = mx;
+ old_my = my;
+
+ ClearPlayerMouseAction();
+
+ Debug("event:finger", "---------- TOUCH ACTION STARTED ----------");
+ }
+ }
+ else if (button == MB_RELEASED && touched)
+ {
+ // finger stopped touching the screen
+
+ touched = FALSE;
+
+ if (tapped)
+ SetPlayerMouseAction(old_mx, old_my, last_button);
+ else
+ SetPlayerMouseAction(old_mx, old_my, MB_RELEASED);
+
+ Debug("event:finger", "---------- TOUCH ACTION STOPPED ----------");
+ }
+
+ if (touched)
+ {
+ // finger moved while touching the screen
+
+ int old_x = getLevelFromScreenX(old_mx);
+ int old_y = getLevelFromScreenY(old_my);
+ int new_x = getLevelFromScreenX(mx);
+ int new_y = getLevelFromScreenY(my);
+
+ if (new_x != old_x || new_y != old_y)
+ tapped = FALSE;
+
+ if (new_x != old_x)
+ {
+ // finger moved left or right from (horizontal) starting position
+
+ int button_nr = (new_x < old_x ? MB_LEFTBUTTON : MB_RIGHTBUTTON);
+
+ SetPlayerMouseAction(old_mx, old_my, button_nr);
+
+ last_button = button_nr;
+
+ Debug("event:finger", "---------- TOUCH ACTION: ROTATING ----------");
+ }
+ else
+ {
+ // finger stays at or returned to (horizontal) starting position
+
+ SetPlayerMouseAction(old_mx, old_my, MB_RELEASED);
+
+ Debug("event:finger", "---------- TOUCH ACTION PAUSED ----------");
+ }
+ }
+}
+
+static void HandleButtonOrFinger_FollowFinger_MM(int mx, int my, int button)
+{
+ static int old_mx = 0, old_my = 0;
+ static int last_button = MB_LEFTBUTTON;
+ static boolean touched = FALSE;
+ static boolean tapped = FALSE;
+
+ // screen tile was tapped (but finger not touching the screen anymore)
+ // (this point will also be reached without receiving a touch event)
+ if (tapped && !touched)
+ {
+ SetPlayerMouseAction(old_mx, old_my, MB_RELEASED);
+
+ tapped = FALSE;
+ }
+
+ // stop here if this function was not triggered by a touch event
+ if (button == -1)
+ return;
+
+ if (button == MB_PRESSED && IN_GFX_FIELD_PLAY(mx, my))
+ {
+ // finger started touching the screen
+
+ touched = TRUE;
+ tapped = TRUE;
+
+ if (!motion_status)
+ {
+ old_mx = mx;
+ old_my = my;
+
+ ClearPlayerMouseAction();
+
+ Debug("event:finger", "---------- TOUCH ACTION STARTED ----------");
+ }
+ }
+ else if (button == MB_RELEASED && touched)
+ {
+ // finger stopped touching the screen
+
+ touched = FALSE;
+
+ if (tapped)
+ SetPlayerMouseAction(old_mx, old_my, last_button);
+ else
+ SetPlayerMouseAction(old_mx, old_my, MB_RELEASED);
+
+ Debug("event:finger", "---------- TOUCH ACTION STOPPED ----------");
+ }
+
+ if (touched)
+ {
+ // finger moved while touching the screen
+
+ int old_x = getLevelFromScreenX(old_mx);
+ int old_y = getLevelFromScreenY(old_my);
+ int new_x = getLevelFromScreenX(mx);
+ int new_y = getLevelFromScreenY(my);
+
+ if (new_x != old_x || new_y != old_y)
+ {
+ // finger moved away from starting position
+
+ int button_nr = getButtonFromTouchPosition(old_x, old_y, mx, my);
+
+ // quickly alternate between clicking and releasing for maximum speed
+ if (FrameCounter % 2 == 0)
+ button_nr = MB_RELEASED;
+
+ SetPlayerMouseAction(old_mx, old_my, button_nr);
+
+ if (button_nr)
+ last_button = button_nr;
+
+ tapped = FALSE;
+
+ Debug("event:finger", "---------- TOUCH ACTION: ROTATING ----------");
+ }
+ else
+ {
+ // finger stays at or returned to starting position
+
+ SetPlayerMouseAction(old_mx, old_my, MB_RELEASED);
+
+ Debug("event:finger", "---------- TOUCH ACTION PAUSED ----------");
+ }
+ }
+}
+
+static void HandleButtonOrFinger_FollowFinger(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 touched = FALSE;
+ 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 (button == MB_PRESSED && IN_GFX_FIELD_PLAY(mx, my))
+ {
+ touched = TRUE;
+
+ 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;
+
+ Debug("event:finger", "---------- TOUCH ACTION STARTED ----------");
+ }
+ }
+ else if (button == MB_RELEASED && touched)
+ {
+ touched = FALSE;
+
+ 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)
+ {
+ Debug("event:finger", "---------- DROP STOPPED ----------");
+
+ HandleKey(setup.input[0].key.drop, KEY_RELEASED);
+ }
+ else
+ {
+ Debug("event:finger", "---------- SNAP STOPPED ----------");
+
+ HandleKey(setup.input[0].key.snap, KEY_RELEASED);
+ }
+ }
+
+ motion_key_x = KSYM_UNDEFINED;
+ motion_key_y = KSYM_UNDEFINED;
+
+ Debug("event:finger", "---------- TOUCH ACTION STOPPED ----------");
+ }
+
+ if (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)
+ game_em.ply[0]->last_move_dir = last_move_dir;
+ else
+ local_player->last_move_dir = last_move_dir;
+
+ // (required to prevent accidentally forcing direction for next movement)
+ last_player_x = local_player->jx;
+ last_player_y = local_player->jy;
+ }
+
+ 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);
+
+ if (player_is_dropping)
+ {
+ Debug("event:finger", "---------- DROP STARTED ----------");
+
+ HandleKey(setup.input[0].key.drop, KEY_PRESSED);
+ }
+ else
+ {
+ Debug("event:finger", "---------- SNAP STARTED ----------");
+
+ HandleKey(setup.input[0].key.snap, KEY_PRESSED);
+ }
+ }
+ else if (dx != 0 || dy != 0)
+ {
+ if (player_is_dropping &&
+ player_drop_count == getPlayerInventorySize(0))
+ {
+ Debug("event:finger", "---------- DROP -> SNAP ----------");
+
+ HandleKey(setup.input[0].key.drop, KEY_RELEASED);
+ HandleKey(setup.input[0].key.snap, KEY_PRESSED);
+
+ player_is_dropping = FALSE;
+ }
+ }
+
+ if (new_motion_key_x != motion_key_x)
+ {
+ Debug("event:finger", "---------- %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);
+ }
+
+ if (new_motion_key_y != motion_key_y)
+ {
+ Debug("event:finger", "---------- %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);
+ }
+
+ motion_key_x = new_motion_key_x;
+ motion_key_y = new_motion_key_y;
+ }
+}
+
+static void HandleButtonOrFinger(int mx, int my, int button)
+{
+ boolean valid_mouse_event = (mx != -1 && my != -1 && button != -1);
+
+ if (game_status != GAME_MODE_PLAYING)
+ return;
+
+ if (level.game_engine_type == GAME_ENGINE_TYPE_MM)
+ {
+ if (strEqual(setup.touch.control_type, TOUCH_CONTROL_WIPE_GESTURES))
+ HandleButtonOrFinger_WipeGestures_MM(mx, my, button);
+ else if (strEqual(setup.touch.control_type, TOUCH_CONTROL_FOLLOW_FINGER))
+ HandleButtonOrFinger_FollowFinger_MM(mx, my, button);
+ else if (strEqual(setup.touch.control_type, TOUCH_CONTROL_VIRTUAL_BUTTONS))
+ SetPlayerMouseAction(mx, my, button); // special case
+ }
+ else
+ {
+ if (strEqual(setup.touch.control_type, TOUCH_CONTROL_FOLLOW_FINGER))
+ HandleButtonOrFinger_FollowFinger(mx, my, button);
+ else if (game.use_mouse_actions && valid_mouse_event)
+ SetPlayerMouseAction(mx, my, button);
+ }
+}
+
+static boolean checkTextInputKey(Key key)
+{
+ // when playing, only handle raw key events and ignore text input
+ if (game_status == GAME_MODE_PLAYING)
+ return FALSE;
+
+ // if Shift or right Alt key is pressed, handle key as text input
+ if ((GetKeyModState() & KMOD_TextInput) != KMOD_None)
+ return TRUE;
+
+ // ignore raw keys as text input when not in text input mode
+ if (KSYM_RAW(key) && !textinput_status)
+ return FALSE;
+
+ // else handle all printable keys as text input
+ return KSYM_PRINTABLE(key);
+}
+
+void HandleTextEvent(TextEvent *event)
+{
+ char *text = event->text;
+ Key key = getKeyFromKeyName(text);
+
+#if DEBUG_EVENTS_TEXT
+ Debug("event:text", "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
+
+ if (checkTextInputKey(key))
+ {
+ // process printable keys (with uppercase etc.) in text input mode
+ HandleKey(key, KEY_PRESSED);
+ HandleKey(key, KEY_RELEASED);
+ }
+}
+
+void HandlePauseResumeEvent(PauseResumeEvent *event)
+{
+ if (event->type == SDL_APP_WILLENTERBACKGROUND)
+ {
+ Mix_PauseMusic();
+ }
+ else if (event->type == SDL_APP_DIDENTERFOREGROUND)
+ {
+ Mix_ResumeMusic();
+ }
+}
+
+void HandleKeyEvent(KeyEvent *event)
+{
+ int key_status = (event->type == EVENT_KEYPRESS ? KEY_PRESSED : KEY_RELEASED);
+ Key key = GetEventKey(event);
+
+#if DEBUG_EVENTS_KEY
+ Debug("event:key", "key was %s, keysym.scancode == %d, keysym.sym == %d, GetKeyModState() = 0x%04x, resulting key == %d (%s)",
+ event->type == EVENT_KEYPRESS ? "pressed" : "released",
+ event->keysym.scancode,
+ event->keysym.sym,
+ GetKeyModState(),
+ key,
+ getKeyNameFromKey(key));
+#endif
+
+#if defined(PLATFORM_ANDROID)
+ if (key == KSYM_Back)
+ {
+ // always map the "back" button to the "escape" key on Android devices
+ key = KSYM_Escape;
+ }
+ else if (key == KSYM_Menu)
+ {
+ // the "menu" button can be used to toggle displaying virtual buttons
+ if (key_status == KEY_PRESSED)
+ SetOverlayEnabled(!GetOverlayEnabled());
+ }
+ else if (!textinput_status)
+ {
+ // for any other "real" key event, disable virtual buttons
+ SetOverlayEnabled(FALSE);
+
+ // for any other "real" key event, disable overlay touch buttons
+ runtime.uses_touch_device = FALSE;
+ }
+#endif
+
+ HandleKeyModState(key, key_status);
+
+ // process all keys if not in text input mode or if non-printable keys
+ if (!checkTextInputKey(key))
+ HandleKey(key, key_status);
+}
+
+static int HandleDropFileEvent(char *filename)
+{
+ Debug("event:dropfile", "filename == '%s'", filename);
+
+ // check and extract dropped zip files into correct user data directory
+ if (!strSuffixLower(filename, ".zip"))
+ {
+ Warn("file '%s' not supported", filename);
+
+ return TREE_TYPE_UNDEFINED;
+ }
+
+ TreeInfo *tree_node = NULL;
+ int tree_type = GetZipFileTreeType(filename);
+ char *directory = TREE_USERDIR(tree_type);
+
+ if (directory == NULL)
+ {
+ Warn("zip file '%s' has invalid content!", filename);
+
+ return TREE_TYPE_UNDEFINED;
+ }
+
+ if (tree_type == TREE_TYPE_LEVEL_DIR &&
+ game_status == GAME_MODE_LEVELS &&
+ leveldir_current->node_parent != NULL)
+ {
+ // extract new level set next to currently selected level set
+ tree_node = leveldir_current;
+
+ // get parent directory of currently selected level set directory
+ directory = getLevelDirFromTreeInfo(leveldir_current->node_parent);
+
+ // use private level directory instead of top-level package level directory
+ if (strPrefix(directory, options.level_directory) &&
+ strEqual(leveldir_current->node_parent->fullpath, "."))
+ directory = getUserLevelDir(NULL);
+ }
+
+ // extract level or artwork set from zip file to target directory
+ char *top_dir = ExtractZipFileIntoDirectory(filename, directory, tree_type);
+
+ if (top_dir == NULL)
+ {
+ // error message already issued by "ExtractZipFileIntoDirectory()"
+
+ return TREE_TYPE_UNDEFINED;
+ }
+
+ // add extracted level or artwork set to tree info structure
+ AddTreeSetToTreeInfo(tree_node, directory, top_dir, tree_type);
+
+ // force restart after adding level collection
+ if (getTreeInfoFromIdentifier(TREE_FIRST_NODE(tree_type), top_dir) == NULL)
+ {
+ Request("Program must be restarted after adding a new level collection!",
+ REQ_CONFIRM);
+
+ CloseAllAndExit(0);
+ }
+
+ // update menu screen (and possibly change current level set)
+ DrawScreenAfterAddingSet(top_dir, tree_type);
+
+ return tree_type;
+}
+
+static void HandleDropTextEvent(char *text)
+{
+ Debug("event:droptext", "text == '%s'", text);
+}
+
+static void HandleDropCompleteEvent(int num_level_sets_succeeded,
+ int num_artwork_sets_succeeded,
+ int num_files_failed)
+{
+ // only show request dialog if no other request dialog already active
+ if (game.request_active)
+ return;
+
+ // this case can happen with drag-and-drop with older SDL versions
+ if (num_level_sets_succeeded == 0 &&
+ num_artwork_sets_succeeded == 0 &&
+ num_files_failed == 0)
+ return;
+
+ char message[100];
+
+ if (num_level_sets_succeeded > 0 || num_artwork_sets_succeeded > 0)
+ {
+ char message_part1[50];
+
+ sprintf(message_part1, "New %s set%s added",
+ (num_artwork_sets_succeeded == 0 ? "level" :
+ num_level_sets_succeeded == 0 ? "artwork" : "level and artwork"),
+ (num_level_sets_succeeded +
+ num_artwork_sets_succeeded > 1 ? "s" : ""));
+
+ if (num_files_failed > 0)
+ sprintf(message, "%s, but %d dropped file%s failed!",
+ message_part1, num_files_failed, num_files_failed > 1 ? "s" : "");
+ else
+ sprintf(message, "%s!", message_part1);
+ }
+ else if (num_files_failed > 0)
+ {
+ sprintf(message, "Failed to process dropped file%s!",
+ num_files_failed > 1 ? "s" : "");
+ }
+
+ Request(message, REQ_CONFIRM);
+}
+
+void HandleDropEvent(Event *event)
+{
+ static boolean confirm_on_drop_complete = FALSE;
+ static int num_level_sets_succeeded = 0;
+ static int num_artwork_sets_succeeded = 0;
+ static int num_files_failed = 0;
+
+ switch (event->type)
+ {
+ case SDL_DROPBEGIN:
+ {
+ confirm_on_drop_complete = TRUE;
+ num_level_sets_succeeded = 0;
+ num_artwork_sets_succeeded = 0;
+ num_files_failed = 0;
+
+ break;
+ }
+
+ case SDL_DROPFILE:
+ {
+ int tree_type = HandleDropFileEvent(event->drop.file);
+
+ if (tree_type == TREE_TYPE_LEVEL_DIR)
+ num_level_sets_succeeded++;
+ else if (tree_type == TREE_TYPE_GRAPHICS_DIR ||
+ tree_type == TREE_TYPE_SOUNDS_DIR ||
+ tree_type == TREE_TYPE_MUSIC_DIR)
+ num_artwork_sets_succeeded++;
+ else
+ num_files_failed++;
+
+ // SDL_DROPBEGIN / SDL_DROPCOMPLETE did not exist in older SDL versions
+ if (!confirm_on_drop_complete)
+ {
+ // process all remaining events, including further SDL_DROPFILE events
+ ClearEventQueue();
+
+ HandleDropCompleteEvent(num_level_sets_succeeded,
+ num_artwork_sets_succeeded,
+ num_files_failed);
+
+ num_level_sets_succeeded = 0;
+ num_artwork_sets_succeeded = 0;
+ num_files_failed = 0;
+ }
+
+ break;
+ }
+
+ case SDL_DROPTEXT:
+ {
+ HandleDropTextEvent(event->drop.file);
+
+ break;
+ }
+
+ case SDL_DROPCOMPLETE:
+ {
+ HandleDropCompleteEvent(num_level_sets_succeeded,
+ num_artwork_sets_succeeded,
+ num_files_failed);
+
+ break;
+ }
+ }
+
+ if (event->drop.file != NULL)
+ SDL_free(event->drop.file);
+}
+
+void HandleUserEvent(UserEvent *event)
+{
+ switch (event->code)
+ {
+ case USEREVENT_ANIM_DELAY_ACTION:
+ case USEREVENT_ANIM_EVENT_ACTION:
+ // execute action functions until matching action was found
+ if (DoKeysymAction(event->value1) ||
+ DoGadgetAction(event->value1) ||
+ DoScreenAction(event->value1))
+ return;
+ break;
+
+ default:
+ break;
+ }
+}
+
+void HandleButton(int mx, int my, int button, int button_nr)
+{
+ static int old_mx = 0, old_my = 0;
+ boolean button_hold = FALSE;
+ boolean handle_gadgets = TRUE;
+ int game_status_last = game_status;
+
+ if (button_nr < 0)
+ {
+ mx = old_mx;
+ my = old_my;
+ button_nr = -button_nr;
+ button_hold = TRUE;
+ }
+ else
+ {
+ old_mx = mx;
+ old_my = my;
+ }
+
+#if defined(PLATFORM_ANDROID)
+ // when playing, only handle gadgets when using "follow finger" controls
+ // or when using touch controls in combination with the MM game engine
+ // or when using gadgets that do not overlap with virtual buttons
+ // or when touch controls are disabled (e.g., with mouse-only levels)
+ handle_gadgets =
+ (game_status != GAME_MODE_PLAYING ||
+ level.game_engine_type == GAME_ENGINE_TYPE_MM ||
+ strEqual(setup.touch.control_type, TOUCH_CONTROL_OFF) ||
+ strEqual(setup.touch.control_type, TOUCH_CONTROL_FOLLOW_FINGER) ||
+ (strEqual(setup.touch.control_type, TOUCH_CONTROL_VIRTUAL_BUTTONS) &&
+ !CheckVirtualButtonPressed(mx, my, button)));
+
+ // always recognize potentially releasing already pressed gadgets
+ if (button == MB_RELEASED)
+ handle_gadgets = TRUE;
+
+ // always recognize pressing or releasing overlay touch buttons
+ if (CheckPosition_OverlayTouchButtons(mx, my, button) && !motion_status)
+ handle_gadgets = TRUE;
+#endif
+
+ if (HandleGlobalAnimClicks(mx, my, button, FALSE))
+ {
+ // do not handle this button event anymore
+ return; // force mouse event not to be handled at all
+ }
+
+ if (handle_gadgets && HandleGadgets(mx, my, button))
+ {
+ // do not handle this button event anymore with position on screen
+ mx = my = -32; // force mouse event to be outside screen tiles
+
+ // do not handle this button event anymore if game status has changed
+ if (game_status != game_status_last)
+ return;
+ }
+
+ 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))
+ return;
+
+ switch (game_status)
+ {
+ case GAME_MODE_TITLE:
+ HandleTitleScreen(mx, my, 0, 0, button);
+ break;
+
+ case GAME_MODE_MAIN:
+ HandleMainMenu(mx, my, 0, 0, button);
+ break;
+
+ case GAME_MODE_PSEUDO_TYPENAME:
+ case GAME_MODE_PSEUDO_TYPENAMES:
+ HandleTypeName(KSYM_Return);
+ break;
+
+ case GAME_MODE_NAMES:
+ HandleChoosePlayerName(mx, my, 0, 0, button);
+ break;
+
+ case GAME_MODE_LEVELS:
+ HandleChooseLevelSet(mx, my, 0, 0, button);
+ break;
+
+ case GAME_MODE_LEVELNR:
+ HandleChooseLevelNr(mx, my, 0, 0, button);
+ break;
+
+ case GAME_MODE_SCORES:
+ HandleHallOfFame(mx, my, 0, 0, button);
+ break;
+
+ case GAME_MODE_SCOREINFO:
+ HandleScoreInfo(mx, my, 0, 0, button);
+ break;
+
+ case GAME_MODE_EDITOR:
+ HandleLevelEditorIdle();
+ break;
+
+ case GAME_MODE_INFO:
+ HandleInfoScreen(mx, my, 0, 0, button);
+ break;
+
+ case GAME_MODE_SETUP:
+ HandleSetupScreen(mx, my, 0, 0, button);
+ break;
+
+ case GAME_MODE_PLAYING:
+ if (!strEqual(setup.touch.control_type, TOUCH_CONTROL_OFF))
+ HandleButtonOrFinger(mx, my, button);
+ else
+ SetPlayerMouseAction(mx, my, button);
+
+#ifdef DEBUG
+ if (button == MB_PRESSED && !motion_status && !button_hold &&
+ IN_GFX_FIELD_PLAY(mx, my) && GetKeyModState() & KMOD_Control)
+ DumpTileFromScreen(mx, my);
+#endif
+
+ break;
+
+ default:
+ break;
+ }
+}
+
+#define MAX_CHEAT_INPUT_LEN 32
+
+static void HandleKeysSpecial(Key key)
+{
+ static char cheat_input[2 * MAX_CHEAT_INPUT_LEN + 1] = "";
+ char letter = getCharFromKey(key);
+ int cheat_input_len = strlen(cheat_input);
+ int i;
+
+ if (letter == 0)
+ return;
+
+ if (cheat_input_len >= 2 * MAX_CHEAT_INPUT_LEN)
+ {
+ for (i = 0; i < MAX_CHEAT_INPUT_LEN + 1; i++)
+ cheat_input[i] = cheat_input[MAX_CHEAT_INPUT_LEN + i];
+
+ cheat_input_len = MAX_CHEAT_INPUT_LEN;
+ }
+
+ cheat_input[cheat_input_len++] = letter;
+ cheat_input[cheat_input_len] = '\0';
+
+#if DEBUG_EVENTS_KEY
+ Debug("event:key:special", "'%s' [%d]", cheat_input, cheat_input_len);
+#endif
+
+ if (game_status == GAME_MODE_MAIN)
+ {
+ if (strSuffix(cheat_input, ":insert-solution-tape") ||
+ strSuffix(cheat_input, ":ist"))
+ {
+ InsertSolutionTape();
+ }
+ else if (strSuffix(cheat_input, ":play-solution-tape") ||
+ strSuffix(cheat_input, ":pst"))
+ {
+ PlaySolutionTape();
+ }
+ else if (strSuffix(cheat_input, ":reload-graphics") ||
+ strSuffix(cheat_input, ":rg"))
+ {
+ ReloadCustomArtwork(1 << ARTWORK_TYPE_GRAPHICS);
+ DrawMainMenu();
+ }
+ else if (strSuffix(cheat_input, ":reload-sounds") ||
+ strSuffix(cheat_input, ":rs"))
+ {
+ ReloadCustomArtwork(1 << ARTWORK_TYPE_SOUNDS);
+ DrawMainMenu();
+ }
+ else if (strSuffix(cheat_input, ":reload-music") ||
+ strSuffix(cheat_input, ":rm"))
+ {
+ ReloadCustomArtwork(1 << ARTWORK_TYPE_MUSIC);
+ DrawMainMenu();
+ }
+ else if (strSuffix(cheat_input, ":reload-artwork") ||
+ strSuffix(cheat_input, ":ra"))
+ {
+ ReloadCustomArtwork(1 << ARTWORK_TYPE_GRAPHICS |
+ 1 << ARTWORK_TYPE_SOUNDS |
+ 1 << ARTWORK_TYPE_MUSIC);
+ DrawMainMenu();
+ }
+ else if (strSuffix(cheat_input, ":dump-level") ||
+ strSuffix(cheat_input, ":dl"))
+ {
+ DumpLevel(&level);
+ }
+ else if (strSuffix(cheat_input, ":dump-tape") ||
+ strSuffix(cheat_input, ":dt"))
+ {
+ DumpTape(&tape);
+ }
+ else if (strSuffix(cheat_input, ":undo-tape") ||
+ strSuffix(cheat_input, ":ut"))
+ {
+ UndoTape();
+ }
+ else if (strSuffix(cheat_input, ":fix-tape") ||
+ strSuffix(cheat_input, ":ft"))
+ {
+ FixTape_ForceSinglePlayer();
+ }
+ else if (strSuffix(cheat_input, ":save-native-level") ||
+ strSuffix(cheat_input, ":snl"))
+ {
+ SaveNativeLevel(&level);
+ }
+ else if (strSuffix(cheat_input, ":frames-per-second") ||
+ strSuffix(cheat_input, ":fps"))
+ {
+ global.show_frames_per_second = !global.show_frames_per_second;
+ }
+ else if (strSuffix(cheat_input, ":xsn"))
+ {
+ tile_cursor.xsn_debug = TRUE;
+ }
+ }
+ else if (game_status == GAME_MODE_PLAYING)
+ {
+#ifdef DEBUG
+ if (strSuffix(cheat_input, ".q"))
+ DEBUG_SetMaximumDynamite();
+#endif
+ }
+ else if (game_status == GAME_MODE_EDITOR)
+ {
+ if (strSuffix(cheat_input, ":dump-brush") ||
+ strSuffix(cheat_input, ":DB"))
+ {
+ DumpBrush();
+ }
+ else if (strSuffix(cheat_input, ":DDB"))
+ {
+ DumpBrush_Small();
+ }
+
+ if (GetKeyModState() & (KMOD_Control | KMOD_Meta))
+ {
+ if (letter == 'x') // copy brush to clipboard (small size)
+ {
+ CopyBrushToClipboard_Small();
+ }
+ else if (letter == 'c') // copy brush to clipboard (normal size)
+ {
+ CopyBrushToClipboard();
+ }
+ else if (letter == 'v') // paste brush from Clipboard
+ {
+ CopyClipboardToBrush();
+ }
+ else if (letter == 'z') // undo or redo last operation
+ {
+ if (GetKeyModState() & KMOD_Shift)
+ RedoLevelEditorOperation();
+ else
+ UndoLevelEditorOperation();
+ }
+ }
+ }
+
+ // special key shortcuts for all game modes
+ if (strSuffix(cheat_input, ":dump-event-actions") ||
+ strSuffix(cheat_input, ":dea") ||
+ strSuffix(cheat_input, ":DEA"))
+ {
+ DumpGadgetIdentifiers();
+ DumpScreenIdentifiers();
+ }
+}
+
+boolean HandleKeysDebug(Key key, int key_status)
+{
+#ifdef DEBUG
+ int i;
+
+ if (key_status != KEY_PRESSED)
+ return FALSE;
+
+ if (game_status == GAME_MODE_PLAYING || !setup.debug.frame_delay_game_only)
+ {
+ boolean mod_key_pressed = ((GetKeyModState() & KMOD_Valid) != 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)
+ Debug("event:key:debug", "frame delay == %d ms", GameFrameDelay);
+ else if (GameFrameDelay != 0)
+ Debug("event:key:debug", "frame delay == %d ms (max. %d fps / %d %%)",
+ GameFrameDelay, ONE_SECOND_DELAY / GameFrameDelay,
+ GAME_FRAME_DELAY * 100 / GameFrameDelay);
+ else
+ Debug("event:key:debug", "frame delay == 0 ms (maximum speed)");
+
+ return TRUE;
+ }
+ }
+ }
+
+ if (game_status == GAME_MODE_PLAYING)
+ {
+ if (key == KSYM_d)
+ {
+ options.debug = !options.debug;
+
+ Debug("event:key:debug", "debug mode %s",
+ (options.debug ? "enabled" : "disabled"));
+
+ return TRUE;
+ }
+ else if (key == KSYM_v)
+ {
+ Debug("event:key:debug", "currently using game engine version %d",
+ game.engine_version);
+
+ return TRUE;
+ }
+ }
+#endif
+
+ return FALSE;
+}
+
+void HandleKey(Key key, int key_status)
+{
+ boolean anyTextGadgetActiveOrJustFinished = anyTextGadgetActive();
+ 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;