diff --git a/source/gui/tests/test_GuiManager.h b/source/gui/tests/test_GuiManager.h index beb1b81bbd..ad415e3d0c 100644 --- a/source/gui/tests/test_GuiManager.h +++ b/source/gui/tests/test_GuiManager.h @@ -32,6 +32,7 @@ #include "ps/GameSetup/GameSetup.h" #include "ps/Hotkey.h" #include "ps/XML/Xeromyces.h" +#include "ps/VideoMode.h" #include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/Object.h" #include "scriptinterface/ScriptConversions.h" @@ -166,11 +167,10 @@ public: hotkeyNotification.key.repeat = 0; // Init input and poll the event. - InitInput(); - in_push_priority_event(hotkeyNotification); - SDL_Event ev; - while (in_poll_event(ev)) - in_dispatch_event(ev); + const std::unique_ptr _{MakeInputHandlers()}; + g_VideoMode.m_InputManager.PushPriorityEvent(hotkeyNotification); + for (SDL_Event& ev : g_VideoMode.m_InputManager.PollEvents()) + g_VideoMode.m_InputManager.DispatchEvent(ev); const ScriptInterface& pageScriptInterface = *(g_GUI->GetActiveGUI()->GetScriptInterface()); ScriptRequest prq(pageScriptInterface); @@ -191,9 +191,9 @@ public: // We are listening to KeyDown events, so repeat shouldn't matter. hotkeyNotification.key.repeat = 1; - in_push_priority_event(hotkeyNotification); - while (in_poll_event(ev)) - in_dispatch_event(ev); + g_VideoMode.m_InputManager.PushPriorityEvent(hotkeyNotification); + for (SDL_Event& ev : g_VideoMode.m_InputManager.PollEvents()) + g_VideoMode.m_InputManager.DispatchEvent(ev); hotkey_pressed_value = false; Script::GetProperty(prq, global, "state_before", &js_hotkey_pressed_value); @@ -206,9 +206,9 @@ public: TS_ASSERT_EQUALS(hotkey_pressed_value, true); hotkeyNotification.type = SDL_KEYUP; - in_push_priority_event(hotkeyNotification); - while (in_poll_event(ev)) - in_dispatch_event(ev); + g_VideoMode.m_InputManager.PushPriorityEvent(hotkeyNotification); + for (SDL_Event& ev : g_VideoMode.m_InputManager.PollEvents()) + g_VideoMode.m_InputManager.DispatchEvent(ev); hotkey_pressed_value = true; Script::GetProperty(prq, global, "state_before", &js_hotkey_pressed_value); diff --git a/source/lib/input.cpp b/source/lib/input.cpp deleted file mode 100644 index 5bc2deee7b..0000000000 --- a/source/lib/input.cpp +++ /dev/null @@ -1,97 +0,0 @@ -/* Copyright (C) 2026 Wildfire Games. - * - * Permission is hereby granted, free of charge, to any person obtaining - * a copy of this software and associated documentation files (the - * "Software"), to deal in the Software without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, sublicense, and/or sell copies of the Software, and to - * permit persons to whom the Software is furnished to do so, subject to - * the following conditions: - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY - * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, - * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE - * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -/* - * SDL input redirector; dispatches to multiple handlers. - */ - -#include "precompiled.h" - -#include "input.h" - -#include "lib/debug.h" -#include "lib/external_libraries/libsdl.h" -#include "lib/status.h" - -#include -#include -#include - -const size_t MAX_HANDLERS = 10; -static InHandler handler_stack[MAX_HANDLERS]; -static size_t handler_stack_top = 0; - -static std::list priority_events; - -void in_add_handler(InHandler handler) -{ - ENSURE(handler); - - if(handler_stack_top >= MAX_HANDLERS) - WARN_IF_ERR(ERR::LIMIT); - - handler_stack[handler_stack_top++] = handler; -} - -void in_reset_handlers() -{ - handler_stack_top = 0; -} - -// send ev to each handler until one returns IN_HANDLED -void in_dispatch_event(const SDL_Event& ev) -{ - for(int i = (int)handler_stack_top-1; i >= 0; i--) - { - ENSURE(handler_stack[i]); - InReaction ret = handler_stack[i](ev); - // .. done, return - if(ret == IN_HANDLED) - return; - // .. next handler - else if(ret == IN_PASS) - continue; - // .. invalid return value - else - DEBUG_WARN_ERR(ERR::LOGIC); // invalid handler return value - } -} - -void in_push_priority_event(const SDL_Event& event) -{ - priority_events.push_back(event); -} - -int in_poll_priority_event(SDL_Event& event) -{ - if (priority_events.empty()) - return 0; - - event = priority_events.front(); - priority_events.pop_front(); - return 1; -} - -int in_poll_event(SDL_Event& event) -{ - return in_poll_priority_event(event) ? 1 : SDL_PollEvent(&event); -} diff --git a/source/lib/input.h b/source/lib/input.h index 8843a4624e..0cd434e186 100644 --- a/source/lib/input.h +++ b/source/lib/input.h @@ -42,29 +42,4 @@ enum InReaction IN_HANDLED = 2 }; -typedef InReaction (*InHandler)(const SDL_Event&); - -// register an input handler, which will receive all subsequent events first. -// events are passed to other handlers if handler returns IN_PASS. -extern void in_add_handler(InHandler handler); - -// remove all registered input handlers -extern void in_reset_handlers(); - -// send event to each handler (newest first) until one returns true -extern void in_dispatch_event(const SDL_Event& event); - -// push an event onto the back of a high-priority queue - the new event will -// be returned by in_poll_event before any standard SDL events -extern void in_push_priority_event(const SDL_Event& event); - -// reads events that were pushed by in_push_priority_event -// returns 1 if an event was read, 0 otherwise. -extern int in_poll_priority_event(SDL_Event& event); - -// reads events that were pushed by in_push_priority_event, or, if there are -// no high-priority events) reads from the SDL event queue with SDL_PollEvent. -// returns 1 if an event was read, 0 otherwise. -extern int in_poll_event(SDL_Event& event); - #endif // #ifndef INCLUDED_INPUT diff --git a/source/main.cpp b/source/main.cpp index 67ce0d754a..412d201d7f 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -41,7 +41,6 @@ that of Atlas depending on commandline parameters. #include "lib/file/file_system.h" #include "lib/file/vfs/vfs.h" #include "lib/frequency_filter.h" -#include "lib/input.h" #include "lib/path.h" #include "lib/posix/posix_types.h" #include "lib/secure_crt.h" @@ -66,6 +65,7 @@ that of Atlas depending on commandline parameters. #include "ps/GameSetup/Paths.h" #include "ps/Globals.h" #include "ps/Hotkey.h" +#include "ps/Input.h" #include "ps/Loader.h" #include "ps/Mod.h" #include "ps/ModInstaller.h" @@ -281,8 +281,7 @@ static void PumpEvents() PROFILE3("dispatch events"); - SDL_Event ev{}; - while (in_poll_event(ev)) + for (SDL_Event& ev : g_VideoMode.m_InputManager.PollEvents()) { PROFILE2("event"); if (g_GUI) @@ -292,7 +291,7 @@ static void PumpEvents() std::string data = Script::StringifyJSON(rq, &tmpVal); PROFILE2_ATTR("%s", data.c_str()); } - in_dispatch_event(ev); + g_VideoMode.m_InputManager.DispatchEvent(ev); } g_TouchInput.Frame(); @@ -524,19 +523,6 @@ static void NonVisualFrame() QuitEngine(EXIT_SUCCESS); } -static void MainControllerInit() -{ - // add additional input handlers only needed by this controller: - - // must be registered after gui_handler. Should mayhap even be last. - in_add_handler(MainInputHandler); -} - -static void MainControllerShutdown() -{ - in_reset_handlers(); -} - static std::optional CreateRLInterface(const CmdLineArgs& args) { if (!args.Has("rl-interface")) @@ -751,58 +737,82 @@ static void RunGameOrAtlas(const std::span argv) std::optional dapInterface{CreateDAPInterface(args)}; #endif // CONFIG2_DAP_INTERFACE - std::optional guiScriptInterface; - - if (isVisual) { - guiScriptInterface.emplace("Engine", "gui", *g_ScriptContext); - InitGraphics(args, 0, installedMods, *g_ScriptContext, *guiScriptInterface); - MainControllerInit(); - } - else if (!InitNonVisual(args)) - g_Shutdown = ShutdownType::Quit; - - try - { - // MSVC doesn't support copy elision in ternary expressions. So we use a lambda instead. - std::optional rlInterface{[&]() -> std::optional + class VisualData + { + public: + VisualData(const CmdLineArgs& args, std::vector& installedMods) : + inputHandlers{InitGraphics(args, 0, installedMods, *g_ScriptContext, + scriptInterface)} { - if (g_Shutdown == ShutdownType::None) - return CreateRLInterface(args); + } + + VisualData(const VisualData&) = delete; + VisualData& operator=(const VisualData&) = delete; + VisualData(VisualData&&) = delete; + VisualData& operator=(VisualData&) = delete; + ~VisualData() = default; + + private: + ScriptInterface scriptInterface{"Engine", "gui", *g_ScriptContext}; + std::unique_ptr inputHandlers; + Input::Handler mainInputHandler{ + g_VideoMode.m_InputManager, Input::Slot::PRIMARY, MainInputHandler}; + }; + + // MSVC doesn't support copy elision in ternary expressions. So we use a lambda instead. + const std::optional visualData{[&]() -> std::optional + { + if (isVisual) + return std::make_optional(args, installedMods); else return std::nullopt; }()}; + if (!isVisual && !InitNonVisual(args)) + g_Shutdown = ShutdownType::Quit; - while (g_Shutdown == ShutdownType::None) + try { - if (isVisual) + // MSVC doesn't support copy elision in ternary expressions. So we use a lambda instead. + std::optional rlInterface{[&]() -> std::optional + { + if (g_Shutdown == ShutdownType::None) + return CreateRLInterface(args); + else + return std::nullopt; + }()}; + + while (g_Shutdown == ShutdownType::None) + { + if (isVisual) + { #if CONFIG2_DAP_INTERFACE - Frame(rlInterface ? &*rlInterface : nullptr, fixedFrameFrequency, dapInterface ? &*dapInterface : nullptr); + Frame(rlInterface ? &*rlInterface : nullptr, fixedFrameFrequency, + dapInterface ? &*dapInterface : nullptr); #else - Frame(rlInterface ? &*rlInterface : nullptr, fixedFrameFrequency); + Frame(rlInterface ? &*rlInterface : nullptr, fixedFrameFrequency); #endif - else if(rlInterface) - rlInterface->TryApplyMessage(); - else - NonVisualFrame(); + } + else if(rlInterface) + rlInterface->TryApplyMessage(); + else + NonVisualFrame(); + } + + } + catch (const RL::SetupError&) + { + rlInterfaceError = true; } + ShutdownNetworkAndUI(); } - catch (const RL::SetupError&) - { - rlInterfaceError = true; - } - - ShutdownNetworkAndUI(); - guiScriptInterface.reset(); #if CONFIG2_DAP_INTERFACE dapInterface.reset(); #endif // CONFIG2_DAP_INTERFACE ShutdownConfigAndSubsequent(); - MainControllerShutdown(); - } while (g_Shutdown == ShutdownType::Restart); if (rlInterfaceError) diff --git a/source/ps/GameSetup/GameSetup.cpp b/source/ps/GameSetup/GameSetup.cpp index 1aeff9e619..9876c004a7 100644 --- a/source/ps/GameSetup/GameSetup.cpp +++ b/source/ps/GameSetup/GameSetup.cpp @@ -35,7 +35,6 @@ #include "lib/file/vfs/vfs.h" #include "lib/file/vfs/vfs_path.h" #include "lib/file/vfs/vfs_util.h" -#include "lib/input.h" #include "lib/path.h" #include "lib/status.h" #include "lib/sysdep/os.h" @@ -254,44 +253,41 @@ static void InitPs(bool setup_gui, const CStrW& gui_page, ScriptInterface* srcSc g_GUI->SwitchPage(gui_page, srcScriptInterface, initData); } -void InitInput() +[[nodiscard]] std::unique_ptr MakeInputHandlers() { g_Joystick.Initialise(); - // register input handlers - // This stack is constructed so the first added, will be the last - // one called. This is important, because each of the handlers - // has the potential to block events to go further down - // in the chain. I.e. the last one in the list added, is the - // only handler that can block all messages before they are - // processed. - in_add_handler(game_view_handler); + std::unique_ptr handlers{std::make_unique()}; + handlers->emplace(g_VideoMode.m_InputManager, Input::Slot::GAME_VIEW, game_view_handler); - in_add_handler(CProfileViewer::InputThunk); + handlers->emplace(g_VideoMode.m_InputManager, Input::Slot::PROFILE_VIEWER, CProfileViewer::InputThunk); - in_add_handler(HotkeyInputActualHandler); + handlers->emplace(g_VideoMode.m_InputManager, Input::Slot::HOTKEY_INPUT, HotkeyInputActualHandler); // gui_handler needs to be registered after (i.e. called before!) the // hotkey handler so that input boxes can be typed in without // setting off hotkeys. - in_add_handler(gui_handler); + handlers->emplace(g_VideoMode.m_InputManager, Input::Slot::GUI, gui_handler); // Likewise for the console. - in_add_handler(conInputHandler); + handlers->emplace(g_VideoMode.m_InputManager, Input::Slot::CONSOLE, conInputHandler); - in_add_handler(touch_input_handler); + handlers->emplace(g_VideoMode.m_InputManager, Input::Slot::TOUCH_INPUT, touch_input_handler); // Should be called after scancode map update (i.e. after the global input, but before UI). // This never blocks the event, but it does some processing necessary for hotkeys, // which are triggered later down the input chain. // (by calling this before the UI, we can use 'EventWouldTriggerHotkey' in the UI). - in_add_handler(HotkeyInputPrepHandler); + handlers->emplace(g_VideoMode.m_InputManager, Input::Slot::HOTKEY_INPUT_PREPARATION, + HotkeyInputPrepHandler); // These two must be called first (i.e. pushed last) // GlobalsInputHandler deals with some important global state, // such as which scancodes are being pressed, mouse buttons pressed, etc. // while HotkeyStateChange updates the map of active hotkeys. - in_add_handler(GlobalsInputHandler); - in_add_handler(HotkeyStateChange); + handlers->emplace(g_VideoMode.m_InputManager, Input::Slot::GLOBAL, GlobalsInputHandler); + handlers->emplace(g_VideoMode.m_InputManager, Input::Slot::HOTKEY_STATE_CHANGE, HotkeyStateChange); + + return handlers; } @@ -635,8 +631,9 @@ bool Init(const CmdLineArgs& args, int flags) return true; } -void InitGraphics(const CmdLineArgs& args, int flags, const std::vector& installedMods, - ScriptContext& scriptContext, ScriptInterface& scriptInterface) +[[nodiscard]] std::unique_ptr InitGraphics(const CmdLineArgs& args, int flags, + const std::vector& installedMods, ScriptContext& scriptContext, + ScriptInterface& scriptInterface) { const bool setup_vmode = (flags & INIT_HAVE_VMODE) == 0; @@ -678,7 +675,7 @@ void InitGraphics(const CmdLineArgs& args, int flags, const std::vector& i // create renderer new CRenderer(g_VideoMode.GetBackendDevice()); - InitInput(); + std::unique_ptr handlers{MakeInputHandlers()}; // TODO: Is this the best place for this? if (VfsDirectoryExists(L"maps/")) @@ -709,6 +706,8 @@ void InitGraphics(const CmdLineArgs& args, int flags, const std::vector& i // (delete game data, switch GUI page, show error, etc.) CancelLoad(CStr(e.what()).FromUTF8()); } + + return handlers; } bool InitNonVisual(const CmdLineArgs& args) diff --git a/source/ps/GameSetup/GameSetup.h b/source/ps/GameSetup/GameSetup.h index a9ad853250..1b971f111c 100644 --- a/source/ps/GameSetup/GameSetup.h +++ b/source/ps/GameSetup/GameSetup.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2024 Wildfire Games. +/* Copyright (C) 2026 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -19,7 +19,9 @@ #define INCLUDED_GAMESETUP #include "ps/CStr.h" +#include "ps/Input.h" +#include #include class CmdLineArgs; @@ -70,13 +72,16 @@ void InitVfs(const CmdLineArgs& args); * `ShutdownConfigAndSubsequent` has to be called later. */ extern bool Init(const CmdLineArgs& args, int flags); -extern void InitInput(); + +using InputHandlers = std::queue>; +[[nodiscard]] std::unique_ptr MakeInputHandlers(); /** * `ShutdownNetworkAndUI` has to be called later. */ -void InitGraphics(const CmdLineArgs& args, int flags, const std::vector& installedMods, - ScriptContext& scriptContext, ScriptInterface& scriptInterface); +[[nodiscard]] std::unique_ptr InitGraphics(const CmdLineArgs& args, int flags, + const std::vector& installedMods, ScriptContext& scriptContext, + ScriptInterface& scriptInterface); /** * `ShutdownNetworkAndUI` has to be called later. diff --git a/source/ps/Hotkey.cpp b/source/ps/Hotkey.cpp index d74d199da1..41062a5785 100644 --- a/source/ps/Hotkey.cpp +++ b/source/ps/Hotkey.cpp @@ -26,6 +26,7 @@ #include "ps/Globals.h" #include "ps/KeyName.h" #include "ps/Profiler2.h" +#include "ps/VideoMode.h" #include #include @@ -429,7 +430,7 @@ InReaction HotkeyInputActualHandler(const SDL_Event& ev) SDL_Event hotkeyPressNotification{}; hotkeyPressNotification.type = hotkey.retriggered ? SDL_HOTKEYPRESS_SILENT : SDL_HOTKEYPRESS; hotkeyPressNotification.user.data1 = const_cast(hotkey.mapping->name.c_str()); - in_push_priority_event(hotkeyPressNotification); + g_VideoMode.m_InputManager.PushPriorityEvent(hotkeyPressNotification); } // Send a HotkeyDown event on every key, mouseButton and mouseWheel event. @@ -444,7 +445,7 @@ InReaction HotkeyInputActualHandler(const SDL_Event& ev) SDL_Event hotkeyDownNotification{}; hotkeyDownNotification.type = SDL_HOTKEYDOWN; hotkeyDownNotification.user.data1 = const_cast(hotkey.mapping->name.c_str()); - in_push_priority_event(hotkeyDownNotification); + g_VideoMode.m_InputManager.PushPriorityEvent(hotkeyDownNotification); } // Release instantaneous events (e.g. mouse wheel) right away. @@ -457,7 +458,7 @@ InReaction HotkeyInputActualHandler(const SDL_Event& ev) SDL_Event hotkeyNotification{}; hotkeyNotification.type = hotkey.wasRetriggered ? SDL_HOTKEYUP_SILENT : SDL_HOTKEYUP; hotkeyNotification.user.data1 = const_cast(hotkey.name); - in_push_priority_event(hotkeyNotification); + g_VideoMode.m_InputManager.PushPriorityEvent(hotkeyNotification); } return IN_PASS; @@ -481,7 +482,7 @@ void ResetActiveHotkeys() SDL_Event hotkeyNotification; hotkeyNotification.type = hotkey.retriggered ? SDL_HOTKEYUP_SILENT : SDL_HOTKEYUP; hotkeyNotification.user.data1 = const_cast(hotkey.mapping->name.c_str()); - in_push_priority_event(hotkeyNotification); + g_VideoMode.m_InputManager.PushPriorityEvent(hotkeyNotification); } pressedHotkeys.clear(); activeScancodes.clear(); diff --git a/source/ps/Input.cpp b/source/ps/Input.cpp new file mode 100644 index 0000000000..9190b2f644 --- /dev/null +++ b/source/ps/Input.cpp @@ -0,0 +1,130 @@ +/* Copyright (C) 2026 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#include "precompiled.h" + +#include "ps/Input.h" + +#include "lib/debug.h" +#include "lib/external_libraries/libsdl.h" +#include "ps/Profile.h" +#include "ps/TouchInput.h" +#include "scriptinterface/JSON.h" +#include "scriptinterface/ScriptConversions.h" +#include "scriptinterface/ScriptRequest.h" + +#include + +namespace Input +{ +Manager::Manager() : + m_PriorityEvents{std::make_unique>()} +{ +} + +Manager::~Manager() = default; + +void Manager::PushPriorityEvent(const SDL_Event& event) +{ + m_PriorityEvents->push(event); +} + +void Manager::DispatchEvent(const SDL_Event& event) +{ + // Looks like std::find_if, but std::find_if does not guarantee the order of the handlers. + for (const auto handler : m_Handlers) + { + if (handler && (*handler)(event) == IN_HANDLED) + return; + } +} + +Manager::PollEventsResult::EventIterator::EventIterator(PollEventsResult& range) : + m_Storage{&range} +{ + ++*this; +} + +Manager::PollEventsResult::EventIterator::reference Manager::PollEventsResult::EventIterator::operator*() +{ + return *m_Storage->m_Event; +} + +Manager::PollEventsResult::EventIterator::pointer Manager::PollEventsResult::EventIterator::operator->() +{ + return &**this; +} + +Manager::PollEventsResult::EventIterator& Manager::PollEventsResult::EventIterator::operator++() +{ + std::queue& priorityEvents{m_Storage->m_PriorityEvents}; + if (!priorityEvents.empty()) + { + *m_Storage->m_Event = priorityEvents.front(); + priorityEvents.pop(); + } + else if (!SDL_PollEvent(m_Storage->m_Event.get())) + m_Storage = nullptr; + + return *this; +} + +Manager::PollEventsResult::EventIterator& Manager::PollEventsResult::EventIterator::operator++(int) +{ + ++(*this); + return *this; +} + +bool Manager::PollEventsResult::EventIterator::operator==(const EventIterator& other) const +{ + return m_Storage == other.m_Storage; +} + +Manager::PollEventsResult::PollEventsResult(std::queue& priorityEvents) : + m_PriorityEvents{priorityEvents}, + m_Event{std::make_unique()} +{ +} + +Manager::PollEventsResult::EventIterator Manager::PollEventsResult::begin() +{ + return EventIterator{*this}; +} + +Manager::PollEventsResult::EventIterator Manager::PollEventsResult::end() +{ + return EventIterator{}; +} + +Manager::PollEventsResult Manager::PollEvents() +{ + return PollEventsResult{*m_PriorityEvents}; +} + +HandlerBase::HandlerBase(HandlerBase*& pos) noexcept : + toReset{&pos} +{ + const auto old = std::exchange(*toReset, this); + ENSURE(old == nullptr && "There is already a handler registered to this slot."); +} + +HandlerBase::~HandlerBase() +{ + const auto old = std::exchange(*toReset, nullptr); + ENSURE(old == this && "No handler is registered to this slot."); +} +} // namespace Input diff --git a/source/ps/Input.h b/source/ps/Input.h new file mode 100644 index 0000000000..7c0c2c4c64 --- /dev/null +++ b/source/ps/Input.h @@ -0,0 +1,187 @@ +/* Copyright (C) 2026 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#ifndef INCLUDED_INPUT_HANDLER +#define INCLUDED_INPUT_HANDLER + +#include "lib/input.h" + +#include +#include +#include +#include +#include +#include +#include + +class ScriptRequest; + +namespace Input +{ +class HandlerBase; + +// A slot for each handler. Numbers are in invocation order. A handler can discard events. The first handler +// is the only which gets all events. +namespace Slot +{ +constexpr std::integral_constant PRIMARY; + +// These two must be called first `globalsInput` deals with some important global state, such as which +// scancodes are being pressed, mouse buttons pressed, etc. while hotkeyStateChange updates the map of +// active hotkeys. +constexpr std::integral_constant HOTKEY_STATE_CHANGE; +constexpr std::integral_constant GLOBAL; + +// Should be called after scancode map update (i.e. after the global input, but before UI). This never +// blocks the event, but it does some processing necessary for hotkeys, which are triggered later down the +// input chain. (by calling this before the UI, we can use `EventWouldTriggerHotkey` in the UI). +constexpr std::integral_constant HOTKEY_INPUT_PREPARATION; + +constexpr std::integral_constant TOUCH_INPUT; + +// The console handler needs to be called before the hotkey handler so that text can be typed in without +// setting off hotkeys. +constexpr std::integral_constant CONSOLE; +// Likewise for gui. +constexpr std::integral_constant GUI; +constexpr std::integral_constant HOTKEY_INPUT; +constexpr std::integral_constant PROFILE_VIEWER; +constexpr std::integral_constant GAME_VIEW; +} + +/** + * Holds a pointer to all registered `HandlerBase`s. + * It does unregister all registered `Handler`s in the destructor. + */ +class Manager +{ +public: + Manager(); + // The `Manager` needs to have a constant memory location (the `Handler`s hold a pointer to it). + Manager(Manager&) = delete; + Manager& operator=(Manager&) = delete; + Manager(Manager&&) = delete; + Manager& operator=(Manager&&) = delete; + ~Manager(); + + void PushPriorityEvent(const SDL_Event& event); + void DispatchEvent(const SDL_Event& event); + + template + HandlerBase*& Get() noexcept + { + return std::get(m_Handlers); + } + + class PollEventsResult + { + public: + class EventIterator + { + public: + using difference_type = std::ptrdiff_t; + using value_type = SDL_Event; + using pointer = value_type*; + using reference = value_type&; + using iterator_category = std::input_iterator_tag; + + EventIterator() = default; + explicit EventIterator(PollEventsResult& range); + + reference operator*(); + pointer operator->(); + + EventIterator& operator++(); + EventIterator& operator++(int); + + bool operator==(const EventIterator& other) const; + + private: + PollEventsResult* m_Storage{nullptr}; + }; + + explicit PollEventsResult(std::queue& priorityEvents); + + EventIterator begin(); + EventIterator end(); + + private: + std::queue& m_PriorityEvents; + const std::unique_ptr m_Event; + }; + + PollEventsResult PollEvents(); + +private: + std::array m_Handlers{{}}; + const std::unique_ptr> m_PriorityEvents; +}; + +/** + * Type-erased callable + */ +class HandlerBase +{ +protected: + /** + * Can't not be constructed themself, use `Handler` instead. + * @param pos On construction the pointer is set to @c this. On + * destruction the pointer will be set to nullptr. + */ + explicit HandlerBase(HandlerBase*& pos) noexcept; + virtual ~HandlerBase(); + + // A `HandlerBase` needs to have a constant memory location (the `Manager` does hold a pointer to it). + HandlerBase(HandlerBase&) = delete; + HandlerBase& operator=(HandlerBase&) = delete; + HandlerBase(HandlerBase&&) = delete; + HandlerBase& operator=(HandlerBase&&) = delete; + +public: + virtual InReaction operator()(const SDL_Event& event) = 0; + +private: + HandlerBase** toReset; +}; + +/** + * Usable type to register a handler to the associated `Manager`. + */ +template Callback> +class Handler final : private HandlerBase +{ +public: + // `slot` specifies when(in which order) the `callback` of this `Handler` is executed. + template + explicit Handler(Manager& manager, std::integral_constant, Callback func) : + HandlerBase{manager.Get()}, + callback{std::move(func)} + { + } + ~Handler() final = default; + +private: + InReaction operator()(const SDL_Event& event) final + { + return callback(event); + } + + Callback callback; +}; +} + +#endif // INCLUDED_INPUT_HANDLER diff --git a/source/ps/VideoMode.h b/source/ps/VideoMode.h index 6a1efeae4a..6211a82194 100644 --- a/source/ps/VideoMode.h +++ b/source/ps/VideoMode.h @@ -18,6 +18,7 @@ #ifndef INCLUDED_VIDEOMODE #define INCLUDED_VIDEOMODE +#include "ps/Input.h" #include "renderer/backend/Backend.h" #include @@ -144,7 +145,10 @@ private: bool m_IsInitialised = false; SDL_Window* m_Window = nullptr; +public: + Input::Manager m_InputManager; +private: // Initial desktop settings. // Frequency is in Hz, and BPP means bits per pixels (not bytes per pixels). int m_PreferredW = 0; diff --git a/source/ps/tests/test_Hotkeys.h b/source/ps/tests/test_Hotkeys.h index ebd050a0f9..ed42d622e4 100644 --- a/source/ps/tests/test_Hotkeys.h +++ b/source/ps/tests/test_Hotkeys.h @@ -32,6 +32,7 @@ #include "ps/Filesystem.h" #include "ps/Globals.h" #include "ps/Hotkey.h" +#include "ps/VideoMode.h" #include #include @@ -52,16 +53,18 @@ private: void fakeInput(const char* key, bool keyDown) { - SDL_Event ev; - ev.type = keyDown ? SDL_KEYDOWN : SDL_KEYUP; - ev.key.repeat = 0; - ev.key.keysym.scancode = SDL_GetScancodeFromName(key); - GlobalsInputHandler(ev); - HotkeyInputPrepHandler(ev); - HotkeyInputActualHandler(ev); + { + SDL_Event ev; + ev.type = keyDown ? SDL_KEYDOWN : SDL_KEYUP; + ev.key.repeat = 0; + ev.key.keysym.scancode = SDL_GetScancodeFromName(key); + GlobalsInputHandler(ev); + HotkeyInputPrepHandler(ev); + HotkeyInputActualHandler(ev); + } hotkeyPress = false; hotkeyUp = false; - while(in_poll_priority_event(ev)) + for (const SDL_Event& ev : g_VideoMode.m_InputManager.PollEvents()) { hotkeyUp |= ev.type == SDL_HOTKEYUP; hotkeyPress |= ev.type == SDL_HOTKEYPRESS; diff --git a/source/ps/tests/test_Input.h b/source/ps/tests/test_Input.h new file mode 100644 index 0000000000..1c9e430336 --- /dev/null +++ b/source/ps/tests/test_Input.h @@ -0,0 +1,168 @@ +/* Copyright (C) 2026 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#include "lib/self_test.h" + +#include "lib/external_libraries/libsdl.h" +#include "ps/Input.h" + +#include + +class TestInput : public CxxTest::TestSuite +{ + static std::uint32_t GetEventType(const int numevents) + { + std::uint32_t eventType{SDL_RegisterEvents(numevents)}; + TS_ASSERT_DIFFERS(eventType, std::numeric_limits::max()); + TS_ASSERT_STR_EQUALS(SDL_GetError(), ""); + return eventType; + } + + static SDL_Event MakeEvent(const std::uint32_t eventType) + { + SDL_Event ev{}; + ev.type = eventType; + return ev; + } + + static void PushEvent(const std::uint32_t eventType) + { + SDL_Event ev{MakeEvent(eventType)}; + TS_ASSERT_EQUALS(SDL_PushEvent(&ev), 1); + TS_ASSERT_STR_EQUALS(SDL_GetError(), ""); + } + + static void PushPriorityEvent(Input::Manager& manager, const std::uint32_t eventType) + { + const SDL_Event ev{MakeEvent(eventType)}; + manager.PushPriorityEvent(ev); + } + +public: + void setUp() + { + SDL_Init(SDL_INIT_EVENTS); + } + + void tearDown() + { + SDL_Quit(); + } + + void test_NoEvent() + { + Input::Manager manager; + auto range = manager.PollEvents(); + TS_ASSERT_EQUALS(std::distance(range.begin(), range.end()), 0); + } + + void test_Event() + { + Input::Manager manager; + PushEvent(GetEventType(1)); + auto range = manager.PollEvents(); + TS_ASSERT_EQUALS(std::distance(range.begin(), range.end()), 1); + } + + void test_PriorityEvent() + { + Input::Manager manager; + PushPriorityEvent(manager, GetEventType(1)); + auto range = manager.PollEvents(); + TS_ASSERT_EQUALS(std::distance(range.begin(), range.end()), 1); + } + + void test_PriorityOrder() + { + Input::Manager manager; + const std::uint32_t eventTypeStart{SDL_RegisterEvents(2)}; + const std::uint32_t priorityEventType{eventTypeStart + 1}; + + PushEvent(eventTypeStart); + PushPriorityEvent(manager, priorityEventType); + PushEvent(eventTypeStart); + + auto range = manager.PollEvents(); + auto iter = range.begin(); + TS_ASSERT_DIFFERS(iter, range.end()); + TS_ASSERT_EQUALS(iter->type, priorityEventType); + ++iter; + TS_ASSERT_DIFFERS(iter, range.end()); + TS_ASSERT_EQUALS(iter->type, eventTypeStart); + ++iter; + TS_ASSERT_DIFFERS(iter, range.end()); + TS_ASSERT_EQUALS(iter->type, eventTypeStart); + ++iter; + TS_ASSERT_EQUALS(iter, range.end()); + } + + void test_Dispatch() + { + Input::Manager manager; + bool triggered{false}; + Input::Handler _{manager, std::integral_constant{}, [&](const SDL_Event&){ + triggered = true; + return IN_HANDLED; + }}; + + TS_ASSERT(!triggered); + SDL_Event ev{MakeEvent(GetEventType(1))}; + manager.DispatchEvent(ev); + TS_ASSERT(triggered); + } + + void test_DispatchFilter() + { + Input::Manager manager; + const std::uint32_t eventTypeStart{SDL_RegisterEvents(2)}; + const std::uint32_t filteredEventType{eventTypeStart + 1}; + [[maybe_unused]] Input::Handler filter{manager, std::integral_constant{}, + [&](const SDL_Event& ev){ + return ev.type == filteredEventType ? IN_HANDLED : IN_PASS; + }}; + + bool triggered{false}; + [[maybe_unused]] Input::Handler test{manager, std::integral_constant{}, + [&](const SDL_Event&){ + triggered = true; + return IN_HANDLED; + }}; + + SDL_Event ev0{MakeEvent(filteredEventType)}; + manager.DispatchEvent(ev0); + TS_ASSERT(!triggered); + SDL_Event ev1{MakeEvent(eventTypeStart)}; + manager.DispatchEvent(ev1); + TS_ASSERT(triggered); + } + + void test_Unsubscribe() + { + Input::Manager manager; + bool triggered{false}; + { + Input::Handler _{manager, std::integral_constant{}, [&](const SDL_Event&){ + triggered = true; + return IN_HANDLED; + }}; + } + + SDL_Event ev{MakeEvent(GetEventType(1))}; + manager.DispatchEvent(ev); + TS_ASSERT(!triggered); + } +}; diff --git a/source/tools/atlas/GameInterface/Handlers/GraphicsSetupHandlers.cpp b/source/tools/atlas/GameInterface/Handlers/GraphicsSetupHandlers.cpp index c369b49cb6..ff9bd43d6f 100644 --- a/source/tools/atlas/GameInterface/Handlers/GraphicsSetupHandlers.cpp +++ b/source/tools/atlas/GameInterface/Handlers/GraphicsSetupHandlers.cpp @@ -74,6 +74,7 @@ const int g_InitFlags = INIT_HAVE_VMODE | INIT_NO_GUI; std::optional g_FileLogger; std::optional g_ScriptInterface; +std::unique_ptr g_InputHandlers; } MESSAGEHANDLER(Init) @@ -131,7 +132,7 @@ MESSAGEHANDLER(InitGraphics) g_VideoMode.CreateBackendDevice(false); g_ScriptInterface.emplace("Engine", "GUIManager", *g_ScriptContext); - InitGraphics(g_AtlasGameLoop->args, g_InitFlags, {}, *g_ScriptContext, *g_ScriptInterface); + g_InputHandlers = InitGraphics(g_AtlasGameLoop->args, g_InitFlags, {}, *g_ScriptContext, *g_ScriptInterface); } @@ -143,7 +144,7 @@ MESSAGEHANDLER(Shutdown) AtlasView::DestroyViews(); g_AtlasGameLoop->view = AtlasView::GetView_None(); - + g_InputHandlers.reset(); ShutdownNetworkAndUI(); g_ScriptInterface.reset(); ShutdownConfigAndSubsequent(); @@ -252,9 +253,8 @@ QUERYHANDLER(RenderLoop) RendererIncrementalLoad(); // Pump SDL events (e.g. hotkeys) - SDL_Event ev{}; - while (in_poll_priority_event(ev)) - in_dispatch_event(ev); + for (SDL_Event& ev : g_VideoMode.m_InputManager.PollEvents()) + g_VideoMode.m_InputManager.DispatchEvent(ev); if (g_GUI) g_GUI->TickObjects(); diff --git a/source/tools/atlas/GameInterface/Handlers/MiscHandlers.cpp b/source/tools/atlas/GameInterface/Handlers/MiscHandlers.cpp index f042936fc7..047e320a03 100644 --- a/source/tools/atlas/GameInterface/Handlers/MiscHandlers.cpp +++ b/source/tools/atlas/GameInterface/Handlers/MiscHandlers.cpp @@ -26,6 +26,7 @@ #include "maths/MathUtil.h" #include "ps/Game.h" #include "ps/GameSetup/Config.h" +#include "ps/VideoMode.h" #include "renderer/Renderer.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/components/ICmpSoundManager.h" @@ -118,7 +119,7 @@ MESSAGEHANDLER(GuiMouseButtonEvent) msg->pos->GetScreenSpace(x, y); ev.button.x = static_cast(Clamp(x, 0, g_xres)); ev.button.y = static_cast(Clamp(y, 0, g_yres)); - in_dispatch_event(ev); + g_VideoMode.m_InputManager.DispatchEvent(ev); } MESSAGEHANDLER(GuiMouseMotionEvent) @@ -129,7 +130,7 @@ MESSAGEHANDLER(GuiMouseMotionEvent) msg->pos->GetScreenSpace(x, y); ev.motion.x = static_cast(Clamp(x, 0, g_xres)); ev.motion.y = static_cast(Clamp(y, 0, g_yres)); - in_dispatch_event(ev); + g_VideoMode.m_InputManager.DispatchEvent(ev); } MESSAGEHANDLER(GuiKeyEvent) @@ -138,7 +139,7 @@ MESSAGEHANDLER(GuiKeyEvent) ev.type = msg->pressed ? SDL_KEYDOWN : SDL_KEYUP; ev.key.keysym.sym = static_cast(static_cast(msg->sdlkey)); ev.key.keysym.scancode = SDL_GetScancodeFromKey(static_cast(static_cast(msg->sdlkey))); - in_dispatch_event(ev); + g_VideoMode.m_InputManager.DispatchEvent(ev); } MESSAGEHANDLER(GuiCharEvent) @@ -151,13 +152,13 @@ MESSAGEHANDLER(GuiCharEvent) ev.text.type = SDL_TEXTEDITING; ev.text.text[0] = static_cast(msg->sdlkey); ev.text.text[1] = '\0'; - in_dispatch_event(ev); + g_VideoMode.m_InputManager.DispatchEvent(ev); ev.type = SDL_TEXTINPUT; ev.text.type = SDL_TEXTINPUT; ev.text.text[0] = static_cast(msg->sdlkey); ev.text.text[1] = '\0'; - in_dispatch_event(ev); + g_VideoMode.m_InputManager.DispatchEvent(ev); } } // namespace AtlasMessage