diff options
Diffstat (limited to 'src/game/client/io')
| -rw-r--r-- | src/game/client/io/gamepad.cc | 72 | ||||
| -rw-r--r-- | src/game/client/io/gamepad.hh | 100 | ||||
| -rw-r--r-- | src/game/client/io/glfw.hh | 42 | ||||
| -rw-r--r-- | src/game/client/io/keyboard.cc | 29 | ||||
| -rw-r--r-- | src/game/client/io/keyboard.hh | 83 | ||||
| -rw-r--r-- | src/game/client/io/mouse.cc | 44 | ||||
| -rw-r--r-- | src/game/client/io/mouse.hh | 141 | ||||
| -rw-r--r-- | src/game/client/io/video.cc | 289 | ||||
| -rw-r--r-- | src/game/client/io/video.hh | 115 |
9 files changed, 821 insertions, 94 deletions
diff --git a/src/game/client/io/gamepad.cc b/src/game/client/io/gamepad.cc index 768b18b..ba65599 100644 --- a/src/game/client/io/gamepad.cc +++ b/src/game/client/io/gamepad.cc @@ -9,12 +9,14 @@ #include "core/config/boolean.hh" #include "core/config/number.hh" + #include "core/io/cmdline.hh" #include "core/io/config_map.hh" +#include "core/io/physfs.hh" + #include "core/math/constexpr.hh" #include "client/gui/settings.hh" -#include "client/io/glfw.hh" #include "client/globals.hh" #include "client/toggles.hh" @@ -48,37 +50,37 @@ static void on_toggle_disable(const ToggleDisabledEvent& event) } } -static void on_glfw_joystick_event(const GlfwJoystickEvent& event) +static void on_joystick_glfw(int joystick_id, int event_type) { - if((event.event_type == GLFW_CONNECTED) && glfwJoystickIsGamepad(event.joystick_id) && (active_gamepad_id == INVALID_GAMEPAD_ID)) { + if((event_type == GLFW_CONNECTED) && glfwJoystickIsGamepad(joystick_id) && (active_gamepad_id == INVALID_GAMEPAD_ID)) { gamepad::available = true; - active_gamepad_id = event.joystick_id; + active_gamepad_id = joystick_id; - for(int i = 0; i < NUM_AXES; gamepad::last_state.axes[i++] = 0.0f) { - // empty + for(int i = 0; i < NUM_AXES; ++i) { + gamepad::last_state.axes[i] = 0.0f; } - for(int i = 0; i < NUM_BUTTONS; gamepad::last_state.buttons[i++] = GLFW_RELEASE) { - // empty + for(int i = 0; i < NUM_BUTTONS; ++i) { + gamepad::last_state.buttons[i] = GLFW_RELEASE; } - spdlog::info("gamepad: detected gamepad: {}", glfwGetGamepadName(event.joystick_id)); + spdlog::info("gamepad: detected gamepad: {}", glfwGetGamepadName(joystick_id)); return; } - if((event.event_type == GLFW_DISCONNECTED) && (active_gamepad_id == event.joystick_id)) { + if((event_type == GLFW_DISCONNECTED) && (active_gamepad_id == joystick_id)) { gamepad::available = false; active_gamepad_id = INVALID_GAMEPAD_ID; - for(int i = 0; i < NUM_AXES; gamepad::last_state.axes[i++] = 0.0f) { - // empty + for(int i = 0; i < NUM_AXES; ++i) { + gamepad::last_state.axes[i] = 0.0f; } - for(int i = 0; i < NUM_BUTTONS; gamepad::last_state.buttons[i++] = GLFW_RELEASE) { - // empty + for(int i = 0; i < NUM_BUTTONS; ++i) { + gamepad::last_state.buttons[i] = GLFW_RELEASE; } spdlog::warn("gamepad: disconnected"); @@ -99,15 +101,12 @@ void gamepad::init(void) settings::add_checkbox(0, gamepad::active, settings_location::GAMEPAD, "gamepad.active", true); settings::add_slider(1, gamepad::deadzone, settings_location::GAMEPAD, "gamepad.deadzone", true, "%.03f"); - auto mappings_path = cmdline::get_cstr("gpmap", "misc/gamecontrollerdb.txt"); - auto mappings_file = PHYSFS_openRead(mappings_path); + std::string mappings_string; + std::string_view mappings_path(cmdline::get("gpmap", "misc/gamecontrollerdb.txt")); - if(mappings_file) { + if(physfs::read_file(mappings_path, mappings_string)) { spdlog::info("gamepad: using mappings from {}", mappings_path); - auto mappings_string = std::string(PHYSFS_fileLength(mappings_file), char(0x00)); - PHYSFS_readBytes(mappings_file, mappings_string.data(), mappings_string.size()); glfwUpdateGamepadMappings(mappings_string.c_str()); - PHYSFS_close(mappings_file); } for(int joystick = 0; joystick <= GLFW_JOYSTICK_LAST; joystick += 1) { @@ -140,7 +139,9 @@ void gamepad::init(void) globals::dispatcher.sink<ToggleEnabledEvent>().connect<&on_toggle_enable>(); globals::dispatcher.sink<ToggleDisabledEvent>().connect<&on_toggle_disable>(); - globals::dispatcher.sink<GlfwJoystickEvent>().connect<&on_glfw_joystick_event>(); + + spdlog::info("gamepad: taking over device callbacks"); + glfwSetJoystickCallback(&on_joystick_glfw); } void gamepad::update_late(void) @@ -152,21 +153,21 @@ void gamepad::update_late(void) if(glfwGetGamepadState(active_gamepad_id, &gamepad::state)) { for(int i = 0; i < NUM_AXES; ++i) { - if((glm::abs(gamepad::state.axes[i]) > GAMEPAD_AXIS_EVENT_THRESHOLD) - && (glm::abs(gamepad::last_state.axes[i]) <= GAMEPAD_AXIS_EVENT_THRESHOLD)) { - GamepadAxisEvent event; - event.action = GLFW_PRESS; - event.axis = i; - globals::dispatcher.enqueue(event); + auto is_press = true; + is_press = is_press && glm::abs(gamepad::state.axes[i] > GAMEPAD_AXIS_EVENT_THRESHOLD); + is_press = is_press && glm::abs(gamepad::last_state.axes[i] <= GAMEPAD_AXIS_EVENT_THRESHOLD); + + if(is_press) { + globals::dispatcher.enqueue(GamepadAxisEvent(i, GLFW_PRESS)); continue; } - if((glm::abs(gamepad::state.axes[i]) <= GAMEPAD_AXIS_EVENT_THRESHOLD) - && (glm::abs(gamepad::last_state.axes[i]) > GAMEPAD_AXIS_EVENT_THRESHOLD)) { - GamepadAxisEvent event; - event.action = GLFW_RELEASE; - event.axis = i; - globals::dispatcher.enqueue(event); + auto is_release = true; + is_release = is_release && glm::abs(gamepad::state.axes[i]) <= GAMEPAD_AXIS_EVENT_THRESHOLD; + is_release = is_release && glm::abs(gamepad::last_state.axes[i]) > GAMEPAD_AXIS_EVENT_THRESHOLD; + + if(is_release) { + globals::dispatcher.enqueue(GamepadAxisEvent(i, GLFW_RELEASE)); continue; } } @@ -177,10 +178,7 @@ void gamepad::update_late(void) continue; } - GamepadButtonEvent event; - event.action = gamepad::state.buttons[i]; - event.button = i; - globals::dispatcher.enqueue(event); + globals::dispatcher.enqueue(GamepadButtonEvent(i, gamepad::state.buttons[i])); } } diff --git a/src/game/client/io/gamepad.hh b/src/game/client/io/gamepad.hh index b506cc2..ff6af4a 100644 --- a/src/game/client/io/gamepad.hh +++ b/src/game/client/io/gamepad.hh @@ -10,6 +10,43 @@ constexpr static int INVALID_GAMEPAD_AXIS = INT_MAX; constexpr static int INVALID_GAMEPAD_BUTTON = INT_MAX; +// This simulates buttons using axes. When an axis +// value exceeds 1.5 times the deadzone, the event is +// queued with a GLFW_PRESS action, when it falls back +// below the threshold, the event is queued with GLFW_RELEASE action +class GamepadAxisEvent final { +public: + constexpr explicit GamepadAxisEvent(int axis, int action); + + constexpr int axis(void) const noexcept; + constexpr int action(void) const noexcept; + + constexpr bool is_axis(int axis) const noexcept; + constexpr bool is_action(int action) const noexcept; + +private: + int m_axis; + int m_action; +}; + +// This smears GLFW event sugar over gamepad polling +// system. Whenever it detects a state change, the event +// is queued with an appropriate action +class GamepadButtonEvent final { +public: + constexpr explicit GamepadButtonEvent(int button, int action); + + constexpr int button(void) const noexcept; + constexpr int action(void) const noexcept; + + constexpr bool is_button(int button) const noexcept; + constexpr bool is_action(int action) const noexcept; + +private: + int m_action; + int m_button; +}; + namespace config { class Boolean; @@ -33,21 +70,54 @@ void init(void); void update_late(void); } // namespace gamepad -// This simulates buttons using axes. When an axis -// value exceeds 1.5 times the deadzone, the event is -// queued with a GLFW_PRESS action, when it falls back -// below the threshold, the event is queued with GLFW_RELEASE action -struct GamepadAxisEvent final { - int action; - int axis; -}; +constexpr GamepadAxisEvent::GamepadAxisEvent(int axis, int action) : m_axis(axis), m_action(action) +{ + // empty +} -// This smears GLFW event sugar over gamepad polling -// system. Whenever it detects a state change, the event -// is queued with an appropriate action -struct GamepadButtonEvent final { - int action; - int button; -}; +constexpr int GamepadAxisEvent::axis(void) const noexcept +{ + return m_axis; +} + +constexpr int GamepadAxisEvent::action(void) const noexcept +{ + return m_action; +} + +constexpr bool GamepadAxisEvent::is_axis(int axis) const noexcept +{ + return m_axis == axis; +} + +constexpr bool GamepadAxisEvent::is_action(int action) const noexcept +{ + return m_action == action; +} + +constexpr GamepadButtonEvent::GamepadButtonEvent(int button, int action) : m_button(button), m_action(action) +{ + // empty +} + +constexpr int GamepadButtonEvent::button(void) const noexcept +{ + return m_button; +} + +constexpr int GamepadButtonEvent::action(void) const noexcept +{ + return m_action; +} + +constexpr bool GamepadButtonEvent::is_button(int button) const noexcept +{ + return m_button == button; +} + +constexpr bool GamepadButtonEvent::is_action(int action) const noexcept +{ + return m_action == action; +} #endif diff --git a/src/game/client/io/glfw.hh b/src/game/client/io/glfw.hh deleted file mode 100644 index 9da978b..0000000 --- a/src/game/client/io/glfw.hh +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-License-Identifier: BSD-2-Clause -// Copyright (c) 2025 Kirill Dmitrievich -// File: glfw.hh -// Description: GLFW events passed through EnTT's signal system - -#ifndef CLIENT_IO_GLFW_HH -#define CLIENT_IO_GLFW_HH -#pragma once - -struct GlfwCursorPosEvent final { - glm::fvec2 pos; -}; - -struct GlfwFramebufferSizeEvent final { - glm::ivec2 size; - float aspect; -}; - -struct GlfwJoystickEvent final { - int joystick_id; - int event_type; -}; - -struct GlfwKeyEvent final { - int key { GLFW_KEY_UNKNOWN }; - int scancode; - int action; - int mods; -}; - -struct GlfwMouseButtonEvent final { - int button { GLFW_KEY_UNKNOWN }; - int action; - int mods; -}; - -struct GlfwScrollEvent final { - float dx; - float dy; -}; - -#endif diff --git a/src/game/client/io/keyboard.cc b/src/game/client/io/keyboard.cc new file mode 100644 index 0000000..71db460 --- /dev/null +++ b/src/game/client/io/keyboard.cc @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: BSD-2-Clause +// Copyright (c) 2025 Kirill Dmitrievich +// File: keyboard.cc; Created: Tue Dec 30 2025 12:27:50 +// Description: Keyboard handling + +#include "client/pch.hh" + +#include "client/io/keyboard.hh" + +#include "client/globals.hh" + +static void on_char_glfw(GLFWwindow* window, unsigned int codepoint) +{ + ImGui_ImplGlfw_CharCallback(window, codepoint); +} + +static void on_key_glfw(GLFWwindow* window, int keycode, int scancode, int action, int modbits) +{ + globals::dispatcher.trigger(KeyEvent(keycode, scancode, action, modbits)); + + ImGui_ImplGlfw_KeyCallback(window, keycode, scancode, action, modbits); +} + +void keyboard::init(void) +{ + spdlog::info("keyboard: taking over device events"); + glfwSetCharCallback(globals::window, &on_char_glfw); + glfwSetKeyCallback(globals::window, &on_key_glfw); +} diff --git a/src/game/client/io/keyboard.hh b/src/game/client/io/keyboard.hh new file mode 100644 index 0000000..566d38b --- /dev/null +++ b/src/game/client/io/keyboard.hh @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: BSD-2-Clause +// Copyright (c) 2025 Kirill Dmitrievich +// File: keyboard.hh; Created: Tue Dec 30 2025 12:27:04 +// Description: Keyboard handling + +#ifndef CLIENT_IO_KEYBOARD_HH +#define CLIENT_IO_KEYBOARD_HH +#pragma once + +class KeyEvent final { +public: + constexpr explicit KeyEvent(int keycode, int scancode, int action, int modbits); + + constexpr int keycode(void) const noexcept; + constexpr int scancode(void) const noexcept; + constexpr int action(void) const noexcept; + constexpr int modbits(void) const noexcept; + + constexpr bool is_keycode(int keycode) const noexcept; + constexpr bool is_action(int action) const noexcept; + constexpr bool has_modbits(int modbits) const noexcept; + + constexpr bool is_valid(void) const noexcept; + +private: + int m_keycode; + int m_scancode; + int m_action; + int m_modbits; +}; + +namespace keyboard +{ +void init(void); +} // namespace keyboard + +constexpr KeyEvent::KeyEvent(int keycode, int scancode, int action, int modbits) + : m_keycode(keycode), m_scancode(scancode), m_action(action), m_modbits(modbits) +{ + // empty +} + +constexpr int KeyEvent::keycode(void) const noexcept +{ + return m_keycode; +} + +constexpr int KeyEvent::scancode(void) const noexcept +{ + return m_scancode; +} + +constexpr int KeyEvent::action(void) const noexcept +{ + return m_action; +} + +constexpr int KeyEvent::modbits(void) const noexcept +{ + return m_modbits; +} + +constexpr bool KeyEvent::is_keycode(int keycode) const noexcept +{ + return m_keycode == keycode; +} + +constexpr bool KeyEvent::is_action(int action) const noexcept +{ + return m_action == action; +} + +constexpr bool KeyEvent::has_modbits(int modbits) const noexcept +{ + return static_cast<bool>(m_modbits & modbits); +} + +constexpr bool KeyEvent::is_valid(void) const noexcept +{ + return m_keycode >= 0 && m_keycode <= GLFW_KEY_LAST; +} + +#endif diff --git a/src/game/client/io/mouse.cc b/src/game/client/io/mouse.cc new file mode 100644 index 0000000..b79365c --- /dev/null +++ b/src/game/client/io/mouse.cc @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: BSD-2-Clause +// Copyright (c) 2025 Kirill Dmitrievich +// File: mouse.cc; Created: Tue Dec 30 2025 12:39:32 +// Description: Mouse and scroll wheel handling + +#include "client/pch.hh" + +#include "client/io/mouse.hh" + +#include "client/globals.hh" + +static void on_cursor_enter_glfw(GLFWwindow* window, int entered) +{ + ImGui_ImplGlfw_CursorEnterCallback(window, entered); +} +static void on_cursor_pos_glfw(GLFWwindow* window, double xpos, double ypos) +{ + globals::dispatcher.trigger(CursorPosEvent(xpos, ypos)); + + ImGui_ImplGlfw_CursorPosCallback(window, xpos, ypos); +} + +static void on_mouse_button_glfw(GLFWwindow* window, int button, int action, int modbits) +{ + globals::dispatcher.trigger(MouseButtonEvent(button, action, modbits)); + + ImGui_ImplGlfw_MouseButtonCallback(window, button, action, modbits); +} + +static void on_scroll_glfw(GLFWwindow* window, double xoffset, double yoffset) +{ + globals::dispatcher.trigger(ScrollEvent(xoffset, yoffset)); + + ImGui_ImplGlfw_ScrollCallback(window, xoffset, yoffset); +} + +void mouse::init(void) +{ + spdlog::info("mouse: taking over device events"); + glfwSetCursorEnterCallback(globals::window, &on_cursor_enter_glfw); + glfwSetCursorPosCallback(globals::window, &on_cursor_pos_glfw); + glfwSetMouseButtonCallback(globals::window, &on_mouse_button_glfw); + glfwSetScrollCallback(globals::window, &on_scroll_glfw); +} diff --git a/src/game/client/io/mouse.hh b/src/game/client/io/mouse.hh new file mode 100644 index 0000000..3a58dc7 --- /dev/null +++ b/src/game/client/io/mouse.hh @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: BSD-2-Clause +// Copyright (c) 2025 Kirill Dmitrievich +// File: mouse.hh; Created: Tue Dec 30 2025 12:33:48 +// Description: Mouse and scroll wheel handling + +#ifndef CLIENT_IO_MOUSE_HH +#define CLIENT_IO_MOUSE_HH +#pragma once + +class CursorPosEvent final { +public: + constexpr explicit CursorPosEvent(double xpos, double ypos); + + constexpr float xpos(void) const noexcept; + constexpr float ypos(void) const noexcept; + constexpr const glm::fvec2& position(void) const noexcept; + +private: + glm::fvec2 m_position; +}; + +class MouseButtonEvent final { +public: + constexpr explicit MouseButtonEvent(int button, int action, int modbits); + + constexpr int button(void) const noexcept; + constexpr int action(void) const noexcept; + constexpr int modbits(void) const noexcept; + + constexpr bool is_button(int button) const noexcept; + constexpr bool is_action(int action) const noexcept; + constexpr bool has_modbits(int modbits) const noexcept; + + constexpr bool is_valid(void) const noexcept; + +private: + int m_button; + int m_action; + int m_modbits; +}; + +class ScrollEvent final { +public: + constexpr explicit ScrollEvent(double xoffset, double yoffset); + + constexpr float xoffset(void) const noexcept; + constexpr float yoffset(void) const noexcept; + constexpr const glm::fvec2& offset(void) const noexcept; + +private: + glm::fvec2 m_offset; +}; + +namespace mouse +{ +void init(void); +} // namespace mouse + +constexpr CursorPosEvent::CursorPosEvent(double xpos, double ypos) +{ + m_position.x = static_cast<float>(xpos); + m_position.y = static_cast<float>(ypos); +} + +constexpr float CursorPosEvent::xpos(void) const noexcept +{ + return m_position.x; +} + +constexpr float CursorPosEvent::ypos(void) const noexcept +{ + return m_position.y; +} + +constexpr const glm::fvec2& CursorPosEvent::position(void) const noexcept +{ + return m_position; +} + +constexpr MouseButtonEvent::MouseButtonEvent(int button, int action, int modbits) : m_button(button), m_action(action), m_modbits(modbits) +{ + // empty +} + +constexpr int MouseButtonEvent::button(void) const noexcept +{ + return m_button; +} + +constexpr int MouseButtonEvent::action(void) const noexcept +{ + return m_action; +} + +constexpr int MouseButtonEvent::modbits(void) const noexcept +{ + return m_modbits; +} + +constexpr bool MouseButtonEvent::is_button(int button) const noexcept +{ + return m_button == button; +} + +constexpr bool MouseButtonEvent::is_action(int action) const noexcept +{ + return m_action == action; +} + +constexpr bool MouseButtonEvent::has_modbits(int modbits) const noexcept +{ + return static_cast<bool>(m_modbits & modbits); +} + +constexpr bool MouseButtonEvent::is_valid(void) const noexcept +{ + return m_button >= 0 && m_button <= GLFW_MOUSE_BUTTON_LAST; +} + +constexpr ScrollEvent::ScrollEvent(double xoffset, double yoffset) +{ + m_offset.x = static_cast<float>(xoffset); + m_offset.y = static_cast<float>(yoffset); +} + +constexpr float ScrollEvent::xoffset(void) const noexcept +{ + return m_offset.x; +} + +constexpr float ScrollEvent::yoffset(void) const noexcept +{ + return m_offset.y; +} + +constexpr const glm::fvec2& ScrollEvent::offset(void) const noexcept +{ + return m_offset; +} + +#endif diff --git a/src/game/client/io/video.cc b/src/game/client/io/video.cc new file mode 100644 index 0000000..361b167 --- /dev/null +++ b/src/game/client/io/video.cc @@ -0,0 +1,289 @@ +// SPDX-License-Identifier: BSD-2-Clause +// Copyright (c) 2025 Kirill Dmitrievich +// File: video.cc; Created: Tue Dec 30 2025 13:00:24 +// Description: Video mode handling + +#include "client/pch.hh" + +#include "client/io/video.hh" + +#include "core/config/boolean.hh" +#include "core/config/string.hh" + +#include "core/io/cmdline.hh" +#include "core/io/config_map.hh" + +#include "core/resource/image.hh" +#include "core/resource/resource.hh" + +#include "core/version.hh" + +#include "shared/splash.hh" + +#include "client/gui/settings.hh" + +#include "client/const.hh" +#include "client/globals.hh" + +static glm::ivec2 last_windowed_size; +static config::Boolean enable_vsync(true); +static config::String current_mode("windowed"); + +static GLFWmonitor* fullscreen_monitor; +static std::vector<VideoMode> fullscreen_modes; + +static void on_glfw_error(int error, const char* description) +{ + spdlog::error("video: GLFW error [{}]: {}", error, description); +} + +static void on_glfw_framebuffer_size(GLFWwindow* window, int wide, int tall) +{ + globals::width = wide; + globals::height = tall; + globals::aspect = static_cast<float>(wide) / static_cast<float>(tall); + + if(nullptr == glfwGetWindowMonitor(window)) { + last_windowed_size.x = wide; + last_windowed_size.y = tall; + } + + globals::dispatcher.trigger(FramebufferSizeEvent(wide, tall)); +} + +static void on_glfw_window_close(GLFWwindow* window) +{ + // We don't really have a good way to + // pass "i want to quit" boolean to the main loop, + // so instead we just raise an external interrupt signal + // which handler latches an internal flag in the main loop + std::raise(SIGINT); +} + +static void on_window_focus_glfw(GLFWwindow* window, int focused) +{ + ImGui_ImplGlfw_WindowFocusCallback(window, focused); +} + +void video::init(void) +{ + last_windowed_size.x = BASE_WIDTH; + last_windowed_size.y = BASE_HEIGHT; + + globals::client_config.add_value("video.enable_vsync", enable_vsync); + globals::client_config.add_value("video.current_mode", current_mode); + +#ifdef __unix__ + // Wayland constantly throws random bullshit at me + // when I'm dealing with pretty much anything cross-platform + // on pretty much any kind of UNIX and Linux distribution + glfwInitHint(GLFW_PLATFORM, GLFW_PLATFORM_X11); +#endif + + glfwSetErrorCallback(&on_glfw_error); + + if(!glfwInit()) { + spdlog::critical("glfw: initialize failed"); + std::terminate(); + } + + unsigned int monitor_index; + auto monitor_arg = cmdline::get("monitor"); + auto monitor_check = std::from_chars(monitor_arg.data(), monitor_arg.data() + monitor_arg.size(), monitor_index); + + if(monitor_check.ec == std::errc()) { + int monitor_count; + const auto monitors = glfwGetMonitors(&monitor_count); + + if(monitor_index < static_cast<unsigned int>(monitor_count)) { + fullscreen_monitor = monitors[monitor_index]; + } + else { + // If the user wants to, say run the game on a monitor + // number 2 and there are only 2 monitors (remember, zero-based index) + // it's a good idea to silently fall back to the primary monitor + fullscreen_monitor = glfwGetPrimaryMonitor(); + } + } + else { + fullscreen_monitor = glfwGetPrimaryMonitor(); + } + + int video_mode_count; + const auto video_modes = glfwGetVideoModes(fullscreen_monitor, &video_mode_count); + + fullscreen_modes.clear(); + fullscreen_modes.reserve(video_mode_count); + + for(int i = 0; i < video_mode_count; ++i) { + auto& mode = video_modes[i]; + + // Only allow video modes that are at least as large as the base resolution + // to be used by the video subsystem, otherwise we're going to end up with + // some bizzare UI scaling issues because there is no UI scale less than 1.0 + if(mode.width >= BASE_WIDTH && mode.height >= BASE_HEIGHT) { + fullscreen_modes.push_back(VideoMode(mode)); + } + } + + // Setup GLFW window hints + glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_API); + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); + glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); + +#if defined(__APPLE__) + // Enable forward compatibility because Apple + // just decided to be the autistic kid of the class + glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); +#endif + + globals::window = glfwCreateWindow(BASE_WIDTH, BASE_HEIGHT, "Client", nullptr, nullptr); + + if(globals::window == nullptr) { + spdlog::critical("glfw: window creation failed"); + std::terminate(); + } + + glfwSetWindowSizeLimits(globals::window, BASE_WIDTH, BASE_HEIGHT, GLFW_DONT_CARE, GLFW_DONT_CARE); + glfwMakeContextCurrent(globals::window); + glfwSwapInterval(1); + + glfwSetFramebufferSizeCallback(globals::window, &on_glfw_framebuffer_size); + glfwSetWindowCloseCallback(globals::window, &on_glfw_window_close); + glfwSetWindowFocusCallback(globals::window, &on_window_focus_glfw); + + if(auto image = resource::load<Image>("textures/gui/window_icon.png")) { + GLFWimage icon_image; + icon_image.width = image->size.x; + icon_image.height = image->size.y; + icon_image.pixels = reinterpret_cast<unsigned char*>(image->pixels); + glfwSetWindowIcon(globals::window, 1, &icon_image); + } + + settings::add_video_mode_select(0, settings_location::VIDEO, "video.current_mode"); + settings::add_checkbox(1, enable_vsync, settings_location::VIDEO, "video.enable_vsync", true); + + update_window_title(); +} + +void video::init_late(void) +{ + std::string mode_string(current_mode.get_value()); + + if(0 == mode_string.compare("windowed")) { + request_windowed(last_windowed_size.x, last_windowed_size.y); + return; + } + + int target_width; + int target_height; + int target_refresh_rate; + + if(3 == std::sscanf(mode_string.c_str(), "%d:%d:%d", &target_width, &target_height, &target_refresh_rate)) { + request_fullscreen(target_width, target_height, target_refresh_rate); + return; + } + + int current_width; + int current_height; + glfwGetFramebufferSize(globals::window, ¤t_width, ¤t_height); + on_glfw_framebuffer_size(globals::window, current_width, current_height); +} + +void video::shutdown(void) +{ + glfwDestroyWindow(globals::window); + glfwTerminate(); +} + +void video::update(void) +{ + glfwGetFramebufferSize(globals::window, &globals::width, &globals::height); + globals::aspect = static_cast<float>(globals::width) / static_cast<float>(globals::height); +} + +void video::update_late(void) +{ + glfwSwapInterval(enable_vsync.get_value() ? 1 : 0); +} + +void video::query_current_mode(int& wide, int& tall) noexcept +{ + glfwGetFramebufferSize(globals::window, &wide, &tall); +} + +void video::query_current_mode(int& wide, int& tall, bool& fullscreen) noexcept +{ + glfwGetFramebufferSize(globals::window, &wide, &tall); + fullscreen = static_cast<bool>(glfwGetWindowMonitor(globals::window)); +} + +const std::vector<VideoMode>& video::query_fullscreen_modes(void) noexcept +{ + return fullscreen_modes; +} + +void video::request_fullscreen(int wide, int tall, int rate) noexcept +{ + assert(wide >= BASE_WIDTH); + assert(tall >= BASE_HEIGHT); + + std::size_t best_index = 0; + auto best_score = std::numeric_limits<int>::max(); + + for(std::size_t i = 0; i < fullscreen_modes.size(); ++i) { + const auto& mode = fullscreen_modes[i]; + + auto score_width = std::abs(mode.wide() - wide); + auto score_height = std::abs(mode.tall() - tall); + auto score_refresh_rate = std::abs(mode.rate() - rate); + auto total_score = score_width + score_height + score_refresh_rate; + + if(total_score < best_score) { + best_score = total_score; + best_index = i; + } + } + + const auto& best_mode = fullscreen_modes[best_index]; + + glfwSetWindowMonitor(globals::window, fullscreen_monitor, 0, 0, best_mode.wide(), best_mode.tall(), best_mode.rate()); + glfwSetWindowAttrib(globals::window, GLFW_AUTO_ICONIFY, GLFW_FALSE); + glfwSetWindowAttrib(globals::window, GLFW_RESIZABLE, GLFW_FALSE); + + current_mode.set(std::format("{}:{}:{}", best_mode.wide(), best_mode.tall(), best_mode.rate())); + + spdlog::debug("video: set mode to: {}x{} ({} Hz)", best_mode.wide(), best_mode.tall(), best_mode.rate()); +} + +void video::request_windowed(int wide, int tall) noexcept +{ + glfwSetWindowMonitor(globals::window, nullptr, 0, 0, wide, tall, GLFW_DONT_CARE); + glfwSetWindowAttrib(globals::window, GLFW_AUTO_ICONIFY, GLFW_FALSE); + glfwSetWindowAttrib(globals::window, GLFW_RESIZABLE, GLFW_TRUE); + + int workarea_xpos, workarea_ypos; + int workarea_width, workarea_height; + glfwGetMonitorWorkarea(fullscreen_monitor, &workarea_xpos, &workarea_ypos, &workarea_width, &workarea_height); + + auto center_x = workarea_xpos + (workarea_width - wide) / 2; + auto center_y = workarea_ypos + (workarea_height - tall) / 2; + + glfwSetWindowPos(globals::window, center_x, center_y); + + current_mode.set("windowed"); + + spdlog::debug("video: set mode to: windowed {}x{}", wide, tall); +} + +void video::request_windowed(void) noexcept +{ + request_windowed(last_windowed_size.x, last_windowed_size.y); +} + +void video::update_window_title(void) +{ + auto title = std::format("Voxelius {}: {}", version::triplet, splash::get()); + glfwSetWindowTitle(globals::window, title.c_str()); +} diff --git a/src/game/client/io/video.hh b/src/game/client/io/video.hh new file mode 100644 index 0000000..858983f --- /dev/null +++ b/src/game/client/io/video.hh @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: BSD-2-Clause +// Copyright (c) 2025 Kirill Dmitrievich +// File: video.hh; Created: Tue Dec 30 2025 12:59:09 +// Description: Video mode handling + +#ifndef CLIENT_IO_VIDEO_HH +#define CLIENT_IO_VIDEO_HH +#pragma once + +class VideoMode final { +public: + constexpr explicit VideoMode(const GLFWvidmode& mode) noexcept; + constexpr explicit VideoMode(int wide, int tall, int rate = GLFW_DONT_CARE) noexcept; + + constexpr int wide(void) const noexcept; + constexpr int tall(void) const noexcept; + constexpr int rate(void) const noexcept; + + constexpr bool operator==(const VideoMode& other) const noexcept; + +private: + int m_wide; + int m_tall; + int m_rate; +}; + +class FramebufferSizeEvent final { +public: + constexpr explicit FramebufferSizeEvent(int wide, int tall) noexcept; + constexpr int wide(void) const noexcept; + constexpr int tall(void) const noexcept; + +private: + int m_wide; + int m_tall; +}; + +namespace video +{ +void init(void); +void init_late(void); +void shutdown(void); +void update(void); +void update_late(void); +} // namespace video + +namespace video +{ +void query_current_mode(int& wide, int& tall) noexcept; +void query_current_mode(int& wide, int& tall, bool& fullscreen) noexcept; +const std::vector<VideoMode>& query_fullscreen_modes(void) noexcept; +} // namespace video + +namespace video +{ +void request_fullscreen(int wide, int tall, int rate) noexcept; +void request_windowed(int wide, int tall) noexcept; +void request_windowed(void) noexcept; +} // namespace video + +namespace video +{ +void update_window_title(void); +} // namespace video + +constexpr VideoMode::VideoMode(int wide, int tall, int rate) noexcept : m_wide(wide), m_tall(tall), m_rate(rate) +{ + // empty +} + +constexpr VideoMode::VideoMode(const GLFWvidmode& mode) noexcept : m_wide(mode.width), m_tall(mode.height), m_rate(mode.refreshRate) +{ + // empty +} + +constexpr int VideoMode::wide(void) const noexcept +{ + return m_wide; +} + +constexpr int VideoMode::tall(void) const noexcept +{ + return m_tall; +} + +constexpr int VideoMode::rate(void) const noexcept +{ + return m_rate; +} + +constexpr bool VideoMode::operator==(const VideoMode& other) const noexcept +{ + auto result = true; + result = result && (m_wide == other.m_wide || m_wide == GLFW_DONT_CARE || other.m_wide == GLFW_DONT_CARE); + result = result && (m_tall == other.m_tall || m_tall == GLFW_DONT_CARE || other.m_tall == GLFW_DONT_CARE); + result = result && (m_rate == other.m_rate || m_rate == GLFW_DONT_CARE || other.m_rate == GLFW_DONT_CARE); + return result; +} + +constexpr FramebufferSizeEvent::FramebufferSizeEvent(int wide, int tall) noexcept : m_wide(wide), m_tall(tall) +{ + // empty +} + +constexpr int FramebufferSizeEvent::wide(void) const noexcept +{ + return m_wide; +} + +constexpr int FramebufferSizeEvent::tall(void) const noexcept +{ + return m_tall; +} + +#endif |
