From 6cd00aacfa22fed6a54a9b812f6b069ad16feec0 Mon Sep 17 00:00:00 2001 From: untodesu Date: Sun, 29 Jun 2025 22:24:42 +0500 Subject: Move game sources into src subdirectory --- src/CMakeLists.txt | 2 + src/core/CMakeLists.txt | 44 ++ src/core/aabb.cc | 59 ++ src/core/aabb.hh | 31 + src/core/angles.hh | 107 ++++ src/core/binfile.cc | 70 +++ src/core/binfile.hh | 10 + src/core/buffer.cc | 207 +++++++ src/core/buffer.hh | 255 ++++++++ src/core/cmdline.cc | 81 +++ src/core/cmdline.hh | 13 + src/core/concepts.hh | 15 + src/core/config.cc | 191 ++++++ src/core/config.hh | 182 ++++++ src/core/constexpr.hh | 191 ++++++ src/core/crc64.cc | 286 +++++++++ src/core/crc64.hh | 12 + src/core/epoch.cc | 39 ++ src/core/epoch.hh | 19 + src/core/floathacks.hh | 56 ++ src/core/image.cc | 112 ++++ src/core/image.hh | 13 + src/core/macros.hh | 19 + src/core/pch.hh | 50 ++ src/core/randomizer.hh | 57 ++ src/core/resource.hh | 18 + src/core/strtools.cc | 54 ++ src/core/strtools.hh | 21 + src/core/vectors.hh | 47 ++ src/core/version.cc | 12 + src/core/version.cc.in | 12 + src/core/version.hh | 12 + src/game/CMakeLists.txt | 11 + src/game/client/CMakeLists.txt | 126 ++++ src/game/client/background.cc | 37 ++ src/game/client/background.hh | 12 + src/game/client/bother.cc | 163 ++++++ src/game/client/bother.hh | 23 + src/game/client/camera.cc | 107 ++++ src/game/client/camera.hh | 32 ++ src/game/client/chat.cc | 260 +++++++++ src/game/client/chat.hh | 21 + src/game/client/chunk_mesher.cc | 467 +++++++++++++++ src/game/client/chunk_mesher.hh | 19 + src/game/client/chunk_quad.hh | 39 ++ src/game/client/chunk_renderer.cc | 196 +++++++ src/game/client/chunk_renderer.hh | 12 + src/game/client/chunk_vbo.hh | 23 + src/game/client/chunk_visibility.cc | 87 +++ src/game/client/chunk_visibility.hh | 12 + src/game/client/const.hh | 27 + src/game/client/crosshair.cc | 41 ++ src/game/client/crosshair.hh | 12 + src/game/client/direct_connection.cc | 140 +++++ src/game/client/direct_connection.hh | 11 + src/game/client/experiments.cc | 77 +++ src/game/client/experiments.hh | 20 + src/game/client/factory.cc | 28 + src/game/client/factory.hh | 12 + src/game/client/game.cc | 684 ++++++++++++++++++++++ src/game/client/game.hh | 36 ++ src/game/client/gamepad.cc | 168 ++++++ src/game/client/gamepad.hh | 45 ++ src/game/client/gamepad_axis.cc | 114 ++++ src/game/client/gamepad_axis.hh | 39 ++ src/game/client/gamepad_button.cc | 90 +++ src/game/client/gamepad_button.hh | 29 + src/game/client/glfw.hh | 37 ++ src/game/client/globals.cc | 48 ++ src/game/client/globals.hh | 66 +++ src/game/client/gui_screen.hh | 14 + src/game/client/hotbar.cc | 178 ++++++ src/game/client/hotbar.hh | 31 + src/game/client/imdraw_ext.cc | 13 + src/game/client/imdraw_ext.hh | 11 + src/game/client/interpolation.cc | 61 ++ src/game/client/interpolation.hh | 10 + src/game/client/keybind.cc | 200 +++++++ src/game/client/keybind.hh | 26 + src/game/client/language.cc | 196 +++++++ src/game/client/language.hh | 43 ++ src/game/client/listener.cc | 38 ++ src/game/client/listener.hh | 10 + src/game/client/main.cc | 441 ++++++++++++++ src/game/client/main_menu.cc | 163 ++++++ src/game/client/main_menu.hh | 12 + src/game/client/message_box.cc | 94 +++ src/game/client/message_box.hh | 21 + src/game/client/metrics.cc | 98 ++++ src/game/client/metrics.hh | 11 + src/game/client/outline.cc | 148 +++++ src/game/client/outline.hh | 20 + src/game/client/pch.hh | 32 ++ src/game/client/play_menu.cc | 548 ++++++++++++++++++ src/game/client/play_menu.hh | 13 + src/game/client/player_look.cc | 146 +++++ src/game/client/player_look.hh | 11 + src/game/client/player_move.cc | 283 +++++++++ src/game/client/player_move.hh | 19 + src/game/client/player_target.cc | 66 +++ src/game/client/player_target.hh | 22 + src/game/client/program.cc | 223 +++++++ src/game/client/program.hh | 38 ++ src/game/client/progress_bar.cc | 110 ++++ src/game/client/progress_bar.hh | 20 + src/game/client/receive.cc | 187 ++++++ src/game/client/receive.hh | 10 + src/game/client/scoreboard.cc | 100 ++++ src/game/client/scoreboard.hh | 11 + src/game/client/screenshot.cc | 82 +++ src/game/client/screenshot.hh | 11 + src/game/client/session.cc | 300 ++++++++++ src/game/client/session.hh | 31 + src/game/client/settings.cc | 1055 ++++++++++++++++++++++++++++++++++ src/game/client/settings.hh | 83 +++ src/game/client/skybox.cc | 11 + src/game/client/skybox.hh | 15 + src/game/client/sound.cc | 199 +++++++ src/game/client/sound.hh | 43 ++ src/game/client/sound_effect.cc | 115 ++++ src/game/client/sound_effect.hh | 10 + src/game/client/sound_emitter.cc | 58 ++ src/game/client/sound_emitter.hh | 21 + src/game/client/splash.cc | 171 ++++++ src/game/client/splash.hh | 12 + src/game/client/status_lines.cc | 79 +++ src/game/client/status_lines.hh | 22 + src/game/client/texture_gui.cc | 110 ++++ src/game/client/texture_gui.hh | 17 + src/game/client/toggles.cc | 154 +++++ src/game/client/toggles.hh | 39 ++ src/game/client/vclient.ico | Bin 0 -> 54361 bytes src/game/client/vclient.rc | 1 + src/game/client/voxel_anims.cc | 29 + src/game/client/voxel_anims.hh | 17 + src/game/client/voxel_atlas.cc | 183 ++++++ src/game/client/voxel_atlas.hh | 29 + src/game/client/voxel_sounds.cc | 86 +++ src/game/client/voxel_sounds.hh | 23 + src/game/client/window_title.cc | 22 + src/game/client/window_title.hh | 10 + src/game/server/CMakeLists.txt | 39 ++ src/game/server/chat.cc | 54 ++ src/game/server/chat.hh | 16 + src/game/server/game.cc | 148 +++++ src/game/server/game.hh | 26 + src/game/server/globals.cc | 18 + src/game/server/globals.hh | 25 + src/game/server/inhabited.hh | 7 + src/game/server/main.cc | 103 ++++ src/game/server/overworld.cc | 380 ++++++++++++ src/game/server/overworld.hh | 63 ++ src/game/server/pch.hh | 7 + src/game/server/receive.cc | 170 ++++++ src/game/server/receive.hh | 10 + src/game/server/sessions.cc | 419 ++++++++++++++ src/game/server/sessions.hh | 53 ++ src/game/server/status.cc | 26 + src/game/server/status.hh | 10 + src/game/server/universe.cc | 215 +++++++ src/game/server/universe.hh | 25 + src/game/server/unloader.cc | 76 +++ src/game/server/unloader.hh | 14 + src/game/server/vserver.ico | Bin 0 -> 60201 bytes src/game/server/vserver.rc | 1 + src/game/server/whitelist.cc | 95 +++ src/game/server/whitelist.hh | 27 + src/game/server/worldgen.cc | 148 +++++ src/game/server/worldgen.hh | 21 + src/game/shared/CMakeLists.txt | 57 ++ src/game/shared/chunk.cc | 69 +++ src/game/shared/chunk.hh | 39 ++ src/game/shared/chunk_aabb.cc | 54 ++ src/game/shared/chunk_aabb.hh | 32 ++ src/game/shared/collision.cc | 169 ++++++ src/game/shared/collision.hh | 19 + src/game/shared/const.hh | 47 ++ src/game/shared/coord.hh | 149 +++++ src/game/shared/dimension.cc | 169 ++++++ src/game/shared/dimension.hh | 85 +++ src/game/shared/factory.cc | 35 ++ src/game/shared/factory.hh | 12 + src/game/shared/feature.cc | 52 ++ src/game/shared/feature.hh | 22 + src/game/shared/game.cc | 121 ++++ src/game/shared/game.hh | 11 + src/game/shared/game_items.cc | 61 ++ src/game/shared/game_items.hh | 26 + src/game/shared/game_voxels.cc | 122 ++++ src/game/shared/game_voxels.hh | 28 + src/game/shared/globals.cc | 12 + src/game/shared/globals.hh | 23 + src/game/shared/gravity.cc | 18 + src/game/shared/gravity.hh | 12 + src/game/shared/grounded.hh | 13 + src/game/shared/head.hh | 14 + src/game/shared/item_registry.cc | 103 ++++ src/game/shared/item_registry.hh | 62 ++ src/game/shared/pch.hh | 25 + src/game/shared/player.hh | 7 + src/game/shared/protocol.cc | 491 ++++++++++++++++ src/game/shared/protocol.hh | 213 +++++++ src/game/shared/ray_dda.cc | 96 ++++ src/game/shared/ray_dda.hh | 37 ++ src/game/shared/splash.cc | 64 +++ src/game/shared/splash.hh | 12 + src/game/shared/stasis.cc | 19 + src/game/shared/stasis.hh | 14 + src/game/shared/threading.cc | 111 ++++ src/game/shared/threading.hh | 50 ++ src/game/shared/transform.cc | 32 ++ src/game/shared/transform.hh | 25 + src/game/shared/types.hh | 44 ++ src/game/shared/velocity.cc | 17 + src/game/shared/velocity.hh | 17 + src/game/shared/voxel_registry.cc | 192 +++++++ src/game/shared/voxel_registry.hh | 144 +++++ src/game/shared/voxel_storage.cc | 46 ++ src/game/shared/voxel_storage.hh | 18 + 219 files changed, 17890 insertions(+) create mode 100644 src/CMakeLists.txt create mode 100644 src/core/CMakeLists.txt create mode 100644 src/core/aabb.cc create mode 100644 src/core/aabb.hh create mode 100644 src/core/angles.hh create mode 100644 src/core/binfile.cc create mode 100644 src/core/binfile.hh create mode 100644 src/core/buffer.cc create mode 100644 src/core/buffer.hh create mode 100644 src/core/cmdline.cc create mode 100644 src/core/cmdline.hh create mode 100644 src/core/concepts.hh create mode 100644 src/core/config.cc create mode 100644 src/core/config.hh create mode 100644 src/core/constexpr.hh create mode 100644 src/core/crc64.cc create mode 100644 src/core/crc64.hh create mode 100644 src/core/epoch.cc create mode 100644 src/core/epoch.hh create mode 100644 src/core/floathacks.hh create mode 100644 src/core/image.cc create mode 100644 src/core/image.hh create mode 100644 src/core/macros.hh create mode 100644 src/core/pch.hh create mode 100644 src/core/randomizer.hh create mode 100644 src/core/resource.hh create mode 100644 src/core/strtools.cc create mode 100644 src/core/strtools.hh create mode 100644 src/core/vectors.hh create mode 100644 src/core/version.cc create mode 100644 src/core/version.cc.in create mode 100644 src/core/version.hh create mode 100644 src/game/CMakeLists.txt create mode 100644 src/game/client/CMakeLists.txt create mode 100644 src/game/client/background.cc create mode 100644 src/game/client/background.hh create mode 100644 src/game/client/bother.cc create mode 100644 src/game/client/bother.hh create mode 100644 src/game/client/camera.cc create mode 100644 src/game/client/camera.hh create mode 100644 src/game/client/chat.cc create mode 100644 src/game/client/chat.hh create mode 100644 src/game/client/chunk_mesher.cc create mode 100644 src/game/client/chunk_mesher.hh create mode 100644 src/game/client/chunk_quad.hh create mode 100644 src/game/client/chunk_renderer.cc create mode 100644 src/game/client/chunk_renderer.hh create mode 100644 src/game/client/chunk_vbo.hh create mode 100644 src/game/client/chunk_visibility.cc create mode 100644 src/game/client/chunk_visibility.hh create mode 100644 src/game/client/const.hh create mode 100644 src/game/client/crosshair.cc create mode 100644 src/game/client/crosshair.hh create mode 100644 src/game/client/direct_connection.cc create mode 100644 src/game/client/direct_connection.hh create mode 100644 src/game/client/experiments.cc create mode 100644 src/game/client/experiments.hh create mode 100644 src/game/client/factory.cc create mode 100644 src/game/client/factory.hh create mode 100644 src/game/client/game.cc create mode 100644 src/game/client/game.hh create mode 100644 src/game/client/gamepad.cc create mode 100644 src/game/client/gamepad.hh create mode 100644 src/game/client/gamepad_axis.cc create mode 100644 src/game/client/gamepad_axis.hh create mode 100644 src/game/client/gamepad_button.cc create mode 100644 src/game/client/gamepad_button.hh create mode 100644 src/game/client/glfw.hh create mode 100644 src/game/client/globals.cc create mode 100644 src/game/client/globals.hh create mode 100644 src/game/client/gui_screen.hh create mode 100644 src/game/client/hotbar.cc create mode 100644 src/game/client/hotbar.hh create mode 100644 src/game/client/imdraw_ext.cc create mode 100644 src/game/client/imdraw_ext.hh create mode 100644 src/game/client/interpolation.cc create mode 100644 src/game/client/interpolation.hh create mode 100644 src/game/client/keybind.cc create mode 100644 src/game/client/keybind.hh create mode 100644 src/game/client/language.cc create mode 100644 src/game/client/language.hh create mode 100644 src/game/client/listener.cc create mode 100644 src/game/client/listener.hh create mode 100644 src/game/client/main.cc create mode 100644 src/game/client/main_menu.cc create mode 100644 src/game/client/main_menu.hh create mode 100644 src/game/client/message_box.cc create mode 100644 src/game/client/message_box.hh create mode 100644 src/game/client/metrics.cc create mode 100644 src/game/client/metrics.hh create mode 100644 src/game/client/outline.cc create mode 100644 src/game/client/outline.hh create mode 100644 src/game/client/pch.hh create mode 100644 src/game/client/play_menu.cc create mode 100644 src/game/client/play_menu.hh create mode 100644 src/game/client/player_look.cc create mode 100644 src/game/client/player_look.hh create mode 100644 src/game/client/player_move.cc create mode 100644 src/game/client/player_move.hh create mode 100644 src/game/client/player_target.cc create mode 100644 src/game/client/player_target.hh create mode 100644 src/game/client/program.cc create mode 100644 src/game/client/program.hh create mode 100644 src/game/client/progress_bar.cc create mode 100644 src/game/client/progress_bar.hh create mode 100644 src/game/client/receive.cc create mode 100644 src/game/client/receive.hh create mode 100644 src/game/client/scoreboard.cc create mode 100644 src/game/client/scoreboard.hh create mode 100644 src/game/client/screenshot.cc create mode 100644 src/game/client/screenshot.hh create mode 100644 src/game/client/session.cc create mode 100644 src/game/client/session.hh create mode 100644 src/game/client/settings.cc create mode 100644 src/game/client/settings.hh create mode 100644 src/game/client/skybox.cc create mode 100644 src/game/client/skybox.hh create mode 100644 src/game/client/sound.cc create mode 100644 src/game/client/sound.hh create mode 100644 src/game/client/sound_effect.cc create mode 100644 src/game/client/sound_effect.hh create mode 100644 src/game/client/sound_emitter.cc create mode 100644 src/game/client/sound_emitter.hh create mode 100644 src/game/client/splash.cc create mode 100644 src/game/client/splash.hh create mode 100644 src/game/client/status_lines.cc create mode 100644 src/game/client/status_lines.hh create mode 100644 src/game/client/texture_gui.cc create mode 100644 src/game/client/texture_gui.hh create mode 100644 src/game/client/toggles.cc create mode 100644 src/game/client/toggles.hh create mode 100644 src/game/client/vclient.ico create mode 100644 src/game/client/vclient.rc create mode 100644 src/game/client/voxel_anims.cc create mode 100644 src/game/client/voxel_anims.hh create mode 100644 src/game/client/voxel_atlas.cc create mode 100644 src/game/client/voxel_atlas.hh create mode 100644 src/game/client/voxel_sounds.cc create mode 100644 src/game/client/voxel_sounds.hh create mode 100644 src/game/client/window_title.cc create mode 100644 src/game/client/window_title.hh create mode 100644 src/game/server/CMakeLists.txt create mode 100644 src/game/server/chat.cc create mode 100644 src/game/server/chat.hh create mode 100644 src/game/server/game.cc create mode 100644 src/game/server/game.hh create mode 100644 src/game/server/globals.cc create mode 100644 src/game/server/globals.hh create mode 100644 src/game/server/inhabited.hh create mode 100644 src/game/server/main.cc create mode 100644 src/game/server/overworld.cc create mode 100644 src/game/server/overworld.hh create mode 100644 src/game/server/pch.hh create mode 100644 src/game/server/receive.cc create mode 100644 src/game/server/receive.hh create mode 100644 src/game/server/sessions.cc create mode 100644 src/game/server/sessions.hh create mode 100644 src/game/server/status.cc create mode 100644 src/game/server/status.hh create mode 100644 src/game/server/universe.cc create mode 100644 src/game/server/universe.hh create mode 100644 src/game/server/unloader.cc create mode 100644 src/game/server/unloader.hh create mode 100644 src/game/server/vserver.ico create mode 100644 src/game/server/vserver.rc create mode 100644 src/game/server/whitelist.cc create mode 100644 src/game/server/whitelist.hh create mode 100644 src/game/server/worldgen.cc create mode 100644 src/game/server/worldgen.hh create mode 100644 src/game/shared/CMakeLists.txt create mode 100644 src/game/shared/chunk.cc create mode 100644 src/game/shared/chunk.hh create mode 100644 src/game/shared/chunk_aabb.cc create mode 100644 src/game/shared/chunk_aabb.hh create mode 100644 src/game/shared/collision.cc create mode 100644 src/game/shared/collision.hh create mode 100644 src/game/shared/const.hh create mode 100644 src/game/shared/coord.hh create mode 100644 src/game/shared/dimension.cc create mode 100644 src/game/shared/dimension.hh create mode 100644 src/game/shared/factory.cc create mode 100644 src/game/shared/factory.hh create mode 100644 src/game/shared/feature.cc create mode 100644 src/game/shared/feature.hh create mode 100644 src/game/shared/game.cc create mode 100644 src/game/shared/game.hh create mode 100644 src/game/shared/game_items.cc create mode 100644 src/game/shared/game_items.hh create mode 100644 src/game/shared/game_voxels.cc create mode 100644 src/game/shared/game_voxels.hh create mode 100644 src/game/shared/globals.cc create mode 100644 src/game/shared/globals.hh create mode 100644 src/game/shared/gravity.cc create mode 100644 src/game/shared/gravity.hh create mode 100644 src/game/shared/grounded.hh create mode 100644 src/game/shared/head.hh create mode 100644 src/game/shared/item_registry.cc create mode 100644 src/game/shared/item_registry.hh create mode 100644 src/game/shared/pch.hh create mode 100644 src/game/shared/player.hh create mode 100644 src/game/shared/protocol.cc create mode 100644 src/game/shared/protocol.hh create mode 100644 src/game/shared/ray_dda.cc create mode 100644 src/game/shared/ray_dda.hh create mode 100644 src/game/shared/splash.cc create mode 100644 src/game/shared/splash.hh create mode 100644 src/game/shared/stasis.cc create mode 100644 src/game/shared/stasis.hh create mode 100644 src/game/shared/threading.cc create mode 100644 src/game/shared/threading.hh create mode 100644 src/game/shared/transform.cc create mode 100644 src/game/shared/transform.hh create mode 100644 src/game/shared/types.hh create mode 100644 src/game/shared/velocity.cc create mode 100644 src/game/shared/velocity.hh create mode 100644 src/game/shared/voxel_registry.cc create mode 100644 src/game/shared/voxel_registry.hh create mode 100644 src/game/shared/voxel_storage.cc create mode 100644 src/game/shared/voxel_storage.hh (limited to 'src') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..ac6b9d0 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,2 @@ +add_subdirectory(core) +add_subdirectory(game) diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt new file mode 100644 index 0000000..13284cb --- /dev/null +++ b/src/core/CMakeLists.txt @@ -0,0 +1,44 @@ +add_library(core STATIC + "${CMAKE_CURRENT_LIST_DIR}/aabb.hh" + "${CMAKE_CURRENT_LIST_DIR}/aabb.cc" + "${CMAKE_CURRENT_LIST_DIR}/binfile.hh" + "${CMAKE_CURRENT_LIST_DIR}/binfile.cc" + "${CMAKE_CURRENT_LIST_DIR}/buffer.hh" + "${CMAKE_CURRENT_LIST_DIR}/buffer.cc" + "${CMAKE_CURRENT_LIST_DIR}/cmdline.hh" + "${CMAKE_CURRENT_LIST_DIR}/cmdline.cc" + "${CMAKE_CURRENT_LIST_DIR}/concepts.hh" + "${CMAKE_CURRENT_LIST_DIR}/config.cc" + "${CMAKE_CURRENT_LIST_DIR}/config.hh" + "${CMAKE_CURRENT_LIST_DIR}/constexpr.hh" + "${CMAKE_CURRENT_LIST_DIR}/crc64.cc" + "${CMAKE_CURRENT_LIST_DIR}/crc64.hh" + "${CMAKE_CURRENT_LIST_DIR}/epoch.cc" + "${CMAKE_CURRENT_LIST_DIR}/epoch.hh" + "${CMAKE_CURRENT_LIST_DIR}/floathacks.hh" + "${CMAKE_CURRENT_LIST_DIR}/image.cc" + "${CMAKE_CURRENT_LIST_DIR}/image.hh" + "${CMAKE_CURRENT_LIST_DIR}/macros.hh" + "${CMAKE_CURRENT_LIST_DIR}/pch.hh" + "${CMAKE_CURRENT_LIST_DIR}/resource.hh" + "${CMAKE_CURRENT_LIST_DIR}/strtools.cc" + "${CMAKE_CURRENT_LIST_DIR}/strtools.hh" + "${CMAKE_CURRENT_LIST_DIR}/version.cc" + "${CMAKE_CURRENT_LIST_DIR}/version.hh") +target_compile_features(core PUBLIC cxx_std_20) +target_include_directories(core PUBLIC "${DEPS_INCLUDE_DIR}") +target_include_directories(core PUBLIC "${PROJECT_SOURCE_DIR}/src") +target_precompile_headers(core PRIVATE "${CMAKE_CURRENT_LIST_DIR}/pch.hh") +target_link_libraries(core PUBLIC enet emhash glm physfs spdlog stb) + +if(WIN32) + target_compile_definitions(core PUBLIC _CRT_SECURE_NO_WARNINGS) + target_compile_definitions(core PUBLIC _USE_MATH_DEFINES) + target_compile_definitions(core PUBLIC NOMINMAX) +endif() + +if(MSVC) + target_compile_options(core PUBLIC /utf-8) +endif() + +configure_file("${CMAKE_CURRENT_LIST_DIR}/version.cc.in" "${CMAKE_CURRENT_LIST_DIR}/version.cc") diff --git a/src/core/aabb.cc b/src/core/aabb.cc new file mode 100644 index 0000000..3661143 --- /dev/null +++ b/src/core/aabb.cc @@ -0,0 +1,59 @@ +#include "core/pch.hh" + +#include "core/aabb.hh" + +AABB::AABB(const glm::fvec3& min, const glm::fvec3& max) +{ + set_bounds(min, max); +} + +void AABB::set_bounds(const glm::fvec3& min, const glm::fvec3& max) +{ + this->min = min; + this->max = max; +} + +void AABB::set_offset(const glm::fvec3& base, const glm::fvec3& size) +{ + this->min = base; + this->max = base + size; +} + +bool AABB::contains(const glm::fvec3& point) const +{ + auto result = true; + result = result && (point.x >= min.x) && (point.x <= max.x); + result = result && (point.y >= min.y) && (point.y <= max.y); + result = result && (point.z >= min.z) && (point.z <= max.z); + return result; +} + +bool AABB::intersect(const AABB& other_box) const +{ + auto result = true; + result = result && (min.x < other_box.max.x) && (max.x > other_box.min.x); + result = result && (min.y < other_box.max.y) && (max.y > other_box.min.y); + result = result && (min.z < other_box.max.z) && (max.z > other_box.min.z); + return result; +} + +AABB AABB::combine_with(const AABB& other_box) const +{ + AABB result; + result.set_bounds(min, other_box.max); + return result; +} + +AABB AABB::multiply_with(const AABB& other_box) const +{ + AABB result; + result.set_bounds(other_box.min, max); + return result; +} + +AABB AABB::push(const glm::fvec3& vector) const +{ + AABB result; + result.set_bounds(min + vector, max + vector); + return result; +} diff --git a/src/core/aabb.hh b/src/core/aabb.hh new file mode 100644 index 0000000..fe07060 --- /dev/null +++ b/src/core/aabb.hh @@ -0,0 +1,31 @@ +#ifndef CORE_AABB_HH +#define CORE_AABB_HH 1 +#pragma once + +#include "core/macros.hh" + +class AABB final { +public: + DECLARE_DEFAULT_CONSTRUCTOR(AABB); + explicit AABB(const glm::fvec3& min, const glm::fvec3& max); + virtual ~AABB(void) = default; + + void set_bounds(const glm::fvec3& min, const glm::fvec3& max); + void set_offset(const glm::fvec3& base, const glm::fvec3& size); + + const glm::fvec3& get_min(void) const; + const glm::fvec3& get_max(void) const; + + bool contains(const glm::fvec3& point) const; + bool intersect(const AABB& other_box) const; + + AABB combine_with(const AABB& other_box) const; + AABB multiply_with(const AABB& other_box) const; + AABB push(const glm::fvec3& vector) const; + +public: + glm::fvec3 min; + glm::fvec3 max; +}; + +#endif /* CORE_AABB_HH */ diff --git a/src/core/angles.hh b/src/core/angles.hh new file mode 100644 index 0000000..a2a3d55 --- /dev/null +++ b/src/core/angles.hh @@ -0,0 +1,107 @@ +#ifndef CORE_ANGLES_HH +#define CORE_ANGLES_HH 1 +#pragma once + +#include "core/constexpr.hh" + +constexpr float A180 = vx::radians(180.0f); +constexpr float A360 = vx::radians(360.0f); + +namespace cxangles +{ +float wrap_180(float angle); +float wrap_360(float angle); +} // namespace cxangles + +namespace cxangles +{ +glm::fvec3 wrap_180(const glm::fvec3& angles); +glm::fvec3 wrap_360(const glm::fvec3& angles); +} // namespace cxangles + +namespace cxangles +{ +void vectors(const glm::fvec3& angles, glm::fvec3& forward); +void vectors(const glm::fvec3& angles, glm::fvec3* forward, glm::fvec3* right, glm::fvec3* up); +} // namespace cxangles + +inline float cxangles::wrap_180(float angle) +{ + const auto result = std::fmod(angle + A180, A360); + + if(result < 0.0f) { + return result + A180; + } + + return result - A180; +} + +inline float cxangles::wrap_360(float angle) +{ + return std::fmod(std::fmod(angle, A360) + A360, A360); +} + +inline glm::fvec3 cxangles::wrap_180(const glm::fvec3& angles) +{ + return glm::fvec3 { + cxangles::wrap_180(angles.x), + cxangles::wrap_180(angles.y), + cxangles::wrap_180(angles.z), + }; +} + +inline glm::fvec3 cxangles::wrap_360(const glm::fvec3& angles) +{ + return glm::fvec3 { + cxangles::wrap_360(angles.x), + cxangles::wrap_360(angles.y), + cxangles::wrap_360(angles.z), + }; +} + +inline void cxangles::vectors(const glm::fvec3& angles, glm::fvec3& forward) +{ + const float cosp = std::cos(angles.x); + const float cosy = std::cos(angles.y); + const float sinp = std::sin(angles.x); + const float siny = std::sin(angles.y); + + forward.x = cosp * siny * (-1.0f); + forward.y = sinp; + forward.z = cosp * cosy * (-1.0f); +} + +inline void cxangles::vectors(const glm::fvec3& angles, glm::fvec3* forward, glm::fvec3* right, glm::fvec3* up) +{ + if(!forward && !right && !up) { + // There's no point in figuring out + // direction vectors if nothing is passed + // in the function to store that stuff in + return; + } + + const auto pcv = glm::cos(angles); + const auto psv = glm::sin(angles); + const auto ncv = pcv * (-1.0f); + const auto nsv = psv * (-1.0f); + + if(forward) { + forward->x = pcv.x * nsv.y; + forward->y = psv.x; + forward->z = pcv.x * ncv.y; + } + + if(right) { + right->x = pcv.z * pcv.y; + right->y = psv.z * pcv.y; + right->z = nsv.y; + } + + if(up) { + up->x = psv.x * psv.y * pcv.z + ncv.y * psv.z; + up->y = pcv.x * pcv.z; + up->z = nsv.x * ncv.y * pcv.z + psv.y * psv.z; + } +} + +#endif /* CORE_ANGLES_HH */ diff --git a/src/core/binfile.cc b/src/core/binfile.cc new file mode 100644 index 0000000..aa39039 --- /dev/null +++ b/src/core/binfile.cc @@ -0,0 +1,70 @@ +#include "core/pch.hh" + +#include "core/binfile.hh" + +#include "core/resource.hh" + +static emhash8::HashMap> resource_map; + +template<> +resource_ptr resource::load(const char* name, unsigned int flags) +{ + auto it = resource_map.find(name); + + if(it != resource_map.cend()) { + // Return an existing resource + return it->second; + } + + auto file = PHYSFS_openRead(name); + + if(file == nullptr) { + spdlog::warn("resource: {}: {}", name, PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode())); + return nullptr; + } + + auto new_resource = std::make_shared(); + new_resource->size = PHYSFS_fileLength(file); + new_resource->buffer = new std::byte[new_resource->size]; + + PHYSFS_readBytes(file, new_resource->buffer, new_resource->size); + PHYSFS_close(file); + + return resource_map.insert_or_assign(name, new_resource).first->second; +} + +template<> +void resource::hard_cleanup(void) +{ + for(const auto& it : resource_map) { + if(it.second.use_count() > 1L) { + spdlog::warn("resource: zombie resource [BinFile] {} [use_count={}]", it.first, it.second.use_count()); + } else { + spdlog::debug("resource: releasing [BinFile] {}", it.first); + } + + delete[] it.second->buffer; + } + + resource_map.clear(); +} + +template<> +void resource::soft_cleanup(void) +{ + auto iter = resource_map.cbegin(); + + while(iter != resource_map.cend()) { + if(iter->second.use_count() == 1L) { + spdlog::debug("resource: releasing [BinFile] {}", iter->first); + + delete[] iter->second->buffer; + + iter = resource_map.erase(iter); + + continue; + } + + iter = std::next(iter); + } +} diff --git a/src/core/binfile.hh b/src/core/binfile.hh new file mode 100644 index 0000000..21dab40 --- /dev/null +++ b/src/core/binfile.hh @@ -0,0 +1,10 @@ +#ifndef CORE_BINFILE_HH +#define CORE_BINFILE_HH 1 +#pragma once + +struct BinFile final { + std::byte* buffer; + std::size_t size; +}; + +#endif /* CORE_BINFILE_HH */ diff --git a/src/core/buffer.cc b/src/core/buffer.cc new file mode 100644 index 0000000..0e18f4f --- /dev/null +++ b/src/core/buffer.cc @@ -0,0 +1,207 @@ +#include "core/pch.hh" + +#include "core/buffer.hh" + +#include "core/constexpr.hh" + +ReadBuffer::ReadBuffer(const void* data, std::size_t size) +{ + reset(data, size); +} + +ReadBuffer::ReadBuffer(const ENetPacket* packet) +{ + reset(packet); +} + +ReadBuffer::ReadBuffer(PHYSFS_File* file) +{ + reset(file); +} + +std::size_t ReadBuffer::size(void) const +{ + return m_vector.size(); +} + +const std::byte* ReadBuffer::data(void) const +{ + return m_vector.data(); +} + +void ReadBuffer::reset(const void* data, std::size_t size) +{ + auto bytes = reinterpret_cast(data); + m_vector.assign(bytes, bytes + size); + m_position = 0U; +} + +void ReadBuffer::reset(const ENetPacket* packet) +{ + auto bytes_ptr = reinterpret_cast(packet->data); + m_vector.assign(bytes_ptr, bytes_ptr + packet->dataLength); + m_position = 0; +} + +void ReadBuffer::reset(PHYSFS_File* file) +{ + m_vector.resize(PHYSFS_fileLength(file)); + m_position = 0; + + PHYSFS_seek(file, 0); + PHYSFS_readBytes(file, m_vector.data(), m_vector.size()); +} + +float ReadBuffer::read_FP32(void) +{ + return floathacks::uint32_to_float(read_UI32()); +} + +std::uint8_t ReadBuffer::read_UI8(void) +{ + if((m_position + 1U) <= m_vector.size()) { + auto result = static_cast(m_vector[m_position]); + m_position += 1U; + return result; + } + + m_position += 1U; + return 0; +} + +std::uint16_t ReadBuffer::read_UI16(void) +{ + if((m_position + 2U) <= m_vector.size()) { + auto result = UINT16_C(0x0000); + result |= static_cast(m_vector[m_position + 0U]) << 8U; + result |= static_cast(m_vector[m_position + 1U]) << 0U; + m_position += 2U; + return result; + } + + m_position += 2U; + return 0; +} + +std::uint32_t ReadBuffer::read_UI32(void) +{ + if((m_position + 4U) <= m_vector.size()) { + auto result = UINT32_C(0x00000000); + result |= static_cast(m_vector[m_position + 0U]) << 24U; + result |= static_cast(m_vector[m_position + 1U]) << 16U; + result |= static_cast(m_vector[m_position + 2U]) << 8U; + result |= static_cast(m_vector[m_position + 3U]) << 0U; + m_position += 4U; + return result; + } + + m_position += 4U; + return 0; +} + +std::uint64_t ReadBuffer::read_UI64(void) +{ + if((m_position + 8U) <= m_vector.size()) { + auto result = UINT64_C(0x0000000000000000); + result |= (0x00000000000000FF & static_cast(m_vector[m_position + 0U])) << 56U; + result |= (0x00000000000000FF & static_cast(m_vector[m_position + 1U])) << 48U; + result |= (0x00000000000000FF & static_cast(m_vector[m_position + 2U])) << 40U; + result |= (0x00000000000000FF & static_cast(m_vector[m_position + 3U])) << 32U; + result |= (0x00000000000000FF & static_cast(m_vector[m_position + 4U])) << 24U; + result |= (0x00000000000000FF & static_cast(m_vector[m_position + 5U])) << 16U; + result |= (0x00000000000000FF & static_cast(m_vector[m_position + 6U])) << 8U; + result |= (0x00000000000000FF & static_cast(m_vector[m_position + 7U])) << 0U; + m_position += 8U; + return result; + } + + m_position += 8U; + return 0; +} + +std::string ReadBuffer::read_string(void) +{ + auto size = static_cast(read_UI16()); + auto result = std::string(); + + for(std::size_t i = 0; i < size; ++i) { + if(m_position < m_vector.size()) { + result.push_back(static_cast(m_vector[m_position])); + } + + m_position += 1U; + } + + return result; +} + +std::size_t WriteBuffer::size(void) const +{ + return m_vector.size(); +} + +const std::byte* WriteBuffer::data(void) const +{ + return m_vector.data(); +} + +void WriteBuffer::reset(void) +{ + m_vector.clear(); +} + +void WriteBuffer::write_UI8(std::uint8_t value) +{ + m_vector.push_back(static_cast(value)); +} + +void WriteBuffer::write_UI16(std::uint16_t value) +{ + m_vector.push_back(static_cast(UINT16_C(0xFF) & ((value & UINT16_C(0xFF00)) >> 8U))); + m_vector.push_back(static_cast(UINT16_C(0xFF) & ((value & UINT16_C(0x00FF)) >> 0U))); +} + +void WriteBuffer::write_UI32(std::uint32_t value) +{ + m_vector.push_back(static_cast(UINT32_C(0xFF) & ((value & UINT32_C(0xFF000000)) >> 24U))); + m_vector.push_back(static_cast(UINT32_C(0xFF) & ((value & UINT32_C(0x00FF0000)) >> 16U))); + m_vector.push_back(static_cast(UINT32_C(0xFF) & ((value & UINT32_C(0x0000FF00)) >> 8U))); + m_vector.push_back(static_cast(UINT32_C(0xFF) & ((value & UINT32_C(0x000000FF)) >> 0U))); +} + +void WriteBuffer::write_UI64(std::uint64_t value) +{ + m_vector.push_back(static_cast(UINT64_C(0xFF) & ((value & UINT64_C(0xFF00000000000000)) >> 56U))); + m_vector.push_back(static_cast(UINT64_C(0xFF) & ((value & UINT64_C(0x00FF000000000000)) >> 48U))); + m_vector.push_back(static_cast(UINT64_C(0xFF) & ((value & UINT64_C(0x0000FF0000000000)) >> 40U))); + m_vector.push_back(static_cast(UINT64_C(0xFF) & ((value & UINT64_C(0x000000FF00000000)) >> 32U))); + m_vector.push_back(static_cast(UINT64_C(0xFF) & ((value & UINT64_C(0x00000000FF000000)) >> 24U))); + m_vector.push_back(static_cast(UINT64_C(0xFF) & ((value & UINT64_C(0x0000000000FF0000)) >> 16U))); + m_vector.push_back(static_cast(UINT64_C(0xFF) & ((value & UINT64_C(0x000000000000FF00)) >> 8U))); + m_vector.push_back(static_cast(UINT64_C(0xFF) & ((value & UINT64_C(0x00000000000000FF)) >> 0U))); +} + +void WriteBuffer::write_string(const std::string& value) +{ + const std::size_t size = vx::min(UINT16_MAX, value.size()); + + write_UI16(static_cast(size)); + + for(std::size_t i = 0; i < size; m_vector.push_back(static_cast(value[i++]))) + ; +} + +PHYSFS_File* WriteBuffer::to_file(const char* path, bool append) const +{ + if(auto file = (append ? PHYSFS_openAppend(path) : PHYSFS_openWrite(path))) { + PHYSFS_writeBytes(file, m_vector.data(), m_vector.size()); + return file; + } + + return nullptr; +} + +ENetPacket* WriteBuffer::to_packet(enet_uint32 flags) const +{ + return enet_packet_create(m_vector.data(), m_vector.size(), flags); +} diff --git a/src/core/buffer.hh b/src/core/buffer.hh new file mode 100644 index 0000000..1397e16 --- /dev/null +++ b/src/core/buffer.hh @@ -0,0 +1,255 @@ +#ifndef CORE_BUFFER_HH +#define CORE_BUFFER_HH 1 + +#include "core/floathacks.hh" +#include "core/macros.hh" + +class ReadBuffer final { +public: + DECLARE_DEFAULT_CONSTRUCTOR(ReadBuffer); + explicit ReadBuffer(const void* data, std::size_t size); + explicit ReadBuffer(const ENetPacket* packet); + explicit ReadBuffer(PHYSFS_File* file); + virtual ~ReadBuffer(void) = default; + + std::size_t size(void) const; + const std::byte* data(void) const; + + void reset(const void* data, std::size_t size); + void reset(const ENetPacket* packet); + void reset(PHYSFS_File* file); + + float read_FP32(void); + std::uint8_t read_UI8(void); + std::uint16_t read_UI16(void); + std::uint32_t read_UI32(void); + std::uint64_t read_UI64(void); + std::string read_string(void); + + inline std::int8_t read_I8(void); + inline std::int16_t read_I16(void); + inline std::int32_t read_I32(void); + inline std::int64_t read_I64(void); + + inline ReadBuffer& operator>>(float& value); + inline ReadBuffer& operator>>(std::int8_t& value); + inline ReadBuffer& operator>>(std::int16_t& value); + inline ReadBuffer& operator>>(std::int32_t& value); + inline ReadBuffer& operator>>(std::int64_t& value); + inline ReadBuffer& operator>>(std::uint8_t& value); + inline ReadBuffer& operator>>(std::uint16_t& value); + inline ReadBuffer& operator>>(std::uint32_t& value); + inline ReadBuffer& operator>>(std::uint64_t& value); + inline ReadBuffer& operator>>(std::string& value); + +private: + std::vector m_vector; + std::size_t m_position; +}; + +class WriteBuffer final { +public: + DECLARE_DEFAULT_CONSTRUCTOR(WriteBuffer); + virtual ~WriteBuffer(void) = default; + + std::size_t size(void) const; + const std::byte* data(void) const; + + void reset(void); + + void write_FP32(float value); + void write_UI8(std::uint8_t value); + void write_UI16(std::uint16_t value); + void write_UI32(std::uint32_t value); + void write_UI64(std::uint64_t value); + void write_string(const std::string& value); + + inline void write_I8(std::int8_t value); + inline void write_I16(std::int16_t value); + inline void write_I32(std::int32_t value); + inline void write_I64(std::int64_t value); + + inline WriteBuffer& operator<<(float value); + inline WriteBuffer& operator<<(std::int8_t value); + inline WriteBuffer& operator<<(std::int16_t value); + inline WriteBuffer& operator<<(std::int32_t value); + inline WriteBuffer& operator<<(std::int64_t value); + inline WriteBuffer& operator<<(std::uint8_t value); + inline WriteBuffer& operator<<(std::uint16_t value); + inline WriteBuffer& operator<<(std::uint32_t value); + inline WriteBuffer& operator<<(std::uint64_t value); + inline WriteBuffer& operator<<(const std::string& value); + + PHYSFS_File* to_file(const char* path, bool append = false) const; + ENetPacket* to_packet(enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE) const; + +private: + std::vector m_vector; +}; + +inline std::int8_t ReadBuffer::read_I8(void) +{ + return static_cast(read_UI8()); +} + +inline std::int16_t ReadBuffer::read_I16(void) +{ + return static_cast(read_UI16()); +} + +inline std::int32_t ReadBuffer::read_I32(void) +{ + return static_cast(read_UI32()); +} + +inline std::int64_t ReadBuffer::read_I64(void) +{ + return static_cast(read_UI64()); +} + +inline ReadBuffer& ReadBuffer::operator>>(float& value) +{ + value = read_FP32(); + return *this; +} + +inline ReadBuffer& ReadBuffer::operator>>(std::int8_t& value) +{ + value = read_I8(); + return *this; +} + +inline ReadBuffer& ReadBuffer::operator>>(std::int16_t& value) +{ + value = read_I16(); + return *this; +} + +inline ReadBuffer& ReadBuffer::operator>>(std::int32_t& value) +{ + value = read_I32(); + return *this; +} + +inline ReadBuffer& ReadBuffer::operator>>(std::int64_t& value) +{ + value = read_I64(); + return *this; +} + +inline ReadBuffer& ReadBuffer::operator>>(std::uint8_t& value) +{ + value = read_UI8(); + return *this; +} + +inline ReadBuffer& ReadBuffer::operator>>(std::uint16_t& value) +{ + value = read_UI16(); + return *this; +} + +inline ReadBuffer& ReadBuffer::operator>>(std::uint32_t& value) +{ + value = read_UI32(); + return *this; +} + +inline ReadBuffer& ReadBuffer::operator>>(std::uint64_t& value) +{ + value = read_UI64(); + return *this; +} + +inline ReadBuffer& ReadBuffer::operator>>(std::string& value) +{ + value = read_string(); + return *this; +} + +inline void WriteBuffer::write_FP32(float value) +{ + write_UI32(floathacks::float_to_uint32(value)); +} + +inline void WriteBuffer::write_I8(std::int8_t value) +{ + write_UI8(static_cast(value)); +} + +inline void WriteBuffer::write_I16(std::int16_t value) +{ + write_UI16(static_cast(value)); +} + +inline void WriteBuffer::write_I32(std::int32_t value) +{ + write_UI32(static_cast(value)); +} + +inline void WriteBuffer::write_I64(std::int64_t value) +{ + write_UI64(static_cast(value)); +} + +inline WriteBuffer& WriteBuffer::operator<<(float value) +{ + write_FP32(value); + return *this; +} + +inline WriteBuffer& WriteBuffer::operator<<(std::int8_t value) +{ + write_I8(value); + return *this; +} + +inline WriteBuffer& WriteBuffer::operator<<(std::int16_t value) +{ + write_I16(value); + return *this; +} + +inline WriteBuffer& WriteBuffer::operator<<(std::int32_t value) +{ + write_I32(value); + return *this; +} + +inline WriteBuffer& WriteBuffer::operator<<(std::int64_t value) +{ + write_I64(value); + return *this; +} + +inline WriteBuffer& WriteBuffer::operator<<(std::uint8_t value) +{ + write_UI8(value); + return *this; +} + +inline WriteBuffer& WriteBuffer::operator<<(std::uint16_t value) +{ + write_UI16(value); + return *this; +} + +inline WriteBuffer& WriteBuffer::operator<<(std::uint32_t value) +{ + write_UI32(value); + return *this; +} + +inline WriteBuffer& WriteBuffer::operator<<(std::uint64_t value) +{ + write_UI64(value); + return *this; +} + +inline WriteBuffer& WriteBuffer::operator<<(const std::string& value) +{ + write_string(value); + return *this; +} + +#endif /* CORE_BUFFER_HH */ diff --git a/src/core/cmdline.cc b/src/core/cmdline.cc new file mode 100644 index 0000000..c83f9e6 --- /dev/null +++ b/src/core/cmdline.cc @@ -0,0 +1,81 @@ +#include "core/pch.hh" + +#include "core/cmdline.hh" + +// Valid options always start with OPTION_PREFIX, can contain +// a bunch of OPTION_PREFIX'es inside and never end with one +constexpr static char OPTION_PREFIX = '-'; + +static std::unordered_map options; + +static inline bool is_option_string(const std::string& string) +{ + if(string.find_last_of(OPTION_PREFIX) >= (string.size() - 1)) { + return false; + } + + return string[0] == OPTION_PREFIX; +} + +static inline std::string get_option(const std::string& string) +{ + std::size_t i; + for(i = 0; string[i] == OPTION_PREFIX; ++i) + ; + return std::string(string.cbegin() + i, string.cend()); +} + +void cmdline::create(int argc, char** argv) +{ + for(int idx = 1; idx < argc; ++idx) { + std::string string = argv[idx]; + + if(!is_option_string(string)) { + spdlog::warn("cmdline: non-argument at {}: {}", idx, string); + continue; + } + + auto option_string = get_option(string); + auto next_idx = idx + 1; + + if(next_idx < argc) { + std::string argument = argv[next_idx]; + + if(!is_option_string(argument)) { + options.insert_or_assign(option_string, argument); + idx = next_idx; + continue; + } + } + + // The option is either last or has no + // argument (happens when there is a valid + // option right next to the one we're parsing) + options.insert_or_assign(option_string, std::string()); + } +} + +void cmdline::insert(const char* option, const char* argument) +{ + if(argument == nullptr) { + options.insert_or_assign(option, std::string()); + } else { + options.insert_or_assign(option, argument); + } +} + +const char* cmdline::get(const char* option, const char* fallback) +{ + auto it = options.find(option); + + if(it == options.cend()) { + return fallback; + } + + return it->second.c_str(); +} + +bool cmdline::contains(const char* option) +{ + return options.count(option); +} diff --git a/src/core/cmdline.hh b/src/core/cmdline.hh new file mode 100644 index 0000000..8441a44 --- /dev/null +++ b/src/core/cmdline.hh @@ -0,0 +1,13 @@ +#ifndef CORE_CMDLINE_HH +#define CORE_CMDLINE_HH 1 +#pragma once + +namespace cmdline +{ +void create(int argc, char** argv); +void insert(const char* option, const char* argument = nullptr); +const char* get(const char* option, const char* fallback = nullptr); +bool contains(const char* option); +} // namespace cmdline + +#endif /* CORE_CMDLINE_HH */ diff --git a/src/core/concepts.hh b/src/core/concepts.hh new file mode 100644 index 0000000..47b01d2 --- /dev/null +++ b/src/core/concepts.hh @@ -0,0 +1,15 @@ +#ifndef CORE_CONCEPTS_HH +#define CORE_CONCEPTS_HH 1 +#pragma once + +namespace vx +{ +template +concept Arithmetic = std::is_arithmetic_v; +template +concept Integer = std::is_integral_v; +template +concept FloatingPoint = std::is_floating_point_v; +} // namespace vx + +#endif /* CORE_CONCEPTS_HH */ diff --git a/src/core/config.cc b/src/core/config.cc new file mode 100644 index 0000000..3202fb6 --- /dev/null +++ b/src/core/config.cc @@ -0,0 +1,191 @@ +#include "core/pch.hh" + +#include "core/config.hh" + +#include "core/cmdline.hh" +#include "core/strtools.hh" +#include "core/version.hh" + +ConfigBoolean::ConfigBoolean(bool default_value) +{ + m_value = default_value; + m_string = ConfigBoolean::to_string(default_value); +} + +void ConfigBoolean::set(const char* value) +{ + m_value = ConfigBoolean::from_string(value); + m_string = ConfigBoolean::to_string(m_value); +} + +const char* ConfigBoolean::get(void) const +{ + return m_string.c_str(); +} + +bool ConfigBoolean::get_value(void) const +{ + return m_value; +} + +void ConfigBoolean::set_value(bool value) +{ + m_value = value; + m_string = ConfigBoolean::to_string(m_value); +} + +const char* ConfigBoolean::to_string(bool value) +{ + if(value) { + return "true"; + } else { + return "false"; + } +} + +bool ConfigBoolean::from_string(const char* value) +{ + if(std::strcmp(value, "false") && !std::strcmp(value, "true")) { + return true; + } else { + return false; + } +} + +ConfigString::ConfigString(const char* default_value) +{ + m_value = default_value; +} + +void ConfigString::set(const char* value) +{ + m_value = value; +} + +const char* ConfigString::get(void) const +{ + return m_value.c_str(); +} + +void Config::load_cmdline(void) +{ + for(auto it : m_values) { + if(auto value = cmdline::get(it.first.c_str())) { + it.second->set(value); + } + } +} + +bool Config::load_file(const char* path) +{ + if(auto file = PHYSFS_openRead(path)) { + auto source = std::string(PHYSFS_fileLength(file), char(0x00)); + PHYSFS_readBytes(file, source.data(), source.size()); + PHYSFS_close(file); + + std::string line; + std::string kv_string; + std::istringstream stream(source); + + while(std::getline(stream, line)) { + auto comment = line.find_first_of('#'); + + if(comment == std::string::npos) { + kv_string = strtools::trim_whitespace(line); + } else { + kv_string = strtools::trim_whitespace(line.substr(0, comment)); + } + + if(strtools::is_whitespace(kv_string)) { + // Ignore empty or commented out lines + continue; + } + + auto separator = kv_string.find('='); + + if(separator == std::string::npos) { + spdlog::warn("config: {}: invalid line: {}", path, line); + continue; + } + + auto kv_name = strtools::trim_whitespace(kv_string.substr(0, separator)); + auto kv_value = strtools::trim_whitespace(kv_string.substr(separator + 1)); + + auto kv_pair = m_values.find(kv_name); + + if(kv_pair == m_values.cend()) { + spdlog::warn("config: {}: unknown key: {}", path, kv_name); + continue; + } + + kv_pair->second->set(kv_value.c_str()); + } + + return true; + } + + return false; +} + +bool Config::save_file(const char* path) const +{ + std::ostringstream stream; + + auto curtime = std::time(nullptr); + + stream << "# Voxelius " << project_version_string << " configuration file" << std::endl; + stream << "# Generated at: " << std::put_time(std::gmtime(&curtime), "%Y-%m-%d %H:%M:%S %z") << std::endl << std::endl; + + for(const auto& it : m_values) { + stream << it.first << "="; + stream << it.second->get(); + stream << std::endl; + } + + if(auto file = PHYSFS_openWrite(path)) { + auto source = stream.str(); + PHYSFS_writeBytes(file, source.data(), source.size()); + PHYSFS_close(file); + return true; + } + + return false; +} + +bool Config::set_value(const char* name, const char* value) +{ + auto kv_pair = m_values.find(name); + + if(kv_pair != m_values.cend()) { + kv_pair->second->set(value); + return true; + } + + return false; +} + +const char* Config::get_value(const char* name) const +{ + auto kv_pair = m_values.find(name); + if(kv_pair != m_values.cend()) { + return kv_pair->second->get(); + } else { + return nullptr; + } +} + +void Config::add_value(const char* name, IConfigValue& vref) +{ + m_values.insert_or_assign(name, &vref); +} + +const IConfigValue* Config::find(const char* name) const +{ + auto kv_pair = m_values.find(name); + + if(kv_pair != m_values.cend()) { + return kv_pair->second; + } else { + return nullptr; + } +} diff --git a/src/core/config.hh b/src/core/config.hh new file mode 100644 index 0000000..a7f8500 --- /dev/null +++ b/src/core/config.hh @@ -0,0 +1,182 @@ +#ifndef CORE_CONFIG_HH +#define CORE_CONFIG_HH 1 +#pragma once + +#include "core/concepts.hh" +#include "core/macros.hh" + +class IConfigValue { +public: + virtual ~IConfigValue(void) = default; + virtual void set(const char* value) = 0; + virtual const char* get(void) const = 0; +}; + +class ConfigBoolean final : public IConfigValue { +public: + explicit ConfigBoolean(bool default_value = false); + virtual ~ConfigBoolean(void) = default; + + virtual void set(const char* value) override; + virtual const char* get(void) const override; + + bool get_value(void) const; + void set_value(bool value); + +private: + bool m_value; + std::string m_string; + +public: + static const char* to_string(bool value); + static bool from_string(const char* value); +}; + +template +class ConfigNumber : public IConfigValue { +public: + explicit ConfigNumber(T default_value = T(0)); + explicit ConfigNumber(T default_value, T min_value, T max_value); + virtual ~ConfigNumber(void) = default; + + virtual void set(const char* value) override; + virtual const char* get(void) const override; + + T get_value(void) const; + void set_value(T value); + + T get_min_value(void) const; + T get_max_value(void) const; + void set_limits(T min_value, T max_value); + +private: + T m_value; + T m_min_value; + T m_max_value; + std::string m_string; +}; + +class ConfigInt final : public ConfigNumber { +public: + using ConfigNumber::ConfigNumber; +}; + +class ConfigFloat final : public ConfigNumber { +public: + using ConfigNumber::ConfigNumber; +}; + +class ConfigUnsigned final : public ConfigNumber { +public: + using ConfigNumber::ConfigNumber; +}; + +class ConfigUnsigned64 final : public ConfigNumber { +public: + using ConfigNumber::ConfigNumber; +}; + +class ConfigSizeType final : public ConfigNumber { +public: + using ConfigNumber::ConfigNumber; +}; + +class ConfigString final : public IConfigValue { +public: + explicit ConfigString(const char* default_value); + virtual ~ConfigString(void) = default; + + virtual void set(const char* value) override; + virtual const char* get(void) const override; + +private: + std::string m_value; +}; + +class Config final { +public: + DECLARE_DEFAULT_CONSTRUCTOR(Config); + virtual ~Config(void) = default; + + void load_cmdline(void); + bool load_file(const char* path); + bool save_file(const char* path) const; + + bool set_value(const char* name, const char* value); + const char* get_value(const char* name) const; + + void add_value(const char* name, IConfigValue& vref); + + const IConfigValue* find(const char* name) const; + +private: + std::unordered_map m_values; +}; + +template +inline ConfigNumber::ConfigNumber(T default_value) +{ + m_value = default_value; + m_min_value = std::numeric_limits::min(); + m_max_value = std::numeric_limits::max(); + m_string = std::to_string(default_value); +} + +template +inline ConfigNumber::ConfigNumber(T default_value, T min_value, T max_value) +{ + m_value = default_value; + m_min_value = min_value; + m_max_value = max_value; + m_string = std::to_string(default_value); +} + +template +inline void ConfigNumber::set(const char* value) +{ + std::istringstream(value) >> m_value; + m_value = std::clamp(m_value, m_min_value, m_max_value); + m_string = std::to_string(m_value); +} + +template +inline const char* ConfigNumber::get(void) const +{ + return m_string.c_str(); +} + +template +inline T ConfigNumber::get_value(void) const +{ + return m_value; +} + +template +inline void ConfigNumber::set_value(T value) +{ + m_value = std::clamp(value, m_min_value, m_max_value); + m_string = std::to_string(m_value); +} + +template +inline T ConfigNumber::get_min_value(void) const +{ + return m_min_value; +} + +template +inline T ConfigNumber::get_max_value(void) const +{ + return m_max_value; +} + +template +inline void ConfigNumber::set_limits(T min_value, T max_value) +{ + m_min_value = min_value; + m_max_value = max_value; + m_value = std::clamp(m_value, m_min_value, m_max_value); + m_string = std::to_string(m_value); +} + +#endif /* CORE_CONFIG_HH */ diff --git a/src/core/constexpr.hh b/src/core/constexpr.hh new file mode 100644 index 0000000..18c83c0 --- /dev/null +++ b/src/core/constexpr.hh @@ -0,0 +1,191 @@ +#ifndef CORE_CONSTEXPR_HH +#define CORE_CONSTEXPR_HH 1 +#pragma once + +#include "core/concepts.hh" + +namespace vx +{ +template +constexpr static inline const T abs(const T x); +template +constexpr static inline const std::size_t array_size(const T (&)[L]); +template +constexpr static inline const T ceil(const F x); +template +constexpr static inline const T degrees(const T x); +template +constexpr static inline const T floor(const F x); +template +constexpr static inline const T clamp(const T x, const T min, const T max); +template +constexpr static inline const T lerp(const T x, const T y, const F a); +template +constexpr static inline const T log2(const T x); +template +constexpr static inline const T max(const T x, const T y); +template +constexpr static inline const T min(const T x, const T y); +template +requires std::is_signed_v +constexpr static inline const T mod_signed(const T x, const T m); +template +constexpr static inline const T pow2(const T x); +template +constexpr static inline const T radians(const T x); +template +constexpr static inline const bool range(const T x, const T min, const T max); +template +constexpr static inline const T sign(const F x); +template +constexpr static inline const T smoothstep(const T x, const T y, const F a); +} // namespace vx + +template +constexpr static inline const T vx::abs(const T x) +{ + if(x < static_cast(0)) { + return -x; + } else { + return x; + } +} + +template +constexpr static inline const std::size_t vx::array_size(const T (&)[L]) +{ + return L; +} + +template +constexpr static inline const T vx::ceil(const F x) +{ + const T ival = static_cast(x); + + if(ival < x) { + return ival + static_cast(1); + } else { + return ival; + } +} + +template +constexpr static inline const T vx::degrees(const T x) +{ + return x * static_cast(180.0) / static_cast(M_PI); +} + +template +constexpr static inline const T vx::floor(const F x) +{ + const T ival = static_cast(x); + + if(ival > x) { + return ival - static_cast(1); + } else { + return ival; + } +} + +template +constexpr static inline const T vx::clamp(const T x, const T min, const T max) +{ + if(x < min) { + return min; + } else if(x > max) { + return max; + } else { + return x; + } +} + +template +constexpr static inline const T vx::lerp(const T x, const T y, const F a) +{ + return static_cast(static_cast(x) * (static_cast(1.0f) - a) + static_cast(y) * a); +} + +template +constexpr static inline const T vx::log2(const T x) +{ + if(x < 2) { + return 0; + } else { + return vx::log2((x + 1) >> 1) + 1; + } +} + +template +constexpr static inline const T vx::max(const T x, const T y) +{ + if(x < y) { + return y; + } else { + return x; + } +} + +template +constexpr static inline const T vx::min(const T x, const T y) +{ + if(x > y) { + return y; + } else { + return x; + } +} + +template +requires std::is_signed_v +constexpr static inline const T vx::mod_signed(const T x, const T m) +{ + auto result = static_cast(x % m); + + if(result < T(0)) { + return result + m; + } else { + return result; + } +} + +template +constexpr static inline const T vx::pow2(const T x) +{ + T value = static_cast(1); + while(value < x) + value *= static_cast(2); + return value; +} + +template +constexpr static inline const T vx::radians(const T x) +{ + return x * static_cast(M_PI) / static_cast(180.0); +} + +template +constexpr static inline const bool vx::range(const T x, const T min, const T max) +{ + return ((x >= min) && (x <= max)); +} + +template +constexpr static inline const T vx::sign(const F x) +{ + if(x < F(0)) { + return T(-1); + } else if(x > F(0)) { + return T(+1); + } else { + return T(0); + } +} + +template +constexpr static inline const T vx::smoothstep(const T x, const T y, const F a) +{ + const F t = vx::clamp((a - x) / (y - x), F(0), F(1)); + return static_cast(t * t * (F(3) - F(2) * t)); +} + +#endif /* CORE_CONSTEXPR_HH */ diff --git a/src/core/crc64.cc b/src/core/crc64.cc new file mode 100644 index 0000000..c2ca53c --- /dev/null +++ b/src/core/crc64.cc @@ -0,0 +1,286 @@ +#include "core/pch.hh" + +#include "core/crc64.hh" + +// The lookup table for CRC64 checksum; this lookup +// table is generated using ECMA-182 compilant parameters: +// - Polynomial: `0x42F0E1EBA9EA3693` +// - Initial value: `0x0000000000000000` +// - Final xor: `0x0000000000000000` +// CRC Calculator: https://www.sunshine2k.de/coding/javascript/crc/crc_js.html +constexpr static const std::uint64_t crc_table[256] = { + 0x0000000000000000, + 0x42F0E1EBA9EA3693, + 0x85E1C3D753D46D26, + 0xC711223CFA3E5BB5, + 0x493366450E42ECDF, + 0x0BC387AEA7A8DA4C, + 0xCCD2A5925D9681F9, + 0x8E224479F47CB76A, + 0x9266CC8A1C85D9BE, + 0xD0962D61B56FEF2D, + 0x17870F5D4F51B498, + 0x5577EEB6E6BB820B, + 0xDB55AACF12C73561, + 0x99A54B24BB2D03F2, + 0x5EB4691841135847, + 0x1C4488F3E8F96ED4, + 0x663D78FF90E185EF, + 0x24CD9914390BB37C, + 0xE3DCBB28C335E8C9, + 0xA12C5AC36ADFDE5A, + 0x2F0E1EBA9EA36930, + 0x6DFEFF5137495FA3, + 0xAAEFDD6DCD770416, + 0xE81F3C86649D3285, + 0xF45BB4758C645C51, + 0xB6AB559E258E6AC2, + 0x71BA77A2DFB03177, + 0x334A9649765A07E4, + 0xBD68D2308226B08E, + 0xFF9833DB2BCC861D, + 0x388911E7D1F2DDA8, + 0x7A79F00C7818EB3B, + 0xCC7AF1FF21C30BDE, + 0x8E8A101488293D4D, + 0x499B3228721766F8, + 0x0B6BD3C3DBFD506B, + 0x854997BA2F81E701, + 0xC7B97651866BD192, + 0x00A8546D7C558A27, + 0x4258B586D5BFBCB4, + 0x5E1C3D753D46D260, + 0x1CECDC9E94ACE4F3, + 0xDBFDFEA26E92BF46, + 0x990D1F49C77889D5, + 0x172F5B3033043EBF, + 0x55DFBADB9AEE082C, + 0x92CE98E760D05399, + 0xD03E790CC93A650A, + 0xAA478900B1228E31, + 0xE8B768EB18C8B8A2, + 0x2FA64AD7E2F6E317, + 0x6D56AB3C4B1CD584, + 0xE374EF45BF6062EE, + 0xA1840EAE168A547D, + 0x66952C92ECB40FC8, + 0x2465CD79455E395B, + 0x3821458AADA7578F, + 0x7AD1A461044D611C, + 0xBDC0865DFE733AA9, + 0xFF3067B657990C3A, + 0x711223CFA3E5BB50, + 0x33E2C2240A0F8DC3, + 0xF4F3E018F031D676, + 0xB60301F359DBE0E5, + 0xDA050215EA6C212F, + 0x98F5E3FE438617BC, + 0x5FE4C1C2B9B84C09, + 0x1D14202910527A9A, + 0x93366450E42ECDF0, + 0xD1C685BB4DC4FB63, + 0x16D7A787B7FAA0D6, + 0x5427466C1E109645, + 0x4863CE9FF6E9F891, + 0x0A932F745F03CE02, + 0xCD820D48A53D95B7, + 0x8F72ECA30CD7A324, + 0x0150A8DAF8AB144E, + 0x43A04931514122DD, + 0x84B16B0DAB7F7968, + 0xC6418AE602954FFB, + 0xBC387AEA7A8DA4C0, + 0xFEC89B01D3679253, + 0x39D9B93D2959C9E6, + 0x7B2958D680B3FF75, + 0xF50B1CAF74CF481F, + 0xB7FBFD44DD257E8C, + 0x70EADF78271B2539, + 0x321A3E938EF113AA, + 0x2E5EB66066087D7E, + 0x6CAE578BCFE24BED, + 0xABBF75B735DC1058, + 0xE94F945C9C3626CB, + 0x676DD025684A91A1, + 0x259D31CEC1A0A732, + 0xE28C13F23B9EFC87, + 0xA07CF2199274CA14, + 0x167FF3EACBAF2AF1, + 0x548F120162451C62, + 0x939E303D987B47D7, + 0xD16ED1D631917144, + 0x5F4C95AFC5EDC62E, + 0x1DBC74446C07F0BD, + 0xDAAD56789639AB08, + 0x985DB7933FD39D9B, + 0x84193F60D72AF34F, + 0xC6E9DE8B7EC0C5DC, + 0x01F8FCB784FE9E69, + 0x43081D5C2D14A8FA, + 0xCD2A5925D9681F90, + 0x8FDAB8CE70822903, + 0x48CB9AF28ABC72B6, + 0x0A3B7B1923564425, + 0x70428B155B4EAF1E, + 0x32B26AFEF2A4998D, + 0xF5A348C2089AC238, + 0xB753A929A170F4AB, + 0x3971ED50550C43C1, + 0x7B810CBBFCE67552, + 0xBC902E8706D82EE7, + 0xFE60CF6CAF321874, + 0xE224479F47CB76A0, + 0xA0D4A674EE214033, + 0x67C58448141F1B86, + 0x253565A3BDF52D15, + 0xAB1721DA49899A7F, + 0xE9E7C031E063ACEC, + 0x2EF6E20D1A5DF759, + 0x6C0603E6B3B7C1CA, + 0xF6FAE5C07D3274CD, + 0xB40A042BD4D8425E, + 0x731B26172EE619EB, + 0x31EBC7FC870C2F78, + 0xBFC9838573709812, + 0xFD39626EDA9AAE81, + 0x3A28405220A4F534, + 0x78D8A1B9894EC3A7, + 0x649C294A61B7AD73, + 0x266CC8A1C85D9BE0, + 0xE17DEA9D3263C055, + 0xA38D0B769B89F6C6, + 0x2DAF4F0F6FF541AC, + 0x6F5FAEE4C61F773F, + 0xA84E8CD83C212C8A, + 0xEABE6D3395CB1A19, + 0x90C79D3FEDD3F122, + 0xD2377CD44439C7B1, + 0x15265EE8BE079C04, + 0x57D6BF0317EDAA97, + 0xD9F4FB7AE3911DFD, + 0x9B041A914A7B2B6E, + 0x5C1538ADB04570DB, + 0x1EE5D94619AF4648, + 0x02A151B5F156289C, + 0x4051B05E58BC1E0F, + 0x87409262A28245BA, + 0xC5B073890B687329, + 0x4B9237F0FF14C443, + 0x0962D61B56FEF2D0, + 0xCE73F427ACC0A965, + 0x8C8315CC052A9FF6, + 0x3A80143F5CF17F13, + 0x7870F5D4F51B4980, + 0xBF61D7E80F251235, + 0xFD913603A6CF24A6, + 0x73B3727A52B393CC, + 0x31439391FB59A55F, + 0xF652B1AD0167FEEA, + 0xB4A25046A88DC879, + 0xA8E6D8B54074A6AD, + 0xEA16395EE99E903E, + 0x2D071B6213A0CB8B, + 0x6FF7FA89BA4AFD18, + 0xE1D5BEF04E364A72, + 0xA3255F1BE7DC7CE1, + 0x64347D271DE22754, + 0x26C49CCCB40811C7, + 0x5CBD6CC0CC10FAFC, + 0x1E4D8D2B65FACC6F, + 0xD95CAF179FC497DA, + 0x9BAC4EFC362EA149, + 0x158E0A85C2521623, + 0x577EEB6E6BB820B0, + 0x906FC95291867B05, + 0xD29F28B9386C4D96, + 0xCEDBA04AD0952342, + 0x8C2B41A1797F15D1, + 0x4B3A639D83414E64, + 0x09CA82762AAB78F7, + 0x87E8C60FDED7CF9D, + 0xC51827E4773DF90E, + 0x020905D88D03A2BB, + 0x40F9E43324E99428, + 0x2CFFE7D5975E55E2, + 0x6E0F063E3EB46371, + 0xA91E2402C48A38C4, + 0xEBEEC5E96D600E57, + 0x65CC8190991CB93D, + 0x273C607B30F68FAE, + 0xE02D4247CAC8D41B, + 0xA2DDA3AC6322E288, + 0xBE992B5F8BDB8C5C, + 0xFC69CAB42231BACF, + 0x3B78E888D80FE17A, + 0x7988096371E5D7E9, + 0xF7AA4D1A85996083, + 0xB55AACF12C735610, + 0x724B8ECDD64D0DA5, + 0x30BB6F267FA73B36, + 0x4AC29F2A07BFD00D, + 0x08327EC1AE55E69E, + 0xCF235CFD546BBD2B, + 0x8DD3BD16FD818BB8, + 0x03F1F96F09FD3CD2, + 0x41011884A0170A41, + 0x86103AB85A2951F4, + 0xC4E0DB53F3C36767, + 0xD8A453A01B3A09B3, + 0x9A54B24BB2D03F20, + 0x5D45907748EE6495, + 0x1FB5719CE1045206, + 0x919735E51578E56C, + 0xD367D40EBC92D3FF, + 0x1476F63246AC884A, + 0x568617D9EF46BED9, + 0xE085162AB69D5E3C, + 0xA275F7C11F7768AF, + 0x6564D5FDE549331A, + 0x279434164CA30589, + 0xA9B6706FB8DFB2E3, + 0xEB46918411358470, + 0x2C57B3B8EB0BDFC5, + 0x6EA7525342E1E956, + 0x72E3DAA0AA188782, + 0x30133B4B03F2B111, + 0xF7021977F9CCEAA4, + 0xB5F2F89C5026DC37, + 0x3BD0BCE5A45A6B5D, + 0x79205D0E0DB05DCE, + 0xBE317F32F78E067B, + 0xFCC19ED95E6430E8, + 0x86B86ED5267CDBD3, + 0xC4488F3E8F96ED40, + 0x0359AD0275A8B6F5, + 0x41A94CE9DC428066, + 0xCF8B0890283E370C, + 0x8D7BE97B81D4019F, + 0x4A6ACB477BEA5A2A, + 0x089A2AACD2006CB9, + 0x14DEA25F3AF9026D, + 0x562E43B4931334FE, + 0x913F6188692D6F4B, + 0xD3CF8063C0C759D8, + 0x5DEDC41A34BBEEB2, + 0x1F1D25F19D51D821, + 0xD80C07CD676F8394, + 0x9AFCE626CE85B507, +}; + +std::uint64_t crc64::get(const void* buffer, std::size_t size, std::uint64_t combine) +{ + auto data = reinterpret_cast(buffer); + for(std::size_t i = 0; i < size; ++i) + combine = crc_table[((combine >> 56) ^ data[i]) & 0xFF] ^ (combine << 8); + return combine; +} + +std::uint64_t crc64::get(const std::vector& buffer, std::uint64_t combine) +{ + return crc64::get(buffer.data(), buffer.size(), combine); +} + +std::uint64_t crc64::get(const std::string& buffer, std::uint64_t combine) +{ + return crc64::get(buffer.data(), buffer.size(), combine); +} diff --git a/src/core/crc64.hh b/src/core/crc64.hh new file mode 100644 index 0000000..da4ad2c --- /dev/null +++ b/src/core/crc64.hh @@ -0,0 +1,12 @@ +#ifndef CORE_CRC64_HH +#define CORE_CRC64_HH 1 +#pragma once + +namespace crc64 +{ +std::uint64_t get(const void* buffer, std::size_t size, std::uint64_t combine = UINT64_C(0)); +std::uint64_t get(const std::vector& buffer, std::uint64_t combine = UINT64_C(0)); +std::uint64_t get(const std::string& buffer, std::uint64_t combine = UINT64_C(0)); +} // namespace crc64 + +#endif /* CORE_CRC64_HH */ diff --git a/src/core/epoch.cc b/src/core/epoch.cc new file mode 100644 index 0000000..978eeb3 --- /dev/null +++ b/src/core/epoch.cc @@ -0,0 +1,39 @@ +#include "core/pch.hh" + +#include "core/epoch.hh" + +std::uint64_t epoch::seconds(void) +{ + const auto elapsed = std::chrono::system_clock::now().time_since_epoch(); + return static_cast(std::chrono::duration_cast(elapsed).count()); +} + +std::uint64_t epoch::milliseconds(void) +{ + const auto elapsed = std::chrono::system_clock::now().time_since_epoch(); + return static_cast(std::chrono::duration_cast(elapsed).count()); +} + +std::uint64_t epoch::microseconds(void) +{ + const auto elapsed = std::chrono::system_clock::now().time_since_epoch(); + return static_cast(std::chrono::duration_cast(elapsed).count()); +} + +std::int64_t epoch::signed_seconds(void) +{ + const auto elapsed = std::chrono::system_clock::now().time_since_epoch(); + return static_cast(std::chrono::duration_cast(elapsed).count()); +} + +std::int64_t epoch::signed_milliseconds(void) +{ + const auto elapsed = std::chrono::system_clock::now().time_since_epoch(); + return static_cast(std::chrono::duration_cast(elapsed).count()); +} + +std::int64_t epoch::signed_microseconds(void) +{ + const auto elapsed = std::chrono::system_clock::now().time_since_epoch(); + return static_cast(std::chrono::duration_cast(elapsed).count()); +} diff --git a/src/core/epoch.hh b/src/core/epoch.hh new file mode 100644 index 0000000..f590f27 --- /dev/null +++ b/src/core/epoch.hh @@ -0,0 +1,19 @@ +#ifndef CORE_EPOCH_HH +#define CORE_EPOCH_HH 1 +#pragma once + +namespace epoch +{ +std::uint64_t seconds(void); +std::uint64_t milliseconds(void); +std::uint64_t microseconds(void); +} // namespace epoch + +namespace epoch +{ +std::int64_t signed_seconds(void); +std::int64_t signed_milliseconds(void); +std::int64_t signed_microseconds(void); +} // namespace epoch + +#endif /* CORE_EPOCH_HH */ diff --git a/src/core/floathacks.hh b/src/core/floathacks.hh new file mode 100644 index 0000000..29b7cac --- /dev/null +++ b/src/core/floathacks.hh @@ -0,0 +1,56 @@ +#ifndef CORE_FLOATHACKS_HH +#define CORE_FLOATHACKS_HH 1 +#pragma once + +namespace floathacks +{ +static inline float int32_to_float(const std::int32_t value); +static inline float uint32_to_float(const std::uint32_t value); +static inline std::int32_t float_to_int32(const float value); +static inline std::uint32_t float_to_uint32(const float value); +} // namespace floathacks + +static_assert(std::numeric_limits::is_iec559, "Floathacks only works with IEEE 754 compliant floats"); +static_assert(sizeof(std::int32_t) == sizeof(float), "Floathacks requires 32-bit integers to match float size"); + +static inline float floathacks::int32_to_float(const std::int32_t value) +{ + union { + std::int32_t src; + float dst; + } hack; + hack.src = value; + return hack.dst; +} + +static inline float floathacks::uint32_to_float(const std::uint32_t value) +{ + union { + std::uint32_t src; + float dst; + } hack; + hack.src = value; + return hack.dst; +} + +static inline std::int32_t floathacks::float_to_int32(const float value) +{ + union { + float src; + std::int32_t dst; + } hack; + hack.src = value; + return hack.dst; +} + +static inline std::uint32_t floathacks::float_to_uint32(const float value) +{ + union { + float src; + std::uint32_t dst; + } hack; + hack.src = value; + return hack.dst; +} + +#endif /* CORE_FLOATHACKS_HH */ diff --git a/src/core/image.cc b/src/core/image.cc new file mode 100644 index 0000000..08be3d4 --- /dev/null +++ b/src/core/image.cc @@ -0,0 +1,112 @@ +#include "core/pch.hh" + +#include "core/image.hh" + +#include "core/resource.hh" + +static emhash8::HashMap> resource_map; + +static int stbi_physfs_read(void* context, char* data, int size) +{ + return PHYSFS_readBytes(reinterpret_cast(context), data, size); +} + +static void stbi_physfs_skip(void* context, int count) +{ + auto file = reinterpret_cast(context); + PHYSFS_seek(file, PHYSFS_tell(file) + count); +} + +static int stbi_physfs_eof(void* context) +{ + return PHYSFS_eof(reinterpret_cast(context)); +} + +template<> +resource_ptr resource::load(const char* name, unsigned int flags) +{ + auto it = resource_map.find(name); + + if(it != resource_map.cend()) { + // Return an existing resource + return it->second; + } + + auto file = PHYSFS_openRead(name); + + if(file == nullptr) { + spdlog::warn("resource: {}: {}", name, PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode())); + return nullptr; + } + + if(flags & IMAGE_LOAD_FLIP) { + stbi_set_flip_vertically_on_load(true); + } else { + stbi_set_flip_vertically_on_load(false); + } + + stbi_io_callbacks callbacks; + callbacks.read = &stbi_physfs_read; + callbacks.skip = &stbi_physfs_skip; + callbacks.eof = &stbi_physfs_eof; + + auto new_resource = std::make_shared(); + + if(flags & IMAGE_LOAD_GRAY) { + new_resource->pixels = stbi_load_from_callbacks(&callbacks, file, &new_resource->size.x, &new_resource->size.y, nullptr, STBI_grey); + } else { + new_resource->pixels = stbi_load_from_callbacks( + &callbacks, file, &new_resource->size.x, &new_resource->size.y, nullptr, STBI_rgb_alpha); + } + + PHYSFS_close(file); + + if(new_resource->pixels == nullptr) { + spdlog::warn("resource: {}: {}", name, stbi_failure_reason()); + return nullptr; + } + + if(new_resource->size.x <= 0 || new_resource->size.y <= 0) { + spdlog::warn("resource {}: invalid dimensions", name); + stbi_image_free(new_resource->pixels); + return nullptr; + } + + return resource_map.insert_or_assign(name, new_resource).first->second; +} + +template<> +void resource::hard_cleanup(void) +{ + for(const auto& it : resource_map) { + if(it.second.use_count() > 1L) { + spdlog::warn("resource: zombie resource [Image] {} [use_count={}]", it.first, it.second.use_count()); + } else { + spdlog::debug("resource: releasing [Image] {}", it.first); + } + + stbi_image_free(it.second->pixels); + } + + resource_map.clear(); +} + +template<> +void resource::soft_cleanup(void) +{ + auto iter = resource_map.cbegin(); + + while(iter != resource_map.cend()) { + if(iter->second.use_count() == 1L) { + spdlog::debug("resource: releasing [Image] {}", iter->first); + + stbi_image_free(iter->second->pixels); + + iter = resource_map.erase(iter); + + continue; + } + + iter = std::next(iter); + } +} diff --git a/src/core/image.hh b/src/core/image.hh new file mode 100644 index 0000000..92d99be --- /dev/null +++ b/src/core/image.hh @@ -0,0 +1,13 @@ +#ifndef CORE_IMAGE_HH +#define CORE_IMAGE_HH 1 +#pragma once + +constexpr static unsigned int IMAGE_LOAD_GRAY = 0x0001U; +constexpr static unsigned int IMAGE_LOAD_FLIP = 0x0002U; + +struct Image final { + stbi_uc* pixels; + glm::ivec2 size; +}; + +#endif /* CORE_IMAGE_HH */ diff --git a/src/core/macros.hh b/src/core/macros.hh new file mode 100644 index 0000000..9a76109 --- /dev/null +++ b/src/core/macros.hh @@ -0,0 +1,19 @@ +#ifndef CORE_MACROS_HH +#define CORE_MACROS_HH 1 +#pragma once + +#define DISABLE_COPY_OPERATORS(class_name) \ +public: \ + explicit class_name(const class_name& other) = delete; \ + class_name& operator=(const class_name& other) = delete + +#define DISABLE_MOVE_OPERATORS(class_name) \ +public: \ + explicit class_name(class_name&& other) = delete; \ + class_name& operator=(class_name&& other) = delete + +#define DECLARE_DEFAULT_CONSTRUCTOR(class_name) \ +public: \ + class_name(void) = default + +#endif /* CORE_MACROS_HH */ diff --git a/src/core/pch.hh b/src/core/pch.hh new file mode 100644 index 0000000..795a287 --- /dev/null +++ b/src/core/pch.hh @@ -0,0 +1,50 @@ +#ifndef CORE_PCH_HH +#define CORE_PCH_HH 1 +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include + +#include +#include +#include +#include + +#include +#include +#include + +#include + +#include + +#include +#include + +#endif /* CORE_PCH_HH */ diff --git a/src/core/randomizer.hh b/src/core/randomizer.hh new file mode 100644 index 0000000..b60b839 --- /dev/null +++ b/src/core/randomizer.hh @@ -0,0 +1,57 @@ +#ifndef CORE_RANDOMIZER_HH +#define CORE_RANDOMIZER_HH 1 +#pragma once + +template +class Randomizer final { +public: + explicit Randomizer(void); + explicit Randomizer(std::uint64_t seed); + virtual ~Randomizer(void) = default; + void add(const T& value); + const T& get(void); + void clear(void); + +private: + std::vector m_vector; + std::mt19937_64 m_twister; + std::uniform_int_distribution m_dist; +}; + +template +inline Randomizer::Randomizer(void) +{ + m_vector.clear(); + m_twister.seed(std::random_device()()); + m_dist = std::uniform_int_distribution(0, 0); +} + +template +inline Randomizer::Randomizer(std::uint64_t seed) +{ + m_vector.clear(); + m_twister.seed(seed); + m_dist = std::uniform_int_distribution(0, 0); +} + +template +inline void Randomizer::add(const T& value) +{ + m_vector.push_back(value); + m_dist = std::uniform_int_distribution(0, m_vector.size() - 1); +} + +template +inline const T& Randomizer::get(void) +{ + return m_vector.at(m_dist(m_twister)); +} + +template +inline void Randomizer::clear(void) +{ + m_vector.clear(); + m_dist = std::uniform_int_distribution(0, 0); +} + +#endif /* CORE_RANDOMIZER_HH */ diff --git a/src/core/resource.hh b/src/core/resource.hh new file mode 100644 index 0000000..ab7b74f --- /dev/null +++ b/src/core/resource.hh @@ -0,0 +1,18 @@ +#ifndef CORE_RESOURCE_HH +#define CORE_RESOURCE_HH 1 +#pragma once + +template +using resource_ptr = std::shared_ptr; + +namespace resource +{ +template +resource_ptr load(const char* name, unsigned int flags = 0U); +template +void hard_cleanup(void); +template +void soft_cleanup(void); +} // namespace resource + +#endif /* CORE_RESOURCE_HH */ diff --git a/src/core/strtools.cc b/src/core/strtools.cc new file mode 100644 index 0000000..4edd86b --- /dev/null +++ b/src/core/strtools.cc @@ -0,0 +1,54 @@ +#include "core/pch.hh" + +#include "core/strtools.hh" + +constexpr static const char* WHITESPACE_CHARS = " \t\r\n"; + +bool strtools::is_whitespace(const std::string& string) +{ + if(string.find_first_not_of(WHITESPACE_CHARS) == std::string::npos) { + return true; + } else if((string.size() == 1) && string[0] == 0x00) { + return true; + } else { + return string.empty(); + } +} + +std::string strtools::join(const std::vector& strings, const std::string& separator) +{ + std::ostringstream stream; + for(const std::string& str : strings) + stream << str << separator; + return stream.str(); +} + +std::vector strtools::split(const std::string& string, const std::string& separator) +{ + std::size_t pos = 0; + std::size_t prev = 0; + std::vector result; + + while((pos = string.find(separator, prev)) != std::string::npos) { + result.push_back(string.substr(prev, pos - prev)); + prev = pos + separator.length(); + } + + if(prev <= string.length()) { + result.push_back(string.substr(prev, string.length() - prev)); + } + + return result; +} + +std::string strtools::trim_whitespace(const std::string& string) +{ + auto su = string.find_first_not_of(WHITESPACE_CHARS); + auto sv = string.find_last_not_of(WHITESPACE_CHARS); + + if(su == std::string::npos) { + return std::string(); + } else { + return string.substr(su, sv - su + 1); + } +} diff --git a/src/core/strtools.hh b/src/core/strtools.hh new file mode 100644 index 0000000..1462978 --- /dev/null +++ b/src/core/strtools.hh @@ -0,0 +1,21 @@ +#ifndef CORE_STRTOOLS_HH +#define CORE_STRTOOLS_HH 1 +#pragma once + +namespace strtools +{ +bool is_whitespace(const std::string& string); +} // namespace strtools + +namespace strtools +{ +std::string join(const std::vector& strings, const std::string& separator); +std::vector split(const std::string& string, const std::string& separator); +} // namespace strtools + +namespace strtools +{ +std::string trim_whitespace(const std::string& string); +} // namespace strtools + +#endif /* CORE_STRTOOLS_HH */ diff --git a/src/core/vectors.hh b/src/core/vectors.hh new file mode 100644 index 0000000..a6e9c75 --- /dev/null +++ b/src/core/vectors.hh @@ -0,0 +1,47 @@ +#ifndef CORE_VECTORS_HH +#define CORE_VECTORS_HH 1 +#pragma once + +#include "core/concepts.hh" + +// core/vectors.hh - because NO ONE would POSSIBLY +// need integer-based distance calculations in a +// game about voxels. That would be INSANE! :D + +namespace vx +{ +template +constexpr static inline const T length2(const glm::vec<2, T>& vector); +template +constexpr static inline const T length2(const glm::vec<3, T>& vector); +template +constexpr static inline const T distance2(const glm::vec<2, T>& vector_a, const glm::vec<2, T>& vector_b); +template +constexpr static inline const T distance2(const glm::vec<3, T>& vector_a, const glm::vec<3, T>& vector_b); +} // namespace vx + +template +constexpr static inline const T vx::length2(const glm::vec<2, T>& vector) +{ + return (vector.x * vector.x) + (vector.y * vector.y); +} + +template +constexpr static inline const T vx::length2(const glm::vec<3, T>& vector) +{ + return (vector.x * vector.x) + (vector.y * vector.y) + (vector.z * vector.z); +} + +template +constexpr static inline const T vx::distance2(const glm::vec<2, T>& vector_a, const glm::vec<2, T>& vector_b) +{ + return vx::length2(vector_a - vector_b); +} + +template +constexpr static inline const T vx::distance2(const glm::vec<3, T>& vector_a, const glm::vec<3, T>& vector_b) +{ + return vx::length2(vector_a - vector_b); +} + +#endif /* CORE_VECTORS_HH */ diff --git a/src/core/version.cc b/src/core/version.cc new file mode 100644 index 0000000..d86564e --- /dev/null +++ b/src/core/version.cc @@ -0,0 +1,12 @@ +#include "core/pch.hh" + +#include "core/version.hh" + +// clang-format off +const unsigned long project_version_major = 0; +const unsigned long project_version_minor = 0; +const unsigned long project_version_patch = 1; +const unsigned long project_version_tweak = 2526; +// clang-format on + +const std::string project_version_string = "0.0.1.2526"; diff --git a/src/core/version.cc.in b/src/core/version.cc.in new file mode 100644 index 0000000..0183ec0 --- /dev/null +++ b/src/core/version.cc.in @@ -0,0 +1,12 @@ +#include "core/pch.hh" + +#include "core/version.hh" + +// clang-format off +const unsigned long project_version_major = ${PROJECT_VERSION_MAJOR}; +const unsigned long project_version_minor = ${PROJECT_VERSION_MINOR}; +const unsigned long project_version_patch = ${PROJECT_VERSION_PATCH}; +const unsigned long project_version_tweak = ${PROJECT_VERSION_TWEAK}; +// clang-format on + +const std::string project_version_string = "${PROJECT_VERSION}"; diff --git a/src/core/version.hh b/src/core/version.hh new file mode 100644 index 0000000..2061c31 --- /dev/null +++ b/src/core/version.hh @@ -0,0 +1,12 @@ +#ifndef CORE_VERSION_HH +#define CORE_VERSION_HH 1 +#pragma once + +extern const unsigned long project_version_major; +extern const unsigned long project_version_minor; +extern const unsigned long project_version_patch; +extern const unsigned long project_version_tweak; + +extern const std::string project_version_string; + +#endif /* CORE_VERSION_HH */ diff --git a/src/game/CMakeLists.txt b/src/game/CMakeLists.txt new file mode 100644 index 0000000..c6c32bf --- /dev/null +++ b/src/game/CMakeLists.txt @@ -0,0 +1,11 @@ +if(BUILD_VCLIENT) + add_subdirectory(client) +endif() + +if(BUILD_VSERVER) + add_subdirectory(server) +endif() + +if(BUILD_VCLIENT OR BUILD_VSERVER) + add_subdirectory(shared) +endif() diff --git a/src/game/client/CMakeLists.txt b/src/game/client/CMakeLists.txt new file mode 100644 index 0000000..ac40148 --- /dev/null +++ b/src/game/client/CMakeLists.txt @@ -0,0 +1,126 @@ +add_executable(vclient + "${CMAKE_CURRENT_LIST_DIR}/background.cc" + "${CMAKE_CURRENT_LIST_DIR}/background.hh" + "${CMAKE_CURRENT_LIST_DIR}/bother.cc" + "${CMAKE_CURRENT_LIST_DIR}/bother.hh" + "${CMAKE_CURRENT_LIST_DIR}/camera.cc" + "${CMAKE_CURRENT_LIST_DIR}/camera.hh" + "${CMAKE_CURRENT_LIST_DIR}/chat.cc" + "${CMAKE_CURRENT_LIST_DIR}/chat.hh" + "${CMAKE_CURRENT_LIST_DIR}/chunk_mesher.cc" + "${CMAKE_CURRENT_LIST_DIR}/chunk_mesher.hh" + "${CMAKE_CURRENT_LIST_DIR}/chunk_quad.hh" + "${CMAKE_CURRENT_LIST_DIR}/chunk_renderer.cc" + "${CMAKE_CURRENT_LIST_DIR}/chunk_renderer.hh" + "${CMAKE_CURRENT_LIST_DIR}/chunk_vbo.hh" + "${CMAKE_CURRENT_LIST_DIR}/chunk_visibility.cc" + "${CMAKE_CURRENT_LIST_DIR}/chunk_visibility.hh" + "${CMAKE_CURRENT_LIST_DIR}/const.hh" + "${CMAKE_CURRENT_LIST_DIR}/crosshair.cc" + "${CMAKE_CURRENT_LIST_DIR}/crosshair.hh" + "${CMAKE_CURRENT_LIST_DIR}/direct_connection.cc" + "${CMAKE_CURRENT_LIST_DIR}/direct_connection.hh" + "${CMAKE_CURRENT_LIST_DIR}/experiments.cc" + "${CMAKE_CURRENT_LIST_DIR}/experiments.hh" + "${CMAKE_CURRENT_LIST_DIR}/factory.cc" + "${CMAKE_CURRENT_LIST_DIR}/factory.hh" + "${CMAKE_CURRENT_LIST_DIR}/game.cc" + "${CMAKE_CURRENT_LIST_DIR}/game.hh" + "${CMAKE_CURRENT_LIST_DIR}/gamepad_axis.cc" + "${CMAKE_CURRENT_LIST_DIR}/gamepad_axis.hh" + "${CMAKE_CURRENT_LIST_DIR}/gamepad_button.cc" + "${CMAKE_CURRENT_LIST_DIR}/gamepad_button.hh" + "${CMAKE_CURRENT_LIST_DIR}/gamepad.cc" + "${CMAKE_CURRENT_LIST_DIR}/gamepad.hh" + "${CMAKE_CURRENT_LIST_DIR}/glfw.hh" + "${CMAKE_CURRENT_LIST_DIR}/globals.cc" + "${CMAKE_CURRENT_LIST_DIR}/globals.hh" + "${CMAKE_CURRENT_LIST_DIR}/gui_screen.hh" + "${CMAKE_CURRENT_LIST_DIR}/hotbar.cc" + "${CMAKE_CURRENT_LIST_DIR}/hotbar.hh" + "${CMAKE_CURRENT_LIST_DIR}/imdraw_ext.cc" + "${CMAKE_CURRENT_LIST_DIR}/imdraw_ext.hh" + "${CMAKE_CURRENT_LIST_DIR}/interpolation.cc" + "${CMAKE_CURRENT_LIST_DIR}/interpolation.hh" + "${CMAKE_CURRENT_LIST_DIR}/keybind.cc" + "${CMAKE_CURRENT_LIST_DIR}/keybind.hh" + "${CMAKE_CURRENT_LIST_DIR}/language.cc" + "${CMAKE_CURRENT_LIST_DIR}/language.hh" + "${CMAKE_CURRENT_LIST_DIR}/listener.cc" + "${CMAKE_CURRENT_LIST_DIR}/listener.hh" + "${CMAKE_CURRENT_LIST_DIR}/main_menu.cc" + "${CMAKE_CURRENT_LIST_DIR}/main_menu.hh" + "${CMAKE_CURRENT_LIST_DIR}/main.cc" + "${CMAKE_CURRENT_LIST_DIR}/message_box.cc" + "${CMAKE_CURRENT_LIST_DIR}/message_box.hh" + "${CMAKE_CURRENT_LIST_DIR}/metrics.cc" + "${CMAKE_CURRENT_LIST_DIR}/metrics.hh" + "${CMAKE_CURRENT_LIST_DIR}/outline.cc" + "${CMAKE_CURRENT_LIST_DIR}/outline.hh" + "${CMAKE_CURRENT_LIST_DIR}/pch.hh" + "${CMAKE_CURRENT_LIST_DIR}/play_menu.cc" + "${CMAKE_CURRENT_LIST_DIR}/play_menu.hh" + "${CMAKE_CURRENT_LIST_DIR}/player_look.cc" + "${CMAKE_CURRENT_LIST_DIR}/player_look.hh" + "${CMAKE_CURRENT_LIST_DIR}/player_move.cc" + "${CMAKE_CURRENT_LIST_DIR}/player_move.hh" + "${CMAKE_CURRENT_LIST_DIR}/player_target.cc" + "${CMAKE_CURRENT_LIST_DIR}/player_target.hh" + "${CMAKE_CURRENT_LIST_DIR}/program.cc" + "${CMAKE_CURRENT_LIST_DIR}/program.hh" + "${CMAKE_CURRENT_LIST_DIR}/progress_bar.cc" + "${CMAKE_CURRENT_LIST_DIR}/progress_bar.hh" + "${CMAKE_CURRENT_LIST_DIR}/receive.cc" + "${CMAKE_CURRENT_LIST_DIR}/receive.hh" + "${CMAKE_CURRENT_LIST_DIR}/scoreboard.cc" + "${CMAKE_CURRENT_LIST_DIR}/scoreboard.hh" + "${CMAKE_CURRENT_LIST_DIR}/screenshot.cc" + "${CMAKE_CURRENT_LIST_DIR}/screenshot.hh" + "${CMAKE_CURRENT_LIST_DIR}/session.cc" + "${CMAKE_CURRENT_LIST_DIR}/session.hh" + "${CMAKE_CURRENT_LIST_DIR}/settings.cc" + "${CMAKE_CURRENT_LIST_DIR}/settings.hh" + "${CMAKE_CURRENT_LIST_DIR}/skybox.cc" + "${CMAKE_CURRENT_LIST_DIR}/skybox.hh" + "${CMAKE_CURRENT_LIST_DIR}/sound_effect.cc" + "${CMAKE_CURRENT_LIST_DIR}/sound_effect.hh" + "${CMAKE_CURRENT_LIST_DIR}/sound_emitter.cc" + "${CMAKE_CURRENT_LIST_DIR}/sound_emitter.hh" + "${CMAKE_CURRENT_LIST_DIR}/sound.cc" + "${CMAKE_CURRENT_LIST_DIR}/sound.hh" + "${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}/texture_gui.cc" + "${CMAKE_CURRENT_LIST_DIR}/texture_gui.hh" + "${CMAKE_CURRENT_LIST_DIR}/toggles.cc" + "${CMAKE_CURRENT_LIST_DIR}/toggles.hh" + "${CMAKE_CURRENT_LIST_DIR}/voxel_anims.cc" + "${CMAKE_CURRENT_LIST_DIR}/voxel_anims.hh" + "${CMAKE_CURRENT_LIST_DIR}/voxel_atlas.cc" + "${CMAKE_CURRENT_LIST_DIR}/voxel_atlas.hh" + "${CMAKE_CURRENT_LIST_DIR}/voxel_sounds.cc" + "${CMAKE_CURRENT_LIST_DIR}/voxel_sounds.hh" + "${CMAKE_CURRENT_LIST_DIR}/window_title.cc" + "${CMAKE_CURRENT_LIST_DIR}/window_title.hh") +target_compile_features(vclient PUBLIC cxx_std_20) +target_compile_definitions(vclient PUBLIC GLFW_INCLUDE_NONE) +target_include_directories(vclient PUBLIC "${DEPS_INCLUDE_DIR}") +target_include_directories(vclient PRIVATE "${PROJECT_SOURCE_DIR}/src") +target_include_directories(vclient PRIVATE "${PROJECT_SOURCE_DIR}/src/game") +target_precompile_headers(vclient PRIVATE "${CMAKE_CURRENT_LIST_DIR}/pch.hh") +target_link_libraries(vclient PUBLIC shared dr_libs glad glfw3 imgui imgui_glfw imgui_opengl3 salad) + +if(WIN32 AND MSVC) + # GLFW defines APIENTRY and ENet includes + # Windows API headers which also define APIENTRY + target_compile_options(vclient PRIVATE /wd4005) +endif() + +if(WIN32) + enable_language(RC) + target_sources(vclient PRIVATE "${CMAKE_CURRENT_LIST_DIR}/vclient.rc") +endif() + +install(TARGETS vclient RUNTIME DESTINATION ".") diff --git a/src/game/client/background.cc b/src/game/client/background.cc new file mode 100644 index 0000000..415b63c --- /dev/null +++ b/src/game/client/background.cc @@ -0,0 +1,37 @@ +#include "client/pch.hh" + +#include "client/background.hh" + +#include "core/constexpr.hh" +#include "core/resource.hh" + +#include "client/globals.hh" +#include "client/texture_gui.hh" + +static resource_ptr texture; + +void background::init(void) +{ + texture = resource::load("textures/gui/background.png", TEXTURE_GUI_LOAD_VFLIP); + + if(texture == nullptr) { + spdlog::critical("background: texture load failed"); + std::terminate(); + } +} + +void background::deinit(void) +{ + texture = nullptr; +} + +void background::layout(void) +{ + auto viewport = ImGui::GetMainViewport(); + auto draw_list = ImGui::GetBackgroundDrawList(); + + auto scaled_width = 0.75f * static_cast(globals::width / globals::gui_scale); + auto scaled_height = 0.75f * static_cast(globals::height / globals::gui_scale); + auto scale_uv = ImVec2(scaled_width / static_cast(texture->size.x), scaled_height / static_cast(texture->size.y)); + draw_list->AddImage(texture->handle, ImVec2(0.0f, 0.0f), viewport->Size, ImVec2(0.0f, 0.0f), scale_uv); +} diff --git a/src/game/client/background.hh b/src/game/client/background.hh new file mode 100644 index 0000000..634caf5 --- /dev/null +++ b/src/game/client/background.hh @@ -0,0 +1,12 @@ +#ifndef CLIENT_BACKGROUND_HH +#define CLIENT_BACKGROUND_HH 1 +#pragma once + +namespace background +{ +void init(void); +void deinit(void); +void layout(void); +} // namespace background + +#endif /* CLIENT_BACKGROUND_HH */ diff --git a/src/game/client/bother.cc b/src/game/client/bother.cc new file mode 100644 index 0000000..8bd7182 --- /dev/null +++ b/src/game/client/bother.cc @@ -0,0 +1,163 @@ +#include "client/pch.hh" + +#include "client/bother.hh" + +#include "shared/protocol.hh" + +#include "client/globals.hh" + +// Maximum amount of peers used for bothering +constexpr static std::size_t BOTHER_PEERS = 4; + +struct BotherQueueItem final { + unsigned int identity; + std::string hostname; + std::uint16_t port; +}; + +static ENetHost* bother_host; +static entt::dispatcher bother_dispatcher; +static std::unordered_set bother_set; +static std::deque bother_queue; + +static void on_status_response_packet(const protocol::StatusResponse& packet) +{ + auto identity = static_cast(reinterpret_cast(packet.peer->data)); + + bother_set.erase(identity); + + BotherResponseEvent event; + event.identity = identity; + event.is_server_unreachable = false; + event.protocol_version = packet.version; + event.num_players = packet.num_players; + event.max_players = packet.max_players; + event.motd = packet.motd; + globals::dispatcher.trigger(event); + + enet_peer_disconnect(packet.peer, protocol::CHANNEL); +} + +void bother::init(void) +{ + bother_host = enet_host_create(nullptr, BOTHER_PEERS, 1, 0, 0); + bother_dispatcher.clear(); + bother_set.clear(); + + bother_dispatcher.sink().connect<&on_status_response_packet>(); +} + +void bother::deinit(void) +{ + enet_host_destroy(bother_host); + bother_dispatcher.clear(); + bother_set.clear(); +} + +void bother::update_late(void) +{ + unsigned int free_peers = 0U; + + // Figure out how much times we can call + // enet_host_connect and reallistically succeed + for(unsigned int i = 0U; i < bother_host->peerCount; ++i) { + if(bother_host->peers[i].state != ENET_PEER_STATE_DISCONNECTED) { + continue; + } + + free_peers += 1U; + } + + for(unsigned int i = 0U; (i < free_peers) && bother_queue.size(); ++i) { + const auto& item = bother_queue.front(); + + ENetAddress address; + enet_address_set_host(&address, item.hostname.c_str()); + address.port = enet_uint16(item.port); + + if(auto peer = enet_host_connect(bother_host, &address, 1, 0)) { + peer->data = reinterpret_cast(static_cast(item.identity)); + bother_set.insert(item.identity); + enet_host_flush(bother_host); + } + + bother_queue.pop_front(); + } + + ENetEvent enet_event; + + if(0 < enet_host_service(bother_host, &enet_event, 0)) { + if(enet_event.type == ENET_EVENT_TYPE_CONNECT) { + protocol::StatusRequest packet; + packet.version = protocol::VERSION; + protocol::send(enet_event.peer, protocol::encode(packet)); + return; + } + + if(enet_event.type == ENET_EVENT_TYPE_RECEIVE) { + protocol::decode(bother_dispatcher, enet_event.packet, enet_event.peer); + enet_packet_destroy(enet_event.packet); + return; + } + + if(enet_event.type == ENET_EVENT_TYPE_DISCONNECT) { + auto identity = static_cast(reinterpret_cast(enet_event.peer->data)); + + if(bother_set.count(identity)) { + BotherResponseEvent event; + event.identity = identity; + event.is_server_unreachable = true; + globals::dispatcher.trigger(event); + } + + bother_set.erase(identity); + + return; + } + } +} + +void bother::ping(unsigned int identity, const char* host, std::uint16_t port) +{ + if(bother_set.count(identity)) { + // Already in the process + return; + } + + for(const auto& item : bother_queue) { + if(item.identity == identity) { + // Already in the queue + return; + } + } + + BotherQueueItem item; + item.identity = identity; + item.hostname = std::string(host); + item.port = port; + + bother_queue.push_back(item); +} + +void bother::cancel(unsigned int identity) +{ + bother_set.erase(identity); + + auto item = bother_queue.cbegin(); + + while(item != bother_queue.cend()) { + if(item->identity == identity) { + item = bother_queue.erase(item); + continue; + } + + item = std::next(item); + } + + for(unsigned int i = 0U; i < bother_host->peerCount; ++i) { + if(bother_host->peers[i].data == reinterpret_cast(static_cast(identity))) { + enet_peer_reset(&bother_host->peers[i]); + break; + } + } +} diff --git a/src/game/client/bother.hh b/src/game/client/bother.hh new file mode 100644 index 0000000..5fbf247 --- /dev/null +++ b/src/game/client/bother.hh @@ -0,0 +1,23 @@ +#ifndef CLIENT_BOTHER_HH +#define CLIENT_BOTHER_HH 1 +#pragma once + +struct BotherResponseEvent final { + unsigned int identity; + bool is_server_unreachable; + std::uint32_t protocol_version; + std::uint16_t num_players; + std::uint16_t max_players; + std::string motd; +}; + +namespace bother +{ +void init(void); +void deinit(void); +void update_late(void); +void ping(unsigned int identity, const char* host, std::uint16_t port); +void cancel(unsigned int identity); +} // namespace bother + +#endif /* CLIENT_BOTHER_HH */ diff --git a/src/game/client/camera.cc b/src/game/client/camera.cc new file mode 100644 index 0000000..724ae66 --- /dev/null +++ b/src/game/client/camera.cc @@ -0,0 +1,107 @@ +#include "client/pch.hh" + +#include "client/camera.hh" + +#include "core/angles.hh" +#include "core/config.hh" + +#include "shared/dimension.hh" +#include "shared/head.hh" +#include "shared/transform.hh" +#include "shared/velocity.hh" + +#include "client/const.hh" +#include "client/globals.hh" +#include "client/player_move.hh" +#include "client/session.hh" +#include "client/settings.hh" +#include "client/toggles.hh" + +ConfigFloat camera::roll_angle(2.0f, 0.0f, 4.0f); +ConfigFloat camera::vertical_fov(90.0f, 45.0f, 110.0f); +ConfigUnsigned camera::view_distance(16U, 4U, 32U); + +glm::fvec3 camera::angles; +glm::fvec3 camera::direction; +glm::fmat4x4 camera::matrix; +chunk_pos camera::position_chunk; +glm::fvec3 camera::position_local; + +static void reset_camera(void) +{ + camera::angles = glm::fvec3(0.0f, 0.0f, 0.0f); + camera::direction = DIR_FORWARD; + camera::matrix = glm::identity(); + camera::position_chunk = chunk_pos(0, 0, 0); + camera::position_local = glm::fvec3(0.0f, 0.0f, 0.0f); +} + +// Gracefully contributed by PQCraft himself in 2024 +// making PlatinumSrc and Voxelius kind of related to each other +static glm::fmat4x4 platinumsrc_viewmatrix(const glm::fvec3& position, const glm::fvec3& angles) +{ + glm::fvec3 forward, up; + cxangles::vectors(angles, &forward, nullptr, &up); + + auto result = glm::identity(); + result[0][0] = forward.y * up.z - forward.z * up.y; + result[1][0] = forward.z * up.x - forward.x * up.z; + result[2][0] = forward.x * up.y - forward.y * up.x; + result[3][0] = -result[0][0] * position.x - result[1][0] * position.y - result[2][0] * position.z; + result[0][1] = up.x; + result[1][1] = up.y; + result[2][1] = up.z; + result[3][1] = -up.x * position.x - up.y * position.y - up.z * position.z; + result[0][2] = -forward.x; + result[1][2] = -forward.y; + result[2][2] = -forward.z; + result[3][2] = forward.x * position.x + forward.y * position.y + forward.z * position.z; + return result; +} + +void camera::init(void) +{ + globals::client_config.add_value("camera.roll_angle", camera::roll_angle); + globals::client_config.add_value("camera.vertical_fov", camera::vertical_fov); + globals::client_config.add_value("camera.view_distance", camera::view_distance); + + settings::add_slider(1, camera::vertical_fov, settings_location::GENERAL, "camera.vertical_fov", true, "%.0f"); + settings::add_slider(0, camera::view_distance, settings_location::VIDEO, "camera.view_distance", false); + settings::add_slider(10, camera::roll_angle, settings_location::VIDEO, "camera.roll_angle", true, "%.01f"); + + reset_camera(); +} + +void camera::update(void) +{ + if(!session::is_ingame()) { + reset_camera(); + return; + } + + const auto& head = globals::dimension->entities.get(globals::player); + const auto& transform = globals::dimension->entities.get(globals::player); + const auto& velocity = globals::dimension->entities.get(globals::player); + + camera::angles = transform.angles + head.angles; + camera::position_chunk = transform.chunk; + camera::position_local = transform.local + head.offset; + + glm::fvec3 right_vector, up_vector; + cxangles::vectors(camera::angles, &camera::direction, &right_vector, &up_vector); + + auto client_angles = camera::angles; + + if(!toggles::get(TOGGLE_PM_FLIGHT)) { + // Apply the quake-like view rolling + client_angles[2] = vx::radians(-camera::roll_angle.get_value() * glm::dot(velocity.value / PMOVE_MAX_SPEED_GROUND, right_vector)); + } + + const auto z_near = 0.01f; + const auto z_far = 1.25f * static_cast(CHUNK_SIZE * camera::view_distance.get_value()); + + auto proj = glm::perspective(vx::radians(camera::vertical_fov.get_value()), globals::aspect, z_near, z_far); + auto view = platinumsrc_viewmatrix(camera::position_local, client_angles); + + camera::matrix = proj * view; +} diff --git a/src/game/client/camera.hh b/src/game/client/camera.hh new file mode 100644 index 0000000..9718720 --- /dev/null +++ b/src/game/client/camera.hh @@ -0,0 +1,32 @@ +#ifndef CLIENT_CAMERA_HH +#define CLIENT_CAMERA_HH 1 +#pragma once + +#include "shared/types.hh" + +class ConfigFloat; +class ConfigUnsigned; + +namespace camera +{ +extern ConfigFloat roll_angle; +extern ConfigFloat vertical_fov; +extern ConfigUnsigned view_distance; +} // namespace camera + +namespace camera +{ +extern glm::fvec3 angles; +extern glm::fvec3 direction; +extern glm::fmat4x4 matrix; +extern chunk_pos position_chunk; +extern glm::fvec3 position_local; +} // namespace camera + +namespace camera +{ +void init(void); +void update(void); +} // namespace camera + +#endif /* CLIENT_CAMERA_HH */ diff --git a/src/game/client/chat.cc b/src/game/client/chat.cc new file mode 100644 index 0000000..0a0c75d --- /dev/null +++ b/src/game/client/chat.cc @@ -0,0 +1,260 @@ +#include "client/pch.hh" + +#include "client/chat.hh" + +#include "core/config.hh" +#include "core/resource.hh" +#include "core/strtools.hh" + +#include "shared/protocol.hh" + +#include "client/game.hh" +#include "client/glfw.hh" +#include "client/globals.hh" +#include "client/gui_screen.hh" +#include "client/keybind.hh" +#include "client/language.hh" +#include "client/session.hh" +#include "client/settings.hh" +#include "client/sound.hh" +#include "client/sound_effect.hh" + +constexpr static ImGuiWindowFlags WINDOW_FLAGS = ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoDecoration; +constexpr static unsigned int MAX_HISTORY_SIZE = 128U; + +struct GuiChatMessage final { + std::uint64_t spawn; + std::string text; + ImVec4 color; +}; + +static ConfigKeyBind key_chat(GLFW_KEY_ENTER); +static ConfigUnsigned history_size(32U, 0U, MAX_HISTORY_SIZE); + +static std::deque history; +static std::string chat_input; +static bool needs_focus; + +static resource_ptr sfx_chat_message; + +static void append_text_message(const std::string& sender, const std::string& text) +{ + GuiChatMessage message; + message.spawn = globals::curtime; + message.text = std::format("<{}> {}", sender, text); + message.color = ImGui::GetStyleColorVec4(ImGuiCol_Text); + history.push_back(message); + + if(sfx_chat_message && session::is_ingame()) { + sound::play_ui(sfx_chat_message, false, 1.0f); + } +} + +static void append_player_join(const std::string& sender) +{ + GuiChatMessage message; + message.spawn = globals::curtime; + message.text = std::format("{} {}", sender, language::resolve("chat.client_join")); + message.color = ImGui::GetStyleColorVec4(ImGuiCol_DragDropTarget); + history.push_back(message); + + if(sfx_chat_message && session::is_ingame()) { + sound::play_ui(sfx_chat_message, false, 1.0f); + } +} + +static void append_player_leave(const std::string& sender, const std::string& reason) +{ + GuiChatMessage message; + message.spawn = globals::curtime; + message.text = std::format("{} {} ({})", sender, language::resolve("chat.client_left"), language::resolve(reason.c_str())); + message.color = ImGui::GetStyleColorVec4(ImGuiCol_DragDropTarget); + history.push_back(message); + + if(sfx_chat_message && session::is_ingame()) { + sound::play_ui(sfx_chat_message, false, 1.0f); + } +} + +static void on_chat_message_packet(const protocol::ChatMessage& packet) +{ + if(packet.type == protocol::ChatMessage::TEXT_MESSAGE) { + append_text_message(packet.sender, packet.message); + return; + } + + if(packet.type == protocol::ChatMessage::PLAYER_JOIN) { + append_player_join(packet.sender); + return; + } + + if(packet.type == protocol::ChatMessage::PLAYER_LEAVE) { + append_player_leave(packet.sender, packet.message); + return; + } +} + +static void on_glfw_key(const GlfwKeyEvent& event) +{ + if(event.action == GLFW_PRESS) { + if((event.key == GLFW_KEY_ENTER) && (globals::gui_screen == GUI_CHAT)) { + if(!strtools::is_whitespace(chat_input)) { + protocol::ChatMessage packet; + packet.type = protocol::ChatMessage::TEXT_MESSAGE; + packet.sender = client_game::username.get(); + packet.message = chat_input; + + protocol::send(session::peer, protocol::encode(packet)); + } + + globals::gui_screen = GUI_SCREEN_NONE; + + chat_input.clear(); + + return; + } + + if((event.key == GLFW_KEY_ESCAPE) && (globals::gui_screen == GUI_CHAT)) { + globals::gui_screen = GUI_SCREEN_NONE; + return; + } + + if(key_chat.equals(event.key) && !globals::gui_screen) { + globals::gui_screen = GUI_CHAT; + needs_focus = true; + return; + } + } +} + +void client_chat::init(void) +{ + globals::client_config.add_value("chat.key", key_chat); + globals::client_config.add_value("chat.history_size", history_size); + + settings::add_keybind(2, key_chat, settings_location::KEYBOARD_MISC, "key.chat"); + settings::add_slider(1, history_size, settings_location::VIDEO_GUI, "chat.history_size", false); + + globals::dispatcher.sink().connect<&on_chat_message_packet>(); + globals::dispatcher.sink().connect<&on_glfw_key>(); + + sfx_chat_message = resource::load("sounds/ui/chat_message.wav"); +} + +void client_chat::init_late(void) +{ +} + +void client_chat::deinit(void) +{ + sfx_chat_message = nullptr; +} + +void client_chat::update(void) +{ + while(history.size() > history_size.get_value()) { + history.pop_front(); + } +} + +void client_chat::layout(void) +{ + auto viewport = ImGui::GetMainViewport(); + auto window_start = ImVec2(0.0f, 0.0f); + auto window_size = ImVec2(0.75f * viewport->Size.x, viewport->Size.y); + + ImGui::SetNextWindowPos(window_start); + ImGui::SetNextWindowSize(window_size); + + ImGui::PushFont(globals::font_chat); + + if(!ImGui::Begin("###chat", nullptr, WINDOW_FLAGS)) { + ImGui::End(); + return; + } + + auto& padding = ImGui::GetStyle().FramePadding; + auto& spacing = ImGui::GetStyle().ItemSpacing; + auto font = ImGui::GetFont(); + + auto draw_list = ImGui::GetWindowDrawList(); + + // The text input widget occupies the bottom part + // of the chat window, we need to reserve some space for it + auto ypos = window_size.y - 2.5f * font->FontSize - 2.0f * padding.y - 2.0f * spacing.y; + + if(globals::gui_screen == GUI_CHAT) { + if(needs_focus) { + ImGui::SetKeyboardFocusHere(); + needs_focus = false; + } + + ImGui::SetNextItemWidth(window_size.x + 32.0f * padding.x); + ImGui::SetCursorScreenPos(ImVec2(padding.x, ypos)); + ImGui::InputText("###chat.input", &chat_input); + } + + if(!client_game::hide_hud && ((globals::gui_screen == GUI_SCREEN_NONE) || (globals::gui_screen == GUI_CHAT))) { + for(auto it = history.crbegin(); it < history.crend(); ++it) { + auto text_size = ImGui::CalcTextSize(it->text.c_str(), it->text.c_str() + it->text.size(), false, window_size.x); + auto rect_size = ImVec2(window_size.x, text_size.y + 2.0f * padding.y); + + auto rect_pos = ImVec2(padding.x, ypos - text_size.y - 2.0f * padding.y); + auto rect_end = ImVec2(rect_pos.x + rect_size.x, rect_pos.y + rect_size.y); + auto text_pos = ImVec2(rect_pos.x + padding.x, rect_pos.y + padding.y); + + auto fadeout_seconds = 10.0f; + auto fadeout = std::exp(-1.0f * std::pow(1.0e-6 * static_cast(globals::curtime - it->spawn) / fadeout_seconds, 10.0f)); + + float rect_alpha; + float text_alpha; + + if(globals::gui_screen == GUI_CHAT) { + rect_alpha = 0.75f; + text_alpha = 1.00f; + } else { + rect_alpha = 0.50f * fadeout; + text_alpha = 1.00f * fadeout; + } + + auto rect_col = ImGui::GetColorU32(ImGuiCol_FrameBg, rect_alpha); + auto text_col = ImGui::GetColorU32(ImVec4(it->color.x, it->color.y, it->color.z, it->color.w * text_alpha)); + + draw_list->AddRectFilled(rect_pos, rect_end, rect_col); + draw_list->AddText( + font, font->FontSize, text_pos, text_col, it->text.c_str(), it->text.c_str() + it->text.size(), window_size.x); + + ypos -= rect_size.y; + } + } + + ImGui::End(); + ImGui::PopFont(); +} + +void client_chat::clear(void) +{ + history.clear(); +} + +void client_chat::refresh_timings(void) +{ + for(auto it = history.begin(); it < history.end(); ++it) { + // Reset the spawn time so the fadeout timer + // is reset; SpawnPlayer handler might call this + it->spawn = globals::curtime; + } +} + +void client_chat::print(const std::string& text) +{ + GuiChatMessage message = {}; + message.spawn = globals::curtime; + message.text = text; + message.color = ImGui::GetStyleColorVec4(ImGuiCol_Text); + history.push_back(message); + + if(sfx_chat_message && session::is_ingame()) { + sound::play_ui(sfx_chat_message, false, 1.0f); + } +} diff --git a/src/game/client/chat.hh b/src/game/client/chat.hh new file mode 100644 index 0000000..f2ee4de --- /dev/null +++ b/src/game/client/chat.hh @@ -0,0 +1,21 @@ +#ifndef CLIENT_CHAT_HH +#define CLIENT_CHAT_HH 1 +#pragma once + +namespace client_chat +{ +void init(void); +void init_late(void); +void deinit(void); +void update(void); +void layout(void); +} // namespace client_chat + +namespace client_chat +{ +void clear(void); +void refresh_timings(void); +void print(const std::string& string); +} // namespace client_chat + +#endif /* CLIENT_CHAT_HH */ diff --git a/src/game/client/chunk_mesher.cc b/src/game/client/chunk_mesher.cc new file mode 100644 index 0000000..0271931 --- /dev/null +++ b/src/game/client/chunk_mesher.cc @@ -0,0 +1,467 @@ +#include "client/pch.hh" + +#include "client/chunk_mesher.hh" + +#include "core/crc64.hh" + +#include "shared/chunk.hh" +#include "shared/coord.hh" +#include "shared/dimension.hh" +#include "shared/threading.hh" +#include "shared/voxel_registry.hh" + +#include "client/chunk_quad.hh" +#include "client/globals.hh" +#include "client/session.hh" +#include "client/voxel_atlas.hh" + +using QuadBuilder = std::vector; + +using CachedChunkCoord = unsigned short; +constexpr static CachedChunkCoord CPOS_ITSELF = 0x0000; +constexpr static CachedChunkCoord CPOS_NORTH = 0x0001; +constexpr static CachedChunkCoord CPOS_SOUTH = 0x0002; +constexpr static CachedChunkCoord CPOS_EAST = 0x0003; +constexpr static CachedChunkCoord CPOS_WEST = 0x0004; +constexpr static CachedChunkCoord CPOS_TOP = 0x0005; +constexpr static CachedChunkCoord CPOS_BOTTOM = 0x0006; +constexpr static const size_t NUM_CACHED_CPOS = 7; + +static const CachedChunkCoord get_cached_cpos(const chunk_pos& pivot, const chunk_pos& cpos) +{ + static const CachedChunkCoord nx[3] = { CPOS_WEST, 0, CPOS_EAST }; + static const CachedChunkCoord ny[3] = { CPOS_BOTTOM, 0, CPOS_TOP }; + static const CachedChunkCoord nz[3] = { CPOS_NORTH, 0, CPOS_SOUTH }; + + if(pivot != cpos) { + chunk_pos delta = pivot - cpos; + delta[0] = vx::clamp(delta[0], -1, 1); + delta[1] = vx::clamp(delta[1], -1, 1); + delta[2] = vx::clamp(delta[2], -1, 1); + + if(delta[0]) { + return nx[delta[0] + 1]; + } else if(delta[1]) { + return ny[delta[1] + 1]; + } else { + return nz[delta[2] + 1]; + } + } + + return CPOS_ITSELF; +} + +static voxel_facing get_facing(voxel_face face, voxel_type type) +{ + if(type == voxel_type::CROSS) { + switch(face) { + case voxel_face::CROSS_NESW: + return voxel_facing::NESW; + case voxel_face::CROSS_NWSE: + return voxel_facing::NWSE; + default: + return voxel_facing::NORTH; + } + } + + switch(face) { + case voxel_face::CUBE_NORTH: + return voxel_facing::NORTH; + case voxel_face::CUBE_SOUTH: + return voxel_facing::SOUTH; + case voxel_face::CUBE_EAST: + return voxel_facing::EAST; + case voxel_face::CUBE_WEST: + return voxel_facing::WEST; + case voxel_face::CUBE_TOP: + return voxel_facing::UP; + case voxel_face::CUBE_BOTTOM: + return voxel_facing::DOWN; + default: + return voxel_facing::NORTH; + } +} + +class GL_MeshingTask final : public Task { +public: + explicit GL_MeshingTask(entt::entity entity, const chunk_pos& cpos); + virtual ~GL_MeshingTask(void) = default; + virtual void process(void) override; + virtual void finalize(void) override; + +private: + bool vis_test(voxel_id voxel, const VoxelInfo* info, const local_pos& lpos) const; + void push_quad_a(const VoxelInfo* info, const glm::fvec3& pos, const glm::fvec2& size, voxel_face face); + void push_quad_v(const VoxelInfo* info, const glm::fvec3& pos, const glm::fvec2& size, voxel_face face, std::size_t entropy); + void make_cube(voxel_id voxel, const VoxelInfo* info, const local_pos& lpos, voxel_vis vis, std::size_t entropy); + void cache_chunk(const chunk_pos& cpos); + +private: + std::array m_cache; + std::vector m_quads_b; // blending + std::vector m_quads_s; // solid + entt::entity m_entity; + chunk_pos m_cpos; +}; + +GL_MeshingTask::GL_MeshingTask(entt::entity entity, const chunk_pos& cpos) +{ + m_entity = entity; + m_cpos = cpos; + + cache_chunk(m_cpos); + cache_chunk(m_cpos + DIR_NORTH); + cache_chunk(m_cpos + DIR_SOUTH); + cache_chunk(m_cpos + DIR_EAST); + cache_chunk(m_cpos + DIR_WEST); + cache_chunk(m_cpos + DIR_DOWN); + cache_chunk(m_cpos + DIR_UP); +} + +void GL_MeshingTask::process(void) +{ + m_quads_b.resize(voxel_atlas::plane_count()); + m_quads_s.resize(voxel_atlas::plane_count()); + + const auto& voxels = m_cache.at(CPOS_ITSELF); + + for(std::size_t i = 0; i < CHUNK_VOLUME; ++i) { + if(m_status == task_status::CANCELLED) { + m_quads_b.clear(); + m_quads_s.clear(); + return; + } + + const auto voxel = voxels[i]; + const auto lpos = coord::to_local(i); + + const auto info = voxel_registry::find(voxel); + + if(info == nullptr) { + // Either a NULL_VOXEL_ID or something went + // horribly wrong and we don't what this is + continue; + } + + voxel_vis vis = 0; + + if(vis_test(voxel, info, lpos + DIR_NORTH)) { + vis |= VIS_NORTH; + } + + if(vis_test(voxel, info, lpos + DIR_SOUTH)) { + vis |= VIS_SOUTH; + } + + if(vis_test(voxel, info, lpos + DIR_EAST)) { + vis |= VIS_EAST; + } + + if(vis_test(voxel, info, lpos + DIR_WEST)) { + vis |= VIS_WEST; + } + + if(vis_test(voxel, info, lpos + DIR_UP)) { + vis |= VIS_UP; + } + + if(vis_test(voxel, info, lpos + DIR_DOWN)) { + vis |= VIS_DOWN; + } + + const auto vpos = coord::to_voxel(m_cpos, lpos); + const auto entropy_src = vpos[0] * vpos[1] * vpos[2]; + const auto entropy = crc64::get(&entropy_src, sizeof(entropy_src)); + + // FIXME: handle different voxel types + make_cube(voxel, info, lpos, vis, entropy); + } +} + +void GL_MeshingTask::finalize(void) +{ + if(!globals::dimension || !globals::dimension->chunks.valid(m_entity)) { + // We either disconnected or something + // else happened that invalidated the entity + return; + } + + auto& component = globals::dimension->chunks.emplace_or_replace(m_entity); + + const std::size_t plane_count_nb = m_quads_s.size(); + const std::size_t plane_count_b = m_quads_b.size(); + + bool has_no_submeshes_b = true; + bool has_no_submeshes_nb = true; + + component.quad_nb.resize(plane_count_nb); + component.quad_b.resize(plane_count_b); + + for(std::size_t plane = 0; plane < plane_count_nb; ++plane) { + QuadBuilder& builder = m_quads_s[plane]; + ChunkVBO& buffer = component.quad_nb[plane]; + + if(builder.empty()) { + if(buffer.handle) { + glDeleteBuffers(1, &buffer.handle); + buffer.handle = 0; + buffer.size = 0; + } + } else { + if(!buffer.handle) { + glGenBuffers(1, &buffer.handle); + } + + glBindBuffer(GL_ARRAY_BUFFER, buffer.handle); + glBufferData(GL_ARRAY_BUFFER, sizeof(ChunkQuad) * builder.size(), builder.data(), GL_STATIC_DRAW); + buffer.size = builder.size(); + has_no_submeshes_nb = false; + } + } + + for(std::size_t plane = 0; plane < plane_count_b; ++plane) { + QuadBuilder& builder = m_quads_b[plane]; + ChunkVBO& buffer = component.quad_b[plane]; + + if(builder.empty()) { + if(buffer.handle) { + glDeleteBuffers(1, &buffer.handle); + buffer.handle = 0; + buffer.size = 0; + } + } else { + if(!buffer.handle) { + glGenBuffers(1, &buffer.handle); + } + + glBindBuffer(GL_ARRAY_BUFFER, buffer.handle); + glBufferData(GL_ARRAY_BUFFER, sizeof(ChunkQuad) * builder.size(), builder.data(), GL_STATIC_DRAW); + buffer.size = builder.size(); + has_no_submeshes_b = false; + } + } + + if(has_no_submeshes_b && has_no_submeshes_nb) { + globals::dimension->chunks.remove(m_entity); + } +} + +bool GL_MeshingTask::vis_test(voxel_id voxel, const VoxelInfo* info, const local_pos& lpos) const +{ + const auto pvpos = coord::to_voxel(m_cpos, lpos); + const auto pcpos = coord::to_chunk(pvpos); + const auto plpos = coord::to_local(pvpos); + const auto index = coord::to_index(plpos); + + const auto cached_cpos = get_cached_cpos(m_cpos, pcpos); + const auto& voxels = m_cache.at(cached_cpos); + const auto neighbour = voxels[index]; + + bool result; + + if(neighbour == NULL_VOXEL_ID) { + result = true; + } else if(neighbour == voxel) { + result = false; + } else if(auto neighbour_info = voxel_registry::find(neighbour)) { + if(neighbour_info->blending != info->blending) { + // Voxel types that use blending are semi-transparent; + // this means they're rendered using a different setup + // and they must have visible faces with opaque voxels + result = neighbour_info->blending; + } else { + result = false; + } + } else { + result = false; + } + + return result; +} + +void GL_MeshingTask::push_quad_a(const VoxelInfo* info, const glm::fvec3& pos, const glm::fvec2& size, voxel_face face) +{ + const voxel_facing facing = get_facing(face, info->type); + const VoxelTexture& vtex = info->textures[static_cast(face)]; + + if(info->blending) { + m_quads_b[vtex.cached_plane].push_back(make_chunk_quad(pos, size, facing, vtex.cached_offset, vtex.paths.size())); + } else { + m_quads_s[vtex.cached_plane].push_back(make_chunk_quad(pos, size, facing, vtex.cached_offset, vtex.paths.size())); + } +} + +void GL_MeshingTask::push_quad_v(const VoxelInfo* info, const glm::fvec3& pos, const glm::fvec2& size, voxel_face face, std::size_t entropy) +{ + const voxel_facing facing = get_facing(face, info->type); + const VoxelTexture& vtex = info->textures[static_cast(face)]; + const std::size_t entropy_mod = entropy % vtex.paths.size(); + + if(info->blending) { + m_quads_b[vtex.cached_plane].push_back(make_chunk_quad(pos, size, facing, vtex.cached_offset + entropy_mod, 0)); + } else { + m_quads_s[vtex.cached_plane].push_back(make_chunk_quad(pos, size, facing, vtex.cached_offset + entropy_mod, 0)); + } +} + +void GL_MeshingTask::make_cube(voxel_id voxel, const VoxelInfo* info, const local_pos& lpos, voxel_vis vis, std::size_t entropy) +{ + const glm::fvec3 fpos = glm::fvec3(lpos); + const glm::fvec2 fsize = glm::fvec2(1.0f, 1.0f); + + if(info->animated) { + if(vis & VIS_NORTH) { + push_quad_a(info, fpos, fsize, voxel_face::CUBE_NORTH); + } + + if(vis & VIS_SOUTH) { + push_quad_a(info, fpos, fsize, voxel_face::CUBE_SOUTH); + } + + if(vis & VIS_EAST) { + push_quad_a(info, fpos, fsize, voxel_face::CUBE_EAST); + } + + if(vis & VIS_WEST) { + push_quad_a(info, fpos, fsize, voxel_face::CUBE_WEST); + } + + if(vis & VIS_UP) { + push_quad_a(info, fpos, fsize, voxel_face::CUBE_TOP); + } + + if(vis & VIS_DOWN) { + push_quad_a(info, fpos, fsize, voxel_face::CUBE_BOTTOM); + } + } else { + if(vis & VIS_NORTH) { + push_quad_v(info, fpos, fsize, voxel_face::CUBE_NORTH, entropy); + } + + if(vis & VIS_SOUTH) { + push_quad_v(info, fpos, fsize, voxel_face::CUBE_SOUTH, entropy); + } + + if(vis & VIS_EAST) { + push_quad_v(info, fpos, fsize, voxel_face::CUBE_EAST, entropy); + } + + if(vis & VIS_WEST) { + push_quad_v(info, fpos, fsize, voxel_face::CUBE_WEST, entropy); + } + + if(vis & VIS_UP) { + push_quad_v(info, fpos, fsize, voxel_face::CUBE_TOP, entropy); + } + + if(vis & VIS_DOWN) { + push_quad_v(info, fpos, fsize, voxel_face::CUBE_BOTTOM, entropy); + } + } +} + +void GL_MeshingTask::cache_chunk(const chunk_pos& cpos) +{ + const auto index = get_cached_cpos(m_cpos, cpos); + + if(const auto chunk = globals::dimension->find_chunk(cpos)) { + m_cache[index] = chunk->get_voxels(); + return; + } +} + +// Bogus internal flag component +struct NeedsMeshingComponent final {}; + +static void on_chunk_create(const ChunkCreateEvent& event) +{ + const std::array neighbours = { + event.cpos + DIR_NORTH, + event.cpos + DIR_SOUTH, + event.cpos + DIR_EAST, + event.cpos + DIR_WEST, + event.cpos + DIR_UP, + event.cpos + DIR_DOWN, + }; + + globals::dimension->chunks.emplace_or_replace(event.chunk->get_entity()); + + for(const chunk_pos& cpos : neighbours) { + if(const Chunk* chunk = globals::dimension->find_chunk(cpos)) { + globals::dimension->chunks.emplace_or_replace(chunk->get_entity()); + continue; + } + } +} + +static void on_chunk_update(const ChunkUpdateEvent& event) +{ + const std::array neighbours = { + event.cpos + DIR_NORTH, + event.cpos + DIR_SOUTH, + event.cpos + DIR_EAST, + event.cpos + DIR_WEST, + event.cpos + DIR_UP, + event.cpos + DIR_DOWN, + }; + + globals::dimension->chunks.emplace_or_replace(event.chunk->get_entity()); + + for(const chunk_pos& cpos : neighbours) { + if(const Chunk* chunk = globals::dimension->find_chunk(cpos)) { + globals::dimension->chunks.emplace_or_replace(chunk->get_entity()); + continue; + } + } +} + +static void on_voxel_set(const VoxelSetEvent& event) +{ + globals::dimension->chunks.emplace_or_replace(event.chunk->get_entity()); + + std::vector neighbours; + + for(int dim = 0; dim < 3; dim += 1) { + chunk_pos offset = chunk_pos(0, 0, 0); + offset[dim] = 1; + + if(event.lpos[dim] == 0) { + neighbours.push_back(event.cpos - offset); + continue; + } + + if(event.lpos[dim] == (CHUNK_SIZE - 1)) { + neighbours.push_back(event.cpos + offset); + continue; + } + } + + for(const chunk_pos& cpos : neighbours) { + if(const Chunk* chunk = globals::dimension->find_chunk(cpos)) { + globals::dimension->chunks.emplace_or_replace(chunk->get_entity()); + continue; + } + } +} + +void chunk_mesher::init(void) +{ + globals::dispatcher.sink().connect<&on_chunk_create>(); + globals::dispatcher.sink().connect<&on_chunk_update>(); + globals::dispatcher.sink().connect<&on_voxel_set>(); +} + +void chunk_mesher::deinit(void) +{ +} + +void chunk_mesher::update(void) +{ + if(session::is_ingame()) { + const auto group = globals::dimension->chunks.group(entt::get); + for(const auto [entity, chunk] : group.each()) { + globals::dimension->chunks.remove(entity); + threading::submit(entity, chunk.cpos); + } + } +} diff --git a/src/game/client/chunk_mesher.hh b/src/game/client/chunk_mesher.hh new file mode 100644 index 0000000..36580ac --- /dev/null +++ b/src/game/client/chunk_mesher.hh @@ -0,0 +1,19 @@ +#ifndef CLIENT_CHUNK_MESHER_HH +#define CLIENT_CHUNK_MESHER_HH 1 +#pragma once + +#include "client/chunk_vbo.hh" + +struct ChunkMeshComponent final { + std::vector quad_nb; + std::vector quad_b; +}; + +namespace chunk_mesher +{ +void init(void); +void deinit(void); +void update(void); +} // namespace chunk_mesher + +#endif /* CLIENT_CHUNK_MESHER_HH */ diff --git a/src/game/client/chunk_quad.hh b/src/game/client/chunk_quad.hh new file mode 100644 index 0000000..337bb1e --- /dev/null +++ b/src/game/client/chunk_quad.hh @@ -0,0 +1,39 @@ +#ifndef CLIENT_CHUNK_QUAD_HH +#define CLIENT_CHUNK_QUAD_HH 1 +#pragma once + +#include "core/constexpr.hh" + +#include "shared/voxel_registry.hh" + +// [0] XXXXXXXXYYYYYYYYZZZZZZZZWWWWHHHH +// [1] FFFFTTTTTTTTTTTAAAAA------------ +using ChunkQuad = std::array; + +constexpr inline static ChunkQuad make_chunk_quad( + const glm::fvec3& position, const glm::fvec2& size, voxel_facing facing, std::size_t texture, std::size_t frames) +{ + ChunkQuad result = {}; + result[0] = 0x00000000; + result[1] = 0x00000000; + + // [0] XXXXXXXXYYYYYYYYZZZZZZZZ-------- + result[0] |= (0x000000FFU & static_cast(position.x * 16.0f)) << 24U; + result[0] |= (0x000000FFU & static_cast(position.y * 16.0f)) << 16U; + result[0] |= (0x000000FFU & static_cast(position.z * 16.0f)) << 8U; + + // [0] ------------------------WWWWHHHH + result[0] |= (0x0000000FU & static_cast(size.x * 16.0f - 1.0f)) << 4U; + result[0] |= (0x0000000FU & static_cast(size.y * 16.0f - 1.0f)); + + // [1] FFFF---------------------------- + result[1] |= (0x0000000FU & static_cast(facing)) << 28U; + + // [1] ----TTTTTTTTTTTAAAAA------------ + result[1] |= (0x000007FFU & static_cast(texture)) << 17U; + result[1] |= (0x0000001FU & static_cast(frames)) << 12U; + + return result; +} + +#endif /* CLIENT_CHUNK_QUAD_HH */ diff --git a/src/game/client/chunk_renderer.cc b/src/game/client/chunk_renderer.cc new file mode 100644 index 0000000..54239af --- /dev/null +++ b/src/game/client/chunk_renderer.cc @@ -0,0 +1,196 @@ +#include "client/pch.hh" + +#include "client/chunk_renderer.hh" + +#include "core/config.hh" + +#include "shared/chunk.hh" +#include "shared/coord.hh" +#include "shared/dimension.hh" + +#include "client/camera.hh" +#include "client/chunk_mesher.hh" +#include "client/chunk_quad.hh" +#include "client/game.hh" +#include "client/globals.hh" +#include "client/outline.hh" +#include "client/program.hh" +#include "client/settings.hh" +#include "client/skybox.hh" +#include "client/toggles.hh" +#include "client/voxel_anims.hh" +#include "client/voxel_atlas.hh" + +// ONLY TOUCH THESE IF THE RESPECTIVE SHADER +// VARIANT MACRO DECLARATIONS LAYOUT CHANGED AS WELL +constexpr static unsigned int WORLD_CURVATURE = 0U; +constexpr static unsigned int WORLD_FOG = 1U; + +static ConfigBoolean depth_sort_chunks(true); + +static GL_Program quad_program; +static std::size_t u_quad_vproj_matrix; +static std::size_t u_quad_world_position; +static std::size_t u_quad_timings; +static std::size_t u_quad_fog_color; +static std::size_t u_quad_view_distance; +static std::size_t u_quad_textures; +static GLuint quad_vaobj; +static GLuint quad_vbo; + +void chunk_renderer::init(void) +{ + globals::client_config.add_value("chunk_renderer.depth_sort_chunks", depth_sort_chunks); + + settings::add_checkbox(5, depth_sort_chunks, settings_location::VIDEO, "chunk_renderer.depth_sort_chunks", false); + + if(!quad_program.setup("shaders/chunk_quad.vert", "shaders/chunk_quad.frag")) { + spdlog::critical("chunk_renderer: quad_program: setup failed"); + std::terminate(); + } + + u_quad_vproj_matrix = quad_program.add_uniform("u_ViewProjMatrix"); + u_quad_world_position = quad_program.add_uniform("u_WorldPosition"); + u_quad_timings = quad_program.add_uniform("u_Timings"); + u_quad_fog_color = quad_program.add_uniform("u_FogColor"); + u_quad_view_distance = quad_program.add_uniform("u_ViewDistance"); + u_quad_textures = quad_program.add_uniform("u_Textures"); + + const glm::fvec3 vertices[4] = { + glm::fvec3(1.0f, 0.0f, 1.0f), + glm::fvec3(1.0f, 0.0f, 0.0f), + glm::fvec3(0.0f, 0.0f, 1.0f), + glm::fvec3(0.0f, 0.0f, 0.0f), + }; + + glGenVertexArrays(1, &quad_vaobj); + glBindVertexArray(quad_vaobj); + + glGenBuffers(1, &quad_vbo); + glBindBuffer(GL_ARRAY_BUFFER, quad_vbo); + glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); + + glEnableVertexAttribArray(0); + glVertexAttribDivisor(0, 0); + glVertexAttribPointer(0, 3, GL_FLOAT, false, sizeof(glm::fvec3), nullptr); +} + +void chunk_renderer::deinit(void) +{ + glDeleteBuffers(1, &quad_vbo); + glDeleteVertexArrays(1, &quad_vaobj); + quad_program.destroy(); +} + +void chunk_renderer::render(void) +{ + glEnable(GL_DEPTH_TEST); + glDepthFunc(GL_LEQUAL); + glLineWidth(1.0f); + + if(toggles::get(TOGGLE_WIREFRAME)) { + glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); + } else { + glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + } + + quad_program.set_variant_vert(WORLD_CURVATURE, client_game::world_curvature.get_value()); + quad_program.set_variant_vert(WORLD_FOG, client_game::fog_mode.get_value()); + quad_program.set_variant_frag(WORLD_FOG, client_game::fog_mode.get_value()); + + if(!quad_program.update()) { + spdlog::critical("chunk_renderer: quad_program: update failed"); + quad_program.destroy(); + std::terminate(); + } + + GLuint timings[3]; + timings[0] = globals::window_frametime; + timings[1] = globals::window_frametime_avg; + timings[2] = voxel_anims::frame; + + const auto group = globals::dimension->chunks.group(entt::get); + + if(depth_sort_chunks.get_value()) { + // FIXME: speed! sorting every frame doesn't look + // like a good idea. Can we store the group elsewhere and + // still have all the up-to-date chunk things inside? + group.sort([](entt::entity ea, entt::entity eb) { + const auto dir_a = globals::dimension->chunks.get(ea).cpos - camera::position_chunk; + const auto dir_b = globals::dimension->chunks.get(eb).cpos - camera::position_chunk; + + const auto da = dir_a[0] * dir_a[0] + dir_a[1] * dir_a[1] + dir_a[2] * dir_a[2]; + const auto db = dir_b[0] * dir_b[0] + dir_b[1] * dir_b[1] + dir_b[2] * dir_b[2]; + + return da > db; + }); + } + + for(std::size_t plane_id = 0; plane_id < voxel_atlas::plane_count(); ++plane_id) { + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D_ARRAY, voxel_atlas::plane_texture(plane_id)); + + glBindVertexArray(quad_vaobj); + + glUseProgram(quad_program.handle); + glUniformMatrix4fv(quad_program.uniforms[u_quad_vproj_matrix].location, 1, false, glm::value_ptr(camera::matrix)); + glUniform3uiv(quad_program.uniforms[u_quad_timings].location, 1, timings); + glUniform4fv(quad_program.uniforms[u_quad_fog_color].location, 1, glm::value_ptr(skybox::fog_color)); + glUniform1f(quad_program.uniforms[u_quad_view_distance].location, camera::view_distance.get_value() * CHUNK_SIZE); + glUniform1i(quad_program.uniforms[u_quad_textures].location, 0); // GL_TEXTURE0 + + glDisable(GL_BLEND); + + glEnable(GL_CULL_FACE); + glCullFace(GL_BACK); + glFrontFace(GL_CCW); + + for(const auto [entity, chunk, mesh] : group.each()) { + if(plane_id < mesh.quad_nb.size() && mesh.quad_nb[plane_id].handle && mesh.quad_nb[plane_id].size) { + const auto wpos = coord::to_fvec3(chunk.cpos - camera::position_chunk); + glUniform3fv(quad_program.uniforms[u_quad_world_position].location, 1, glm::value_ptr(wpos)); + + glBindBuffer(GL_ARRAY_BUFFER, mesh.quad_nb[plane_id].handle); + + glEnableVertexAttribArray(1); + glVertexAttribDivisor(1, 1); + glVertexAttribIPointer(1, 2, GL_UNSIGNED_INT, sizeof(ChunkQuad), nullptr); + + glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, mesh.quad_nb[plane_id].size); + + globals::num_drawcalls += 1; + globals::num_triangles += 2 * mesh.quad_nb[plane_id].size; + } + } + + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + for(const auto [entity, chunk, mesh] : group.each()) { + if(plane_id < mesh.quad_b.size() && mesh.quad_b[plane_id].handle && mesh.quad_b[plane_id].size) { + const auto wpos = coord::to_fvec3(chunk.cpos - camera::position_chunk); + glUniform3fv(quad_program.uniforms[u_quad_world_position].location, 1, glm::value_ptr(wpos)); + + glBindBuffer(GL_ARRAY_BUFFER, mesh.quad_b[plane_id].handle); + + glEnableVertexAttribArray(1); + glVertexAttribDivisor(1, 1); + glVertexAttribIPointer(1, 2, GL_UNSIGNED_INT, sizeof(ChunkQuad), nullptr); + + glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, mesh.quad_b[plane_id].size); + + globals::num_drawcalls += 1; + globals::num_triangles += 2 * mesh.quad_b[plane_id].size; + } + } + } + + if(toggles::get(TOGGLE_CHUNK_AABB)) { + outline::prepare(); + + for(const auto [entity, chunk, mesh] : group.each()) { + const auto size = glm::fvec3(CHUNK_SIZE, CHUNK_SIZE, CHUNK_SIZE); + outline::cube(chunk.cpos, glm::fvec3(0.0f, 0.0f, 0.0f), size, 1.0f, glm::fvec4(1.0f, 1.0f, 0.0f, 1.0f)); + } + } +} diff --git a/src/game/client/chunk_renderer.hh b/src/game/client/chunk_renderer.hh new file mode 100644 index 0000000..3ebcf76 --- /dev/null +++ b/src/game/client/chunk_renderer.hh @@ -0,0 +1,12 @@ +#ifndef CLIENT_CHUNK_RENDERER_HH +#define CLIENT_CHUNK_RENDERER_HH 1 +#pragma once + +namespace chunk_renderer +{ +void init(void); +void deinit(void); +void render(void); +} // namespace chunk_renderer + +#endif /* CLIENT_CHUNK_RENDERER_HH */ diff --git a/src/game/client/chunk_vbo.hh b/src/game/client/chunk_vbo.hh new file mode 100644 index 0000000..ba27552 --- /dev/null +++ b/src/game/client/chunk_vbo.hh @@ -0,0 +1,23 @@ +#ifndef CLIENT_CHUNK_VBO_HH +#define CLIENT_CHUNK_VBO_HH 1 +#pragma once + +class ChunkVBO final { +public: + std::size_t size; + GLuint handle; + +public: + inline ~ChunkVBO(void) + { + // The ChunkVBO structure is meant to be a part + // of the ChunkMesh component within the EnTT registry; + // When the registry is cleaned or a chunk is removed, components + // are expected to be safely disposed of so we need a destructor; + if(handle) { + glDeleteBuffers(1, &handle); + } + } +}; + +#endif /* CLIENT_CHUNK_VBO_HH */ diff --git a/src/game/client/chunk_visibility.cc b/src/game/client/chunk_visibility.cc new file mode 100644 index 0000000..f832529 --- /dev/null +++ b/src/game/client/chunk_visibility.cc @@ -0,0 +1,87 @@ +#include "client/pch.hh" + +#include "client/chunk_visibility.hh" + +#include "core/config.hh" +#include "core/vectors.hh" + +#include "shared/chunk.hh" +#include "shared/chunk_aabb.hh" +#include "shared/dimension.hh" +#include "shared/protocol.hh" + +#include "client/camera.hh" +#include "client/globals.hh" +#include "client/session.hh" + +// Sending a somewhat large amount of network packets +// can easily overwhelm both client, server and the network +// channel created between the two. To prevent this from happening +// we throttle the client's ever increasing itch for new chunks +constexpr static unsigned int MAX_CHUNKS_REQUESTS_PER_FRAME = 16U; + +static ChunkAABB current_view_box; +static ChunkAABB previous_view_box; +static std::vector requests; + +static void update_requests(void) +{ + requests.clear(); + + for(auto cx = current_view_box.min.x; cx != current_view_box.max.x; cx += 1) + for(auto cy = current_view_box.min.y; cy != current_view_box.max.y; cy += 1) + for(auto cz = current_view_box.min.z; cz != current_view_box.max.z; cz += 1) { + auto cpos = chunk_pos(cx, cy, cz); + + if(!globals::dimension->find_chunk(cpos)) { + requests.push_back(cpos); + } + } + + std::sort(requests.begin(), requests.end(), [](const chunk_pos& cpos_a, const chunk_pos& cpos_b) { + auto da = vx::distance2(cpos_a, camera::position_chunk); + auto db = vx::distance2(cpos_b, camera::position_chunk); + return da > db; + }); +} + +void chunk_visibility::update_late(void) +{ + current_view_box.min = camera::position_chunk - static_cast(camera::view_distance.get_value()); + current_view_box.max = camera::position_chunk + static_cast(camera::view_distance.get_value()); + + if(!session::is_ingame()) { + // This makes sure the previous view box + // is always different from the current one + previous_view_box.min = chunk_pos(INT32_MIN, INT32_MIN, INT32_MIN); + previous_view_box.max = chunk_pos(INT32_MAX, INT32_MAX, INT32_MAX); + return; + } + + if((current_view_box.min != previous_view_box.min) || (current_view_box.max != previous_view_box.max)) { + update_requests(); + } + + for(unsigned int i = 0U; i < MAX_CHUNKS_REQUESTS_PER_FRAME; ++i) { + if(requests.empty()) { + // Done sending requests + break; + } + + protocol::RequestChunk packet; + packet.cpos = requests.back(); + protocol::send(session::peer, protocol::encode(packet)); + + requests.pop_back(); + } + + auto view = globals::dimension->chunks.view(); + + for(const auto [entity, chunk] : view.each()) { + if(!current_view_box.contains(chunk.cpos)) { + globals::dimension->remove_chunk(entity); + } + } + + previous_view_box = current_view_box; +} diff --git a/src/game/client/chunk_visibility.hh b/src/game/client/chunk_visibility.hh new file mode 100644 index 0000000..70352c9 --- /dev/null +++ b/src/game/client/chunk_visibility.hh @@ -0,0 +1,12 @@ +#ifndef CLIENT_CHUNK_VISIBILITY_HH +#define CLIENT_CHUNK_VISIBILITY_HH 1 +#pragma once + +#include "shared/types.hh" + +namespace chunk_visibility +{ +void update_late(void); +} // namespace chunk_visibility + +#endif /* CLIENT_CHUNK_VISIBILITY_HH */ diff --git a/src/game/client/const.hh b/src/game/client/const.hh new file mode 100644 index 0000000..9bd8346 --- /dev/null +++ b/src/game/client/const.hh @@ -0,0 +1,27 @@ +#ifndef CLIENT_CONST_HH +#define CLIENT_CONST_HH 1 +#pragma once + +#include "shared/const.hh" + +// This key is then going to be reserved for only +// the debug toggles and users won't be able to +// use this key for conventional gameplay things +constexpr static int DEBUG_KEY = GLFW_KEY_F3; + +constexpr static int BASE_WIDTH = 320; +constexpr static int BASE_HEIGHT = 240; + +constexpr static int MIN_WIDTH = 2 * BASE_WIDTH; +constexpr static int MIN_HEIGHT = 2 * BASE_HEIGHT; + +constexpr static int DEFAULT_WIDTH = 720; +constexpr static int DEFAULT_HEIGHT = 480; + +static_assert(DEFAULT_WIDTH >= MIN_WIDTH); +static_assert(DEFAULT_HEIGHT >= MIN_HEIGHT); + +constexpr static float MIN_PITCH = 0.0625f; +constexpr static float MAX_PITCH = 10.0f; + +#endif /* CLIENT_CONST_HH */ diff --git a/src/game/client/crosshair.cc b/src/game/client/crosshair.cc new file mode 100644 index 0000000..84a9a73 --- /dev/null +++ b/src/game/client/crosshair.cc @@ -0,0 +1,41 @@ +#include "client/pch.hh" + +#include "client/crosshair.hh" + +#include "core/constexpr.hh" +#include "core/resource.hh" + +#include "client/globals.hh" +#include "client/session.hh" +#include "client/texture_gui.hh" + +static resource_ptr texture; + +void crosshair::init(void) +{ + texture = resource::load( + "textures/gui/hud_crosshair.png", TEXTURE_GUI_LOAD_CLAMP_S | TEXTURE_GUI_LOAD_CLAMP_T | TEXTURE_GUI_LOAD_VFLIP); + + if(texture == nullptr) { + spdlog::critical("crosshair: texture load failed"); + std::terminate(); + } +} + +void crosshair::deinit(void) +{ + texture = nullptr; +} + +void crosshair::layout(void) +{ + auto viewport = ImGui::GetMainViewport(); + auto draw_list = ImGui::GetForegroundDrawList(); + + auto scaled_width = vx::max(texture->size.x, globals::gui_scale * texture->size.x / 2); + auto scaled_height = vx::max(texture->size.y, globals::gui_scale * texture->size.y / 2); + auto start = ImVec2( + static_cast(0.5f * viewport->Size.x) - (scaled_width / 2), static_cast(0.5f * viewport->Size.y) - (scaled_height / 2)); + auto end = ImVec2(start.x + scaled_width, start.y + scaled_height); + draw_list->AddImage(texture->handle, start, end); +} diff --git a/src/game/client/crosshair.hh b/src/game/client/crosshair.hh new file mode 100644 index 0000000..6525792 --- /dev/null +++ b/src/game/client/crosshair.hh @@ -0,0 +1,12 @@ +#ifndef CLIENT_CROSSHAIR_HH +#define CLIENT_CROSSHAIR_HH 1 +#pragma once + +namespace crosshair +{ +void init(void); +void deinit(void); +void layout(void); +} // namespace crosshair + +#endif /* CLIENT_CROSSHAIR_HH */ diff --git a/src/game/client/direct_connection.cc b/src/game/client/direct_connection.cc new file mode 100644 index 0000000..c2efc4e --- /dev/null +++ b/src/game/client/direct_connection.cc @@ -0,0 +1,140 @@ +#include "client/pch.hh" + +#include "client/direct_connection.hh" + +#include "core/config.hh" +#include "core/strtools.hh" + +#include "shared/protocol.hh" + +#include "client/game.hh" +#include "client/glfw.hh" +#include "client/globals.hh" +#include "client/gui_screen.hh" +#include "client/language.hh" +#include "client/session.hh" + +constexpr static ImGuiWindowFlags WINDOW_FLAGS = ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoDecoration; + +static std::string str_title; +static std::string str_connect; +static std::string str_cancel; + +static std::string str_hostname; +static std::string str_password; + +static std::string direct_hostname; +static std::string direct_password; + +static void on_glfw_key(const GlfwKeyEvent& event) +{ + if((event.key == GLFW_KEY_ESCAPE) && (event.action == GLFW_PRESS)) { + if(globals::gui_screen == GUI_DIRECT_CONNECTION) { + globals::gui_screen = GUI_PLAY_MENU; + return; + } + } +} + +static void on_language_set(const LanguageSetEvent& event) +{ + str_title = language::resolve("direct_connection.title"); + str_connect = language::resolve_gui("direct_connection.connect"); + str_cancel = language::resolve_gui("direct_connection.cancel"); + + str_hostname = language::resolve("direct_connection.hostname"); + str_password = language::resolve("direct_connection.password"); +} + +static void connect_to_server(void) +{ + auto parts = strtools::split(direct_hostname, ":"); + std::string parsed_hostname; + std::uint16_t parsed_port; + + if(!parts[0].empty()) { + parsed_hostname = parts[0]; + } else { + parsed_hostname = std::string("localhost"); + } + + if(parts.size() >= 2) { + parsed_port = vx::clamp(strtoul(parts[1].c_str(), nullptr, 10), 1024, UINT16_MAX); + } else { + parsed_port = protocol::PORT; + } + + session::connect(parsed_hostname.c_str(), parsed_port, direct_password.c_str()); +} + +void direct_connection::init(void) +{ + globals::dispatcher.sink().connect<&on_glfw_key>(); + globals::dispatcher.sink().connect<&on_language_set>(); +} + +void direct_connection::layout(void) +{ + auto viewport = ImGui::GetMainViewport(); + auto window_start = ImVec2(0.25f * viewport->Size.x, 0.20f * viewport->Size.y); + auto window_size = ImVec2(0.50f * viewport->Size.x, 0.80f * viewport->Size.y); + + ImGui::SetNextWindowPos(window_start); + ImGui::SetNextWindowSize(window_size); + + if(ImGui::Begin("###UIDirectConnect", nullptr, WINDOW_FLAGS)) { + const float title_width = ImGui::CalcTextSize(str_title.c_str()).x; + ImGui::SetCursorPosX(0.5f * (window_size.x - title_width)); + ImGui::TextUnformatted(str_title.c_str()); + + ImGui::Dummy(ImVec2(0.0f, 16.0f * globals::gui_scale)); + + ImGuiInputTextFlags hostname_flags = ImGuiInputTextFlags_CharsNoBlank; + + if(client_game::streamer_mode.get_value()) { + // Hide server hostname to avoid things like + // followers flooding the server that is streamed online + hostname_flags |= ImGuiInputTextFlags_Password; + } + + auto avail_width = ImGui::GetContentRegionAvail().x; + + ImGui::PushItemWidth(avail_width); + + ImGui::InputText("###UIDirectConnect_hostname", &direct_hostname, hostname_flags); + + if(ImGui::BeginItemTooltip()) { + ImGui::PushTextWrapPos(ImGui::GetFontSize() * 16.0f); + ImGui::TextUnformatted(str_hostname.c_str()); + ImGui::PopTextWrapPos(); + ImGui::EndTooltip(); + } + + ImGui::InputText("###UIDirectConnect_password", &direct_password, ImGuiInputTextFlags_Password); + + if(ImGui::BeginItemTooltip()) { + ImGui::PushTextWrapPos(ImGui::GetFontSize() * 16.0f); + ImGui::TextUnformatted(str_password.c_str()); + ImGui::PopTextWrapPos(); + ImGui::EndTooltip(); + } + + ImGui::PopItemWidth(); + + ImGui::Dummy(ImVec2(0.0f, 4.0f * globals::gui_scale)); + + ImGui::BeginDisabled(strtools::is_whitespace(direct_hostname)); + + if(ImGui::Button(str_connect.c_str(), ImVec2(avail_width, 0.0f))) { + connect_to_server(); + } + + ImGui::EndDisabled(); + + if(ImGui::Button(str_cancel.c_str(), ImVec2(avail_width, 0.0f))) { + globals::gui_screen = GUI_PLAY_MENU; + } + } + + ImGui::End(); +} diff --git a/src/game/client/direct_connection.hh b/src/game/client/direct_connection.hh new file mode 100644 index 0000000..f94bcaf --- /dev/null +++ b/src/game/client/direct_connection.hh @@ -0,0 +1,11 @@ +#ifndef CLIENT_DIRECT_CONNECTION_HH +#define CLIENT_DIRECT_CONNECTION_HH 1 +#pragma once + +namespace direct_connection +{ +void init(void); +void layout(void); +} // namespace direct_connection + +#endif /* CLIENT_DIRECT_CONNECTION_HH */ diff --git a/src/game/client/experiments.cc b/src/game/client/experiments.cc new file mode 100644 index 0000000..70353b5 --- /dev/null +++ b/src/game/client/experiments.cc @@ -0,0 +1,77 @@ +#include "client/pch.hh" + +#include "client/experiments.hh" + +#include "shared/dimension.hh" +#include "shared/game_items.hh" +#include "shared/game_voxels.hh" +#include "shared/item_registry.hh" + +#include "client/chat.hh" +#include "client/glfw.hh" +#include "client/globals.hh" +#include "client/hotbar.hh" +#include "client/player_target.hh" +#include "client/session.hh" +#include "client/status_lines.hh" + +static void on_glfw_mouse_button(const GlfwMouseButtonEvent& event) +{ + if(!globals::gui_screen && session::is_ingame()) { + if((event.action == GLFW_PRESS) && (player_target::voxel != NULL_VOXEL_ID)) { + if(event.button == GLFW_MOUSE_BUTTON_LEFT) { + experiments::attack(); + return; + } + + if(event.button == GLFW_MOUSE_BUTTON_RIGHT) { + experiments::interact(); + return; + } + } + } +} + +void experiments::init(void) +{ + globals::dispatcher.sink().connect<&on_glfw_mouse_button>(); +} + +void experiments::init_late(void) +{ + hotbar::slots[0] = game_items::cobblestone; + hotbar::slots[1] = game_items::stone; + hotbar::slots[2] = game_items::dirt; + hotbar::slots[3] = game_items::grass; + hotbar::slots[4] = game_items::oak_leaves; + hotbar::slots[5] = game_items::oak_planks; + hotbar::slots[6] = game_items::oak_log; + hotbar::slots[7] = game_items::glass; + hotbar::slots[8] = game_items::slime; +} + +void experiments::deinit(void) +{ +} + +void experiments::update(void) +{ +} + +void experiments::update_late(void) +{ +} + +void experiments::attack(void) +{ + globals::dimension->set_voxel(NULL_VOXEL_ID, player_target::coord); +} + +void experiments::interact(void) +{ + if(auto info = item_registry::find(hotbar::slots[hotbar::active_slot])) { + if(info->place_voxel != NULL_VOXEL_ID) { + globals::dimension->set_voxel(info->place_voxel, player_target::coord + player_target::normal); + } + } +} diff --git a/src/game/client/experiments.hh b/src/game/client/experiments.hh new file mode 100644 index 0000000..ae20426 --- /dev/null +++ b/src/game/client/experiments.hh @@ -0,0 +1,20 @@ +#ifndef CLIENT_EXPERIMENTS_HH +#define CLIENT_EXPERIMENTS_HH 1 +#pragma once + +namespace experiments +{ +void init(void); +void init_late(void); +void deinit(void); +void update(void); +void update_late(void); +} // namespace experiments + +namespace experiments +{ +void attack(void); +void interact(void); +} // namespace experiments + +#endif /* CLIENT_EXPERIMENTS_HH */ diff --git a/src/game/client/factory.cc b/src/game/client/factory.cc new file mode 100644 index 0000000..4c1c24e --- /dev/null +++ b/src/game/client/factory.cc @@ -0,0 +1,28 @@ +#include "client/pch.hh" + +#include "client/factory.hh" + +#include "shared/dimension.hh" +#include "shared/factory.hh" +#include "shared/head.hh" +#include "shared/transform.hh" + +#include "client/globals.hh" +#include "client/sound_emitter.hh" + +void client_factory::create_player(Dimension* dimension, entt::entity entity) +{ + shared_factory::create_player(dimension, entity); + + const auto& head = dimension->entities.get(entity); + dimension->entities.emplace_or_replace(entity, head); + dimension->entities.emplace_or_replace(entity, head); + + const auto& transform = dimension->entities.get(entity); + dimension->entities.emplace_or_replace(entity, transform); + dimension->entities.emplace_or_replace(entity, transform); + + if(globals::sound_ctx) { + dimension->entities.emplace_or_replace(entity); + } +} diff --git a/src/game/client/factory.hh b/src/game/client/factory.hh new file mode 100644 index 0000000..6f883c2 --- /dev/null +++ b/src/game/client/factory.hh @@ -0,0 +1,12 @@ +#ifndef CLIENT_FACTORY_HH +#define CLIENT_FACTORY_HH 1 +#pragma once + +class Dimension; + +namespace client_factory +{ +void create_player(Dimension* dimension, entt::entity entity); +} // namespace client_factory + +#endif /* CLIENT_FACTORY_HH */ diff --git a/src/game/client/game.cc b/src/game/client/game.cc new file mode 100644 index 0000000..c930f38 --- /dev/null +++ b/src/game/client/game.cc @@ -0,0 +1,684 @@ +#include "client/pch.hh" + +#include "client/game.hh" + +#include "core/angles.hh" +#include "core/binfile.hh" +#include "core/config.hh" +#include "core/resource.hh" + +#include "shared/collision.hh" +#include "shared/coord.hh" +#include "shared/dimension.hh" +#include "shared/game_items.hh" +#include "shared/game_voxels.hh" +#include "shared/gravity.hh" +#include "shared/head.hh" +#include "shared/item_registry.hh" +#include "shared/player.hh" +#include "shared/protocol.hh" +#include "shared/ray_dda.hh" +#include "shared/stasis.hh" +#include "shared/transform.hh" +#include "shared/velocity.hh" +#include "shared/voxel_registry.hh" + +#include "client/background.hh" +#include "client/bother.hh" +#include "client/camera.hh" +#include "client/chat.hh" +#include "client/chunk_mesher.hh" +#include "client/chunk_renderer.hh" +#include "client/chunk_visibility.hh" +#include "client/const.hh" +#include "client/crosshair.hh" +#include "client/direct_connection.hh" +#include "client/experiments.hh" +#include "client/gamepad.hh" +#include "client/glfw.hh" +#include "client/globals.hh" +#include "client/gui_screen.hh" +#include "client/hotbar.hh" +#include "client/interpolation.hh" +#include "client/keybind.hh" +#include "client/language.hh" +#include "client/listener.hh" +#include "client/main_menu.hh" +#include "client/message_box.hh" +#include "client/metrics.hh" +#include "client/outline.hh" +#include "client/play_menu.hh" +#include "client/player_look.hh" +#include "client/player_move.hh" +#include "client/player_target.hh" +#include "client/progress_bar.hh" +#include "client/receive.hh" +#include "client/scoreboard.hh" +#include "client/screenshot.hh" +#include "client/session.hh" +#include "client/settings.hh" +#include "client/skybox.hh" +#include "client/sound.hh" +#include "client/sound_emitter.hh" +#include "client/splash.hh" +#include "client/status_lines.hh" +#include "client/texture_gui.hh" +#include "client/toggles.hh" +#include "client/voxel_anims.hh" +#include "client/voxel_atlas.hh" +#include "client/voxel_sounds.hh" +#include "client/window_title.hh" + +ConfigBoolean client_game::streamer_mode(false); +ConfigBoolean client_game::vertical_sync(true); +ConfigBoolean client_game::world_curvature(true); +ConfigUnsigned client_game::fog_mode(1U, 0U, 2U); +ConfigString client_game::username("player"); + +bool client_game::hide_hud = false; + +static ConfigKeyBind hide_hud_toggle(GLFW_KEY_F1); + +static resource_ptr bin_unscii16; +static resource_ptr bin_unscii8; + +static void on_glfw_framebuffer_size(const GlfwFramebufferSizeEvent& event) +{ + auto width_float = static_cast(event.size.x); + auto height_float = static_cast(event.size.y); + auto wscale = vx::max(1U, vx::floor(width_float / static_cast(BASE_WIDTH))); + auto hscale = vx::max(1U, vx::floor(height_float / static_cast(BASE_HEIGHT))); + auto scale = vx::min(wscale, hscale); + + if(globals::gui_scale != scale) { + auto& io = ImGui::GetIO(); + auto& style = ImGui::GetStyle(); + + ImFontConfig font_config; + font_config.FontDataOwnedByAtlas = false; + + io.Fonts->Clear(); + + ImFontGlyphRangesBuilder builder; + + // This should cover a hefty range of glyph ranges. + // UNDONE: just slap the whole UNICODE Plane-0 here? + builder.AddRanges(io.Fonts->GetGlyphRangesDefault()); + builder.AddRanges(io.Fonts->GetGlyphRangesCyrillic()); + builder.AddRanges(io.Fonts->GetGlyphRangesGreek()); + builder.AddRanges(io.Fonts->GetGlyphRangesJapanese()); + + ImVector ranges = {}; + builder.BuildRanges(&ranges); + + globals::font_default = io.Fonts->AddFontFromMemoryTTF( + bin_unscii16->buffer, bin_unscii16->size, 16.0f * scale, &font_config, ranges.Data); + globals::font_chat = io.Fonts->AddFontFromMemoryTTF( + bin_unscii16->buffer, bin_unscii16->size, 8.0f * scale, &font_config, ranges.Data); + globals::font_debug = io.Fonts->AddFontFromMemoryTTF(bin_unscii8->buffer, bin_unscii8->size, 4.0f * scale, &font_config); + + // Re-assign the default font + io.FontDefault = globals::font_default; + + // This should be here!!! Just calling Build() + // on the font atlas does not invalidate internal + // device objects defined by the implementation!!! + ImGui_ImplOpenGL3_CreateDeviceObjects(); + + if(globals::gui_scale) { + // Well, ImGuiStyle::ScaleAllSizes indeed takes + // the scale values as a RELATIVE scaling, not as + // absolute. So I have to make a special crutch + style.ScaleAllSizes(static_cast(scale) / static_cast(globals::gui_scale)); + } + + globals::gui_scale = scale; + } + + if(globals::world_fbo) { + glDeleteRenderbuffers(1, &globals::world_fbo_depth); + glDeleteTextures(1, &globals::world_fbo_color); + glDeleteFramebuffers(1, &globals::world_fbo); + } + + glGenFramebuffers(1, &globals::world_fbo); + glGenTextures(1, &globals::world_fbo_color); + glGenRenderbuffers(1, &globals::world_fbo_depth); + + glBindTexture(GL_TEXTURE_2D, globals::world_fbo_color); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, event.size.x, event.size.y, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr); + + glBindRenderbuffer(GL_RENDERBUFFER, globals::world_fbo_depth); + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, event.size.x, event.size.y); + + glBindFramebuffer(GL_FRAMEBUFFER, globals::world_fbo); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, globals::world_fbo_color, 0); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, globals::world_fbo_depth); + + if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { + spdlog::critical("opengl: world framebuffer is incomplete"); + glDeleteRenderbuffers(1, &globals::world_fbo_depth); + glDeleteTextures(1, &globals::world_fbo_color); + glDeleteFramebuffers(1, &globals::world_fbo); + std::terminate(); + } +} + +static void on_glfw_key(const GlfwKeyEvent& event) +{ + if(!globals::gui_keybind_ptr && hide_hud_toggle.equals(event.key) && (event.action == GLFW_PRESS)) { + client_game::hide_hud = !client_game::hide_hud; + } +} + +void client_game::init(void) +{ + bin_unscii16 = resource::load("fonts/unscii-16.ttf"); + bin_unscii8 = resource::load("fonts/unscii-8.ttf"); + + if((bin_unscii16 == nullptr) || (bin_unscii8 == nullptr)) { + spdlog::critical("client_game: font loading failed"); + std::terminate(); + } + + client_splash::init(); + 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); + globals::client_config.add_value("game.key.toggle_hide_hud", hide_hud_toggle); + + 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); + settings::add_keybind(4, hide_hud_toggle, settings_location::KEYBOARD_MISC, "game.key.toggle_hide_hud"); + + globals::client_host = enet_host_create(nullptr, 1, 1, 0, 0); + + if(!globals::client_host) { + spdlog::critical("game: unable to setup an ENet host"); + std::terminate(); + } + + language::init(); + + session::init(); + + player_look::init(); + player_move::init(); + player_target::init(); + + gamepad::init(); + + camera::init(); + + voxel_anims::init(); + + outline::init(); + chunk_mesher::init(); + chunk_renderer::init(); + + globals::world_fbo = 0; + globals::world_fbo_color = 0; + globals::world_fbo_depth = 0; + + voxel_sounds::init(); + + skybox::init(); + + ImGuiStyle& style = ImGui::GetStyle(); + + // Black buttons on a dark background + // may be harder to read than the text on them + style.FrameBorderSize = 1.0; + style.TabBorderSize = 1.0; + + // Rounding on elements looks cool but I am + // aiming for a more or less blocky and + // visually simple HiDPI-friendly UI style + style.TabRounding = 0.0f; + style.GrabRounding = 0.0f; + style.ChildRounding = 0.0f; + style.FrameRounding = 0.0f; + style.PopupRounding = 0.0f; + style.WindowRounding = 0.0f; + style.ScrollbarRounding = 0.0f; + + style.Colors[ImGuiCol_Text] = ImVec4(1.00f, 1.00f, 1.00f, 1.00f); + style.Colors[ImGuiCol_TextDisabled] = ImVec4(0.50f, 0.50f, 0.50f, 1.00f); + style.Colors[ImGuiCol_WindowBg] = ImVec4(0.06f, 0.06f, 0.06f, 0.94f); + style.Colors[ImGuiCol_ChildBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + style.Colors[ImGuiCol_PopupBg] = ImVec4(0.08f, 0.08f, 0.08f, 0.94f); + style.Colors[ImGuiCol_Border] = ImVec4(0.79f, 0.79f, 0.79f, 0.50f); + style.Colors[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + style.Colors[ImGuiCol_FrameBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.54f); + style.Colors[ImGuiCol_FrameBgHovered] = ImVec4(0.36f, 0.36f, 0.36f, 0.40f); + style.Colors[ImGuiCol_FrameBgActive] = ImVec4(0.63f, 0.63f, 0.63f, 0.67f); + style.Colors[ImGuiCol_TitleBg] = ImVec4(0.04f, 0.04f, 0.04f, 1.00f); + style.Colors[ImGuiCol_TitleBgActive] = ImVec4(0.00f, 0.00f, 0.00f, 1.00f); + style.Colors[ImGuiCol_TitleBgCollapsed] = ImVec4(0.00f, 0.00f, 0.00f, 0.51f); + style.Colors[ImGuiCol_MenuBarBg] = ImVec4(0.14f, 0.14f, 0.14f, 1.00f); + style.Colors[ImGuiCol_ScrollbarBg] = ImVec4(0.02f, 0.02f, 0.02f, 0.53f); + style.Colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.00f, 0.00f, 0.00f, 0.75f); + style.Colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.12f, 0.12f, 0.12f, 1.00f); + style.Colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.25f, 0.25f, 0.25f, 1.00f); + style.Colors[ImGuiCol_CheckMark] = ImVec4(1.00f, 1.00f, 1.00f, 1.00f); + style.Colors[ImGuiCol_SliderGrab] = ImVec4(0.81f, 0.81f, 0.81f, 0.75f); + style.Colors[ImGuiCol_SliderGrabActive] = ImVec4(0.00f, 0.00f, 0.00f, 1.00f); + style.Colors[ImGuiCol_Button] = ImVec4(0.00f, 0.00f, 0.00f, 1.00f); + style.Colors[ImGuiCol_ButtonHovered] = ImVec4(0.12f, 0.12f, 0.12f, 1.00f); + style.Colors[ImGuiCol_ButtonActive] = ImVec4(0.25f, 0.25f, 0.25f, 1.00f); + style.Colors[ImGuiCol_Header] = ImVec4(0.00f, 0.00f, 0.00f, 0.75f); + style.Colors[ImGuiCol_HeaderHovered] = ImVec4(0.12f, 0.12f, 0.12f, 1.00f); + style.Colors[ImGuiCol_HeaderActive] = ImVec4(0.25f, 0.25f, 0.25f, 1.00f); + style.Colors[ImGuiCol_Separator] = ImVec4(0.49f, 0.49f, 0.49f, 0.50f); + style.Colors[ImGuiCol_SeparatorHovered] = ImVec4(0.56f, 0.56f, 0.56f, 0.78f); + style.Colors[ImGuiCol_SeparatorActive] = ImVec4(0.90f, 0.90f, 0.90f, 1.00f); + style.Colors[ImGuiCol_ResizeGrip] = ImVec4(0.34f, 0.34f, 0.34f, 0.20f); + style.Colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.57f, 0.57f, 0.57f, 0.67f); + style.Colors[ImGuiCol_ResizeGripActive] = ImVec4(1.00f, 1.00f, 1.00f, 0.95f); + style.Colors[ImGuiCol_Tab] = ImVec4(0.00f, 0.00f, 0.00f, 0.75f); + style.Colors[ImGuiCol_TabHovered] = ImVec4(0.12f, 0.12f, 0.12f, 1.00f); + style.Colors[ImGuiCol_TabActive] = ImVec4(0.25f, 0.25f, 0.25f, 1.00f); + style.Colors[ImGuiCol_TabUnfocused] = ImVec4(0.13f, 0.13f, 0.13f, 0.97f); + style.Colors[ImGuiCol_TabUnfocusedActive] = ImVec4(0.44f, 0.44f, 0.44f, 1.00f); + style.Colors[ImGuiCol_PlotLines] = ImVec4(0.61f, 0.61f, 0.61f, 1.00f); + style.Colors[ImGuiCol_PlotLinesHovered] = ImVec4(0.69f, 0.00f, 0.00f, 1.00f); + style.Colors[ImGuiCol_PlotHistogram] = ImVec4(0.00f, 1.00f, 0.20f, 1.00f); + style.Colors[ImGuiCol_PlotHistogramHovered] = ImVec4(1.00f, 0.60f, 0.00f, 1.00f); + style.Colors[ImGuiCol_TableHeaderBg] = ImVec4(0.19f, 0.19f, 0.20f, 1.00f); + style.Colors[ImGuiCol_TableBorderStrong] = ImVec4(0.31f, 0.31f, 0.35f, 1.00f); + style.Colors[ImGuiCol_TableBorderLight] = ImVec4(0.23f, 0.23f, 0.25f, 1.00f); + style.Colors[ImGuiCol_TableRowBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + style.Colors[ImGuiCol_TableRowBgAlt] = ImVec4(1.00f, 1.00f, 1.00f, 0.06f); + style.Colors[ImGuiCol_TextSelectedBg] = ImVec4(0.61f, 0.61f, 0.61f, 0.35f); + style.Colors[ImGuiCol_DragDropTarget] = ImVec4(1.00f, 1.00f, 0.00f, 1.00f); + style.Colors[ImGuiCol_NavHighlight] = ImVec4(0.50f, 0.50f, 0.50f, 1.00f); + style.Colors[ImGuiCol_NavWindowingHighlight] = ImVec4(1.00f, 1.00f, 1.00f, 0.70f); + style.Colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.80f, 0.80f, 0.80f, 0.20f); + style.Colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.80f, 0.80f, 0.80f, 0.35f); + + // Making my own Game UI for Source Engine + // taught me one important thing: dimensions + // of UI elements must be calculated at semi-runtime + // so there's simply no point for an INI file. + ImGui::GetIO().IniFilename = nullptr; + + toggles::init(); + + background::init(); + + scoreboard::init(); + + client_chat::init(); + + bother::init(); + + main_menu::init(); + play_menu::init(); + progress_bar::init(); + message_box::init(); + direct_connection::init(); + + crosshair::init(); + hotbar::init(); + metrics::init(); + status_lines::init(); + + screenshot::init(); + + globals::gui_keybind_ptr = nullptr; + globals::gui_scale = 0U; + globals::gui_screen = GUI_MAIN_MENU; + + sound::init_config(); + + if(globals::sound_ctx) { + sound::init(); + } + + client_receive::init(); + + experiments::init(); + + globals::dispatcher.sink().connect<&on_glfw_framebuffer_size>(); + globals::dispatcher.sink().connect<&on_glfw_key>(); +} + +void client_game::init_late(void) +{ + toggles::init_late(); + + if(globals::sound_ctx) { + sound::init_late(); + } + + language::init_late(); + + settings::init_late(); + + client_chat::init_late(); + + status_lines::init_late(); + + game_voxels::populate(); + game_items::populate(); + + std::size_t max_texture_count = 0; + + // Figure out the total texture count + // NOTE: this is very debug, early and a quite + // conservative limit choice; there must be a better + // way to make this limit way smaller than it currently is + for(const std::shared_ptr& info : voxel_registry::voxels) { + for(const VoxelTexture& vtex : info->textures) { + max_texture_count += vtex.paths.size(); + } + } + + // UNDONE: asset packs for non-16x16 stuff + voxel_atlas::create(16, 16, max_texture_count); + + for(std::shared_ptr& info : voxel_registry::voxels) { + for(VoxelTexture& vtex : info->textures) { + if(auto strip = voxel_atlas::find_or_load(vtex.paths)) { + vtex.cached_offset = strip->offset; + vtex.cached_plane = strip->plane; + continue; + } + + spdlog::critical("client_gl: {}: failed to load atlas strips", info->name); + std::terminate(); + } + } + + voxel_atlas::generate_mipmaps(); + + for(std::shared_ptr& info : item_registry::items) { + info->cached_texture = resource::load(info->texture.c_str(), TEXTURE_GUI_LOAD_CLAMP_S | TEXTURE_GUI_LOAD_CLAMP_T); + } + + experiments::init_late(); + + client_splash::init_late(); + + window_title::update(); +} + +void client_game::deinit(void) +{ + voxel_sounds::deinit(); + + experiments::deinit(); + + session::deinit(); + + if(globals::sound_ctx) { + sound::deinit(); + } + + hotbar::deinit(); + main_menu::deinit(); + play_menu::deinit(); + + bother::deinit(); + + client_chat::deinit(); + + background::deinit(); + + crosshair::deinit(); + + delete globals::dimension; + globals::player = entt::null; + globals::dimension = nullptr; + + item_registry::purge(); + voxel_registry::purge(); + + voxel_atlas::destroy(); + + glDeleteRenderbuffers(1, &globals::world_fbo_depth); + glDeleteTextures(1, &globals::world_fbo_color); + glDeleteFramebuffers(1, &globals::world_fbo); + + outline::deinit(); + chunk_renderer::deinit(); + chunk_mesher::deinit(); + + enet_host_destroy(globals::client_host); + + bin_unscii8 = nullptr; + bin_unscii16 = nullptr; +} + +void client_game::fixed_update(void) +{ + player_move::fixed_update(); + + // Only update world simulation gamesystems + // if the player can actually observe all the + // changes these gamesystems cause visually + if(session::is_ingame()) { + CollisionComponent::fixed_update(globals::dimension); + VelocityComponent::fixed_update(globals::dimension); + TransformComponent::fixed_update(globals::dimension); + GravityComponent::fixed_update(globals::dimension); + StasisComponent::fixed_update(globals::dimension); + } +} + +void client_game::fixed_update_late(void) +{ + if(session::is_ingame()) { + const auto& head = globals::dimension->entities.get(globals::player); + const auto& transform = globals::dimension->entities.get(globals::player); + const auto& velocity = globals::dimension->entities.get(globals::player); + + protocol::EntityHead head_packet; + head_packet.entity = entt::null; // ignored by server + head_packet.angles = head.angles; + + protocol::EntityTransform transform_packet; + transform_packet.entity = entt::null; // ignored by server + transform_packet.angles = transform.angles; + transform_packet.chunk = transform.chunk; + transform_packet.local = transform.local; + + protocol::EntityVelocity velocity_packet; + velocity_packet.entity = entt::null; // ignored by server + velocity_packet.value = velocity.value; + + protocol::send(session::peer, protocol::encode(head_packet)); + protocol::send(session::peer, protocol::encode(transform_packet)); + protocol::send(session::peer, protocol::encode(velocity_packet)); + } +} + +void client_game::update(void) +{ + if(session::is_ingame()) { + if(toggles::get(TOGGLE_PM_FLIGHT)) { + globals::dimension->entities.remove(globals::player); + } else { + globals::dimension->entities.emplace_or_replace(globals::player); + } + } + + if(globals::sound_ctx) { + sound::update(); + + listener::update(); + + SoundEmitterComponent::update(); + } + + interpolation::update(); + + player_target::update(); + + camera::update(); + + voxel_anims::update(); + + chunk_mesher::update(); + + client_chat::update(); + + experiments::update(); +} + +void client_game::update_late(void) +{ + ENetEvent enet_event; + + while(0 < enet_host_service(globals::client_host, &enet_event, 0)) { + switch(enet_event.type) { + case ENET_EVENT_TYPE_CONNECT: + session::send_login_request(); + break; + case ENET_EVENT_TYPE_DISCONNECT: + session::invalidate(); + break; + case ENET_EVENT_TYPE_RECEIVE: + protocol::decode(globals::dispatcher, enet_event.packet, enet_event.peer); + enet_packet_destroy(enet_event.packet); + break; + } + } + + player_look::update_late(); + player_move::update_late(); + + play_menu::update_late(); + + bother::update_late(); + + experiments::update_late(); + + gamepad::update_late(); + + chunk_visibility::update_late(); + + if(client_game::vertical_sync.get_value()) { + glfwSwapInterval(1); + } else { + glfwSwapInterval(0); + } +} + +void client_game::render(void) +{ + glViewport(0, 0, globals::width, globals::height); + glBindFramebuffer(GL_FRAMEBUFFER, globals::world_fbo); + glClearColor(skybox::fog_color.r, skybox::fog_color.g, skybox::fog_color.b, 1.000f); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + if(globals::dimension) { + chunk_renderer::render(); + } + + glEnable(GL_DEPTH_TEST); + + player_target::render(); + + if(globals::dimension) { + auto group = globals::dimension->entities.group( + entt::get); + + outline::prepare(); + + for(const auto [entity, collision, head, transform] : group.each()) { + if(entity == globals::player) { + // Don't render ourselves + continue; + } + + glm::fvec3 forward; + cxangles::vectors(transform.angles + head.angles, forward); + forward *= 2.0f; + + glm::fvec3 hull_size = collision.aabb.max - collision.aabb.min; + glm::fvec3 hull_fpos = transform.local + collision.aabb.min; + glm::fvec3 look = transform.local + head.offset; + + outline::cube(transform.chunk, hull_fpos, hull_size, 1.0f, glm::fvec4(1.0f, 0.0f, 0.0f, 1.0f)); + outline::line(transform.chunk, look, forward, 1.0f, glm::fvec4(0.9f, 0.9f, 0.9f, 1.0f)); + } + } + + glEnable(GL_DEPTH_TEST); + + glViewport(0, 0, globals::width, globals::height); + glClearColor(0.000f, 0.000f, 0.000f, 1.000f); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glClear(GL_COLOR_BUFFER_BIT); + + glBindFramebuffer(GL_READ_FRAMEBUFFER, globals::world_fbo); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + glBlitFramebuffer(0, 0, globals::width, globals::height, 0, 0, globals::width, globals::height, GL_COLOR_BUFFER_BIT, GL_NEAREST); +} + +void client_game::layout(void) +{ + if(!session::is_ingame()) { + background::layout(); + } + + if(!globals::gui_screen || (globals::gui_screen == GUI_CHAT)) { + if(toggles::get(TOGGLE_METRICS_UI) && !client_game::hide_hud) { + // This contains Minecraft-esque debug information + // about the hardware, world state and other + // things that might be uesful + metrics::layout(); + } + } + + if(session::is_ingame()) { + client_chat::layout(); + scoreboard::layout(); + + if(!globals::gui_screen && !client_game::hide_hud) { + hotbar::layout(); + status_lines::layout(); + crosshair::layout(); + } + } + + if(globals::gui_screen) { + if(session::is_ingame() && (globals::gui_screen != GUI_CHAT)) { + const float width_f = static_cast(globals::width); + const float height_f = static_cast(globals::height); + const ImU32 darken = ImGui::GetColorU32(ImVec4(0.00f, 0.00f, 0.00f, 0.75f)); + ImGui::GetBackgroundDrawList()->AddRectFilled(ImVec2(), ImVec2(width_f, height_f), darken); + } + + switch(globals::gui_screen) { + case GUI_MAIN_MENU: + main_menu::layout(); + break; + case GUI_PLAY_MENU: + play_menu::layout(); + break; + case GUI_SETTINGS: + settings::layout(); + break; + case GUI_PROGRESS_BAR: + progress_bar::layout(); + break; + case GUI_MESSAGE_BOX: + message_box::layout(); + break; + case GUI_DIRECT_CONNECTION: + direct_connection::layout(); + break; + } + } +} diff --git a/src/game/client/game.hh b/src/game/client/game.hh new file mode 100644 index 0000000..f3c6fc4 --- /dev/null +++ b/src/game/client/game.hh @@ -0,0 +1,36 @@ +#ifndef CLIENT_GAME_HH +#define CLIENT_GAME_HH 1 +#pragma once + +class ConfigBoolean; +class ConfigString; +class ConfigUnsigned; + +namespace client_game +{ +extern ConfigBoolean streamer_mode; +extern ConfigBoolean vertical_sync; +extern ConfigBoolean world_curvature; +extern ConfigUnsigned fog_mode; +extern ConfigString username; +} // namespace client_game + +namespace client_game +{ +extern bool hide_hud; +} // namespace client_game + +namespace client_game +{ +void init(void); +void init_late(void); +void deinit(void); +void fixed_update(void); +void fixed_update_late(void); +void update(void); +void update_late(void); +void render(void); +void layout(void); +} // namespace client_game + +#endif /* CLIENT_GAME_HH */ diff --git a/src/game/client/gamepad.cc b/src/game/client/gamepad.cc new file mode 100644 index 0000000..6cbcb3f --- /dev/null +++ b/src/game/client/gamepad.cc @@ -0,0 +1,168 @@ +#include "client/pch.hh" + +#include "client/gamepad.hh" + +#include "core/cmdline.hh" +#include "core/config.hh" +#include "core/constexpr.hh" + +#include "client/glfw.hh" +#include "client/globals.hh" +#include "client/settings.hh" +#include "client/toggles.hh" + +constexpr static int INVALID_GAMEPAD_ID = INT_MAX; +constexpr static std::size_t NUM_AXES = static_cast(GLFW_GAMEPAD_AXIS_LAST + 1); +constexpr static std::size_t NUM_BUTTONS = static_cast(GLFW_GAMEPAD_BUTTON_LAST + 1); +constexpr static float GAMEPAD_AXIS_EVENT_THRESHOLD = 0.5f; + +static int active_gamepad_id; + +bool gamepad::available = false; +ConfigFloat gamepad::deadzone(0.00f, 0.00f, 0.66f); +ConfigBoolean gamepad::active(false); +GLFWgamepadstate gamepad::state; +GLFWgamepadstate gamepad::last_state; + +static void on_toggle_enable(const ToggleEnabledEvent& event) +{ + if(event.type == TOGGLE_USE_GAMEPAD) { + gamepad::active.set_value(true); + return; + } +} + +static void on_toggle_disable(const ToggleDisabledEvent& event) +{ + if(event.type == TOGGLE_USE_GAMEPAD) { + gamepad::active.set_value(false); + return; + } +} + +static void on_glfw_joystick_event(const GlfwJoystickEvent& event) +{ + if((event.event_type == GLFW_CONNECTED) && glfwJoystickIsGamepad(event.joystick_id) && (active_gamepad_id == INVALID_GAMEPAD_ID)) { + gamepad::available = true; + + active_gamepad_id = event.joystick_id; + + for(int i = 0; i < NUM_AXES; gamepad::last_state.axes[i++] = 0.0f) + ; + for(int i = 0; i < NUM_BUTTONS; gamepad::last_state.buttons[i++] = GLFW_RELEASE) + ; + + spdlog::info("gamepad: detected gamepad: {}", glfwGetGamepadName(event.joystick_id)); + + return; + } + + if((event.event_type == GLFW_DISCONNECTED) && (active_gamepad_id == event.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) + ; + for(int i = 0; i < NUM_BUTTONS; gamepad::last_state.buttons[i++] = GLFW_RELEASE) + ; + + spdlog::warn("gamepad: disconnected"); + + return; + } +} + +void gamepad::init(void) +{ + gamepad::available = false; + + active_gamepad_id = INVALID_GAMEPAD_ID; + + globals::client_config.add_value("gamepad.deadzone", gamepad::deadzone); + globals::client_config.add_value("gamepad.active", gamepad::active); + + 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("gpmap", "misc/gamecontrollerdb.txt"); + auto mappings_file = PHYSFS_openRead(mappings_path); + + if(mappings_file) { + 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) { + if(glfwJoystickIsGamepad(joystick)) { + gamepad::available = true; + + active_gamepad_id = joystick; + + for(int i = 0; i < NUM_AXES; gamepad::last_state.axes[i++] = 0.0f) + ; + for(int i = 0; i < NUM_BUTTONS; gamepad::last_state.buttons[i++] = GLFW_RELEASE) + ; + + spdlog::info("gamepad: detected gamepad: {}", glfwGetGamepadName(joystick)); + + break; + } + } + + for(int i = 0; i < NUM_AXES; gamepad::state.axes[i++] = 0.0f) + ; + for(int i = 0; i < NUM_BUTTONS; gamepad::state.buttons[i++] = GLFW_RELEASE) + ; + + globals::dispatcher.sink().connect<&on_toggle_enable>(); + globals::dispatcher.sink().connect<&on_toggle_disable>(); + globals::dispatcher.sink().connect<&on_glfw_joystick_event>(); +} + +void gamepad::update_late(void) +{ + if(active_gamepad_id == INVALID_GAMEPAD_ID) { + // No active gamepad found + return; + } + + if(glfwGetGamepadState(active_gamepad_id, &gamepad::state)) { + for(int i = 0; i < NUM_AXES; ++i) { + if((vx::abs(gamepad::state.axes[i]) > GAMEPAD_AXIS_EVENT_THRESHOLD) + && (vx::abs(gamepad::last_state.axes[i]) <= GAMEPAD_AXIS_EVENT_THRESHOLD)) { + GamepadAxisEvent event; + event.action = GLFW_PRESS; + event.axis = i; + globals::dispatcher.enqueue(event); + continue; + } + + if((vx::abs(gamepad::state.axes[i]) <= GAMEPAD_AXIS_EVENT_THRESHOLD) + && (vx::abs(gamepad::last_state.axes[i]) > GAMEPAD_AXIS_EVENT_THRESHOLD)) { + GamepadAxisEvent event; + event.action = GLFW_RELEASE; + event.axis = i; + globals::dispatcher.enqueue(event); + continue; + } + } + + for(int i = 0; i < NUM_BUTTONS; ++i) { + if(gamepad::state.buttons[i] == gamepad::last_state.buttons[i]) { + // Nothing happens + continue; + } + + GamepadButtonEvent event; + event.action = gamepad::state.buttons[i]; + event.button = i; + globals::dispatcher.enqueue(event); + } + } + + gamepad::last_state = gamepad::state; +} diff --git a/src/game/client/gamepad.hh b/src/game/client/gamepad.hh new file mode 100644 index 0000000..d2483b7 --- /dev/null +++ b/src/game/client/gamepad.hh @@ -0,0 +1,45 @@ +#ifndef CLIENT_GAMEPAD_HH +#define CLIENT_GAMEPAD_HH 1 +#pragma once + +constexpr static int INVALID_GAMEPAD_AXIS = INT_MAX; +constexpr static int INVALID_GAMEPAD_BUTTON = INT_MAX; + +class ConfigBoolean; +class ConfigFloat; + +struct GLFWgamepadstate; + +namespace gamepad +{ +extern bool available; +extern ConfigFloat deadzone; +extern ConfigBoolean active; +extern GLFWgamepadstate state; +extern GLFWgamepadstate last_state; +} // namespace gamepad + +namespace gamepad +{ +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; +}; + +// 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; +}; + +#endif /* CLIENT_GAMEPAD_HH */ diff --git a/src/game/client/gamepad_axis.cc b/src/game/client/gamepad_axis.cc new file mode 100644 index 0000000..546c647 --- /dev/null +++ b/src/game/client/gamepad_axis.cc @@ -0,0 +1,114 @@ +#include "client/pch.hh" + +#include "client/gamepad_axis.hh" + +#include "core/constexpr.hh" + +#include "client/gamepad.hh" + +constexpr static const char* UNKNOWN_AXIS_NAME = "UNKNOWN"; + +static const std::pair axis_names[] = { + { GLFW_GAMEPAD_AXIS_LEFT_X, "LEFT_X" }, + { GLFW_GAMEPAD_AXIS_LEFT_Y, "LEFT_Y" }, + { GLFW_GAMEPAD_AXIS_RIGHT_X, "RIGHT_X" }, + { GLFW_GAMEPAD_AXIS_RIGHT_Y, "RIGHT_Y" }, + { GLFW_GAMEPAD_AXIS_LEFT_TRIGGER, "LEFT_TRIG" }, + { GLFW_GAMEPAD_AXIS_RIGHT_TRIGGER, "RIGHT_TRIG" }, +}; + +static const char* get_axis_name(int axis) +{ + for(const auto& it : axis_names) { + if(it.first != axis) { + continue; + } + + return it.second; + } + + return UNKNOWN_AXIS_NAME; +} + +ConfigGamepadAxis::ConfigGamepadAxis(void) : ConfigGamepadAxis(INVALID_GAMEPAD_AXIS, false) +{ +} + +ConfigGamepadAxis::ConfigGamepadAxis(int axis, bool inverted) +{ + m_inverted = inverted; + m_gamepad_axis = axis; + m_name = get_axis_name(axis); + m_full_string = std::format("{}:{}", m_name, m_inverted ? 1U : 0U); +} + +const char* ConfigGamepadAxis::get(void) const +{ + return m_full_string.c_str(); +} + +void ConfigGamepadAxis::set(const char* value) +{ + char new_name[64]; + unsigned int new_invert; + + if(2 == std::sscanf(value, "%63[^:]:%u", new_name, &new_invert)) { + for(const auto& it : axis_names) { + if(!std::strcmp(it.second, new_name)) { + m_inverted = new_invert; + m_gamepad_axis = it.first; + m_name = get_axis_name(m_gamepad_axis); + m_full_string = std::format("{}:{}", m_name, m_inverted ? 1U : 0U); + return; + } + } + } + + m_inverted = false; + m_gamepad_axis = INVALID_GAMEPAD_AXIS; + m_name = UNKNOWN_AXIS_NAME; + m_full_string = std::format("{}:{}", m_name, m_inverted ? 1U : 0U); +} + +int ConfigGamepadAxis::get_axis(void) const +{ + return m_gamepad_axis; +} + +void ConfigGamepadAxis::set_axis(int axis) +{ + m_gamepad_axis = axis; + m_name = get_axis_name(axis); + m_full_string = std::format("{}:{}", m_name, m_inverted ? 1U : 0U); +} + +bool ConfigGamepadAxis::is_inverted(void) const +{ + return m_inverted; +} + +void ConfigGamepadAxis::set_inverted(bool inverted) +{ + m_inverted = inverted; + m_full_string = std::format("{}:{}", m_name, m_inverted ? 1U : 0U); +} + +float ConfigGamepadAxis::get_value(const GLFWgamepadstate& state, float deadzone) const +{ + if(m_gamepad_axis <= vx::array_size(state.axes)) { + auto value = state.axes[m_gamepad_axis]; + + if(vx::abs(value) > deadzone) { + return m_inverted ? -value : value; + } + + return 0.0f; + } + + return 0.0f; +} + +const char* ConfigGamepadAxis::get_name(void) const +{ + return m_name; +} diff --git a/src/game/client/gamepad_axis.hh b/src/game/client/gamepad_axis.hh new file mode 100644 index 0000000..c0ed6ee --- /dev/null +++ b/src/game/client/gamepad_axis.hh @@ -0,0 +1,39 @@ +#ifndef CLIENT_GAMEPAD_AXIS_HH +#define CLIENT_GAMEPAD_AXIS_HH 1 +#pragma once + +#include "core/config.hh" + +struct GLFWgamepadstate; + +class ConfigGamepadAxis final : public IConfigValue { +public: + explicit ConfigGamepadAxis(void); + explicit ConfigGamepadAxis(int axis, bool inverted); + virtual ~ConfigGamepadAxis(void) = default; + + virtual const char* get(void) const override; + virtual void set(const char* value) override; + + int get_axis(void) const; + void set_axis(int axis); + + bool is_inverted(void) const; + void set_inverted(bool inverted); + + float get_value(const GLFWgamepadstate& state, float deadzone = 0.0f) const; + + // Conventional get/set methods implemented by + // this configuration value actually contain the + // inversion flag. Since we're updating that flag + // in the UI by means of a separate checkbox, we only need the name here + const char* get_name(void) const; + +private: + bool m_inverted; + int m_gamepad_axis; + std::string m_full_string; + const char* m_name; +}; + +#endif /* CLIENT_GAMEPAD_AXIS_HH */ diff --git a/src/game/client/gamepad_button.cc b/src/game/client/gamepad_button.cc new file mode 100644 index 0000000..dd3dca7 --- /dev/null +++ b/src/game/client/gamepad_button.cc @@ -0,0 +1,90 @@ +#include "client/pch.hh" + +#include "client/gamepad_button.hh" + +#include "core/constexpr.hh" + +#include "client/gamepad.hh" + +constexpr static const char* UNKNOWN_BUTTON_NAME = "UNKNOWN"; + +static const std::pair button_names[] = { + { GLFW_GAMEPAD_BUTTON_A, "A" }, + { GLFW_GAMEPAD_BUTTON_B, "B" }, + { GLFW_GAMEPAD_BUTTON_X, "X" }, + { GLFW_GAMEPAD_BUTTON_Y, "Y" }, + { GLFW_GAMEPAD_BUTTON_LEFT_BUMPER, "L_BUMP" }, + { GLFW_GAMEPAD_BUTTON_RIGHT_BUMPER, "R_BUMP" }, + { GLFW_GAMEPAD_BUTTON_BACK, "BACK" }, + { GLFW_GAMEPAD_BUTTON_START, "START" }, + { GLFW_GAMEPAD_BUTTON_GUIDE, "GUIDE" }, + { GLFW_GAMEPAD_BUTTON_LEFT_THUMB, "L_THUMB" }, + { GLFW_GAMEPAD_BUTTON_RIGHT_THUMB, "R_THUMB" }, + { GLFW_GAMEPAD_BUTTON_DPAD_UP, "DPAD_UP" }, + { GLFW_GAMEPAD_BUTTON_DPAD_RIGHT, "DPAD_RIGHT" }, + { GLFW_GAMEPAD_BUTTON_DPAD_DOWN, "DPAD_DOWN" }, + { GLFW_GAMEPAD_BUTTON_DPAD_LEFT, "DPAD_LEFT" }, +}; + +static const char* get_button_name(int button) +{ + for(const auto& it : button_names) { + if(it.first == button) { + return it.second; + } + } + + return UNKNOWN_BUTTON_NAME; +} + +ConfigGamepadButton::ConfigGamepadButton(void) +{ + m_gamepad_button = INVALID_GAMEPAD_BUTTON; + m_name = UNKNOWN_BUTTON_NAME; +} + +ConfigGamepadButton::ConfigGamepadButton(int button) +{ + m_gamepad_button = button; + m_name = get_button_name(button); +} + +const char* ConfigGamepadButton::get(void) const +{ + return m_name; +} + +void ConfigGamepadButton::set(const char* value) +{ + for(const auto& it : button_names) { + if(!std::strcmp(it.second, value)) { + m_gamepad_button = it.first; + m_name = it.second; + return; + } + } + + m_gamepad_button = INVALID_GAMEPAD_BUTTON; + m_name = UNKNOWN_BUTTON_NAME; +} + +int ConfigGamepadButton::get_button(void) const +{ + return m_gamepad_button; +} + +void ConfigGamepadButton::set_button(int button) +{ + m_gamepad_button = button; + m_name = get_button_name(button); +} + +bool ConfigGamepadButton::equals(int button) const +{ + return m_gamepad_button == button; +} + +bool ConfigGamepadButton::is_pressed(const GLFWgamepadstate& state) const +{ + return m_gamepad_button < vx::array_size(state.buttons) && state.buttons[m_gamepad_button] == GLFW_PRESS; +} diff --git a/src/game/client/gamepad_button.hh b/src/game/client/gamepad_button.hh new file mode 100644 index 0000000..04b3a41 --- /dev/null +++ b/src/game/client/gamepad_button.hh @@ -0,0 +1,29 @@ +#ifndef CLIENT_GAMEPAD_BUTTON_HH +#define CLIENT_GAMEPAD_BUTTON_HH 1 +#pragma once + +#include "core/config.hh" + +struct GLFWgamepadstate; + +class ConfigGamepadButton final : public IConfigValue { +public: + explicit ConfigGamepadButton(void); + explicit ConfigGamepadButton(int button); + virtual ~ConfigGamepadButton(void) = default; + + virtual const char* get(void) const override; + virtual void set(const char* value) override; + + int get_button(void) const; + void set_button(int button); + + bool equals(int button) const; + bool is_pressed(const GLFWgamepadstate& state) const; + +private: + int m_gamepad_button; + const char* m_name; +}; + +#endif /* CLIENT_GAMEPAD_BUTTON_HH */ diff --git a/src/game/client/glfw.hh b/src/game/client/glfw.hh new file mode 100644 index 0000000..9cdf734 --- /dev/null +++ b/src/game/client/glfw.hh @@ -0,0 +1,37 @@ +#ifndef CLIENTFW +#define CLIENTFW 1 +#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 /* CLIENTFW */ diff --git a/src/game/client/globals.cc b/src/game/client/globals.cc new file mode 100644 index 0000000..6e00680 --- /dev/null +++ b/src/game/client/globals.cc @@ -0,0 +1,48 @@ +#include "client/pch.hh" + +#include "client/globals.hh" + +#include "core/config.hh" + +#include "client/gui_screen.hh" + +Config globals::client_config; + +GLFWwindow* globals::window; + +float globals::window_frametime; +float globals::window_frametime_avg; +std::uint64_t globals::window_frametime_us; +std::uint64_t globals::window_framecount; + +std::uint64_t globals::fixed_accumulator; + +int globals::width; +int globals::height; +float globals::aspect; + +GLuint globals::world_fbo; +GLuint globals::world_fbo_color; +GLuint globals::world_fbo_depth; + +std::size_t globals::num_drawcalls; +std::size_t globals::num_triangles; + +ENetHost* globals::client_host; + +Dimension* globals::dimension = nullptr; +entt::entity globals::player; + +ImFont* globals::font_debug; +ImFont* globals::font_default; +ImFont* globals::font_chat; + +ConfigKeyBind* globals::gui_keybind_ptr = nullptr; +ConfigGamepadAxis* globals::gui_gamepad_axis_ptr = nullptr; +ConfigGamepadButton* globals::gui_gamepad_button_ptr = nullptr; + +unsigned int globals::gui_scale = 0U; +unsigned int globals::gui_screen = GUI_SCREEN_NONE; + +ALCdevice* globals::sound_dev; +ALCcontext* globals::sound_ctx; diff --git a/src/game/client/globals.hh b/src/game/client/globals.hh new file mode 100644 index 0000000..3fc2223 --- /dev/null +++ b/src/game/client/globals.hh @@ -0,0 +1,66 @@ +#ifndef CLIENTOBALS_HH +#define CLIENTOBALS_HH 1 +#pragma once + +#include "shared/globals.hh" + +class Config; +class ConfigKeyBind; +class ConfigGamepadAxis; +class ConfigGamepadButton; + +struct GLFWwindow; +struct ImFont; + +class Dimension; + +namespace globals +{ +extern Config client_config; + +extern GLFWwindow* window; + +// Some gamesystems that aren't really +// gameplay-oriented might still use client +// framerate to interpolate discrete things +// so it's still a good idea to keep these available +extern float window_frametime; +extern float window_frametime_avg; +extern std::uint64_t window_frametime_us; +extern std::uint64_t window_framecount; + +// https://gafferongames.com/post/fix_your_timestep/ +extern std::uint64_t fixed_accumulator; + +extern int width; +extern int height; +extern float aspect; + +extern GLuint world_fbo; +extern GLuint world_fbo_color; +extern GLuint world_fbo_depth; + +extern std::size_t num_drawcalls; +extern std::size_t num_triangles; + +extern ENetHost* client_host; + +extern Dimension* dimension; +extern entt::entity player; + +extern ImFont* font_debug; +extern ImFont* font_default; +extern ImFont* font_chat; + +extern ConfigKeyBind* gui_keybind_ptr; +extern ConfigGamepadAxis* gui_gamepad_axis_ptr; +extern ConfigGamepadButton* gui_gamepad_button_ptr; + +extern unsigned int gui_scale; +extern unsigned int gui_screen; + +extern ALCdevice* sound_dev; +extern ALCcontext* sound_ctx; +} // namespace globals + +#endif /* CLIENTOBALS_HH */ diff --git a/src/game/client/gui_screen.hh b/src/game/client/gui_screen.hh new file mode 100644 index 0000000..edad116 --- /dev/null +++ b/src/game/client/gui_screen.hh @@ -0,0 +1,14 @@ +#ifndef CLIENT_GUI_SCREEN_HH +#define CLIENT_GUI_SCREEN_HH 1 +#pragma once + +constexpr static unsigned int GUI_SCREEN_NONE = 0x0000U; +constexpr static unsigned int GUI_MAIN_MENU = 0x0001U; +constexpr static unsigned int GUI_PLAY_MENU = 0x0002U; +constexpr static unsigned int GUI_SETTINGS = 0x0003U; +constexpr static unsigned int GUI_PROGRESS_BAR = 0x0004U; +constexpr static unsigned int GUI_MESSAGE_BOX = 0x0005U; +constexpr static unsigned int GUI_CHAT = 0x0006U; +constexpr static unsigned int GUI_DIRECT_CONNECTION = 0x0007U; + +#endif /* CLIENT_GUI_SCREEN_HH */ diff --git a/src/game/client/hotbar.cc b/src/game/client/hotbar.cc new file mode 100644 index 0000000..a2ed859 --- /dev/null +++ b/src/game/client/hotbar.cc @@ -0,0 +1,178 @@ +#include "client/pch.hh" + +#include "client/hotbar.hh" + +#include "core/config.hh" +#include "core/resource.hh" + +#include "shared/item_registry.hh" + +#include "client/glfw.hh" +#include "client/globals.hh" +#include "client/keybind.hh" +#include "client/settings.hh" +#include "client/status_lines.hh" +#include "client/texture_gui.hh" + +constexpr static float ITEM_SIZE = 20.0f; +constexpr static float ITEM_PADDING = 2.0f; +constexpr static float SELECTOR_PADDING = 1.0f; +constexpr static float HOTBAR_PADDING = 2.0f; + +unsigned int hotbar::active_slot = 0U; +item_id hotbar::slots[HOTBAR_SIZE]; + +static ConfigKeyBind hotbar_keys[HOTBAR_SIZE]; + +static resource_ptr hotbar_background; +static resource_ptr hotbar_selector; + +static ImU32 get_color_alpha(ImGuiCol style_color, float alpha) +{ + const auto& color = ImGui::GetStyleColorVec4(style_color); + return ImGui::GetColorU32(ImVec4(color.x, color.y, color.z, alpha)); +} + +static void update_hotbar_item(void) +{ + if(hotbar::slots[hotbar::active_slot] == NULL_ITEM_ID) { + status_lines::unset(STATUS_HOTBAR); + return; + } + + if(auto info = item_registry::find(hotbar::slots[hotbar::active_slot])) { + status_lines::set(STATUS_HOTBAR, info->name, ImVec4(1.0f, 1.0f, 1.0f, 1.0f), 5.0f); + return; + } +} + +static void on_glfw_key(const GlfwKeyEvent& event) +{ + if((event.action == GLFW_PRESS) && !globals::gui_screen) { + for(unsigned int i = 0U; i < HOTBAR_SIZE; ++i) { + if(hotbar_keys[i].equals(event.key)) { + hotbar::active_slot = i; + update_hotbar_item(); + break; + } + } + } +} + +static void on_glfw_scroll(const GlfwScrollEvent& event) +{ + if(!globals::gui_screen) { + if(event.dy < 0.0) { + hotbar::next_slot(); + return; + } + + if(event.dy > 0.0) { + hotbar::prev_slot(); + return; + } + } +} + +void hotbar::init(void) +{ + hotbar_keys[0].set_key(GLFW_KEY_1); + hotbar_keys[1].set_key(GLFW_KEY_2); + hotbar_keys[2].set_key(GLFW_KEY_3); + hotbar_keys[3].set_key(GLFW_KEY_4); + hotbar_keys[4].set_key(GLFW_KEY_5); + hotbar_keys[5].set_key(GLFW_KEY_6); + hotbar_keys[6].set_key(GLFW_KEY_7); + hotbar_keys[7].set_key(GLFW_KEY_8); + hotbar_keys[8].set_key(GLFW_KEY_9); + + globals::client_config.add_value("hotbar.key.0", hotbar_keys[0]); + globals::client_config.add_value("hotbar.key.1", hotbar_keys[1]); + globals::client_config.add_value("hotbar.key.3", hotbar_keys[2]); + globals::client_config.add_value("hotbar.key.4", hotbar_keys[3]); + globals::client_config.add_value("hotbar.key.5", hotbar_keys[4]); + globals::client_config.add_value("hotbar.key.6", hotbar_keys[5]); + globals::client_config.add_value("hotbar.key.7", hotbar_keys[6]); + globals::client_config.add_value("hotbar.key.8", hotbar_keys[7]); + globals::client_config.add_value("hotbar.key.9", hotbar_keys[8]); + + settings::add_keybind(10, hotbar_keys[0], settings_location::KEYBOARD_GAMEPLAY, "hotbar.slot.0"); + settings::add_keybind(11, hotbar_keys[1], settings_location::KEYBOARD_GAMEPLAY, "hotbar.slot.1"); + settings::add_keybind(12, hotbar_keys[2], settings_location::KEYBOARD_GAMEPLAY, "hotbar.slot.2"); + settings::add_keybind(13, hotbar_keys[3], settings_location::KEYBOARD_GAMEPLAY, "hotbar.slot.3"); + settings::add_keybind(14, hotbar_keys[4], settings_location::KEYBOARD_GAMEPLAY, "hotbar.slot.4"); + settings::add_keybind(15, hotbar_keys[5], settings_location::KEYBOARD_GAMEPLAY, "hotbar.slot.5"); + settings::add_keybind(16, hotbar_keys[6], settings_location::KEYBOARD_GAMEPLAY, "hotbar.slot.6"); + settings::add_keybind(17, hotbar_keys[7], settings_location::KEYBOARD_GAMEPLAY, "hotbar.slot.7"); + settings::add_keybind(18, hotbar_keys[8], settings_location::KEYBOARD_GAMEPLAY, "hotbar.slot.8"); + + hotbar_background = resource::load("textures/gui/hud_hotbar.png", TEXTURE_GUI_LOAD_CLAMP_S | TEXTURE_GUI_LOAD_CLAMP_T); + hotbar_selector = resource::load("textures/gui/hud_selector.png", TEXTURE_GUI_LOAD_CLAMP_S | TEXTURE_GUI_LOAD_CLAMP_T); + + globals::dispatcher.sink().connect<&on_glfw_key>(); + globals::dispatcher.sink().connect<&on_glfw_scroll>(); +} + +void hotbar::deinit(void) +{ + hotbar_background = nullptr; + hotbar_selector = nullptr; +} + +void hotbar::layout(void) +{ + auto& style = ImGui::GetStyle(); + + auto item_size = ITEM_SIZE * globals::gui_scale; + auto hotbar_width = HOTBAR_SIZE * item_size; + auto hotbar_padding = HOTBAR_PADDING * globals::gui_scale; + + auto viewport = ImGui::GetMainViewport(); + auto draw_list = ImGui::GetForegroundDrawList(); + + // Draw the hotbar background image + auto background_start = ImVec2(0.5f * viewport->Size.x - 0.5f * hotbar_width, viewport->Size.y - item_size - hotbar_padding); + auto background_end = ImVec2(background_start.x + hotbar_width, background_start.y + item_size); + draw_list->AddImage(hotbar_background->handle, background_start, background_end); + + // Draw the hotbar selector image + auto selector_padding_a = SELECTOR_PADDING * globals::gui_scale; + auto selector_padding_b = SELECTOR_PADDING * globals::gui_scale * 2.0f; + auto selector_start = ImVec2( + background_start.x + hotbar::active_slot * item_size - selector_padding_a, background_start.y - selector_padding_a); + auto selector_end = ImVec2(selector_start.x + item_size + selector_padding_b, selector_start.y + item_size + selector_padding_b); + draw_list->AddImage(hotbar_selector->handle, selector_start, selector_end); + + // Figure out item texture padding values + auto item_padding_a = ITEM_PADDING * globals::gui_scale; + auto item_padding_b = ITEM_PADDING * globals::gui_scale * 2.0f; + + // Draw individual item textures in the hotbar + for(std::size_t i = 0; i < HOTBAR_SIZE; ++i) { + const auto info = item_registry::find(hotbar::slots[i]); + + if((info == nullptr) || (info->cached_texture == nullptr)) { + // There's either no item in the slot + // or the item doesn't have a texture + continue; + } + + const auto item_start = ImVec2(background_start.x + i * item_size + item_padding_a, background_start.y + item_padding_a); + const auto item_end = ImVec2(item_start.x + item_size - item_padding_b, item_start.y + item_size - item_padding_b); + draw_list->AddImage(info->cached_texture->handle, item_start, item_end); + } +} + +void hotbar::next_slot(void) +{ + hotbar::active_slot += 1U; + hotbar::active_slot %= HOTBAR_SIZE; + update_hotbar_item(); +} + +void hotbar::prev_slot(void) +{ + hotbar::active_slot += HOTBAR_SIZE - 1U; + hotbar::active_slot %= HOTBAR_SIZE; + update_hotbar_item(); +} diff --git a/src/game/client/hotbar.hh b/src/game/client/hotbar.hh new file mode 100644 index 0000000..318c631 --- /dev/null +++ b/src/game/client/hotbar.hh @@ -0,0 +1,31 @@ +#ifndef CLIENT_HOTBAR_HH +#define CLIENT_HOTBAR_HH 1 +#pragma once + +#include "shared/types.hh" + +// TODO: design an inventory system and an item +// registry and integrate the hotbar into that system + +constexpr static unsigned int HOTBAR_SIZE = 9U; + +namespace hotbar +{ +extern unsigned int active_slot; +extern item_id slots[HOTBAR_SIZE]; +} // namespace hotbar + +namespace hotbar +{ +void init(void); +void deinit(void); +void layout(void); +} // namespace hotbar + +namespace hotbar +{ +void next_slot(void); +void prev_slot(void); +} // namespace hotbar + +#endif /* CLIENT_HOTBAR_HH */ diff --git a/src/game/client/imdraw_ext.cc b/src/game/client/imdraw_ext.cc new file mode 100644 index 0000000..67df3c8 --- /dev/null +++ b/src/game/client/imdraw_ext.cc @@ -0,0 +1,13 @@ +#include "client/pch.hh" + +#include "client/imdraw_ext.hh" + +#include "client/globals.hh" + +void imdraw_ext::text_shadow( + const std::string& text, const ImVec2& position, ImU32 text_color, ImU32 shadow_color, ImFont* font, ImDrawList* draw_list) +{ + const auto shadow_position = ImVec2(position.x + 0.5f * globals::gui_scale, position.y + 0.5f * globals::gui_scale); + draw_list->AddText(font, font->FontSize, shadow_position, shadow_color, text.c_str(), text.c_str() + text.size()); + draw_list->AddText(font, font->FontSize, position, text_color, text.c_str(), text.c_str() + text.size()); +} diff --git a/src/game/client/imdraw_ext.hh b/src/game/client/imdraw_ext.hh new file mode 100644 index 0000000..0a84e69 --- /dev/null +++ b/src/game/client/imdraw_ext.hh @@ -0,0 +1,11 @@ +#ifndef CLIENT_IMDRAW_EXT_HH +#define CLIENT_IMDRAW_EXT_HH 1 +#pragma once + +namespace imdraw_ext +{ +void text_shadow( + const std::string& text, const ImVec2& position, ImU32 text_color, ImU32 shadow_color, ImFont* font, ImDrawList* draw_list); +} // namespace imdraw_ext + +#endif /* CLIENT_IMDRAW_EXT_HH */ diff --git a/src/game/client/interpolation.cc b/src/game/client/interpolation.cc new file mode 100644 index 0000000..27b6dfd --- /dev/null +++ b/src/game/client/interpolation.cc @@ -0,0 +1,61 @@ +#include "client/pch.hh" + +#include "client/interpolation.hh" + +#include "core/constexpr.hh" + +#include "shared/coord.hh" +#include "shared/dimension.hh" +#include "shared/head.hh" +#include "shared/transform.hh" + +#include "client/globals.hh" + +static void transform_interpolate(float alpha) +{ + auto group = globals::dimension->entities.group(entt::get); + + for(auto [entity, interp, current, previous] : group.each()) { + interp.angles[0] = vx::lerp(previous.angles[0], current.angles[0], alpha); + interp.angles[1] = vx::lerp(previous.angles[1], current.angles[1], alpha); + interp.angles[2] = vx::lerp(previous.angles[2], current.angles[2], alpha); + + // Figure out previous chunk-local floating-point coordinates transformed + // to the current WorldCoord's chunk domain coordinates; we're interpolating + // against these instead of using previous.position.local to prevent jittering + auto previous_shift = coord::to_relative(current.chunk, current.local, previous.chunk, previous.local); + auto previous_local = current.local + previous_shift; + + interp.chunk.x = current.chunk.x; + interp.chunk.y = current.chunk.y; + interp.chunk.z = current.chunk.z; + + interp.local.x = vx::lerp(previous_local.x, current.local.x, alpha); + interp.local.y = vx::lerp(previous_local.y, current.local.y, alpha); + interp.local.z = vx::lerp(previous_local.z, current.local.z, alpha); + } +} + +static void head_interpolate(float alpha) +{ + auto group = globals::dimension->entities.group(entt::get); + + for(auto [entity, interp, current, previous] : group.each()) { + interp.angles[0] = vx::lerp(previous.angles[0], current.angles[0], alpha); + interp.angles[1] = vx::lerp(previous.angles[1], current.angles[1], alpha); + interp.angles[2] = vx::lerp(previous.angles[2], current.angles[2], alpha); + + interp.offset.x = vx::lerp(previous.offset.x, current.offset.x, alpha); + interp.offset.y = vx::lerp(previous.offset.y, current.offset.y, alpha); + interp.offset.z = vx::lerp(previous.offset.z, current.offset.z, alpha); + } +} + +void interpolation::update(void) +{ + if(globals::dimension) { + auto alpha = static_cast(globals::fixed_accumulator) / static_cast(globals::fixed_frametime_us); + transform_interpolate(alpha); + head_interpolate(alpha); + } +} \ No newline at end of file diff --git a/src/game/client/interpolation.hh b/src/game/client/interpolation.hh new file mode 100644 index 0000000..3565a26 --- /dev/null +++ b/src/game/client/interpolation.hh @@ -0,0 +1,10 @@ +#ifndef CLIENT_INTERPOLATION_HH +#define CLIENT_INTERPOLATION_HH 1 +#pragma once + +namespace interpolation +{ +void update(void); +} // namespace interpolation + +#endif /* CLIENT_INTERPOLATION_HH */ diff --git a/src/game/client/keybind.cc b/src/game/client/keybind.cc new file mode 100644 index 0000000..d47397d --- /dev/null +++ b/src/game/client/keybind.cc @@ -0,0 +1,200 @@ +#include "client/pch.hh" + +#include "client/keybind.hh" + +#include "core/constexpr.hh" + +#include "client/const.hh" + +constexpr static const char* UNKNOWN_KEY_NAME = "UNKNOWN"; + +static const std::pair key_names[] = { + { GLFW_KEY_SPACE, "SPACE" }, + { GLFW_KEY_APOSTROPHE, "'" }, + { GLFW_KEY_COMMA, "," }, + { GLFW_KEY_MINUS, "-" }, + { GLFW_KEY_PERIOD, "." }, + { GLFW_KEY_SLASH, "/" }, + { GLFW_KEY_0, "0" }, + { GLFW_KEY_1, "1" }, + { GLFW_KEY_2, "2" }, + { GLFW_KEY_3, "3" }, + { GLFW_KEY_4, "4" }, + { GLFW_KEY_5, "5" }, + { GLFW_KEY_6, "6" }, + { GLFW_KEY_7, "7" }, + { GLFW_KEY_8, "8" }, + { GLFW_KEY_9, "9" }, + { GLFW_KEY_SEMICOLON, ";" }, + { GLFW_KEY_EQUAL, "=" }, + { GLFW_KEY_A, "A" }, + { GLFW_KEY_B, "B" }, + { GLFW_KEY_C, "C" }, + { GLFW_KEY_D, "D" }, + { GLFW_KEY_E, "E" }, + { GLFW_KEY_F, "F" }, + { GLFW_KEY_G, "G" }, + { GLFW_KEY_H, "H" }, + { GLFW_KEY_I, "I" }, + { GLFW_KEY_J, "J" }, + { GLFW_KEY_K, "K" }, + { GLFW_KEY_L, "L" }, + { GLFW_KEY_M, "M" }, + { GLFW_KEY_N, "N" }, + { GLFW_KEY_O, "O" }, + { GLFW_KEY_P, "P" }, + { GLFW_KEY_Q, "Q" }, + { GLFW_KEY_R, "R" }, + { GLFW_KEY_S, "S" }, + { GLFW_KEY_T, "T" }, + { GLFW_KEY_U, "U" }, + { GLFW_KEY_V, "V" }, + { GLFW_KEY_W, "W" }, + { GLFW_KEY_X, "X" }, + { GLFW_KEY_Y, "Y" }, + { GLFW_KEY_Z, "Z" }, + { GLFW_KEY_LEFT_BRACKET, "[" }, + { GLFW_KEY_BACKSLASH, "\\" }, + { GLFW_KEY_RIGHT_BRACKET, "]" }, + { GLFW_KEY_GRAVE_ACCENT, "`" }, + { GLFW_KEY_WORLD_1, "WORLD_1" }, + { GLFW_KEY_WORLD_2, "WORLD_2" }, + { GLFW_KEY_ESCAPE, "ESCAPE" }, + { GLFW_KEY_ENTER, "ENTER" }, + { GLFW_KEY_TAB, "TAB" }, + { GLFW_KEY_BACKSPACE, "BACKSPACE" }, + { GLFW_KEY_INSERT, "INSERT" }, + { GLFW_KEY_DELETE, "DELETE" }, + { GLFW_KEY_RIGHT, "RIGHT" }, + { GLFW_KEY_LEFT, "LEFT" }, + { GLFW_KEY_DOWN, "DOWN" }, + { GLFW_KEY_UP, "UP" }, + { GLFW_KEY_PAGE_UP, "PAGE_UP" }, + { GLFW_KEY_PAGE_DOWN, "PAGE_DOWN" }, + { GLFW_KEY_HOME, "HOME" }, + { GLFW_KEY_END, "END" }, + { GLFW_KEY_CAPS_LOCK, "CAPS_LOCK" }, + { GLFW_KEY_SCROLL_LOCK, "SCROLL_LOCK" }, + { GLFW_KEY_NUM_LOCK, "NUM_LOCK" }, + { GLFW_KEY_PRINT_SCREEN, "PRINT_SCREEN" }, + { GLFW_KEY_PAUSE, "PAUSE" }, + { GLFW_KEY_F1, "F1" }, + { GLFW_KEY_F2, "F2" }, + { GLFW_KEY_F3, "F3" }, + { GLFW_KEY_F4, "F4" }, + { GLFW_KEY_F5, "F5" }, + { GLFW_KEY_F6, "F6" }, + { GLFW_KEY_F7, "F7" }, + { GLFW_KEY_F8, "F8" }, + { GLFW_KEY_F9, "F9" }, + { GLFW_KEY_F10, "F10" }, + { GLFW_KEY_F11, "F11" }, + { GLFW_KEY_F12, "F12" }, + { GLFW_KEY_F13, "F13" }, + { GLFW_KEY_F14, "F14" }, + { GLFW_KEY_F15, "F15" }, + { GLFW_KEY_F16, "F16" }, + { GLFW_KEY_F17, "F17" }, + { GLFW_KEY_F18, "F18" }, + { GLFW_KEY_F19, "F19" }, + { GLFW_KEY_F20, "F20" }, + { GLFW_KEY_F21, "F21" }, + { GLFW_KEY_F22, "F22" }, + { GLFW_KEY_F23, "F23" }, + { GLFW_KEY_F24, "F24" }, + { GLFW_KEY_F25, "F25" }, + { GLFW_KEY_KP_0, "KEYPAD_0" }, + { GLFW_KEY_KP_1, "KEYPAD_1" }, + { GLFW_KEY_KP_2, "KEYPAD_2" }, + { GLFW_KEY_KP_3, "KEYPAD_3" }, + { GLFW_KEY_KP_4, "KEYPAD_4" }, + { GLFW_KEY_KP_5, "KEYPAD_5" }, + { GLFW_KEY_KP_6, "KEYPAD_6" }, + { GLFW_KEY_KP_7, "KEYPAD_7" }, + { GLFW_KEY_KP_8, "KEYPAD_8" }, + { GLFW_KEY_KP_9, "KEYPAD_9" }, + { GLFW_KEY_KP_DECIMAL, "KEYPAD_POINT" }, + { GLFW_KEY_KP_DIVIDE, "KEYPAD_DIV" }, + { GLFW_KEY_KP_MULTIPLY, "KEYPAD_MUL" }, + { GLFW_KEY_KP_SUBTRACT, "KEYPAD_MINUS" }, + { GLFW_KEY_KP_ADD, "KEYPAD_PLUS" }, + { GLFW_KEY_KP_ENTER, "KEYPAD_ENTER" }, + { GLFW_KEY_KP_EQUAL, "KEYPAD_EQUAL" }, + { GLFW_KEY_LEFT_SHIFT, "LEFT_SHIFT" }, + { GLFW_KEY_LEFT_CONTROL, "LEFT_CTRL" }, + { GLFW_KEY_LEFT_ALT, "LEFT_ALT" }, + { GLFW_KEY_LEFT_SUPER, "LEFT_SUPER" }, + { GLFW_KEY_RIGHT_SHIFT, "RIGHT_SHIFT" }, + { GLFW_KEY_RIGHT_CONTROL, "RIGHT_CTRL" }, + { GLFW_KEY_RIGHT_ALT, "RIGHT_ALT" }, + { GLFW_KEY_RIGHT_SUPER, "RIGHT_SUPER" }, + { GLFW_KEY_MENU, "MENU" }, +}; + +static const char* get_key_name(int keycode) +{ + for(const auto& it : key_names) { + if(it.first == keycode) { + return it.second; + } + } + + return UNKNOWN_KEY_NAME; +} + +ConfigKeyBind::ConfigKeyBind(void) +{ + m_glfw_keycode = GLFW_KEY_UNKNOWN; + m_name = UNKNOWN_KEY_NAME; +} + +ConfigKeyBind::ConfigKeyBind(int default_value) +{ + if(default_value == DEBUG_KEY) { + m_glfw_keycode = GLFW_KEY_UNKNOWN; + m_name = UNKNOWN_KEY_NAME; + } else { + m_glfw_keycode = default_value; + m_name = get_key_name(default_value); + } +} + +void ConfigKeyBind::set(const char* value) +{ + for(const auto& it : key_names) { + if((it.first != DEBUG_KEY) && !std::strcmp(it.second, value)) { + m_glfw_keycode = it.first; + m_name = it.second; + return; + } + } + + m_glfw_keycode = GLFW_KEY_UNKNOWN; + m_name = UNKNOWN_KEY_NAME; +} + +const char* ConfigKeyBind::get(void) const +{ + return m_name; +} + +void ConfigKeyBind::set_key(int keycode) +{ + if(keycode == DEBUG_KEY) { + m_glfw_keycode = GLFW_KEY_UNKNOWN; + m_name = UNKNOWN_KEY_NAME; + } else { + m_glfw_keycode = keycode; + m_name = get_key_name(keycode); + } +} + +int ConfigKeyBind::get_key(void) const +{ + return m_glfw_keycode; +} + +bool ConfigKeyBind::equals(int keycode) const +{ + return m_glfw_keycode == keycode; +} diff --git a/src/game/client/keybind.hh b/src/game/client/keybind.hh new file mode 100644 index 0000000..8cf3c3c --- /dev/null +++ b/src/game/client/keybind.hh @@ -0,0 +1,26 @@ +#ifndef CLIENT_KEYBIND_HH +#define CLIENT_KEYBIND_HH 1 +#pragma once + +#include "core/config.hh" + +class ConfigKeyBind final : public IConfigValue { +public: + explicit ConfigKeyBind(void); + explicit ConfigKeyBind(int default_value); + virtual ~ConfigKeyBind(void) = default; + + virtual void set(const char* value) override; + virtual const char* get(void) const override; + + void set_key(int keycode); + int get_key(void) const; + + bool equals(int keycode) const; + +private: + const char* m_name; + int m_glfw_keycode; +}; + +#endif /* CLIENT_KEYBIND_HH */ diff --git a/src/game/client/language.cc b/src/game/client/language.cc new file mode 100644 index 0000000..2ae0bc6 --- /dev/null +++ b/src/game/client/language.cc @@ -0,0 +1,196 @@ +#include "client/pch.hh" + +#include "client/language.hh" + +#include "core/config.hh" + +#include "client/globals.hh" +#include "client/settings.hh" + +constexpr static const char* DEFAULT_LANGUAGE = "en_US"; + +// Available languages are kept in a special manifest file which +// is essentially a key-value map of semi-IETF-compliant language tags +// and the language's endonym; after reading the manifest, the translation +// system knows what language it can load and will act accordingly +constexpr static const char* MANIFEST_PATH = "lang/manifest.json"; + +static LanguageManifest manifest; +static LanguageIterator current_language; +static std::unordered_map language_map; +static std::unordered_map ietf_map; +static ConfigString config_language(DEFAULT_LANGUAGE); + +static void send_language_event(LanguageIterator new_language) +{ + LanguageSetEvent event; + event.new_language = new_language; + globals::dispatcher.trigger(event); +} + +void language::init(void) +{ + globals::client_config.add_value("language", config_language); + + settings::add_language_select(0, settings_location::GENERAL, "language"); + + auto file = PHYSFS_openRead(MANIFEST_PATH); + + if(file == nullptr) { + spdlog::critical("language: {}: {}", MANIFEST_PATH, PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode())); + std::terminate(); + } + + auto source = std::string(PHYSFS_fileLength(file), char(0x00)); + PHYSFS_readBytes(file, source.data(), source.size()); + PHYSFS_close(file); + + auto jsonv = json_parse_string(source.c_str()); + const auto json = json_value_get_object(jsonv); + const auto count = json_object_get_count(json); + + if((jsonv == nullptr) || (json == nullptr) || (count == 0)) { + spdlog::critical("language: {}: parse error", MANIFEST_PATH); + json_value_free(jsonv); + std::terminate(); + } + + for(std::size_t i = 0; i < count; ++i) { + const auto ietf = json_object_get_name(json, i); + const auto value = json_object_get_value_at(json, i); + const auto endonym = json_value_get_string(value); + + if(ietf && endonym) { + LanguageInfo info; + info.ietf = std::string(ietf); + info.endonym = std::string(endonym); + info.display = std::format("{} ({})", endonym, ietf); + manifest.push_back(info); + } + } + + for(auto it = manifest.cbegin(); it != manifest.cend(); ++it) { + ietf_map.emplace(it->ietf, it); + } + + json_value_free(jsonv); + + // This is temporary! This value will + // be overriden in init_late after the + // config system updates config_language + current_language = manifest.cend(); +} + +void language::init_late(void) +{ + auto user_language = ietf_map.find(config_language.get()); + + if(user_language != ietf_map.cend()) { + language::set(user_language->second); + return; + } + + auto fallback = ietf_map.find(DEFAULT_LANGUAGE); + + if(fallback != ietf_map.cend()) { + language::set(fallback->second); + return; + } + + spdlog::critical("language: we're doomed!"); + spdlog::critical("language: {} doesn't exist!", DEFAULT_LANGUAGE); + std::terminate(); +} + +void language::set(LanguageIterator new_language) +{ + if(new_language != manifest.cend()) { + auto path = std::format("lang/lang.{}.json", new_language->ietf); + + auto file = PHYSFS_openRead(path.c_str()); + + if(file == nullptr) { + spdlog::warn("language: {}: {}", path, PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode())); + send_language_event(new_language); + return; + } + + auto source = std::string(PHYSFS_fileLength(file), char(0x00)); + PHYSFS_readBytes(file, source.data(), source.size()); + PHYSFS_close(file); + + auto jsonv = json_parse_string(source.c_str()); + const auto json = json_value_get_object(jsonv); + const auto count = json_object_get_count(json); + + if((jsonv == nullptr) || (json == nullptr) || (count == 0)) { + spdlog::warn("language: {}: parse error", path); + send_language_event(new_language); + json_value_free(jsonv); + return; + } + + language_map.clear(); + + for(size_t i = 0; i < count; ++i) { + const auto key = json_object_get_name(json, i); + const auto value = json_object_get_value_at(json, i); + const auto value_str = json_value_get_string(value); + + if(key && value_str) { + language_map.emplace(key, value_str); + continue; + } + } + + json_value_free(jsonv); + + current_language = new_language; + config_language.set(new_language->ietf.c_str()); + } + + send_language_event(new_language); +} + +LanguageIterator language::get_current(void) +{ + return current_language; +} + +LanguageIterator language::find(const char* ietf) +{ + const auto it = ietf_map.find(ietf); + if(it != ietf_map.cend()) { + return it->second; + } else { + return manifest.cend(); + } +} + +LanguageIterator language::cbegin(void) +{ + return manifest.cbegin(); +} + +LanguageIterator language::cend(void) +{ + return manifest.cend(); +} + +const char* language::resolve(const char* key) +{ + const auto it = language_map.find(key); + if(it != language_map.cend()) { + return it->second.c_str(); + } else { + return key; + } +} + +std::string language::resolve_gui(const char* key) +{ + // We need window tags to retain their hierarchy when a language + // dynamically changes; ImGui allows to provide hidden unique identifiers + // to GUI primitives that have their name change dynamically, so we're using this + return std::format("{}###{}", language::resolve(key), key); +} diff --git a/src/game/client/language.hh b/src/game/client/language.hh new file mode 100644 index 0000000..680cd92 --- /dev/null +++ b/src/game/client/language.hh @@ -0,0 +1,43 @@ +#ifndef CLIENT_LANGUAGE_HH +#define CLIENT_LANGUAGE_HH 1 +#pragma once + +struct LanguageInfo final { + std::string endonym; // Language's self-name + std::string display; // Display for the settings GUI + std::string ietf; // Semi-compliant language abbreviation +}; + +using LanguageManifest = std::vector; +using LanguageIterator = LanguageManifest::const_iterator; + +struct LanguageSetEvent final { + LanguageIterator new_language; +}; + +namespace language +{ +void init(void); +void init_late(void); +} // namespace language + +namespace language +{ +void set(LanguageIterator new_language); +} // namespace language + +namespace language +{ +LanguageIterator get_current(void); +LanguageIterator find(const char* ietf); +LanguageIterator cbegin(void); +LanguageIterator cend(void); +} // namespace language + +namespace language +{ +const char* resolve(const char* key); +std::string resolve_gui(const char* key); +} // namespace language + +#endif /* CLIENT_LANGUAGE_HH */ diff --git a/src/game/client/listener.cc b/src/game/client/listener.cc new file mode 100644 index 0000000..6b691eb --- /dev/null +++ b/src/game/client/listener.cc @@ -0,0 +1,38 @@ +#include "client/pch.hh" + +#include "client/listener.hh" + +#include "core/config.hh" +#include "core/constexpr.hh" + +#include "shared/dimension.hh" +#include "shared/velocity.hh" + +#include "client/camera.hh" +#include "client/const.hh" +#include "client/globals.hh" +#include "client/session.hh" +#include "client/sound.hh" + +void listener::update(void) +{ + if(session::is_ingame()) { + const auto& velocity = globals::dimension->entities.get(globals::player).value; + const auto& position = camera::position_local; + + alListener3f(AL_POSITION, position.x, position.y, position.z); + alListener3f(AL_VELOCITY, velocity.x, velocity.y, velocity.z); + + float orientation[6]; + orientation[0] = camera::direction.x; + orientation[1] = camera::direction.y; + orientation[2] = camera::direction.z; + orientation[3] = DIR_UP.x; + orientation[4] = DIR_UP.y; + orientation[5] = DIR_UP.z; + + alListenerfv(AL_ORIENTATION, orientation); + } + + alListenerf(AL_GAIN, vx::clamp(sound::volume_master.get_value() * 0.01f, 0.0f, 1.0f)); +} diff --git a/src/game/client/listener.hh b/src/game/client/listener.hh new file mode 100644 index 0000000..731babc --- /dev/null +++ b/src/game/client/listener.hh @@ -0,0 +1,10 @@ +#ifndef CLIENT_LISTENER_HH +#define CLIENT_LISTENER_HH 1 +#pragma once + +namespace listener +{ +void update(void); +} // namespace listener + +#endif /* CLIENT_LISTENER_HH */ diff --git a/src/game/client/main.cc b/src/game/client/main.cc new file mode 100644 index 0000000..d9e915c --- /dev/null +++ b/src/game/client/main.cc @@ -0,0 +1,441 @@ +#include "client/pch.hh" + +#include "core/binfile.hh" +#include "core/cmdline.hh" +#include "core/config.hh" +#include "core/epoch.hh" +#include "core/image.hh" +#include "core/resource.hh" +#include "core/version.hh" + +#include "shared/game.hh" +#include "shared/splash.hh" +#include "shared/threading.hh" + +#include "client/const.hh" +#include "client/game.hh" +#include "client/glfw.hh" +#include "client/globals.hh" +#include "client/sound_effect.hh" +#include "client/texture_gui.hh" +#include "client/window_title.hh" + +#if defined(_WIN32) +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(xpos); + event.pos.y = static_cast(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(width) / static_cast(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(dx); + event.dy = static_cast(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); +} + +static void GLAD_API_PTR on_opengl_message( + GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar* message, const void* param) +{ + spdlog::info("opengl: {}", reinterpret_cast(message)); +} + +static void on_termination_signal(int) +{ + spdlog::warn("client: received termination signal"); + glfwSetWindowShouldClose(globals::window, true); +} + +int main(int argc, char** argv) +{ + cmdline::create(argc, argv); + +#if defined(_WIN32) +#if defined(NDEBUG) + if(GetConsoleWindow() && !cmdline::contains("debug")) { + // Hide the console window on release builds + // unless explicitly specified to preserve it instead + FreeConsole(); + } +#else + if(GetConsoleWindow() && cmdline::contains("nodebug")) { + // Hide the console window on debug builds when + // explicitly specified by the user to hide it + FreeConsole(); + } +#endif +#endif + + shared_game::init(argc, argv); + + spdlog::info("Voxelius Client {}", project_version_string); + + 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); + + globals::window = glfwCreateWindow(DEFAULT_WIDTH, DEFAULT_HEIGHT, "Client", nullptr, nullptr); + + if(!globals::window) { + spdlog::critical("glfw: failed to open a window"); + std::terminate(); + } + + std::signal(SIGINT, &on_termination_signal); + std::signal(SIGTERM, &on_termination_signal); + + glfwMakeContextCurrent(globals::window); + glfwSwapInterval(1); + + if(!gladLoadGL(&glfwGetProcAddress)) { + spdlog::critical("glad: failed to load function pointers"); + std::terminate(); + } + + if(GLAD_GL_KHR_debug) { + if(!cmdline::contains("nodebug")) { + glEnable(GL_DEBUG_OUTPUT); + glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS); + glDebugMessageCallback(&on_opengl_message, nullptr); + + // NVIDIA drivers tend to spam quote-unquote "useful" + // information about buffer usage into the debug callback + static const std::uint32_t ignore_nvidia_131185 = 131185; + glDebugMessageControl(GL_DEBUG_SOURCE_API, GL_DEBUG_TYPE_OTHER, GL_DONT_CARE, 1, &ignore_nvidia_131185, GL_FALSE); + } else { + spdlog::warn("glad: nodebug command line parameter found"); + spdlog::warn("glad: OpenGL errors will not be logged"); + } + } else { + spdlog::warn("glad: KHR_debug extension not supported"); + spdlog::warn("glad: OpenGL errors will not be logged"); + } + + spdlog::info("opengl: version: {}", reinterpret_cast(glGetString(GL_VERSION))); + spdlog::info("opengl: renderer: {}", reinterpret_cast(glGetString(GL_RENDERER))); + + glDisable(GL_MULTISAMPLE); + + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGui::StyleColorsDark(); + 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("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(image->pixels); + glfwSetWindowIcon(globals::window, 1, &icon_image); + } + + if(cmdline::contains("nosound")) { + spdlog::warn("client: sound disabled [per command line]"); + globals::sound_dev = nullptr; + globals::sound_ctx = nullptr; + } else { + if(!saladLoadALdefault()) { + spdlog::warn("client: sound disabled [openal loading failed]"); + globals::sound_dev = nullptr; + globals::sound_ctx = nullptr; + } else { + globals::sound_dev = alcOpenDevice(nullptr); + + if(globals::sound_dev == nullptr) { + spdlog::warn("client: sound disabled [no device]"); + globals::sound_ctx = nullptr; + } else { + spdlog::info("sound: {}", reinterpret_cast(alcGetString(globals::sound_dev, ALC_DEVICE_SPECIFIER))); + + globals::sound_ctx = alcCreateContext(globals::sound_dev, nullptr); + + if(globals::sound_ctx == nullptr) { + spdlog::warn("client: sound disabled [context creation failed]"); + alcCloseDevice(globals::sound_dev); + globals::sound_dev = nullptr; + } else { + alcMakeContextCurrent(globals::sound_ctx); + } + } + } + } + + splash::init_client(); + + window_title::update(); + + ImGuiIO& io = ImGui::GetIO(); + io.ConfigFlags &= ~ImGuiConfigFlags_NavEnableGamepad; + io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; + + globals::fixed_frametime = 0.0f; + globals::fixed_frametime_avg = 0.0f; + globals::fixed_frametime_us = UINT64_MAX; + globals::fixed_framecount = 0; + + globals::curtime = epoch::microseconds(); + + globals::window_frametime = 0.0f; + globals::window_frametime_avg = 0.0f; + globals::window_frametime_us = 0; + globals::window_framecount = 0; + + int vmode_width = DEFAULT_WIDTH; + int vmode_height = DEFAULT_HEIGHT; + + if(auto vmode = cmdline::get("mode")) { + std::sscanf(vmode, "%dx%d", &vmode_width, &vmode_height); + vmode_height = vx::max(vmode_height, MIN_HEIGHT); + vmode_width = vx::max(vmode_width, MIN_WIDTH); + } + + glfwSetWindowSize(globals::window, vmode_width, vmode_height); + + 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(); + + client_game::init_late(); + + auto last_curtime = globals::curtime; + + while(!glfwWindowShouldClose(globals::window)) { + globals::curtime = epoch::microseconds(); + + globals::window_frametime_us = globals::curtime - last_curtime; + globals::window_frametime = static_cast(globals::window_frametime_us) / 1000000.0f; + globals::window_frametime_avg += globals::window_frametime; + globals::window_frametime_avg *= 0.5f; + + if(globals::fixed_frametime_us == UINT64_MAX) { + globals::fixed_framecount = 0; + globals::fixed_accumulator = 0; + } else { + globals::fixed_accumulator += globals::window_frametime_us; + globals::fixed_framecount = globals::fixed_accumulator / globals::fixed_frametime_us; + globals::fixed_accumulator %= globals::fixed_frametime_us; + } + + globals::num_drawcalls = 0; + globals::num_triangles = 0; + + last_curtime = globals::curtime; + + for(std::uint64_t i = 0; i < globals::fixed_framecount; ++i) + client_game::fixed_update(); + client_game::update(); + + ImGui_ImplOpenGL3_NewFrame(); + ImGui_ImplGlfw_NewFrame(); + ImGui::NewFrame(); + + glDisable(GL_BLEND); + + glDisable(GL_DEPTH_TEST); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glViewport(0, 0, globals::width, globals::height); + + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + + // Make sure there is no stray program object + // being bound to the context. Usually third-party + // overlay software (such as RivaTuner) injects itself + // into the rendering loop and binds internal objects, + // which creates an incomprehensible visual mess + glUseProgram(0); + + client_game::render(); + + client_game::layout(); + + ImGui::Render(); + ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); + + glfwSwapBuffers(globals::window); + + for(std::uint64_t i = 0; i < globals::fixed_framecount; ++i) + client_game::fixed_update_late(); + client_game::update_late(); + + glfwPollEvents(); + + // EnTT provides two ways of dispatching events: + // queued and immediate. When glfwPollEvents() is + // called, immediate events are triggered across + // the application, whilst queued ones are triggered + // later by calling entt::dispatcher::update() + globals::dispatcher.update(); + + globals::window_framecount += 1; + + resource::soft_cleanup(); + resource::soft_cleanup(); + + resource::soft_cleanup(); + resource::soft_cleanup(); + + threading::update(); + } + + client_game::deinit(); + + resource::hard_cleanup(); + resource::hard_cleanup(); + + resource::hard_cleanup(); + resource::hard_cleanup(); + + spdlog::info("client: shutdown after {} frames", globals::window_framecount); + spdlog::info("client: average framerate: {:.03f} FPS", 1.0f / globals::window_frametime_avg); + spdlog::info("client: average frametime: {:.03f} ms", 1000.0f * globals::window_frametime_avg); + + ImGui_ImplOpenGL3_Shutdown(); + ImGui_ImplGlfw_Shutdown(); + ImGui::DestroyContext(); + + if(globals::sound_ctx) { + alcMakeContextCurrent(nullptr); + alcDestroyContext(globals::sound_ctx); + alcCloseDevice(globals::sound_dev); + } + + glfwDestroyWindow(globals::window); + glfwTerminate(); + + globals::client_config.save_file("client.conf"); + + threading::deinit(); + + shared_game::deinit(); + + return EXIT_SUCCESS; +} diff --git a/src/game/client/main_menu.cc b/src/game/client/main_menu.cc new file mode 100644 index 0000000..39763ec --- /dev/null +++ b/src/game/client/main_menu.cc @@ -0,0 +1,163 @@ +#include "client/pch.hh" + +#include "client/main_menu.hh" + +#include "core/constexpr.hh" +#include "core/resource.hh" +#include "core/version.hh" + +#include "client/glfw.hh" +#include "client/globals.hh" +#include "client/gui_screen.hh" +#include "client/language.hh" +#include "client/session.hh" +#include "client/texture_gui.hh" +#include "client/window_title.hh" + +constexpr static ImGuiWindowFlags WINDOW_FLAGS = ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoDecoration; + +static std::string str_play; +static std::string str_resume; +static std::string str_settings; +static std::string str_leave; +static std::string str_quit; + +static resource_ptr title; +static float title_aspect; + +static void on_glfw_key(const GlfwKeyEvent& event) +{ + if(session::is_ingame() && (event.key == GLFW_KEY_ESCAPE) && (event.action == GLFW_PRESS)) { + if(globals::gui_screen == GUI_SCREEN_NONE) { + globals::gui_screen = GUI_MAIN_MENU; + return; + } + + if(globals::gui_screen == GUI_MAIN_MENU) { + globals::gui_screen = GUI_SCREEN_NONE; + return; + } + } +} + +static void on_language_set(const LanguageSetEvent& event) +{ + str_play = language::resolve_gui("main_menu.play"); + str_resume = language::resolve_gui("main_menu.resume"); + str_settings = language::resolve("main_menu.settings"); + str_leave = language::resolve("main_menu.leave"); + str_quit = language::resolve("main_menu.quit"); +} + +void main_menu::init(void) +{ + title = resource::load("textures/gui/menu_title.png", TEXTURE_GUI_LOAD_CLAMP_S | TEXTURE_GUI_LOAD_CLAMP_T); + + if(title == nullptr) { + spdlog::critical("main_menu: texture load failed"); + std::terminate(); + } + + if(title->size.x > title->size.y) { + title_aspect = static_cast(title->size.x) / static_cast(title->size.y); + } else { + title_aspect = static_cast(title->size.y) / static_cast(title->size.x); + } + + globals::dispatcher.sink().connect<&on_glfw_key>(); + globals::dispatcher.sink().connect<&on_language_set>(); +} + +void main_menu::deinit(void) +{ + title = nullptr; +} + +void main_menu::layout(void) +{ + const auto viewport = ImGui::GetMainViewport(); + const auto window_start = ImVec2(0.0f, viewport->Size.y * 0.15f); + const auto window_size = ImVec2(viewport->Size.x, viewport->Size.y); + + ImGui::SetNextWindowPos(window_start); + ImGui::SetNextWindowSize(window_size); + + if(ImGui::Begin("###main_menu", nullptr, WINDOW_FLAGS)) { + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0.0f, 2.0f * globals::gui_scale)); + + if(session::is_ingame()) { + ImGui::Dummy(ImVec2(0.0f, 32.0f * globals::gui_scale)); + } else { + auto reference_height = 0.225f * window_size.y; + auto image_width = vx::min(window_size.x, reference_height * title_aspect); + auto image_height = image_width / title_aspect; + ImGui::SetCursorPosX(0.5f * (window_size.x - image_width)); + ImGui::Image(title->handle, ImVec2(image_width, image_height)); + } + + ImGui::Dummy(ImVec2(0.0f, 24.0f * globals::gui_scale)); + + const float button_width = 240.0f * globals::gui_scale; + const float button_xpos = 0.5f * (window_size.x - button_width); + + if(session::is_ingame()) { + ImGui::SetCursorPosX(button_xpos); + + if(ImGui::Button(str_resume.c_str(), ImVec2(button_width, 0.0f))) { + globals::gui_screen = GUI_SCREEN_NONE; + } + + ImGui::Spacing(); + } else { + ImGui::SetCursorPosX(button_xpos); + + if(ImGui::Button(str_play.c_str(), ImVec2(button_width, 0.0f))) { + globals::gui_screen = GUI_PLAY_MENU; + } + + ImGui::Spacing(); + } + + ImGui::SetCursorPosX(button_xpos); + + if(ImGui::Button(str_settings.c_str(), ImVec2(button_width, 0.0f))) { + globals::gui_screen = GUI_SETTINGS; + } + + ImGui::Spacing(); + + if(session::is_ingame()) { + ImGui::SetCursorPosX(button_xpos); + + if(ImGui::Button(str_leave.c_str(), ImVec2(button_width, 0.0f))) { + session::disconnect("protocol.client_disconnect"); + globals::gui_screen = GUI_PLAY_MENU; + window_title::update(); + } + + ImGui::Spacing(); + } else { + ImGui::SetCursorPosX(button_xpos); + + if(ImGui::Button(str_quit.c_str(), ImVec2(button_width, 0.0f))) { + glfwSetWindowShouldClose(globals::window, true); + } + + ImGui::Spacing(); + } + + if(!session::is_ingame()) { + const auto& padding = ImGui::GetStyle().FramePadding; + const auto& spacing = ImGui::GetStyle().ItemSpacing; + + ImGui::PushFont(globals::font_debug); + ImGui::SetCursorScreenPos(ImVec2(padding.x + spacing.x, window_size.y - globals::font_debug->FontSize - padding.y - spacing.y)); + ImGui::Text("Voxelius %s", project_version_string.c_str()); + ImGui::PopFont(); + } + + ImGui::PopStyleVar(); + } + + ImGui::End(); +} diff --git a/src/game/client/main_menu.hh b/src/game/client/main_menu.hh new file mode 100644 index 0000000..9166722 --- /dev/null +++ b/src/game/client/main_menu.hh @@ -0,0 +1,12 @@ +#ifndef CLIENT_MAIN_MENU_HH +#define CLIENT_MAIN_MENU_HH 1 +#pragma once + +namespace main_menu +{ +void init(void); +void deinit(void); +void layout(void); +} // namespace main_menu + +#endif /* CLIENT_MAIN_MENU_HH */ diff --git a/src/game/client/message_box.cc b/src/game/client/message_box.cc new file mode 100644 index 0000000..da1b715 --- /dev/null +++ b/src/game/client/message_box.cc @@ -0,0 +1,94 @@ +#include "client/pch.hh" + +#include "client/message_box.hh" + +#include "client/globals.hh" +#include "client/gui_screen.hh" +#include "client/language.hh" + +constexpr static ImGuiWindowFlags WINDOW_FLAGS = ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoDecoration; + +struct Button final { + message_box_action action; + std::string str_title; +}; + +static std::string str_title; +static std::string str_subtitle; +static std::vector