/* * This software is licensed under the terms of the MIT License. * See COPYING for further information. * --- * Copyright (c) 2011-2024, Lukas Weber . * Copyright (c) 2012-2024, Andrei Alexeyev . */ #include "events.h" #include "config.h" #include "global.h" #include "transition.h" #include "video.h" static hrtime_t keyrepeat_paused_until; static int global_handlers_lock = 0; static DYNAMIC_ARRAY(EventHandler) global_handlers_pending; static DYNAMIC_ARRAY(EventHandler) global_handlers; static DYNAMIC_ARRAY(SDL_Event) deferred_events; uint32_t sdl_first_user_event; static void events_register_default_handlers(void); static void events_unregister_default_handlers(void); #define MAX_ACTIVE_HANDLERS 32 /* * Public API */ void events_init(void) { sdl_first_user_event = SDL_RegisterEvents(NUM_TAISEI_EVENTS); if(sdl_first_user_event == (uint32_t)-1) { char *s = "You have exhausted the SDL userevent pool. " "How you managed that is beyond me, but congratulations."; log_fatal("%s", s); } SDL_EventState(SDL_MOUSEMOTION, SDL_DISABLE); events_register_default_handlers(); } void events_shutdown(void) { events_unregister_default_handlers(); dynarray_free_data(&deferred_events); #ifdef DEBUG dynarray_foreach_elem(&global_handlers, EventHandler *h, { log_warn("Global event handler was not unregistered: %p", UNION_CAST(EventHandlerProc, void*, h->proc)); }); #endif dynarray_free_data(&global_handlers); dynarray_free_data(&global_handlers_pending); } static bool events_invoke_handler(SDL_Event *event, EventHandler *handler) { assert(handler->proc != NULL); assert(global_handlers_lock > 0); if(!handler->event_type || handler->event_type == event->type) { bool result = handler->proc(event, handler->arg); return result; } return false; } static inline int prio_index(EventPriority prio) { return prio - EPRIO_FIRST; } void events_register_handler(EventHandler *handler) { assert(sdl_first_user_event > 0); assert(handler->proc != NULL); assert(handler->priority >= EPRIO_FIRST); assert(handler->priority <= EPRIO_LAST); if(global_handlers_lock) { dynarray_append(&global_handlers_pending, *handler); } else { dynarray_append(&global_handlers, *handler); } // don't bother sorting, since most of the time we will need to re-sort it // together with local handlers when polling } static bool hfilter_remove_pending(const void *pelem, void *ignored) { const EventHandler *h = pelem; return !h->_private.removal_pending; } void events_unregister_handler(EventHandlerProc proc) { dynarray_foreach_elem(&global_handlers, EventHandler *h, { if(h->proc == proc && !h->_private.removal_pending) { h->_private.removal_pending = true; break; } }); if(global_handlers_lock) { assert(global_handlers_lock > 0); } else { dynarray_filter(&global_handlers, hfilter_remove_pending, NULL); } } static void events_apply_flags(EventFlags flags) { if(flags & EFLAG_TEXT) { if(!SDL_IsTextInputActive()) { SDL_StartTextInput(); } } else { if(SDL_IsTextInputActive()) { SDL_StopTextInput(); } } TaiseiEvent type; for(type = TE_MENU_FIRST; type <= TE_MENU_LAST; ++type) { SDL_EventState(MAKE_TAISEI_EVENT(type), (bool)(flags & EFLAG_MENU)); } for(type = TE_GAME_FIRST; type <= TE_GAME_LAST; ++type) { SDL_EventState(MAKE_TAISEI_EVENT(type), (bool)(flags & EFLAG_GAME)); } } static int enqueue_event_handlers(int max, EventHandler *queue[max], EventHandler local_handlers[]) { // counting sort! int cnt = 0; int pcount[NUM_EPRIOS] = { 0 }; EventHandler *temp[max]; assert(global_handlers.num_elements <= max); dynarray_foreach_elem(&global_handlers, EventHandler *h, { ++pcount[prio_index(h->priority)]; temp[cnt++] = h; }); if(local_handlers) { for(EventHandler *h = local_handlers; h->proc; ++h) { int pidx = prio_index(h->priority); assert((uint)pidx < ARRAY_SIZE(pcount)); ++pcount[pidx]; temp[cnt++] = h; assert(cnt < max); } } for(int i = 0, total = 0; i < NUM_EPRIOS; ++i) { int t = total; total += pcount[i]; pcount[i] = t; } for(int i = 0; i < cnt; ++i) { EventHandler *h = temp[i]; int p = prio_index(h->priority); queue[pcount[p]] = h; ++pcount[p]; } return cnt; } static void push_event(SDL_Event *e) { /* * NOTE: The SDL_PushEvent() function is a wrapper around SDL_PeepEvents that also sets the * timestamp field and calls the event filter function and event watchers. We don't use any of * that, and setting the timestamp involves an expensive system call, so avoid it. */ if(UNLIKELY(SDL_PeepEvents(e, 1, SDL_ADDEVENT, 0, 0) <= 0)) { log_sdl_error(LOG_ERROR, "SDL_PeepEvents"); } } void events_poll(EventHandler *handlers, EventFlags flags) { events_apply_flags(flags); events_emit(TE_FRAME, 0, NULL, NULL); ++global_handlers_lock; EventHandler *hqueue[MAX_ACTIVE_HANDLERS]; int hqueue_size = enqueue_event_handlers(ARRAY_SIZE(hqueue), hqueue, handlers); for(;;) { if(!(flags & EFLAG_NOPUMP)) { SDL_PumpEvents(); } SDL_Event events[8]; int nevents = SDL_PeepEvents(events, ARRAY_SIZE(events), SDL_GETEVENT, SDL_FIRSTEVENT, SDL_LASTEVENT); if(UNLIKELY(nevents < 0)) { log_sdl_error(LOG_ERROR, "SDL_PeepEvents"); } if(nevents == 0) { break; } for(SDL_Event *e = events, *end = events + nevents; e < end; ++e) { for(int i = 0; i < hqueue_size; ++i) { EventHandler *h = NOT_NULL(hqueue[i]); if(h->_private.removal_pending) { continue; } if(events_invoke_handler(e, h)) { break; } } } } if(--global_handlers_lock == 0) { dynarray_filter(&global_handlers, hfilter_remove_pending, NULL); dynarray_ensure_capacity(&global_handlers, global_handlers.num_elements + global_handlers_pending.num_elements); dynarray_foreach_elem(&global_handlers_pending, EventHandler *h, { *dynarray_append(&global_handlers) = *h; }); global_handlers_pending.num_elements = 0; } dynarray_foreach_elem(&deferred_events, SDL_Event *evt, { push_event(evt); }); deferred_events.num_elements = 0; } void events_emit(TaiseiEvent type, int32_t code, void *data1, void *data2) { assert(TAISEI_EVENT_VALID(type)); uint32_t sdltype = MAKE_TAISEI_EVENT(type); assert(IS_TAISEI_EVENT(sdltype)); if(!SDL_EventState(sdltype, SDL_QUERY)) { return; } SDL_Event event = { 0 }; event.type = sdltype; event.user.code = code; event.user.data1 = data1; event.user.data2 = data2; push_event(&event); } void events_defer(SDL_Event *evt) { dynarray_append(&deferred_events, *evt); } void events_pause_keyrepeat(void) { // workaround for SDL bug // https://bugzilla.libsdl.org/show_bug.cgi?id=3287 keyrepeat_paused_until = time_get() + HRTIME_RESOLUTION / 4; } /* * Default handlers */ static bool events_handler_quit(SDL_Event *event, void *arg); static bool events_handler_keyrepeat_workaround(SDL_Event *event, void *arg); static bool events_handler_clipboard(SDL_Event *event, void *arg); static bool events_handler_hotkeys(SDL_Event *event, void *arg); static bool events_handler_key_down(SDL_Event *event, void *arg); static bool events_handler_key_up(SDL_Event *event, void *arg); attr_unused static bool events_handler_debug_winevt(SDL_Event *event, void *arg) { // copied from SDL wiki almost verbatim if(event->type == SDL_WINDOWEVENT) { switch(event->window.event) { case SDL_WINDOWEVENT_SHOWN: log_info("Window %d shown", event->window.windowID); break; case SDL_WINDOWEVENT_HIDDEN: log_info("Window %d hidden", event->window.windowID); break; case SDL_WINDOWEVENT_EXPOSED: log_info("Window %d exposed", event->window.windowID); break; case SDL_WINDOWEVENT_MOVED: log_info("Window %d moved to %d,%d", event->window.windowID, event->window.data1, event->window.data2); break; case SDL_WINDOWEVENT_RESIZED: log_info("Window %d resized to %dx%d", event->window.windowID, event->window.data1, event->window.data2); break; case SDL_WINDOWEVENT_SIZE_CHANGED: log_info("Window %d size changed to %dx%d", event->window.windowID, event->window.data1, event->window.data2); break; case SDL_WINDOWEVENT_MINIMIZED: log_info("Window %d minimized", event->window.windowID); break; case SDL_WINDOWEVENT_MAXIMIZED: log_info("Window %d maximized", event->window.windowID); break; case SDL_WINDOWEVENT_RESTORED: log_info("Window %d restored", event->window.windowID); break; case SDL_WINDOWEVENT_ENTER: log_info("Mouse entered window %d", event->window.windowID); break; case SDL_WINDOWEVENT_LEAVE: log_info("Mouse left window %d", event->window.windowID); break; case SDL_WINDOWEVENT_FOCUS_GAINED: log_info("Window %d gained keyboard focus", event->window.windowID); break; case SDL_WINDOWEVENT_FOCUS_LOST: log_info("Window %d lost keyboard focus", event->window.windowID); break; case SDL_WINDOWEVENT_CLOSE: log_info("Window %d closed", event->window.windowID); break; case SDL_WINDOWEVENT_TAKE_FOCUS: log_info("Window %d is offered a focus", event->window.windowID); break; case SDL_WINDOWEVENT_HIT_TEST: log_info("Window %d has a special hit test", event->window.windowID); break; default: log_warn("Window %d got unknown event %d", event->window.windowID, event->window.event); break; } } return false; } static EventHandler default_handlers[] = { #ifdef DEBUG_WINDOW_EVENTS { .proc = events_handler_debug_winevt, .priority = EPRIO_SYSTEM, .event_type = SDL_WINDOWEVENT }, #endif { .proc = events_handler_quit, .priority = EPRIO_SYSTEM, .event_type = SDL_QUIT }, { .proc = events_handler_keyrepeat_workaround, .priority = EPRIO_CAPTURE, .event_type = 0 }, { .proc = events_handler_clipboard, .priority = EPRIO_CAPTURE, .event_type = SDL_KEYDOWN }, { .proc = events_handler_hotkeys, .priority = EPRIO_HOTKEYS, .event_type = SDL_KEYDOWN }, { .proc = events_handler_key_down, .priority = EPRIO_TRANSLATION, .event_type = SDL_KEYDOWN }, { .proc = events_handler_key_up, .priority = EPRIO_TRANSLATION, .event_type = SDL_KEYUP }, {NULL} }; static void events_register_default_handlers(void) { for(EventHandler *h = default_handlers; h->proc; ++h) { events_register_handler(h); } } static void events_unregister_default_handlers(void) { for(EventHandler *h = default_handlers; h->proc; ++h) { events_unregister_handler(h->proc); } } static bool events_handler_quit(SDL_Event *event, void *arg) { taisei_quit(); return true; } static bool events_handler_keyrepeat_workaround(SDL_Event *event, void *arg) { hrtime_t timenow = time_get(); if(event->type != SDL_KEYDOWN) { uint32_t te = TAISEI_EVENT(event->type); if(te < TE_MENU_FIRST || te > TE_MENU_LAST) { return false; } } if(timenow < keyrepeat_paused_until) { log_debug( "Prevented a potentially bogus key repeat (%"PRIuTIME" remaining). " "This is an SDL bug. See https://bugzilla.libsdl.org/show_bug.cgi?id=3287", keyrepeat_paused_until - timenow ); return true; } return false; } static bool events_handler_clipboard(SDL_Event *event, void *arg) { if(!SDL_IsTextInputActive()) { return false; } if(event->key.keysym.mod & KMOD_CTRL && event->key.keysym.scancode == SDL_SCANCODE_V) { if(SDL_HasClipboardText()) { memset(event, 0, sizeof(SDL_Event)); event->type = MAKE_TAISEI_EVENT(TE_CLIPBOARD_PASTE); } else { return true; } } return false; } static bool events_handler_key_down(SDL_Event *event, void *arg) { SDL_Scancode scan = event->key.keysym.scancode; bool repeat = event->key.repeat; if(video_get_backend() == VIDEO_BACKEND_EMSCRIPTEN && scan == SDL_SCANCODE_TAB) { scan = SDL_SCANCODE_ESCAPE; } /* * Emit menu events */ struct eventmap_s { int scancode; TaiseiEvent event; } menu_event_map[] = { // order matters // handle all the hardcoded controls first to prevent accidentally overriding them with unusable ones { SDL_SCANCODE_DOWN, TE_MENU_CURSOR_DOWN }, { SDL_SCANCODE_UP, TE_MENU_CURSOR_UP }, { SDL_SCANCODE_RIGHT, TE_MENU_CURSOR_RIGHT }, { SDL_SCANCODE_LEFT, TE_MENU_CURSOR_LEFT }, { SDL_SCANCODE_RETURN, TE_MENU_ACCEPT }, { SDL_SCANCODE_ESCAPE, TE_MENU_ABORT }, { config_get_int(CONFIG_KEY_DOWN), TE_MENU_CURSOR_DOWN }, { config_get_int(CONFIG_KEY_UP), TE_MENU_CURSOR_UP }, { config_get_int(CONFIG_KEY_RIGHT), TE_MENU_CURSOR_RIGHT }, { config_get_int(CONFIG_KEY_LEFT), TE_MENU_CURSOR_LEFT }, { config_get_int(CONFIG_KEY_SHOT), TE_MENU_ACCEPT }, { config_get_int(CONFIG_KEY_BOMB), TE_MENU_ABORT }, {SDL_SCANCODE_UNKNOWN, -1} }; if(!repeat || transition.state == TRANS_IDLE) { for(struct eventmap_s *m = menu_event_map; m->scancode != SDL_SCANCODE_UNKNOWN; ++m) { if(scan == m->scancode && (!repeat || m->event != TE_MENU_ACCEPT)) { events_emit(m->event, 0, (void*)(intptr_t)INDEV_KEYBOARD, NULL); break; } } } /* * Emit game events */ if(!repeat) { if(scan == config_get_int(CONFIG_KEY_PAUSE) || scan == SDL_SCANCODE_ESCAPE) { events_emit(TE_GAME_PAUSE, 0, NULL, NULL); } else { int key = config_key_from_scancode(scan); if(key >= 0) { events_emit(TE_GAME_KEY_DOWN, key, (void*)(intptr_t)INDEV_KEYBOARD, NULL); } } } return false; } static bool events_handler_key_up(SDL_Event *event, void *arg) { SDL_Scancode scan = event->key.keysym.scancode; /* * Emit game events */ int key = config_key_from_scancode(scan); if(key >= 0) { events_emit(TE_GAME_KEY_UP, key, (void*)(intptr_t)INDEV_KEYBOARD, NULL); } return false; } static bool events_handler_hotkeys(SDL_Event *event, void *arg) { if(event->key.repeat) { return false; } SDL_Scancode scan = event->key.keysym.scancode; SDL_Keymod mod = event->key.keysym.mod; if(scan == config_get_int(CONFIG_KEY_SCREENSHOT)) { bool viewport_only = (mod & KMOD_ALT); video_take_screenshot(viewport_only); return true; } if((scan == SDL_SCANCODE_RETURN && (mod & KMOD_ALT)) || scan == config_get_int(CONFIG_KEY_FULLSCREEN)) { video_set_fullscreen(!video_is_fullscreen()); return true; } if(scan == config_get_int(CONFIG_KEY_TOGGLE_AUDIO)) { config_set_int(CONFIG_MUTE_AUDIO, !config_get_int(CONFIG_MUTE_AUDIO)); return true; } if(scan == config_get_int(CONFIG_KEY_RELOAD_RESOURCES)) { res_reload_all(); return true; } return false; }