summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authoruntodesu <kirill@untode.su>2025-12-30 13:20:33 +0500
committeruntodesu <kirill@untode.su>2025-12-30 13:20:33 +0500
commita1c83d56f41e6f2e0ad86dcd76d1446bfc60a37c (patch)
tree5754f0cd6ef3678c2e9d9c31174ae435d463c8ed /src
parent49d3dff9e98e70e599dfd3059f85bb08ae247fe5 (diff)
downloadvoxelius-a1c83d56f41e6f2e0ad86dcd76d1446bfc60a37c.tar.bz2
voxelius-a1c83d56f41e6f2e0ad86dcd76d1446bfc60a37c.zip
begin working on qf ports (#21)
Diffstat (limited to 'src')
-rw-r--r--src/game/client/experiments.cc12
-rw-r--r--src/game/client/game.cc30
-rw-r--r--src/game/client/game.hh1
-rw-r--r--src/game/client/gui/CMakeLists.txt4
-rw-r--r--src/game/client/gui/settings.hh1
-rw-r--r--src/game/client/gui/window_title.cc19
-rw-r--r--src/game/client/gui/window_title.hh15
-rw-r--r--src/game/client/io/gamepad.cc72
-rw-r--r--src/game/client/io/gamepad.hh100
-rw-r--r--src/game/client/io/glfw.hh42
-rw-r--r--src/game/client/io/keyboard.cc29
-rw-r--r--src/game/client/io/keyboard.hh83
-rw-r--r--src/game/client/io/mouse.cc44
-rw-r--r--src/game/client/io/mouse.hh141
-rw-r--r--src/game/client/io/video.cc289
-rw-r--r--src/game/client/io/video.hh115
-rw-r--r--src/game/client/main.cc177
17 files changed, 860 insertions, 314 deletions
diff --git a/src/game/client/experiments.cc b/src/game/client/experiments.cc
index dcd3c7d..14637ae 100644
--- a/src/game/client/experiments.cc
+++ b/src/game/client/experiments.cc
@@ -17,23 +17,23 @@
#include "client/gui/hotbar.hh"
#include "client/gui/status_lines.hh"
-#include "client/io/glfw.hh"
+#include "client/io/mouse.hh"
#include "client/world/player_target.hh"
#include "client/globals.hh"
#include "client/session.hh"
-static void on_glfw_mouse_button(const GlfwMouseButtonEvent& event)
+static void on_mouse_button(const MouseButtonEvent& event)
{
if(!globals::gui_screen && session::is_ingame()) {
- if((event.action == GLFW_PRESS) && player_target::voxel) {
- if(event.button == GLFW_MOUSE_BUTTON_LEFT) {
+ if(event.is_action(GLFW_PRESS) && player_target::voxel) {
+ if(event.is_button(GLFW_MOUSE_BUTTON_LEFT)) {
experiments::attack();
return;
}
- if(event.button == GLFW_MOUSE_BUTTON_RIGHT) {
+ if(event.is_button(GLFW_MOUSE_BUTTON_RIGHT)) {
experiments::interact();
return;
}
@@ -43,7 +43,7 @@ static void on_glfw_mouse_button(const GlfwMouseButtonEvent& event)
void experiments::init(void)
{
- globals::dispatcher.sink<GlfwMouseButtonEvent>().connect<&on_glfw_mouse_button>();
+ globals::dispatcher.sink<MouseButtonEvent>().connect<&on_mouse_button>();
}
void experiments::init_late(void)
diff --git a/src/game/client/game.cc b/src/game/client/game.cc
index 09445c4..9b55b4b 100644
--- a/src/game/client/game.cc
+++ b/src/game/client/game.cc
@@ -64,11 +64,11 @@
#include "client/gui/settings.hh"
#include "client/gui/splash.hh"
#include "client/gui/status_lines.hh"
-#include "client/gui/window_title.hh"
#include "client/io/gamepad.hh"
-#include "client/io/glfw.hh"
+#include "client/io/keyboard.hh"
#include "client/io/sound.hh"
+#include "client/io/video.hh"
#include "client/resource/texture_gui.hh"
@@ -93,7 +93,6 @@
constexpr static int PIXEL_SIZE = 2;
config::Boolean client_game::streamer_mode(false);
-config::Boolean client_game::vertical_sync(true);
config::Boolean client_game::world_curvature(true);
config::Unsigned client_game::fog_mode(1U, 0U, 2U);
config::String client_game::username("player");
@@ -125,7 +124,7 @@ static ImFont* load_font(std::string_view path, float size, ImFontConfig& font_c
return font_ptr;
}
-static void on_glfw_framebuffer_size(const GlfwFramebufferSizeEvent& event)
+static void on_framebuffer_size(const FramebufferSizeEvent& event)
{
if(globals::world_fbo) {
glDeleteRenderbuffers(1, &globals::world_fbo_depth);
@@ -137,8 +136,8 @@ static void on_glfw_framebuffer_size(const GlfwFramebufferSizeEvent& event)
glGenTextures(1, &globals::world_fbo_color);
glGenRenderbuffers(1, &globals::world_fbo_depth);
- scaled_width = event.size.x / PIXEL_SIZE;
- scaled_height = event.size.y / PIXEL_SIZE;
+ scaled_width = event.wide() / PIXEL_SIZE;
+ scaled_height = event.tall() / PIXEL_SIZE;
glBindTexture(GL_TEXTURE_2D, globals::world_fbo_color);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, scaled_width, scaled_height, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr);
@@ -159,9 +158,9 @@ static void on_glfw_framebuffer_size(const GlfwFramebufferSizeEvent& event)
}
}
-static void on_glfw_key(const GlfwKeyEvent& event)
+static void on_key(const KeyEvent& event)
{
- if(!globals::gui_keybind_ptr && hide_hud_toggle.equals(event.key) && (event.action == GLFW_PRESS)) {
+ if(!globals::gui_keybind_ptr && hide_hud_toggle.equals(event.keycode()) && (event.is_action(GLFW_PRESS))) {
client_game::hide_hud = !client_game::hide_hud;
}
}
@@ -191,7 +190,6 @@ void client_game::init(void)
client_splash::render();
globals::client_config.add_value("game.streamer_mode", client_game::streamer_mode);
- globals::client_config.add_value("game.vertical_sync", client_game::vertical_sync);
globals::client_config.add_value("game.world_curvature", client_game::world_curvature);
globals::client_config.add_value("game.fog_mode", client_game::fog_mode);
globals::client_config.add_value("game.username", client_game::username);
@@ -200,7 +198,6 @@ void client_game::init(void)
settings::init();
settings::add_checkbox(0, client_game::streamer_mode, settings_location::VIDEO_GUI, "game.streamer_mode", true);
- settings::add_checkbox(5, client_game::vertical_sync, settings_location::VIDEO, "game.vertical_sync", false);
settings::add_checkbox(4, client_game::world_curvature, settings_location::VIDEO, "game.world_curvature", true);
settings::add_stepper(3, client_game::fog_mode, settings_location::VIDEO, "game.fog_mode", false);
settings::add_input(1, client_game::username, settings_location::GENERAL, "game.username", true, false);
@@ -351,8 +348,8 @@ void client_game::init(void)
experiments::init();
- globals::dispatcher.sink<GlfwFramebufferSizeEvent>().connect<&on_glfw_framebuffer_size>();
- globals::dispatcher.sink<GlfwKeyEvent>().connect<&on_glfw_key>();
+ globals::dispatcher.sink<FramebufferSizeEvent>().connect<&on_framebuffer_size>();
+ globals::dispatcher.sink<KeyEvent>().connect<&on_key>();
}
void client_game::init_late(void)
@@ -428,7 +425,7 @@ void client_game::init_late(void)
client_splash::init_late();
- window_title::update();
+ video::update_window_title();
}
void client_game::shutdown(void)
@@ -591,13 +588,6 @@ void client_game::update_late(void)
gamepad::update_late();
chunk_visibility::update_late();
-
- if(client_game::vertical_sync.get_value()) {
- glfwSwapInterval(1);
- }
- else {
- glfwSwapInterval(0);
- }
}
void client_game::render(void)
diff --git a/src/game/client/game.hh b/src/game/client/game.hh
index 7ed663b..66b28cd 100644
--- a/src/game/client/game.hh
+++ b/src/game/client/game.hh
@@ -17,7 +17,6 @@ class Unsigned;
namespace client_game
{
extern config::Boolean streamer_mode;
-extern config::Boolean vertical_sync;
extern config::Boolean world_curvature;
extern config::Unsigned fog_mode;
extern config::String username;
diff --git a/src/game/client/gui/CMakeLists.txt b/src/game/client/gui/CMakeLists.txt
index e3d8b7a..3eae65f 100644
--- a/src/game/client/gui/CMakeLists.txt
+++ b/src/game/client/gui/CMakeLists.txt
@@ -37,6 +37,4 @@ target_sources(vclient PRIVATE
"${CMAKE_CURRENT_LIST_DIR}/splash.cc"
"${CMAKE_CURRENT_LIST_DIR}/splash.hh"
"${CMAKE_CURRENT_LIST_DIR}/status_lines.cc"
- "${CMAKE_CURRENT_LIST_DIR}/status_lines.hh"
- "${CMAKE_CURRENT_LIST_DIR}/window_title.cc"
- "${CMAKE_CURRENT_LIST_DIR}/window_title.hh")
+ "${CMAKE_CURRENT_LIST_DIR}/status_lines.hh")
diff --git a/src/game/client/gui/settings.hh b/src/game/client/gui/settings.hh
index 1a3a1f8..b48ab8c 100644
--- a/src/game/client/gui/settings.hh
+++ b/src/game/client/gui/settings.hh
@@ -94,6 +94,7 @@ void add_gamepad_button(int priority, config::GamepadButton& value, settings_loc
namespace settings
{
void add_language_select(int priority, settings_location location, std::string_view name);
+void add_video_mode_select(int priority, settings_location location, std::string_view name);
} // namespace settings
#endif
diff --git a/src/game/client/gui/window_title.cc b/src/game/client/gui/window_title.cc
deleted file mode 100644
index ea65b64..0000000
--- a/src/game/client/gui/window_title.cc
+++ /dev/null
@@ -1,19 +0,0 @@
-// SPDX-License-Identifier: BSD-2-Clause
-// Copyright (c) 2025 Kirill Dmitrievich
-// File: window_title.cc
-// Description: Random MOTD in the window title
-
-#include "client/pch.hh"
-
-#include "client/gui/window_title.hh"
-
-#include "core/version.hh"
-
-#include "shared/splash.hh"
-
-#include "client/globals.hh"
-
-void window_title::update(void)
-{
- glfwSetWindowTitle(globals::window, std::format("Voxelius {}: {}", version::triplet, splash::get()).c_str());
-}
diff --git a/src/game/client/gui/window_title.hh b/src/game/client/gui/window_title.hh
deleted file mode 100644
index 78f7326..0000000
--- a/src/game/client/gui/window_title.hh
+++ /dev/null
@@ -1,15 +0,0 @@
-// SPDX-License-Identifier: BSD-2-Clause
-// Copyright (c) 2025 Kirill Dmitrievich
-// File: window_title.hh
-// Description: Random MOTD in the window title
-
-#ifndef CLIENT_GUI_WINDOW_TITLE_HH
-#define CLIENT_GUI_WINDOW_TITLE_HH
-#pragma once
-
-namespace window_title
-{
-void update(void);
-} // namespace window_title
-
-#endif
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, &current_width, &current_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
diff --git a/src/game/client/main.cc b/src/game/client/main.cc
index ab45db5..b58d840 100644
--- a/src/game/client/main.cc
+++ b/src/game/client/main.cc
@@ -19,9 +19,10 @@
#include "shared/game.hh"
#include "shared/splash.hh"
-#include "client/gui/window_title.hh"
-
-#include "client/io/glfw.hh"
+#include "client/io/gamepad.hh"
+#include "client/io/keyboard.hh"
+#include "client/io/mouse.hh"
+#include "client/io/video.hh"
#include "client/resource/sound_effect.hh"
#include "client/resource/texture_gui.hh"
@@ -35,101 +36,7 @@ extern "C" __declspec(dllexport) unsigned long NvOptimusEnablement = 0x00000001;
extern "C" __declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1;
#endif
-static void on_glfw_error(int code, const char* message)
-{
- spdlog::error("glfw: {}", message);
-}
-
-static void on_glfw_char(GLFWwindow* window, unsigned int codepoint)
-{
- ImGui_ImplGlfw_CharCallback(window, codepoint);
-}
-
-static void on_glfw_cursor_enter(GLFWwindow* window, int entered)
-{
- ImGui_ImplGlfw_CursorEnterCallback(window, entered);
-}
-
-static void on_glfw_cursor_pos(GLFWwindow* window, double xpos, double ypos)
-{
- GlfwCursorPosEvent event;
- event.pos.x = static_cast<float>(xpos);
- event.pos.y = static_cast<float>(ypos);
- globals::dispatcher.trigger(event);
-
- ImGui_ImplGlfw_CursorPosCallback(window, xpos, ypos);
-}
-
-static void on_glfw_framebuffer_size(GLFWwindow* window, int width, int height)
-{
- if(glfwGetWindowAttrib(window, GLFW_ICONIFIED)) {
- // Don't do anything if the window was just
- // iconified (minimized); as it turns out minimized
- // windows on WIN32 seem to be forced into 0x0
- return;
- }
-
- globals::width = width;
- globals::height = height;
- globals::aspect = static_cast<float>(width) / static_cast<float>(height);
-
- GlfwFramebufferSizeEvent fb_event;
- fb_event.size.x = globals::width;
- fb_event.size.y = globals::height;
- fb_event.aspect = globals::aspect;
- globals::dispatcher.trigger(fb_event);
-}
-
-static void on_glfw_key(GLFWwindow* window, int key, int scancode, int action, int mods)
-{
- GlfwKeyEvent event;
- event.key = key;
- event.scancode = scancode;
- event.action = action;
- event.mods = mods;
- globals::dispatcher.trigger(event);
-
- ImGui_ImplGlfw_KeyCallback(window, key, scancode, action, mods);
-}
-
-static void on_glfw_joystick(int joystick_id, int event_type)
-{
- GlfwJoystickEvent event;
- event.joystick_id = joystick_id;
- event.event_type = event_type;
- globals::dispatcher.trigger(event);
-}
-
-static void on_glfw_monitor_event(GLFWmonitor* monitor, int event)
-{
- ImGui_ImplGlfw_MonitorCallback(monitor, event);
-}
-
-static void on_glfw_mouse_button(GLFWwindow* window, int button, int action, int mods)
-{
- GlfwMouseButtonEvent event;
- event.button = button;
- event.action = action;
- event.mods = mods;
- globals::dispatcher.trigger(event);
-
- ImGui_ImplGlfw_MouseButtonCallback(window, button, action, mods);
-}
-
-static void on_glfw_scroll(GLFWwindow* window, double dx, double dy)
-{
- GlfwScrollEvent event;
- event.dx = static_cast<float>(dx);
- event.dy = static_cast<float>(dy);
- globals::dispatcher.trigger(event);
-
- ImGui_ImplGlfw_ScrollCallback(window, dx, dy);
-}
-
-static void on_glfw_window_focus(GLFWwindow* window, int focused)
-{
- ImGui_ImplGlfw_WindowFocusCallback(window, focused);
-}
+std::atomic_bool is_running;
static void GLAD_API_PTR on_opengl_message(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar* message,
const void* param)
@@ -140,7 +47,8 @@ static void GLAD_API_PTR on_opengl_message(GLenum source, GLenum type, GLuint id
static void on_termination_signal(int)
{
spdlog::warn("client: received termination signal");
- glfwSetWindowShouldClose(globals::window, true);
+
+ is_running = false;
}
int main(int argc, char** argv)
@@ -167,32 +75,11 @@ int main(int argc, char** argv)
spdlog::info("Voxelius Client {}", version::full);
- glfwSetErrorCallback(&on_glfw_error);
-
-#if defined(__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
-
- if(!glfwInit()) {
- spdlog::critical("glfw: init failed");
- std::terminate();
- }
-
- glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_API);
- glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
- glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
- glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
- glfwWindowHint(GLFW_SAMPLES, 0);
+ video::init();
- globals::window = glfwCreateWindow(DEFAULT_WIDTH, DEFAULT_HEIGHT, "Client", nullptr, nullptr);
-
- if(!globals::window) {
- spdlog::critical("glfw: failed to open a window");
- std::terminate();
- }
+ keyboard::init();
+ mouse::init();
+ gamepad::init();
std::signal(SIGINT, &on_termination_signal);
std::signal(SIGTERM, &on_termination_signal);
@@ -241,31 +128,6 @@ int main(int argc, char** argv)
ImGui_ImplGlfw_InitForOpenGL(globals::window, false);
ImGui_ImplOpenGL3_Init(nullptr);
- // The UI is scaled against a resolution defined by BASE_WIDTH and BASE_HEIGHT
- // constants. However, UI scale of 1 doesn't look that good, so the window size is
- // limited to a resolution that allows at least UI scale of 2 and is defined by MIN_WIDTH and MIN_HEIGHT.
- glfwSetWindowSizeLimits(globals::window, MIN_WIDTH, MIN_HEIGHT, GLFW_DONT_CARE, GLFW_DONT_CARE);
-
- glfwSetCharCallback(globals::window, &on_glfw_char);
- glfwSetCursorEnterCallback(globals::window, &on_glfw_cursor_enter);
- glfwSetCursorPosCallback(globals::window, &on_glfw_cursor_pos);
- glfwSetFramebufferSizeCallback(globals::window, &on_glfw_framebuffer_size);
- glfwSetKeyCallback(globals::window, &on_glfw_key);
- glfwSetMouseButtonCallback(globals::window, &on_glfw_mouse_button);
- glfwSetScrollCallback(globals::window, &on_glfw_scroll);
- glfwSetWindowFocusCallback(globals::window, &on_glfw_window_focus);
-
- glfwSetJoystickCallback(&on_glfw_joystick);
- glfwSetMonitorCallback(&on_glfw_monitor_event);
-
- 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);
- }
-
if(cmdline::contains("nosound")) {
spdlog::warn("client: sound disabled [per command line]");
globals::sound_dev = nullptr;
@@ -303,7 +165,7 @@ int main(int argc, char** argv)
splash::init_client();
- window_title::update();
+ video::update_window_title();
ImGuiIO& io = ImGui::GetIO();
io.ConfigFlags &= ~ImGuiConfigFlags_NavEnableGamepad;
@@ -334,15 +196,13 @@ int main(int argc, char** argv)
client_game::init();
- int wwidth, wheight;
- glfwGetFramebufferSize(globals::window, &wwidth, &wheight);
- on_glfw_framebuffer_size(globals::window, wwidth, wheight);
-
threading::init();
globals::client_config.load_file("client.conf");
globals::client_config.load_cmdline();
+ video::init_late();
+
client_game::init_late();
auto last_curtime = globals::curtime;
@@ -370,6 +230,8 @@ int main(int argc, char** argv)
last_curtime = globals::curtime;
+ video::update();
+
for(std::uint64_t i = 0; i < globals::fixed_framecount; ++i)
client_game::fixed_update();
client_game::update();
@@ -403,6 +265,10 @@ int main(int argc, char** argv)
glfwSwapBuffers(globals::window);
+ video::update_late();
+
+ gamepad::update_late();
+
for(std::uint64_t i = 0; i < globals::fixed_framecount; ++i)
client_game::fixed_update_late();
client_game::update_late();
@@ -441,8 +307,7 @@ int main(int argc, char** argv)
alcCloseDevice(globals::sound_dev);
}
- glfwDestroyWindow(globals::window);
- glfwTerminate();
+ video::shutdown();
globals::client_config.save_file("client.conf");