From a615ef7cf18d718186d0002e76f6dd27d876c0d7 Mon Sep 17 00:00:00 2001 From: "C. J. Howard" Date: Mon, 6 Feb 2023 02:33:12 +0800 Subject: [PATCH] Add i18n namespace. Switch to using hash strings for localized strings. Change settings to be dict-based. Improve path finding on windows. Improve window management. Add serializer and deserializer template classes --- CMakeLists.txt | 1 + CONTRIBUTING.md | 5 +- README.md | 8 +- src/application.cpp | 344 ++++++++++---- src/application.hpp | 82 +++- src/color/aces.hpp | 2 +- src/debug/log.cpp | 26 -- src/debug/log.hpp | 43 +- src/event/queue.hpp | 12 +- src/game/context.hpp | 31 +- src/game/controls.cpp | 2 +- src/game/fonts.cpp | 61 +-- src/game/graphics.cpp | 50 +-- src/game/save.cpp | 101 ----- src/game/settings.hpp | 67 +++ src/game/state/boot.cpp | 419 +++++++++--------- src/game/state/boot.hpp | 4 +- src/game/state/controls-menu.cpp | 18 +- src/game/state/credits.cpp | 24 +- src/game/state/extras-menu.cpp | 16 +- src/game/state/gamepad-config-menu.cpp | 22 +- src/game/state/graphics-menu.cpp | 198 ++++----- src/game/state/keyboard-config-menu.cpp | 22 +- src/game/state/language-menu.cpp | 89 ++-- src/game/state/main-menu.cpp | 35 +- src/game/state/nest-selection.cpp | 41 +- src/game/state/nuptial-flight.cpp | 27 +- src/game/state/options-menu.cpp | 25 +- src/game/state/pause-menu.cpp | 23 +- src/game/state/sound-menu.cpp | 38 +- src/game/state/splash.cpp | 20 +- src/game/{save.hpp => strings.cpp} | 30 +- src/game/strings.hpp | 42 ++ src/game/system/subterrain.cpp | 8 +- src/game/world.cpp | 179 +++----- src/gl/gl.hpp | 42 +- src/i18n/i18n.hpp | 26 ++ src/i18n/string-map.cpp | 51 +++ .../string-table.hpp => i18n/string-map.hpp} | 36 +- .../type-id.hpp => i18n/string-table.hpp} | 30 +- src/input/event.hpp | 66 ++- src/main.cpp | 38 +- src/render/anti-aliasing-method.hpp | 4 +- src/render/passes/fxaa-pass.cpp | 1 + src/render/passes/ground-pass.cpp | 1 - src/render/passes/material-pass.cpp | 2 - src/render/passes/sky-pass.cpp | 1 - src/resources/deserialize-context.cpp | 176 ++++++++ src/resources/deserialize-context.hpp | 152 +++++++ ...string-table.cpp => deserialize-error.hpp} | 33 +- src/resources/deserializer.cpp | 125 ++++++ src/resources/deserializer.hpp | 42 ++ src/resources/dict-loader.cpp | 43 ++ src/resources/resource-loader.cpp | 46 +- src/resources/resource-manager.cpp | 31 ++ src/resources/resource-manager.hpp | 2 + src/resources/serialize-context.cpp | 144 ++++++ src/resources/serialize-context.hpp | 100 +++++ src/resources/serialize-error.hpp | 33 ++ src/resources/serializer.cpp | 120 +++++ src/resources/serializer.hpp | 42 ++ src/resources/string-table-loader.cpp | 144 ++---- src/utility/dict.cpp | 168 +++++++ src/utility/dict.hpp | 34 ++ src/utility/paths.cpp | 89 ++-- src/utility/paths.hpp | 34 +- 66 files changed, 2634 insertions(+), 1337 deletions(-) delete mode 100644 src/game/save.cpp create mode 100644 src/game/settings.hpp rename src/game/{save.hpp => strings.cpp} (72%) create mode 100644 src/game/strings.hpp create mode 100644 src/i18n/i18n.hpp create mode 100644 src/i18n/string-map.cpp rename src/{resources/string-table.hpp => i18n/string-map.hpp} (50%) rename src/{utility/type-id.hpp => i18n/string-table.hpp} (63%) create mode 100644 src/resources/deserialize-context.cpp create mode 100644 src/resources/deserialize-context.hpp rename src/resources/{string-table.cpp => deserialize-error.hpp} (57%) create mode 100644 src/resources/deserializer.cpp create mode 100644 src/resources/deserializer.hpp create mode 100644 src/resources/dict-loader.cpp create mode 100644 src/resources/serialize-context.cpp create mode 100644 src/resources/serialize-context.hpp create mode 100644 src/resources/serialize-error.hpp create mode 100644 src/resources/serializer.cpp create mode 100644 src/resources/serializer.hpp create mode 100644 src/utility/dict.cpp create mode 100644 src/utility/dict.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 0b41e73..e01a1f4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,6 @@ cmake_minimum_required(VERSION 3.25) + option(VERSION_STRING "Project version string" "0.0.0") project(antkeeper VERSION ${VERSION_STRING} LANGUAGES CXX) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ecfe138..d684169 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ Thanks for taking the time to make Antkeeper better! ❤🐜 To report a bug, please do one of the following: -* [Open an issue](https://git.antkeeper.com/antkeeper/source/issues/new) on Antkeeper Git +* [Open an issue](https://github.com/antkeeper/antkeeper-source/issues) on GitHub * [Email us](mailto:contact@antkeeper.com) a description of the bug * [Chat with us](https://discord.gg/AQA955HbK3) about the bug on Discord @@ -19,7 +19,7 @@ To suggest changes or new features, please do one of the following: ## Contributing Code -If you have written code that fixes a bug or provides new functionality, please [open a pull request](https://git.antkeeper.com/antkeeper/source/pulls) on Antkeeper Git. +If you have written code that fixes a bug or provides new functionality, please [open a pull request](https://github.com/antkeeper/antkeeper-source/pulls) on GitHub. ### Coding Conventions @@ -27,4 +27,3 @@ Ensure that all code contributions adhere to the following conventions: * C++ STL naming conventions * Allman indent style with tabs -* Unlimited line length diff --git a/README.md b/README.md index d7ff31a..6305f7a 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,16 @@ # Antkeeper Source -Antkeeper is an ant colony simulation game currently in development for Windows, Mac, and Linux. This repository contains all of the source code to Antkeeper. +Antkeeper is a 3D ant colony simulation game currently in development for Windows, Mac, and Linux. This repository contains all of the source code to Antkeeper. Head over to [antkeeper.com](https://antkeeper.com/) if you're interested in following the development of the game or purchasing a copy when it's released. Antkeeper is an indie game with a single developer, so feel free to reach out to me personally with any questions, comments, or feedback you may have. ## Building -Download the Antkeeper build system, source code, and all dependencies via the [Antkeeper superbuild](https://git.antkeeper.com/antkeeper/superbuild) repository: +Download the Antkeeper source code, build system, and all dependencies via the [Antkeeper superbuild](https://github.com/antkeeper/antkeeper-superbuild) repository: - git clone --recursive https://git.antkeeper.com/antkeeper/superbuild.git + git clone --recursive https://github.com/antkeeper/antkeeper-superbuild.git -Detailed configuration and build instructions can be found in the [README](https://git.antkeeper.com/antkeeper/superbuild/src/branch/master/README.md) of the superbuild repository. +Detailed configuration and build instructions can be found in the [README](https://github.com/antkeeper/antkeeper-superbuild/blob/master/README.md) of the superbuild repository. ## Documentation diff --git a/src/application.cpp b/src/application.cpp index f39672e..4961836 100644 --- a/src/application.cpp +++ b/src/application.cpp @@ -22,36 +22,51 @@ #include "debug/log.hpp" #include "input/scancode.hpp" #include "math/map.hpp" -#include "resources/image.hpp" #include #include -#include -#include #include #include -application::application(): +application::application +( + const std::string& window_title, + int window_x, + int window_y, + int window_w, + int window_h, + bool maximized, + bool fullscreen, + bool v_sync +): closed(false), + maximized(false), fullscreen(true), v_sync(false), cursor_visible(true), - display_dimensions({0, 0}), + display_size({0, 0}), display_dpi(0.0f), - window_dimensions({0, 0}), - viewport_dimensions({0, 0}), + windowed_position({-1, -1}), + windowed_size({-1, -1}), + viewport_size({-1, -1}), mouse_position({0, 0}), sdl_window(nullptr), sdl_gl_context(nullptr) { - // Log SDL compiled version - SDL_version sdl_compiled_version; - SDL_VERSION(&sdl_compiled_version); - debug::log::debug("Compiled against SDL {}.{}.{}", sdl_compiled_version.major, sdl_compiled_version.minor, sdl_compiled_version.patch); - - // Log SDL linked version - SDL_version sdl_linked_version; - SDL_GetVersion(&sdl_linked_version); - debug::log::debug("Linking against SDL {}.{}.{}", sdl_linked_version.major, sdl_linked_version.minor, sdl_linked_version.patch); + // Log SDL info + // SDL_version sdl_compiled_version; + // SDL_version sdl_linked_version; + // SDL_VERSION(&sdl_compiled_version); + // SDL_GetVersion(&sdl_linked_version); + // debug::log::info + // ( + // "SDL compiled version: {}.{}.{}; linked version: {}.{}.{}", + // sdl_compiled_version.major, + // sdl_compiled_version.minor, + // sdl_compiled_version.patch, + // sdl_linked_version.major, + // sdl_linked_version.minor, + // sdl_linked_version.patch + // ); // Init SDL events and video subsystems debug::log::trace("Initializing SDL events and video subsystems..."); @@ -62,6 +77,53 @@ application::application(): } debug::log::trace("Initialized SDL events and video subsystems"); + // Query displays + debug::log::trace("Querying displays..."); + + const int sdl_display_count = SDL_GetNumVideoDisplays(); + if (sdl_display_count < 1) + { + debug::log::fatal("No displays detected: {}", SDL_GetError()); + throw std::runtime_error("No displays detected"); + } + + debug::log::info("Display count: {}", sdl_display_count); + + for (int i = 0; i < sdl_display_count; ++i) + { + // Query display mode + SDL_DisplayMode sdl_display_mode; + if (SDL_GetDesktopDisplayMode(i, &sdl_display_mode) != 0) + { + debug::log::error("Failed to get mode of display {}: {}", i, SDL_GetError()); + SDL_ClearError(); + continue; + } + + // Query display name + const char* sdl_display_name = SDL_GetDisplayName(i); + if (!sdl_display_name) + { + debug::log::warning("Failed to get name of display {}: {}", i, SDL_GetError()); + SDL_ClearError(); + sdl_display_name = ""; + } + + // Query display DPI + float sdl_display_dpi; + if (SDL_GetDisplayDPI(i, &sdl_display_dpi, nullptr, nullptr) != 0) + { + const float default_dpi = 96.0f; + debug::log::warning("Failed to get DPI of display {}: {}; Defaulting to {} DPI", i, SDL_GetError(), default_dpi); + SDL_ClearError(); + } + + // Log display information + debug::log::info("Display {} name: \"{}\"; resolution: {}x{}; refresh rate: {}Hz; DPI: {}", i, sdl_display_name, sdl_display_mode.w, sdl_display_mode.h, sdl_display_mode.refresh_rate, sdl_display_dpi); + } + + debug::log::trace("Queried displays"); + // Detect display dimensions SDL_DisplayMode sdl_desktop_display_mode; if (SDL_GetDesktopDisplayMode(0, &sdl_desktop_display_mode) != 0) @@ -69,7 +131,7 @@ application::application(): debug::log::fatal("Failed to detect desktop display mode: {}", SDL_GetError()); throw std::runtime_error("Failed to detect desktop display mode"); } - display_dimensions = {sdl_desktop_display_mode.w, sdl_desktop_display_mode.h}; + display_size = {sdl_desktop_display_mode.w, sdl_desktop_display_mode.h}; // Detect display DPI if (SDL_GetDisplayDPI(0, &display_dpi, nullptr, nullptr) != 0) @@ -104,21 +166,55 @@ application::application(): SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, config::opengl_min_depth_size); SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, config::opengl_min_stencil_size); + Uint32 sdl_window_flags = SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE; + if (fullscreen) + { + sdl_window_flags |= SDL_WINDOW_FULLSCREEN_DESKTOP; + } + if (maximized) + { + sdl_window_flags |= SDL_WINDOW_MAXIMIZED; + } + if (window_x == -1 && window_y == -1) + { + window_x = SDL_WINDOWPOS_CENTERED; + window_y = SDL_WINDOWPOS_CENTERED; + } + if (window_w <= 0 || window_h <= 0) + { + window_w = sdl_desktop_display_mode.w / 2; + window_h = sdl_desktop_display_mode.h / 2; + } + // Create a hidden fullscreen window - debug::log::trace("Creating {}x{} window...", display_dimensions[0], display_dimensions[1]); + debug::log::trace("Creating window..."); sdl_window = SDL_CreateWindow ( - config::application_name, - SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, - display_dimensions[0], display_dimensions[1], - SDL_WINDOW_OPENGL | SDL_WINDOW_FULLSCREEN_DESKTOP | SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_HIDDEN + window_title.c_str(), + window_x, + window_y, + window_w, + window_h, + sdl_window_flags ); if (!sdl_window) { - debug::log::fatal("Failed to create {}x{} window: {}", display_dimensions[0], display_dimensions[1], SDL_GetError()); + debug::log::fatal("Failed to create {}x{} window: {}", display_size[0], display_size[1], SDL_GetError()); throw std::runtime_error("Failed to create SDL window"); } - debug::log::trace("Created {}x{} window", display_dimensions[0], display_dimensions[1]); + debug::log::trace("Created window"); + + + if (window_x != SDL_WINDOWPOS_CENTERED && window_y != SDL_WINDOWPOS_CENTERED) + { + this->windowed_position = {window_x, window_y}; + } + this->windowed_size = {window_w, window_h}; + this->maximized = maximized; + this->fullscreen = fullscreen; + + // Set hard window minimum size + SDL_SetWindowMinimumSize(sdl_window, 160, 120); // Create OpenGL context debug::log::trace("Creating OpenGL {}.{} context...", config::opengl_version_major, config::opengl_version_minor); @@ -156,7 +252,7 @@ application::application(): SDL_GL_GetAttribute(SDL_GL_ALPHA_SIZE, &opengl_context_alpha_size); SDL_GL_GetAttribute(SDL_GL_DEPTH_SIZE, &opengl_context_depth_size); SDL_GL_GetAttribute(SDL_GL_STENCIL_SIZE, &opengl_context_stencil_size); - debug::log::info("OpenGL context default framebuffer format: R{}G{}B{}A{}D{}S{}", opengl_context_red_size, opengl_context_green_size, opengl_context_blue_size, opengl_context_alpha_size, opengl_context_depth_size, opengl_context_stencil_size); + debug::log::info("OpenGL context format: R{}G{}B{}A{}D{}S{}", opengl_context_red_size, opengl_context_green_size, opengl_context_blue_size, opengl_context_alpha_size, opengl_context_depth_size, opengl_context_stencil_size); // Compare OpenGL context default framebuffer format with request format if (opengl_context_red_size < config::opengl_min_red_size || @@ -168,7 +264,7 @@ application::application(): { debug::log::warning ( - "OpenGL default framebuffer format (R{}G{}B{}A{}D{}S{}) does not meet minimum requested format (R{}G{}B{}A{}D{}S{})", + "OpenGL context format (R{}G{}B{}A{}D{}S{}) does not meet minimum requested format (R{}G{}B{}A{}D{}S{})", opengl_context_red_size, opengl_context_green_size, opengl_context_blue_size, opengl_context_alpha_size, opengl_context_depth_size, opengl_context_stencil_size, config::opengl_min_red_size, config::opengl_min_green_size, config::opengl_min_blue_size, config::opengl_min_alpha_size, config::opengl_min_depth_size, config::opengl_min_stencil_size ); @@ -183,13 +279,27 @@ application::application(): } debug::log::trace("Loaded OpenGL functions"); - // Update window size and viewport size - SDL_GetWindowSize(sdl_window, &window_dimensions[0], &window_dimensions[1]); - SDL_GL_GetDrawableSize(sdl_window, &viewport_dimensions[0], &viewport_dimensions[1]); + // Log OpenGL context information + debug::log::info + ( + "OpenGL vendor: {}; renderer: {}; version: {}; shading language version: {}", + reinterpret_cast(glGetString(GL_VENDOR)), + reinterpret_cast(glGetString(GL_RENDERER)), + reinterpret_cast(glGetString(GL_VERSION)), + reinterpret_cast(glGetString(GL_SHADING_LANGUAGE_VERSION)) + ); + + // Clear window color + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + swap_buffers(); // Set v-sync mode - set_v_sync(true); - + set_v_sync(v_sync); + + // Update viewport size + SDL_GL_GetDrawableSize(sdl_window, &viewport_size[0], &viewport_size[1]); + // Init SDL joystick and controller subsystems debug::log::trace("Initializing SDL joystick and controller subsystems..."); if (SDL_InitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER) != 0) @@ -203,6 +313,7 @@ application::application(): // Setup rasterizer rasterizer = new gl::rasterizer(); + rasterizer->context_resized(viewport_size[0], viewport_size[1]); // Register keyboard and mouse with input device manager device_manager.register_device(keyboard); @@ -266,8 +377,8 @@ void application::set_relative_mouse_mode(bool enabled) void application::resize_window(int width, int height) { - int x = (display_dimensions[0] >> 1) - (width >> 1); - int y = (display_dimensions[1] >> 1) - (height >> 1); + int x = (display_size[0] >> 1) - (width >> 1); + int y = (display_size[1] >> 1) - (height >> 1); // Resize and center window SDL_SetWindowPosition(sdl_window, x, y); @@ -282,58 +393,51 @@ void application::set_fullscreen(bool fullscreen) if (fullscreen) { - SDL_HideWindow(sdl_window); SDL_SetWindowFullscreen(sdl_window, SDL_WINDOW_FULLSCREEN_DESKTOP); - SDL_ShowWindow(sdl_window); } else { SDL_SetWindowFullscreen(sdl_window, 0); - SDL_SetWindowBordered(sdl_window, SDL_TRUE); - SDL_SetWindowResizable(sdl_window, SDL_TRUE); } } } void application::set_v_sync(bool v_sync) { - if (this->v_sync != v_sync) + if (v_sync) { - if (v_sync) + debug::log::trace("Enabling adaptive v-sync..."); + if (SDL_GL_SetSwapInterval(-1) != 0) { - debug::log::trace("Enabling adaptive v-sync..."); - if (SDL_GL_SetSwapInterval(-1) != 0) + debug::log::error("Failed to enable adaptive v-sync: {}", SDL_GetError()); + debug::log::trace("Enabling synchronized v-sync..."); + if (SDL_GL_SetSwapInterval(1) != 0) { - debug::log::error("Failed to enable adaptive v-sync: {}", SDL_GetError()); - debug::log::trace("Enabling synchronized v-sync..."); - if (SDL_GL_SetSwapInterval(1) != 0) - { - debug::log::error("Failed to enable synchronized v-sync: {}", SDL_GetError()); - } - else - { - this->v_sync = v_sync; - debug::log::debug("Enabled synchronized v-sync"); - } + debug::log::error("Failed to enable synchronized v-sync: {}", SDL_GetError()); } else { this->v_sync = v_sync; - debug::log::debug("Enabled adaptive v-sync"); + debug::log::debug("Enabled synchronized v-sync"); } } else { - debug::log::trace("Disabling v-sync..."); - if (SDL_GL_SetSwapInterval(0) != 0) - { - debug::log::error("Failed to disable v-sync: {}", SDL_GetError()); - } - else - { - this->v_sync = v_sync; - debug::log::debug("Disabled v-sync"); - } + this->v_sync = v_sync; + debug::log::debug("Enabled adaptive v-sync"); + } + } + else + { + debug::log::trace("Disabling v-sync..."); + if (SDL_GL_SetSwapInterval(0) != 0) + { + debug::log::error("Failed to disable v-sync: {}", SDL_GetError()); + } + else + { + this->v_sync = v_sync; + debug::log::debug("Disabled v-sync"); } } } @@ -525,27 +629,97 @@ void application::process_events() switch (sdl_event.window.event) { case SDL_WINDOWEVENT_SIZE_CHANGED: - window_resized(); + { + // Query SDL window parameters + SDL_Window* sdl_window = SDL_GetWindowFromID(sdl_event.window.windowID); + const auto sdl_window_flags = SDL_GetWindowFlags(sdl_window); + int sdl_window_drawable_w = 0; + int sdl_window_drawable_h = 0; + SDL_GL_GetDrawableSize(sdl_window, &sdl_window_drawable_w, &sdl_window_drawable_h); + + // Build window resized event + input::event::window_resized event; + event.window = nullptr; + event.size.x() = static_cast(sdl_event.window.data1); + event.size.y() = static_cast(sdl_event.window.data2); + event.maximized = sdl_window_flags & SDL_WINDOW_MAXIMIZED; + event.fullscreen = sdl_window_flags & SDL_WINDOW_FULLSCREEN; + event.viewport_size.x() = static_cast(sdl_window_drawable_w); + event.viewport_size.y() = static_cast(sdl_window_drawable_h); + + // Update windowed size + if (!event.maximized && !event.fullscreen) + { + windowed_size = event.size; + } + + // Update GL context size + rasterizer->context_resized(event.viewport_size.x(), event.viewport_size.y()); + + // Publish window resized event + window_resized_publisher.publish(event); break; + } + + case SDL_WINDOWEVENT_MOVED: + { + // Query SDL window parameters + SDL_Window* sdl_window = SDL_GetWindowFromID(sdl_event.window.windowID); + const auto sdl_window_flags = SDL_GetWindowFlags(sdl_window); + + // Build window moved event + input::event::window_moved event; + event.window = nullptr; + event.position.x() = static_cast(sdl_event.window.data1); + event.position.y() = static_cast(sdl_event.window.data2); + event.maximized = sdl_window_flags & SDL_WINDOW_MAXIMIZED; + event.fullscreen = sdl_window_flags & SDL_WINDOW_FULLSCREEN; + + // Update windowed position + if (!event.maximized && !event.fullscreen) + { + windowed_position = event.position; + } + + // Publish window moved event + window_moved_publisher.publish(event); + break; + } case SDL_WINDOWEVENT_FOCUS_GAINED: - debug::log::debug("Window focus gained"); - window_focus_changed_publisher.publish({true}); + // Build and publish window focused gained event + window_focus_changed_publisher.publish({nullptr, true}); break; case SDL_WINDOWEVENT_FOCUS_LOST: - debug::log::debug("Window focus lost"); - window_focus_changed_publisher.publish({false}); + // Build and publish window focused lost event + window_focus_changed_publisher.publish({nullptr, false}); break; - case SDL_WINDOWEVENT_MOVED: - debug::log::trace("Window moved to ({}, {})", sdl_event.window.data1, sdl_event.window.data2); - window_moved_publisher.publish({static_cast(sdl_event.window.data1), static_cast(sdl_event.window.data2)}); + case SDL_WINDOWEVENT_MAXIMIZED: + // Update window maximized + maximized = true; + + // Build and publish window maximized event + window_maximized_publisher.publish({nullptr}); + break; + + case SDL_WINDOWEVENT_RESTORED: + // Update window maximized + maximized = false; + + // Build and publish window restored event + window_restored_publisher.publish({nullptr}); + break; + + case SDL_WINDOWEVENT_MINIMIZED: + // Build and publish window minimized event + window_minimized_publisher.publish({nullptr}); break; [[unlikely]] case SDL_WINDOWEVENT_CLOSE: - debug::log::info("Window closed"); - window_closed_publisher.publish({}); + // Build and publish window closed event + window_closed_publisher.publish({nullptr}); break; default: @@ -630,6 +804,9 @@ void application::process_events() close(); break; } + + default: + break; } } @@ -639,24 +816,3 @@ void application::process_events() // mouse.move(mouse_x, mouse_y, mouse_dx, mouse_dy); // } } - -void application::window_resized() -{ - // Update window size and viewport size - SDL_GetWindowSize(sdl_window, &window_dimensions[0], &window_dimensions[1]); - SDL_GL_GetDrawableSize(sdl_window, &viewport_dimensions[0], &viewport_dimensions[1]); - - debug::log::debug("Window resized to {}x{}", window_dimensions[0], window_dimensions[1]); - - rasterizer->context_resized(viewport_dimensions[0], viewport_dimensions[1]); - - window_resized_publisher.publish - ( - { - window_dimensions[0], - window_dimensions[1], - viewport_dimensions[0], - viewport_dimensions[1] - } - ); -} diff --git a/src/application.hpp b/src/application.hpp index fb57b3f..fd60632 100644 --- a/src/application.hpp +++ b/src/application.hpp @@ -45,7 +45,17 @@ public: /** * Constructs and initializes an application. */ - application(); + application + ( + const std::string& window_title, + int window_x, + int window_y, + int window_w, + int window_h, + bool maximized, + bool fullscreen, + bool v_sync + ); /** * Destructs an application. @@ -87,9 +97,9 @@ public: void resize_window(int width, int height); /** - * Puts the application window into either fullscreen or windowed mode. + * Puts the application window into either fullscreen or window mode. * - * @param fullscreen `true` if the window should be fullscreen, `false` if it should be windowed. + * @param fullscreen `true` if the window should be fullscreen, `false` if it should be window. */ void set_fullscreen(bool fullscreen); @@ -110,16 +120,22 @@ public: void add_game_controller_mappings(const void* mappings, std::size_t size); /// Returns the dimensions of the current display. - [[nodiscard]] const int2& get_display_dimensions() const; + [[nodiscard]] const int2& get_display_size() const; /// Returns the DPI of the display. [[nodiscard]] float get_display_dpi() const; - /// Returns the dimensions of the window. - [[nodiscard]] const int2& get_window_dimensions() const; + /// Returns the position of the window when not maximized or fullscreen. + [[nodiscard]] const int2& get_windowed_position() const; + + /// Returns the dimensions of the window when not maximized or fullscreen. + [[nodiscard]] const int2& get_windowed_size() const; /// Returns the dimensions of the window's drawable viewport. - [[nodiscard]] const int2& get_viewport_dimensions() const; + [[nodiscard]] const int2& get_viewport_size() const; + + /// Returns `true` if the window is maximized, `false` otherwise. + [[nodiscard]] bool is_maximized() const; /// Returns `true` if the window is in fullscreen mode, `false` otherwise. [[nodiscard]] bool is_fullscreen() const; @@ -172,17 +188,38 @@ public: return window_resized_publisher.channel(); } + /// Returns the channel through which window maximized events are published. + [[nodiscard]] inline event::channel& get_window_maximized_channel() noexcept + { + return window_maximized_publisher.channel(); + } + + /// Returns the channel through which window restored events are published. + [[nodiscard]] inline event::channel& get_window_restored_channel() noexcept + { + return window_restored_publisher.channel(); + } + + /// Returns the channel through which window minimized events are published. + [[nodiscard]] inline event::channel& get_window_minimized_channel() noexcept + { + return window_minimized_publisher.channel(); + } + private: + void window_moved(); void window_resized(); bool closed; + bool maximized; bool fullscreen; bool v_sync; bool cursor_visible; - int2 display_dimensions; + int2 display_size; float display_dpi; - int2 window_dimensions; - int2 viewport_dimensions; + int2 windowed_position; + int2 windowed_size; + int2 viewport_size; int2 mouse_position; SDL_Window* sdl_window; @@ -200,11 +237,14 @@ private: event::publisher window_focus_changed_publisher; event::publisher window_moved_publisher; event::publisher window_resized_publisher; + event::publisher window_maximized_publisher; + event::publisher window_restored_publisher; + event::publisher window_minimized_publisher; }; -inline const int2& application::get_display_dimensions() const +inline const int2& application::get_display_size() const { - return display_dimensions; + return display_size; } inline float application::get_display_dpi() const @@ -212,14 +252,24 @@ inline float application::get_display_dpi() const return display_dpi; } -inline const int2& application::get_window_dimensions() const +inline const int2& application::get_windowed_position() const +{ + return windowed_position; +} + +inline const int2& application::get_windowed_size() const +{ + return windowed_size; +} + +inline const int2& application::get_viewport_size() const { - return window_dimensions; + return viewport_size; } -inline const int2& application::get_viewport_dimensions() const +inline bool application::is_maximized() const { - return viewport_dimensions; + return maximized; } inline bool application::is_fullscreen() const diff --git a/src/color/aces.hpp b/src/color/aces.hpp index c3feb05..4683659 100644 --- a/src/color/aces.hpp +++ b/src/color/aces.hpp @@ -31,7 +31,7 @@ namespace aces { /// CIE xy chromaticity coordinates of the ACES white point (~D60). template -constexpr math::vector2 white_point = {T{0.32168}, {0.33767}}; +constexpr math::vector2 white_point = {T{0.32168}, T{0.33767}}; /// ACES AP0 color space. template diff --git a/src/debug/log.cpp b/src/debug/log.cpp index ce4ad3f..ee87942 100644 --- a/src/debug/log.cpp +++ b/src/debug/log.cpp @@ -28,31 +28,5 @@ logger& default_logger() noexcept return instance; } -void push_task(const std::string& description, std::source_location&& location) -{ - default_logger().log(description + " {", message_severity::info, std::forward(location)); -} - -void pop_task(int status, const std::string& description, std::source_location&& location) -{ - std::string message = "} => "; - if (status == EXIT_SUCCESS) - { - message += "success"; - - default_logger().log(std::move(message), message_severity::info, std::forward(location)); - } - else - { - message += "failure"; - if (!description.empty()) - { - message += "(" + description + ")"; - } - - default_logger().log(std::move(message), message_severity::error, std::forward(location)); - } -} - } // namespace log } // namespace debug diff --git a/src/debug/log.hpp b/src/debug/log.hpp index 79a349e..c2d34c1 100644 --- a/src/debug/log.hpp +++ b/src/debug/log.hpp @@ -90,7 +90,8 @@ message(std::string_view, Args&&...) -> message; using trace = message; #else // Disable trace message logging. - inline void trace([[maybe_unused]] ...) noexcept {}; + template + inline void trace([[maybe_unused]] Args&&...) noexcept {}; #endif #if (ANTKEEPER_DEBUG_LOG_MIN_MESSAGE_SEVERITY <= ANTKEEPER_DEBUG_LOG_MESSAGE_SEVERITY_DEBUG) @@ -103,7 +104,8 @@ message(std::string_view, Args&&...) -> message; using debug = message; #else // Disable debug message logging. - inline void debug([[maybe_unused]] ...) noexcept {}; + template + inline void debug([[maybe_unused]] Args&&...) noexcept {}; #endif #if (ANTKEEPER_DEBUG_LOG_MIN_MESSAGE_SEVERITY <= ANTKEEPER_DEBUG_LOG_MESSAGE_SEVERITY_INFO) @@ -116,7 +118,8 @@ message(std::string_view, Args&&...) -> message; using info = message; #else // Disable info message logging. - inline void info([[maybe_unused]] ...) noexcept {}; + template + inline void info([[maybe_unused]] Args&&...) noexcept {}; #endif #if (ANTKEEPER_DEBUG_LOG_MIN_MESSAGE_SEVERITY <= ANTKEEPER_DEBUG_LOG_MESSAGE_SEVERITY_WARNING) @@ -129,7 +132,8 @@ message(std::string_view, Args&&...) -> message; using warning = message; #else // Disable warning message logging. - inline void warning([[maybe_unused]] ...) noexcept {}; + template + inline void warning([[maybe_unused]] Args&&...) noexcept {}; #endif #if (ANTKEEPER_DEBUG_LOG_MIN_MESSAGE_SEVERITY <= ANTKEEPER_DEBUG_LOG_MESSAGE_SEVERITY_ERROR) @@ -142,7 +146,8 @@ message(std::string_view, Args&&...) -> message; using error = message; #else // Disable error message logging. - inline void error([[maybe_unused]] ...) noexcept {}; + template + inline void error([[maybe_unused]] Args&&...) noexcept {}; #endif #if (ANTKEEPER_DEBUG_LOG_MIN_MESSAGE_SEVERITY <= ANTKEEPER_DEBUG_LOG_MESSAGE_SEVERITY_FATAL) @@ -155,34 +160,10 @@ message(std::string_view, Args&&...) -> message; using fatal = message; #else // Disable fatal error message logging. - inline void fatal([[maybe_unused]] ...) noexcept {}; + template + inline void fatal([[maybe_unused]] Args&&...) noexcept {}; #endif -/** - * Pushes a task onto the default logger's task stack and writes its description the log. - * - * @param description Task description. - * @param location Source location from which the message was sent. - */ -void push_task -( - const std::string& description, - std::source_location&& location = std::source_location::current() -); - -/** - * Pops a task off the default logger's task stack and writes its status to the log. - * - * @param status Exit status of the task. A value of `0` or `EXIT_SUCCESS` indicates the task exited successfully. A non-zero exit status indicates the task failed. - * @param description Error code description. - */ -void pop_task -( - int status, - const std::string& description = std::string(), - std::source_location&& location = std::source_location::current() -); - } // namespace log } // namespace debug diff --git a/src/event/queue.hpp b/src/event/queue.hpp index 4ae753e..53b90ba 100644 --- a/src/event/queue.hpp +++ b/src/event/queue.hpp @@ -20,15 +20,15 @@ #ifndef ANTKEEPER_EVENT_QUEUE_HPP #define ANTKEEPER_EVENT_QUEUE_HPP +#include "event/subscriber.hpp" +#include "event/subscription.hpp" #include #include #include #include #include +#include #include -#include "event/subscriber.hpp" -#include "event/subscription.hpp" -#include "utility/type-id.hpp" namespace event { @@ -54,7 +54,7 @@ public: std::shared_ptr shared_subscriber = std::make_shared(std::make_any>(std::move(subscriber))); // Append subscriber to subscriber list and store iterator - auto iterator = subscribers.emplace(type_id, shared_subscriber); + auto iterator = subscribers.emplace(std::type_index(typeid(T)), shared_subscriber); // Construct and return a shared subscription object which removes the subscriber from the subscriber list when unsubscribed or destructed return std::make_shared @@ -126,7 +126,7 @@ private: void distribute(const T& message) const { // For each subscriber of the given message type - const auto range = subscribers.equal_range(type_id); + const auto range = subscribers.equal_range(std::type_index(typeid(T))); for (auto i = range.first; i != range.second; ++i) { // Send message to subscriber @@ -134,7 +134,7 @@ private: } } - std::multimap> subscribers; + std::multimap> subscribers; std::list> messages; }; diff --git a/src/game/context.hpp b/src/game/context.hpp index 5fd91ca..98de396 100644 --- a/src/game/context.hpp +++ b/src/game/context.hpp @@ -42,12 +42,14 @@ #include "render/material-property.hpp" #include "render/material.hpp" #include "resources/json.hpp" -#include "resources/string-table.hpp" #include "scene/scene.hpp" #include "state-machine.hpp" #include "type/bitmap-font.hpp" #include "type/typeface.hpp" +#include "utility/dict.hpp" #include "utility/fundamental-types.hpp" +#include "i18n/string-table.hpp" +#include "i18n/string-map.hpp" #include #include #include @@ -121,6 +123,9 @@ namespace game { /// Container for data shared between game states. struct context { + // Configuration + dict* settings; + /// Hierarchichal state machine hsm::state_machine state_machine; std::function resume_callback; @@ -162,7 +167,8 @@ struct context // Paths std::filesystem::path data_path; - std::filesystem::path config_path; + std::filesystem::path local_config_path; + std::filesystem::path shared_config_path; std::filesystem::path mods_path; std::filesystem::path saves_path; std::filesystem::path screenshots_path; @@ -172,16 +178,11 @@ struct context // Resources resource_manager* resource_manager; - // Configuration - json* config; - // Localization - std::string language_code; - int language_count; - int language_index; - string_table* string_table; - string_table_map string_table_map; - std::unordered_map* strings; + std::uint16_t language_index; + std::uint16_t language_count; + i18n::string_table* string_table; + std::vector string_maps; std::unordered_map typefaces; type::bitmap_font debug_font; type::bitmap_font menu_font; @@ -206,6 +207,7 @@ struct context render::renderer* renderer; int2 render_resolution; float render_scale; + int shadow_map_resolution; gl::vertex_buffer* billboard_vbo; gl::vertex_array* billboard_vao; render::material* fallback_material; @@ -237,8 +239,12 @@ struct context scene::collection* ui_scene; scene::camera* ui_camera; scene::billboard* camera_flash_billboard; - float font_size; + float font_scale; bool dyslexia_font; + float debug_font_size_pt; + float menu_font_size_pt; + float title_font_size_pt; + std::vector> menu_select_callbacks; std::vector> menu_left_callbacks; std::vector> menu_right_callbacks; @@ -310,7 +316,6 @@ struct context const ecoregion* active_ecoregion; - bool bloom_enabled; render::anti_aliasing_method anti_aliasing_method; std::shared_ptr<::event::subscription> ui_resize_subscription; diff --git a/src/game/controls.cpp b/src/game/controls.cpp index 94ff2dc..71e9015 100644 --- a/src/game/controls.cpp +++ b/src/game/controls.cpp @@ -358,7 +358,7 @@ void save_control_profile(game::context& ctx) /* std::filesystem::path path; if (ctx.config->contains("control_profile")) - path = ctx.config_path / "controls" / (*ctx.config)["control_profile"].get(); + path = ctx.shared_config_path / "controls" / (*ctx.config)["control_profile"].get(); debug::log::trace("Saving control profile to \"{}\"...", path.string()); try diff --git a/src/game/fonts.cpp b/src/game/fonts.cpp index f20229e..4dff569 100644 --- a/src/game/fonts.cpp +++ b/src/game/fonts.cpp @@ -25,8 +25,12 @@ #include "gl/texture-filter.hpp" #include "render/material.hpp" #include "render/material-flags.hpp" +#include "utility/hash/fnv1a.hpp" +#include "game/strings.hpp" #include +using namespace hash::literals; + namespace game { static void build_bitmap_font(const type::typeface& typeface, float size, const std::unordered_set& charset, type::bitmap_font& font, render::material& material, gl::shader_program* shader_program) @@ -72,12 +76,9 @@ void load_fonts(game::context& ctx) bool dyslexia_font_loaded = false; if (ctx.dyslexia_font) { - if (auto it = ctx.strings->find("font_dyslexia"); it != ctx.strings->end() && !it->second.empty() && it->second[0] != '#') - { - debug::log::info(it->second); - ctx.typefaces["dyslexia"] = ctx.resource_manager->load(it->second); - dyslexia_font_loaded = true; - } + const auto dyslexia_font_path = get_string(ctx, "font_dyslexia"_fnv1a32); + ctx.typefaces["dyslexia"] = ctx.resource_manager->load(dyslexia_font_path); + dyslexia_font_loaded = true; } // Load typefaces @@ -91,12 +92,13 @@ void load_fonts(game::context& ctx) else { // Load standard typefaces - if (auto it = ctx.strings->find("font_serif"); it != ctx.strings->end()) - ctx.typefaces["serif"] = ctx.resource_manager->load(it->second); - if (auto it = ctx.strings->find("font_sans_serif"); it != ctx.strings->end()) - ctx.typefaces["sans_serif"] = ctx.resource_manager->load(it->second); - if (auto it = ctx.strings->find("font_monospace"); it != ctx.strings->end()) - ctx.typefaces["monospace"] = ctx.resource_manager->load(it->second); + const auto serif_font_path = get_string(ctx, "font_serif"_fnv1a32); + const auto sans_serif_font_path = get_string(ctx, "font_sans_serif"_fnv1a32); + const auto monospace_font_path = get_string(ctx, "font_monospace"_fnv1a32); + + ctx.typefaces["serif"] = ctx.resource_manager->load(serif_font_path); + ctx.typefaces["sans_serif"] = ctx.resource_manager->load(sans_serif_font_path); + ctx.typefaces["monospace"] = ctx.resource_manager->load(monospace_font_path); } // Build character set @@ -107,7 +109,8 @@ void load_fonts(game::context& ctx) charset.insert(code); // Add all character codes from game strings - for (auto it = ctx.strings->begin(); it != ctx.strings->end(); ++it) + const auto& string_map = ctx.string_maps[ctx.language_index]; + for (auto it = string_map.begin(); it != string_map.end(); ++it) { // Convert UTF-8 string to UTF-32 std::wstring_convert, char32_t> convert; @@ -122,47 +125,25 @@ void load_fonts(game::context& ctx) // Load bitmap font shader gl::shader_program* bitmap_font_shader = ctx.resource_manager->load("bitmap-font.glsl"); - // Determine font point sizes - float debug_font_size_pt = 12.0f; - float menu_font_size_pt = 12.0f; - float title_font_size_pt = 12.0f; - if (ctx.config->contains("debug_font_size")) - debug_font_size_pt = (*ctx.config)["debug_font_size"].get(); - if (ctx.config->contains("menu_font_size")) - menu_font_size_pt = (*ctx.config)["menu_font_size"].get(); - if (ctx.config->contains("title_font_size")) - title_font_size_pt = (*ctx.config)["title_font_size"].get(); - - // Scale font point sizes - const float font_size = (*ctx.config)["font_size"].get(); - debug_font_size_pt *= font_size; - menu_font_size_pt *= font_size; - title_font_size_pt *= font_size; - - // Convert font point sizes to pixel sizes - const float dpi = ctx.app->get_display_dpi(); - const float debug_font_size_px = (debug_font_size_pt * dpi) / 72.0f; - const float menu_font_size_px = (menu_font_size_pt * dpi) / 72.0f; - const float title_font_size_px = (title_font_size_pt * dpi) / 72.0f; - - debug::log::info("font size: " + std::to_string(menu_font_size_px)); + // Point size to pixel size conversion factor + const float pt_to_px = (ctx.app->get_display_dpi() / 72.0f) * ctx.font_scale; // Build debug font if (auto it = ctx.typefaces.find("monospace"); it != ctx.typefaces.end()) { - build_bitmap_font(*it->second, debug_font_size_px, charset, ctx.debug_font, ctx.debug_font_material, bitmap_font_shader); + build_bitmap_font(*it->second, ctx.debug_font_size_pt * pt_to_px, charset, ctx.debug_font, ctx.debug_font_material, bitmap_font_shader); } // Build menu font if (auto it = ctx.typefaces.find("sans_serif"); it != ctx.typefaces.end()) { - build_bitmap_font(*it->second, menu_font_size_px, charset, ctx.menu_font, ctx.menu_font_material, bitmap_font_shader); + build_bitmap_font(*it->second, ctx.menu_font_size_pt * pt_to_px, charset, ctx.menu_font, ctx.menu_font_material, bitmap_font_shader); } // Build title font if (auto it = ctx.typefaces.find("serif"); it != ctx.typefaces.end()) { - build_bitmap_font(*it->second, title_font_size_px, charset, ctx.title_font, ctx.title_font_material, bitmap_font_shader); + build_bitmap_font(*it->second, ctx.title_font_size_pt * pt_to_px, charset, ctx.title_font, ctx.title_font_material, bitmap_font_shader); } } diff --git a/src/game/graphics.cpp b/src/game/graphics.cpp index 4dc84bc..9c8910b 100644 --- a/src/game/graphics.cpp +++ b/src/game/graphics.cpp @@ -44,14 +44,9 @@ void create_framebuffers(game::context& ctx) { debug::log::trace("Creating framebuffers..."); - // Load render resolution scale from config - ctx.render_scale = 1.0f; - if (ctx.config->contains("render_scale")) - ctx.render_scale = (*ctx.config)["render_scale"].get(); - // Calculate render resolution - const int2& viewport_dimensions = ctx.app->get_viewport_dimensions(); - ctx.render_resolution = {static_cast(viewport_dimensions.x() * ctx.render_scale + 0.5f), static_cast(viewport_dimensions.y() * ctx.render_scale + 0.5f)}; + const int2& viewport_size = ctx.app->get_viewport_size(); + ctx.render_resolution = {static_cast(viewport_size.x() * ctx.render_scale + 0.5f), static_cast(viewport_size.y() * ctx.render_scale + 0.5f)}; // Create HDR framebuffer (32F color, 32F depth) ctx.hdr_color_texture = new gl::texture_2d(ctx.render_resolution.x(), ctx.render_resolution.y(), gl::pixel_type::float_32, gl::pixel_format::rgb); @@ -82,17 +77,12 @@ void create_framebuffers(game::context& ctx) ctx.ldr_framebuffer_b = new gl::framebuffer(ctx.render_resolution.x(), ctx.render_resolution.y()); ctx.ldr_framebuffer_b->attach(gl::framebuffer_attachment_type::color, ctx.ldr_color_texture_b); - // Load shadow map resolution from config - int shadow_map_resolution = 4096; - if (ctx.config->contains("shadow_map_resolution")) - shadow_map_resolution = (*ctx.config)["shadow_map_resolution"].get(); - // Create shadow map framebuffer - ctx.shadow_map_depth_texture = new gl::texture_2d(shadow_map_resolution, shadow_map_resolution, gl::pixel_type::float_32, gl::pixel_format::d); + ctx.shadow_map_depth_texture = new gl::texture_2d(ctx.shadow_map_resolution, ctx.shadow_map_resolution, gl::pixel_type::float_32, gl::pixel_format::d); ctx.shadow_map_depth_texture->set_wrapping(gl::texture_wrapping::extend, gl::texture_wrapping::extend); ctx.shadow_map_depth_texture->set_filters(gl::texture_min_filter::linear, gl::texture_mag_filter::linear); ctx.shadow_map_depth_texture->set_max_anisotropy(0.0f); - ctx.shadow_map_framebuffer = new gl::framebuffer(shadow_map_resolution, shadow_map_resolution); + ctx.shadow_map_framebuffer = new gl::framebuffer(ctx.shadow_map_resolution, ctx.shadow_map_resolution); ctx.shadow_map_framebuffer->attach(gl::framebuffer_attachment_type::depth, ctx.shadow_map_depth_texture); debug::log::trace("Created framebuffers"); @@ -138,8 +128,8 @@ void change_render_resolution(game::context& ctx, float scale) ctx.render_scale = scale; // Recalculate render resolution - const int2& viewport_dimensions = ctx.app->get_viewport_dimensions(); - ctx.render_resolution = {static_cast(viewport_dimensions.x() * ctx.render_scale + 0.5f), static_cast(viewport_dimensions.y() * ctx.render_scale + 0.5f)}; + const int2& viewport_size = ctx.app->get_viewport_size(); + ctx.render_resolution = {static_cast(viewport_size.x() * ctx.render_scale + 0.5f), static_cast(viewport_size.y() * ctx.render_scale + 0.5f)}; // Resize HDR framebuffer and attachments ctx.hdr_framebuffer->resize({ctx.render_resolution.x(), ctx.render_resolution.y()}); @@ -156,7 +146,7 @@ void change_render_resolution(game::context& ctx, float scale) ctx.bloom_pass->resize(); // Enable or disable resample pass - if (viewport_dimensions.x() != ctx.render_resolution.x() || viewport_dimensions.y() != ctx.render_resolution.y()) + if (viewport_size.x() != ctx.render_resolution.x() || viewport_size.y() != ctx.render_resolution.y()) { ctx.resample_pass->set_enabled(true); } @@ -176,21 +166,21 @@ void save_screenshot(game::context& ctx) const std::string screenshot_filename = std::format("{0}-{1:%Y%m%d}T{1:%H%M%S}Z.png", config::application_name, time); // Determine path to screenshot file - std::filesystem::path screenshot_filepath = ctx.config_path / "gallery" / screenshot_filename; + std::filesystem::path screenshot_filepath = ctx.screenshots_path / screenshot_filename; std::string screenshot_filepath_string = screenshot_filepath.string(); debug::log::debug("Saving screenshot to \"{}\"...", screenshot_filepath_string); // Get viewport dimensions - const int2& viewport_dimensions = ctx.app->get_viewport_dimensions(); + const int2& viewport_size = ctx.app->get_viewport_size(); // Allocate screenshot image std::shared_ptr frame = std::make_shared(); frame->format(1, 3); - frame->resize(viewport_dimensions.x(), viewport_dimensions.y()); + frame->resize(viewport_size.x(), viewport_size.y()); // Read pixel data from backbuffer into image glReadBuffer(GL_BACK); - glReadPixels(0, 0, viewport_dimensions.x(), viewport_dimensions.y(), GL_RGB, GL_UNSIGNED_BYTE, frame->data()); + glReadPixels(0, 0, viewport_size.x(), viewport_size.y(), GL_RGB, GL_UNSIGNED_BYTE, frame->data()); // Write screenshot file in separate thread std::thread @@ -207,24 +197,6 @@ void save_screenshot(game::context& ctx) } -void toggle_bloom(game::context& ctx, bool enabled) -{ - if (enabled) - { - ctx.bloom_pass->set_mip_chain_length(6); - ctx.bloom_pass->set_enabled(true); - ctx.common_final_pass->set_bloom_weight(0.04f); - } - else - { - ctx.bloom_pass->set_mip_chain_length(0); - ctx.bloom_pass->set_enabled(false); - ctx.common_final_pass->set_bloom_weight(0.0f); - } - - ctx.bloom_enabled = enabled; -} - void select_anti_aliasing_method(game::context& ctx, render::anti_aliasing_method method) { // Switch AA method diff --git a/src/game/save.cpp b/src/game/save.cpp deleted file mode 100644 index e3bd578..0000000 --- a/src/game/save.cpp +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright (C) 2023 Christopher J. Howard - * - * This file is part of Antkeeper source code. - * - * Antkeeper source code is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Antkeeper source code is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Antkeeper source code. If not, see . - */ - -#include "game/save.hpp" -#include "application.hpp" -#include "debug/log.hpp" -#include "resources/json.hpp" -#include - -namespace game { -namespace save { - -void colony(game::context& ctx) -{ - std::filesystem::path path = ctx.saves_path / "colony.sav"; - const std::string path_string = path.string(); - - debug::log::trace("Saving colony to \"{}\"...", path_string); - try - { - // Construct JSON data describing the colony - json data; - - auto& colony = data["colony"]; - { - auto& species = colony["species"]; - { - auto& morphology = species["morphology"]; - { - - } - - auto& diet = species["diet"]; - auto& aggression = species["aggression"]; - auto& nest = species["nest"]; - } - - auto& habitat = colony["habitat"]; - { - auto& biome = habitat["biome"]; - auto& nest = habitat["nest"]; - { - nest["entrance"] = {0, 0, 0}; - } - } - - auto& members = colony["members"]; - members = json::array(); - { - - } - } - - std::ofstream file(path); - file << data; - - debug::log::trace("Saved colony to \"{}\"", path_string); - } - catch (...) - { - debug::log::error("Failed to save colony to \"{}\"", path_string); - } -} - -void config(game::context& ctx) -{ - std::filesystem::path path = ctx.config_path / "config.json"; - const std::string path_string = path.string(); - - debug::log::trace("Saving config to \"{}\"...", path_string); - try - { - std::ofstream file(path); - file << *(ctx.config); - - debug::log::trace("Saved config to \"{}\"", path_string); - } - catch (...) - { - debug::log::error("Failed to save config to \"{}\"", path_string); - } -} - -} // namespace save -} // namespace game diff --git a/src/game/settings.hpp b/src/game/settings.hpp new file mode 100644 index 0000000..c68887c --- /dev/null +++ b/src/game/settings.hpp @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2023 Christopher J. Howard + * + * This file is part of Antkeeper source code. + * + * Antkeeper source code is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Antkeeper source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Antkeeper source code. If not, see . + */ + +#ifndef ANTKEEPER_GAME_SETTINGS_HPP +#define ANTKEEPER_GAME_SETTINGS_HPP + +#include "game/context.hpp" +#include "debug/log.hpp" + +namespace game { + +/** + * Reads a setting if found, inserts a setting if not found, and overwrites a setting if a type mismatch occurs. + * + * @tparam T Setting value type. + * + * @param[in] ctx Game context. + * @param[in] key Setting key. + * @param[in,out] Setting value. + * + * @return `true` if the setting was read, `false` if the setting was written. + */ +template +bool read_or_write_setting(game::context& ctx, std::uint32_t key, T& value) +{ + if (auto i = ctx.settings->find(key); i != ctx.settings->end()) + { + try + { + value = std::any_cast(i->second); + } + catch (const std::bad_any_cast& e) + { + debug::log::error("Setting type mismatch ({:x}={})", key, value); + i->second = value; + return false; + } + } + else + { + debug::log::trace("Setting key not found ({:x}={})", key, value); + (*ctx.settings)[key] = value; + return false; + } + + return true; +} + +} // namespace game + +#endif // ANTKEEPER_GAME_SETTINGS_HPP diff --git a/src/game/state/boot.cpp b/src/game/state/boot.cpp index 303853e..32215d1 100644 --- a/src/game/state/boot.cpp +++ b/src/game/state/boot.cpp @@ -33,7 +33,7 @@ #include "game/fonts.hpp" #include "game/graphics.hpp" #include "game/menu.hpp" -#include "game/save.hpp" +#include "game/strings.hpp" #include "game/state/boot.hpp" #include "game/state/splash.hpp" #include "game/system/astronomy.hpp" @@ -52,6 +52,7 @@ #include "game/system/subterrain.hpp" #include "game/system/terrain.hpp" #include "game/system/vegetation.hpp" +#include "game/settings.hpp" #include "gl/framebuffer.hpp" #include "gl/pixel-format.hpp" #include "gl/pixel-type.hpp" @@ -86,6 +87,8 @@ #include "resources/resource-manager.hpp" #include "scene/scene.hpp" #include "utility/paths.hpp" +#include "utility/dict.hpp" +#include "utility/hash/fnv1a.hpp" #include #include #include @@ -95,6 +98,8 @@ #include #include +using namespace hash::literals; + namespace game { namespace state { @@ -104,16 +109,50 @@ boot::boot(game::context& ctx, int argc, char** argv): // Boot process debug::log::trace("Booting up..."); - // Allocate application - ctx.app = new application(); - - // Parse command line options - parse_options(argc, argv); + // Parse command line arguments + parse_arguments(argc, argv); + // Setup resource management setup_resources(); - load_config(); - load_strings(); + + // Load settings + load_settings(); + + // Default window settings + std::string window_title = config::application_name; + int window_x = -1; + int window_y = -1; + int window_w = -1; + int window_h = -1; + bool maximized = true; + bool fullscreen = true; + bool v_sync = true; + + // Read window settings + read_or_write_setting(ctx, "window_title"_fnv1a32, window_title); + read_or_write_setting(ctx, "window_x"_fnv1a32, window_x); + read_or_write_setting(ctx, "window_y"_fnv1a32, window_y); + read_or_write_setting(ctx, "window_w"_fnv1a32, window_w); + read_or_write_setting(ctx, "window_h"_fnv1a32, window_h); + read_or_write_setting(ctx, "maximized"_fnv1a32, maximized); + read_or_write_setting(ctx, "fullscreen"_fnv1a32, fullscreen); + read_or_write_setting(ctx, "v_sync"_fnv1a32, v_sync); + + // Allocate application + ctx.app = new application + ( + window_title, + window_x, + window_y, + window_w, + window_h, + maximized, + fullscreen, + v_sync + ); + setup_window(); + load_strings(); setup_rendering(); setup_audio(); setup_scenes(); @@ -140,6 +179,21 @@ boot::~boot() { debug::log::trace("Booting down..."); + // Update window settings + const auto& windowed_position = ctx.app->get_windowed_position(); + const auto& windowed_size = ctx.app->get_windowed_size(); + const bool maximized = ctx.app->is_maximized(); + const bool fullscreen = ctx.app->is_fullscreen(); + (*ctx.settings)["window_x"_fnv1a32] = windowed_position.x(); + (*ctx.settings)["window_y"_fnv1a32] = windowed_position.y(); + (*ctx.settings)["window_w"_fnv1a32] = windowed_size.x(); + (*ctx.settings)["window_h"_fnv1a32] = windowed_size.y(); + (*ctx.settings)["maximized"_fnv1a32] = maximized; + (*ctx.settings)["fullscreen"_fnv1a32] = fullscreen; + + // Save settings + ctx.resource_manager->save>(ctx.settings, "settings.cfg"); + shutdown_audio(); // Close application @@ -149,7 +203,7 @@ boot::~boot() debug::log::trace("Boot down complete"); } -void boot::parse_options(int argc, char** argv) +void boot::parse_arguments(int argc, char** argv) { debug::log::trace("Parsing {} command line arguments...", argc); @@ -163,8 +217,8 @@ void boot::parse_options(int argc, char** argv) ("n,new-game", "Starts a new game") ("q,quick-start", "Skips to the main menu") ("r,reset", "Restores all settings to default") - ("v,vsync", "Enables or disables v-sync", cxxopts::value()) - ("w,windowed", "Starts in windowed mode"); + ("v,v_sync", "Enables or disables v-sync", cxxopts::value()) + ("w,window", "Starts in window mode"); auto result = options.parse(argc, argv); // --continue @@ -191,12 +245,12 @@ void boot::parse_options(int argc, char** argv) if (result.count("reset")) option_reset = true; - // --vsync - if (result.count("vsync")) - option_v_sync = (result["vsync"].as()) ? true : false; + // --v_sync + if (result.count("v_sync")) + option_v_sync = (result["v_sync"].as()) ? true : false; - // --windowed - if (result.count("windowed")) + // --window + if (result.count("window")) option_windowed = true; debug::log::trace("Parsed {} command line arguments", argc); @@ -207,27 +261,43 @@ void boot::parse_options(int argc, char** argv) } } +void boot::load_settings() +{ + ctx.settings = ctx.resource_manager->load>("settings.cfg"); + if (!ctx.settings) + { + debug::log::info("Settings not found"); + ctx.settings = new dict(); + } +} + void boot::setup_resources() { // Setup resource manager ctx.resource_manager = new resource_manager(); // Detect paths - ctx.data_path = get_data_path(config::application_name); - ctx.config_path = get_config_path(config::application_name); - ctx.mods_path = ctx.config_path / "mods"; - ctx.saves_path = ctx.config_path / "saves"; - ctx.screenshots_path = ctx.config_path / "gallery"; - ctx.controls_path = ctx.config_path / "controls"; + ctx.data_path = get_executable_data_path(); + ctx.local_config_path = get_local_config_path() / config::application_name; + ctx.shared_config_path = get_shared_config_path() / config::application_name; + ctx.mods_path = ctx.data_path / "mods"; + ctx.saves_path = ctx.shared_config_path / "saves"; + ctx.screenshots_path = ctx.shared_config_path / "gallery"; + ctx.controls_path = ctx.shared_config_path / "controls"; // Log resource paths debug::log::info("Data path: \"{}\"", ctx.data_path.string()); - debug::log::info("Config path: \"{}\"", ctx.config_path.string()); + debug::log::info("Local config path: \"{}\"", ctx.local_config_path.string()); + debug::log::info("Shared config path: \"{}\"", ctx.shared_config_path.string()); + + // Set write dir + ctx.resource_manager->set_write_dir(ctx.shared_config_path); // Create nonexistent config directories std::vector config_paths; - config_paths.push_back(ctx.config_path); - config_paths.push_back(ctx.mods_path); + config_paths.push_back(ctx.local_config_path); + config_paths.push_back(ctx.shared_config_path); + //config_paths.push_back(ctx.mods_path); config_paths.push_back(ctx.saves_path); config_paths.push_back(ctx.screenshots_path); config_paths.push_back(ctx.controls_path); @@ -251,10 +321,16 @@ void boot::setup_resources() // Scan for mods std::vector mod_paths; - for (const auto& directory_entry: std::filesystem::directory_iterator(ctx.mods_path)) + if (std::filesystem::is_directory(ctx.mods_path)) { - if (directory_entry.is_directory()) - mod_paths.push_back(directory_entry.path()); + for (const auto& entry: std::filesystem::directory_iterator{ctx.mods_path}) + { + if (entry.is_directory() || (entry.is_regular_file() && entry.path().extension() == ".zip")) + { + mod_paths.push_back(entry.path()); + debug::log::info("Found mod \"{}\"", entry.path().filename().string()); + } + } } // Determine data package path @@ -271,10 +347,13 @@ void boot::setup_resources() // Mount mods for (const std::filesystem::path& mod_path: mod_paths) + { ctx.resource_manager->mount(ctx.mods_path / mod_path); + } // Mount config path - ctx.resource_manager->mount(ctx.config_path); + ctx.resource_manager->mount(ctx.local_config_path); + ctx.resource_manager->mount(ctx.shared_config_path); // Mount data package ctx.resource_manager->mount(ctx.data_package_path); @@ -284,110 +363,74 @@ void boot::setup_resources() ctx.resource_manager->include("/"); } -void boot::load_config() +void boot::setup_window() { - debug::log::trace("Loading config..."); + // debug::log::trace("Setting up window..."); - // Load config file - ctx.config = ctx.resource_manager->load("config.json"); - if (ctx.config) - { - debug::log::trace("Loaded config"); - } - else - { - debug::log::error("Failed to load config"); - } + // application* app = ctx.app; + + // ctx.app->get_rasterizer()->set_clear_color(0.0f, 0.0f, 0.0f, 1.0f); + // ctx.app->get_rasterizer()->clear_framebuffer(true, false, false); + // app->show_window(); + // ctx.app->swap_buffers(); + + // debug::log::trace("Set up window"); } void boot::load_strings() { debug::log::trace("Loading strings..."); - ctx.string_table = ctx.resource_manager->load("strings.csv"); + // Default strings settings + ctx.language_index = 0; + + // Read strings settings + read_or_write_setting(ctx, "language_index"_fnv1a32, ctx.language_index); + + // Load string table + ctx.string_table = ctx.resource_manager->load("strings.tsv"); - build_string_table_map(&ctx.string_table_map, *ctx.string_table); + // Count languages + ctx.language_count = static_cast((*ctx.string_table)[0].size() - 2); - ctx.language_code = (*ctx.config)["language"].get(); - ctx.language_index = -1; - for (int i = 2; i < (*ctx.string_table)[0].size(); ++i) + if (ctx.language_index >= ctx.language_count) { - if ((*ctx.string_table)[0][i] == ctx.language_code) - ctx.language_index = i - 2; + debug::log::error("Language index ({}) exceeds language count ({}). Language index reset to 0", ctx.language_index, ctx.language_count); + ctx.language_index = 0; + (*ctx.settings)["language_index"_fnv1a32] = ctx.language_index; } - ctx.language_count = (*ctx.string_table)[0].size() - 2; - debug::log::info("Languages available: {}", ctx.language_count); - debug::log::info("Language index: {}", ctx.language_index); - debug::log::info("Language code: {}", ctx.language_code); + // Build string map + ctx.string_maps.resize(ctx.language_count); + i18n::build_string_map(*ctx.string_table, 0, ctx.language_index + 2, ctx.string_maps[ctx.language_index]); - ctx.strings = &ctx.string_table_map[ctx.language_code]; + // Log language info + debug::log::info("Language index: {}; code: {}", ctx.language_index, get_string(ctx, "language_code"_fnv1a32)); - debug::log::trace("Loaded strings"); -} - -void boot::setup_window() -{ - debug::log::trace("Setting up window..."); - - application* app = ctx.app; - json* config = ctx.config; + // Change window title + const std::string window_title = get_string(ctx, "application_title"_fnv1a32); + ctx.app->set_title(window_title); - // Set fullscreen or windowed mode - bool fullscreen = true; - if (option_fullscreen.has_value()) - fullscreen = true; - else if (option_windowed.has_value()) - fullscreen = false; - else if (config->contains("fullscreen")) - fullscreen = (*config)["fullscreen"].get(); - app->set_fullscreen(fullscreen); - - // Set resolution - const auto& display_dimensions = ctx.app->get_display_dimensions(); - int2 resolution = {display_dimensions[0], display_dimensions[1]}; - if (fullscreen) - { - if (config->contains("fullscreen_resolution")) - { - resolution.x() = (*config)["fullscreen_resolution"][0].get(); - resolution.y() = (*config)["fullscreen_resolution"][1].get(); - } - } - else - { - if (config->contains("windowed_resolution")) - { - resolution.x() = (*config)["windowed_resolution"][0].get(); - resolution.y() = (*config)["windowed_resolution"][1].get(); - } - } - app->resize_window(resolution.x(), resolution.y()); + // Update window title setting + (*ctx.settings)["window_title"_fnv1a32] = window_title; - // Set v-sync - bool v_sync = true; - if (option_v_sync.has_value()) - v_sync = (option_v_sync.value() != 0); - else if (config->contains("v_sync")) - v_sync = (*config)["v_sync"].get(); - app->set_v_sync(v_sync); - - // Set title - app->set_title((*ctx.strings)["application_title"]); - - // Show window - ctx.app->get_rasterizer()->set_clear_color(0.0f, 0.0f, 0.0f, 1.0f); - ctx.app->get_rasterizer()->clear_framebuffer(true, false, false); - app->show_window(); - ctx.app->swap_buffers(); - - debug::log::trace("Set up window"); + debug::log::trace("Loaded strings"); } void boot::setup_rendering() { debug::log::trace("Setting up rendering..."); + // Default rendering settings + ctx.render_scale = 1.0f; + ctx.anti_aliasing_method = render::anti_aliasing_method::fxaa; + ctx.shadow_map_resolution = 4096; + + // Read rendering settings + read_or_write_setting(ctx, "render_scale"_fnv1a32, ctx.render_scale); + read_or_write_setting(ctx, "anti_aliasing_method"_fnv1a32, *reinterpret_cast*>(&ctx.anti_aliasing_method)); + read_or_write_setting(ctx, "shadow_map_resolution"_fnv1a32, ctx.shadow_map_resolution); + // Get rasterizer from application ctx.rasterizer = ctx.app->get_rasterizer(); @@ -421,38 +464,11 @@ void boot::setup_rendering() ctx.resample_pass->set_source_texture(ctx.ldr_color_texture_b); ctx.resample_pass->set_enabled(false); - // Toggle bloom according to settings - ctx.bloom_enabled = true; - if (ctx.config->contains("bloom_enabled")) - ctx.bloom_enabled = (*ctx.config)["bloom_enabled"].get(); - graphics::toggle_bloom(ctx, ctx.bloom_enabled); - // Configure anti-aliasing according to settings - ctx.anti_aliasing_method = render::anti_aliasing_method::fxaa; - if (ctx.config->contains("anti_aliasing_method")) - { - const std::string aa_method = (*ctx.config)["anti_aliasing_method"].get(); - if (aa_method == "fxaa") - { - ctx.anti_aliasing_method = render::anti_aliasing_method::fxaa; - } - else - { - ctx.anti_aliasing_method = render::anti_aliasing_method::none; - } - } graphics::select_anti_aliasing_method(ctx, ctx.anti_aliasing_method); // Configure render scaling according to settings - ctx.render_scale = 1.0f; - if (ctx.config->contains("render_scale")) - { - ctx.render_scale = (*ctx.config)["render_scale"].get(); - if (ctx.render_scale != 1.0f) - { - graphics::change_render_resolution(ctx, ctx.render_scale); - } - } + graphics::change_render_resolution(ctx, ctx.render_scale); } // Setup UI compositor @@ -593,35 +609,21 @@ void boot::setup_audio() { debug::log::trace("Setting up audio..."); - // Load master volume config + // Default audio settings ctx.master_volume = 1.0f; - if (ctx.config->contains("master_volume")) - ctx.master_volume = (*ctx.config)["master_volume"].get(); - - // Load ambience volume config ctx.ambience_volume = 1.0f; - if (ctx.config->contains("ambience_volume")) - ctx.ambience_volume = (*ctx.config)["ambience_volume"].get(); - - // Load effects volume config ctx.effects_volume = 1.0f; - if (ctx.config->contains("effects_volume")) - ctx.effects_volume = (*ctx.config)["effects_volume"].get(); - - // Load mono audio config ctx.mono_audio = false; - if (ctx.config->contains("mono_audio")) - ctx.mono_audio = (*ctx.config)["mono_audio"].get(); - - // Load captions config ctx.captions = false; - if (ctx.config->contains("captions")) - ctx.captions = (*ctx.config)["captions"].get(); - - // Load captions size config ctx.captions_size = 1.0f; - if (ctx.config->contains("captions_size")) - ctx.captions_size = (*ctx.config)["captions_size"].get(); + + // Read audio settings + read_or_write_setting(ctx, "master_volume"_fnv1a32, ctx.master_volume); + read_or_write_setting(ctx, "ambience_volume"_fnv1a32, ctx.ambience_volume); + read_or_write_setting(ctx, "effects_volume"_fnv1a32, ctx.effects_volume); + read_or_write_setting(ctx, "mono_audio"_fnv1a32, ctx.mono_audio); + read_or_write_setting(ctx, "captions"_fnv1a32, ctx.captions); + read_or_write_setting(ctx, "captions_size"_fnv1a32, ctx.captions_size); // Open audio device debug::log::trace("Opening audio device..."); @@ -683,17 +685,16 @@ void boot::setup_scenes() debug::log::trace("Setting up scenes..."); // Get default framebuffer - const auto& viewport_dimensions = ctx.rasterizer->get_default_framebuffer().get_dimensions(); - const float viewport_aspect_ratio = static_cast(viewport_dimensions[0]) / static_cast(viewport_dimensions[1]); + const auto& viewport_size = ctx.app->get_viewport_size(); + const float viewport_aspect_ratio = static_cast(viewport_size[0]) / static_cast(viewport_size[1]); // Setup UI camera ctx.ui_camera = new scene::camera(); ctx.ui_camera->set_compositor(ctx.ui_compositor); - auto viewport = ctx.app->get_viewport_dimensions(); - float clip_left = -viewport[0] * 0.5f; - float clip_right = viewport[0] * 0.5f; - float clip_top = -viewport[1] * 0.5f; - float clip_bottom = viewport[1] * 0.5f; + float clip_left = -viewport_size[0] * 0.5f; + float clip_right = viewport_size[0] * 0.5f; + float clip_top = -viewport_size[1] * 0.5f; + float clip_bottom = viewport_size[1] * 0.5f; float clip_near = 0.0f; float clip_far = 1000.0f; ctx.ui_camera->set_orthographic(clip_left, clip_right, clip_top, clip_bottom, clip_near, clip_far); @@ -728,7 +729,7 @@ void boot::setup_scenes() ctx.menu_bg_billboard = new scene::billboard(); ctx.menu_bg_billboard->set_active(false); ctx.menu_bg_billboard->set_material(menu_bg_material); - ctx.menu_bg_billboard->set_scale({(float)viewport_dimensions[0] * 0.5f, (float)viewport_dimensions[1] * 0.5f, 1.0f}); + ctx.menu_bg_billboard->set_scale({(float)viewport_size[0] * 0.5f, (float)viewport_size[1] * 0.5f, 1.0f}); ctx.menu_bg_billboard->set_translation({0.0f, 0.0f, -100.0f}); ctx.menu_bg_billboard->update_tweens(); @@ -744,7 +745,7 @@ void boot::setup_scenes() ctx.camera_flash_billboard = new scene::billboard(); ctx.camera_flash_billboard->set_material(flash_material); - ctx.camera_flash_billboard->set_scale({(float)viewport_dimensions[0] * 0.5f, (float)viewport_dimensions[1] * 0.5f, 1.0f}); + ctx.camera_flash_billboard->set_scale({(float)viewport_size[0] * 0.5f, (float)viewport_size[1] * 0.5f, 1.0f}); ctx.camera_flash_billboard->set_translation({0.0f, 0.0f, 0.0f}); ctx.camera_flash_billboard->update_tweens(); @@ -882,8 +883,8 @@ void boot::setup_entities() void boot::setup_systems() { - const auto& viewport_dimensions = ctx.app->get_viewport_dimensions(); - float4 viewport = {0.0f, 0.0f, static_cast(viewport_dimensions[0]), static_cast(viewport_dimensions[1])}; + const auto& viewport_size = ctx.app->get_viewport_size(); + float4 viewport = {0.0f, 0.0f, static_cast(viewport_size[0]), static_cast(viewport_size[1])}; // Setup terrain system ctx.terrain_system = new game::system::terrain(*ctx.entity_registry); @@ -977,18 +978,16 @@ void boot::setup_controls() debug::log::trace("Loading controls..."); try { - // If a control profile is set in the config file - if (ctx.config->contains("control_profile")) - { - // Load control profile - json* profile = ctx.resource_manager->load((*ctx.config)["control_profile"].get()); + // Load control profile + // if (ctx.config->contains("control_profile")) + // { + // json* profile = ctx.resource_manager->load((*ctx.config)["control_profile"].get()); - // Apply control profile - if (profile) - { - game::apply_control_profile(ctx, *profile); - } - } + // if (profile) + // { + // game::apply_control_profile(ctx, *profile); + // } + // } // Calibrate gamepads // for (input::gamepad* gamepad: ctx.app->get_gamepads()) @@ -1026,20 +1025,17 @@ void boot::setup_controls() { bool fullscreen = !ctx.app->is_fullscreen(); + // Toggle fullscreen ctx.app->set_fullscreen(fullscreen); + // Update fullscreen setting + (*ctx.settings)["fullscreen"_fnv1a32] = fullscreen; + if (!fullscreen) { - int2 resolution; - resolution.x() = (*ctx.config)["windowed_resolution"][0].get(); - resolution.y() = (*ctx.config)["windowed_resolution"][1].get(); - - ctx.app->resize_window(resolution.x(), resolution.y()); + // Restore window size and position + //ctx.app->resize_window(resolution.x(), resolution.y()); } - - // Save display mode config - (*ctx.config)["fullscreen"] = fullscreen; - game::save::config(ctx); } ) ); @@ -1081,15 +1077,19 @@ void boot::setup_controls() void boot::setup_ui() { - // Load font size config - ctx.font_size = 1.0f; - if (ctx.config->contains("font_size")) - ctx.font_size = (*ctx.config)["font_size"].get(); - - // Load dyslexia font config + // Default UI settings + ctx.font_scale = 1.0f; + ctx.debug_font_size_pt = 12.0f; + ctx.menu_font_size_pt = 22.0f; + ctx.title_font_size_pt = 80.0f; ctx.dyslexia_font = false; - if (ctx.config->contains("dyslexia_font")) - ctx.dyslexia_font = (*ctx.config)["dyslexia_font"].get(); + + // Read UI settings + read_or_write_setting(ctx, "font_scale"_fnv1a32, ctx.font_scale); + read_or_write_setting(ctx, "debug_font_size_pt"_fnv1a32, ctx.debug_font_size_pt); + read_or_write_setting(ctx, "menu_font_size_pt"_fnv1a32, ctx.menu_font_size_pt); + read_or_write_setting(ctx, "title_font_size_pt"_fnv1a32, ctx.title_font_size_pt); + read_or_write_setting(ctx, "dyslexia_font"_fnv1a32, ctx.dyslexia_font); // Load fonts debug::log::trace("Loading fonts..."); @@ -1108,10 +1108,10 @@ void boot::setup_ui() ( [&](const auto& event) { - const float clip_left = static_cast(event.viewport_width) * -0.5f; - const float clip_right = static_cast(event.viewport_width) * 0.5f; - const float clip_top = static_cast(event.viewport_height) * -0.5f; - const float clip_bottom = static_cast(event.viewport_height) * 0.5f; + const float clip_left = static_cast(event.viewport_size.x()) * -0.5f; + const float clip_right = static_cast(event.viewport_size.x()) * 0.5f; + const float clip_top = static_cast(event.viewport_size.y()) * -0.5f; + const float clip_bottom = static_cast(event.viewport_size.y()) * 0.5f; const float clip_near = ctx.ui_camera->get_clip_near(); const float clip_far = ctx.ui_camera->get_clip_far(); @@ -1131,11 +1131,14 @@ void boot::setup_debugging() void boot::setup_loop() { + // Default loop settings + double update_frequency = 60.0; + + // Read loop settings + read_or_write_setting(ctx, "update_frequency"_fnv1a32, update_frequency); + // Set update frequency - if (ctx.config->contains("update_frequency")) - { - ctx.loop.set_update_frequency((*ctx.config)["update_frequency"].get()); - } + ctx.loop.set_update_frequency(update_frequency); // Set update callback ctx.loop.set_update_callback diff --git a/src/game/state/boot.hpp b/src/game/state/boot.hpp index a183ec5..21d0495 100644 --- a/src/game/state/boot.hpp +++ b/src/game/state/boot.hpp @@ -48,9 +48,9 @@ public: virtual ~boot(); private: - void parse_options(int argc, char** argv); + void parse_arguments(int argc, char** argv); void setup_resources(); - void load_config(); + void load_settings(); void load_strings(); void setup_window(); void setup_rendering(); diff --git a/src/game/state/controls-menu.cpp b/src/game/state/controls-menu.cpp index 36397d7..52d9874 100644 --- a/src/game/state/controls-menu.cpp +++ b/src/game/state/controls-menu.cpp @@ -25,6 +25,10 @@ #include "scene/text.hpp" #include "debug/log.hpp" #include "game/menu.hpp" +#include "game/strings.hpp" +#include "utility/hash/fnv1a.hpp" + +using namespace hash::literals; namespace game { namespace state { @@ -32,7 +36,7 @@ namespace state { controls_menu::controls_menu(game::context& ctx): game::state::base(ctx) { - debug::log::push_task("Entering controls menu state"); + debug::log::trace("Entering controls menu state..."); // Construct menu item texts scene::text* keyboard_text = new scene::text(); @@ -45,9 +49,9 @@ controls_menu::controls_menu(game::context& ctx): ctx.menu_item_texts.push_back({back_text, nullptr}); // Set content of menu item texts - keyboard_text->set_content((*ctx.strings)["controls_menu_keyboard"]); - gamepad_text->set_content((*ctx.strings)["controls_menu_gamepad"]); - back_text->set_content((*ctx.strings)["back"]); + keyboard_text->set_content(get_string(ctx, "controls_menu_keyboard"_fnv1a32)); + gamepad_text->set_content(get_string(ctx, "controls_menu_gamepad"_fnv1a32)); + back_text->set_content(get_string(ctx, "back"_fnv1a32)); // Init menu item index game::menu::init_menu_item_index(ctx, "controls"); @@ -151,12 +155,12 @@ controls_menu::controls_menu(game::context& ctx): // Fade in menu game::menu::fade_in(ctx, nullptr); - debug::log::pop_task(EXIT_SUCCESS); + debug::log::trace("Entered controls menu state"); } controls_menu::~controls_menu() { - debug::log::push_task("Exiting options menu state"); + debug::log::trace("Exiting options menu state..."); // Destruct menu game::menu::clear_controls(ctx); @@ -165,7 +169,7 @@ controls_menu::~controls_menu() game::menu::remove_text_from_ui(ctx); game::menu::delete_text(ctx); - debug::log::pop_task(EXIT_SUCCESS); + debug::log::trace("Exited controls menu state"); } } // namespace state diff --git a/src/game/state/credits.cpp b/src/game/state/credits.cpp index 39d9a88..27351f4 100644 --- a/src/game/state/credits.cpp +++ b/src/game/state/credits.cpp @@ -26,6 +26,10 @@ #include "application.hpp" #include "scene/text.hpp" #include "debug/log.hpp" +#include "game/strings.hpp" +#include "utility/hash/fnv1a.hpp" + +using namespace hash::literals; namespace game { namespace state { @@ -33,13 +37,13 @@ namespace state { credits::credits(game::context& ctx): game::state::base(ctx) { - debug::log::push_task("Entering credits state"); + debug::log::trace("Entering credits state..."); // Construct credits text credits_text.set_material(&ctx.menu_font_material); credits_text.set_font(&ctx.menu_font); credits_text.set_color({1.0f, 1.0f, 1.0f, 0.0f}); - credits_text.set_content((*ctx.strings)["credits"]); + credits_text.set_content(get_string(ctx, "credits"_fnv1a32)); // Align credits text const auto& credits_aabb = static_cast&>(credits_text.get_local_bounds()); @@ -48,13 +52,9 @@ credits::credits(game::context& ctx): credits_text.set_translation({std::round(-credits_w * 0.5f), std::round(-credits_h * 0.5f), 0.0f}); credits_text.update_tweens(); - // Load animation timing configuration - double credits_fade_in_duration = 0.0; - double credits_scroll_duration = 0.0; - if (ctx.config->contains("credits_fade_in_duration")) - credits_fade_in_duration = (*ctx.config)["credits_fade_in_duration"].get(); - if (ctx.config->contains("credits_scroll_duration")) - credits_scroll_duration = (*ctx.config)["credits_scroll_duration"].get(); + // Set up animation timing configuration + const double credits_fade_in_duration = 0.5; + const double credits_scroll_duration = 5.0; auto set_credits_opacity = [this](int channel, const float& opacity) { @@ -98,12 +98,12 @@ credits::credits(game::context& ctx): ctx.ui_scene->add_object(&credits_text); - debug::log::pop_task(EXIT_SUCCESS); + debug::log::trace("Entered credits state"); } credits::~credits() { - debug::log::push_task("Exiting credits state"); + debug::log::trace("Exiting credits state..."); // Disable credits skipper ctx.input_mapper.disconnect(); @@ -114,7 +114,7 @@ credits::~credits() // Destruct credits animations ctx.animator->remove_animation(&credits_fade_in_animation); - debug::log::pop_task(EXIT_SUCCESS); + debug::log::trace("Exited credits state"); } } // namespace state diff --git a/src/game/state/extras-menu.cpp b/src/game/state/extras-menu.cpp index 3b91bb2..4b53452 100644 --- a/src/game/state/extras-menu.cpp +++ b/src/game/state/extras-menu.cpp @@ -25,6 +25,10 @@ #include "debug/log.hpp" #include "game/fonts.hpp" #include "game/menu.hpp" +#include "game/strings.hpp" +#include "utility/hash/fnv1a.hpp" + +using namespace hash::literals; namespace game { namespace state { @@ -32,7 +36,7 @@ namespace state { extras_menu::extras_menu(game::context& ctx): game::state::base(ctx) { - debug::log::push_task("Entering extras menu state"); + debug::log::trace("Entering extras menu state..."); // Construct menu item texts scene::text* credits_text = new scene::text(); @@ -43,8 +47,8 @@ extras_menu::extras_menu(game::context& ctx): ctx.menu_item_texts.push_back({back_text, nullptr}); // Set content of menu item texts - credits_text->set_content((*ctx.strings)["extras_menu_credits"]); - back_text->set_content((*ctx.strings)["back"]); + credits_text->set_content(get_string(ctx, "extras_menu_credits"_fnv1a32)); + back_text->set_content(get_string(ctx, "back"_fnv1a32)); // Init menu item index game::menu::init_menu_item_index(ctx, "extras"); @@ -123,12 +127,12 @@ extras_menu::extras_menu(game::context& ctx): // Fade in menu game::menu::fade_in(ctx, nullptr); - debug::log::pop_task(EXIT_SUCCESS); + debug::log::trace("Entered extras menu state"); } extras_menu::~extras_menu() { - debug::log::push_task("Exiting extras menu state"); + debug::log::trace("Exiting extras menu state..."); // Destruct menu game::menu::clear_controls(ctx); @@ -137,7 +141,7 @@ extras_menu::~extras_menu() game::menu::remove_text_from_ui(ctx); game::menu::delete_text(ctx); - debug::log::pop_task(EXIT_SUCCESS); + debug::log::trace("Exited extras menu state"); } } // namespace state diff --git a/src/game/state/gamepad-config-menu.cpp b/src/game/state/gamepad-config-menu.cpp index c57814f..d6a3d2c 100644 --- a/src/game/state/gamepad-config-menu.cpp +++ b/src/game/state/gamepad-config-menu.cpp @@ -26,6 +26,10 @@ #include "resources/resource-manager.hpp" #include "game/menu.hpp" #include "game/controls.hpp" +#include "game/strings.hpp" +#include "utility/hash/fnv1a.hpp" + +using namespace hash::literals; namespace game { namespace state { @@ -33,7 +37,7 @@ namespace state { gamepad_config_menu::gamepad_config_menu(game::context& ctx): game::state::base(ctx) { - debug::log::push_task("Entering gamepad config menu state"); + debug::log::trace("Entering gamepad config menu state..."); // Add camera control menu items add_control_item("move_forward"); @@ -54,7 +58,7 @@ gamepad_config_menu::gamepad_config_menu(game::context& ctx): ctx.menu_item_texts.push_back({back_text, nullptr}); // Set content of menu item texts - back_text->set_content((*ctx.strings)["back"]); + back_text->set_content(get_string(ctx, "back"_fnv1a32)); // Init menu item index game::menu::init_menu_item_index(ctx, "gamepad_config"); @@ -108,12 +112,12 @@ gamepad_config_menu::gamepad_config_menu(game::context& ctx): // Fade in menu game::menu::fade_in(ctx, nullptr); - debug::log::pop_task(EXIT_SUCCESS); + debug::log::trace("Entered gamepad config menu state"); } gamepad_config_menu::~gamepad_config_menu() { - debug::log::push_task("Exiting gamepad config menu state"); + debug::log::trace("Exiting gamepad config menu state..."); // Destruct menu game::menu::clear_controls(ctx); @@ -125,7 +129,7 @@ gamepad_config_menu::~gamepad_config_menu() // Save control profile game::save_control_profile(ctx); - debug::log::pop_task(EXIT_SUCCESS); + debug::log::trace("Exited gamepad config menu state"); } std::string gamepad_config_menu::get_binding_string(input::control* control) @@ -292,11 +296,7 @@ void gamepad_config_menu::add_control_item(const std::string& control_name) ctx.menu_item_texts.push_back({name_text, value_text}); // Set content of name text - std::string string_name = "control_" + control_name; - if (auto it = ctx.strings->find(string_name); it != ctx.strings->end()) - name_text->set_content(it->second); - else - name_text->set_content(control_name); + name_text->set_content(control_name); // Set content of value text //value_text->set_content(get_binding_string(control)); @@ -304,7 +304,7 @@ void gamepad_config_menu::add_control_item(const std::string& control_name) auto select_callback = [this, &ctx = this->ctx, value_text]() { // Clear binding string from value text - value_text->set_content((*ctx.strings)["ellipsis"]); + value_text->set_content(get_string(ctx, "ellipsis"_fnv1a32)); game::menu::align_text(ctx); game::menu::update_text_tweens(ctx); diff --git a/src/game/state/graphics-menu.cpp b/src/game/state/graphics-menu.cpp index 5830fbd..a168d72 100644 --- a/src/game/state/graphics-menu.cpp +++ b/src/game/state/graphics-menu.cpp @@ -26,6 +26,10 @@ #include "game/menu.hpp" #include "game/graphics.hpp" #include "animation/timeline.hpp" +#include "game/strings.hpp" +#include "utility/hash/fnv1a.hpp" + +using namespace hash::literals; namespace game { namespace state { @@ -35,7 +39,7 @@ static void update_value_text_content(game::context* ctx); graphics_menu::graphics_menu(game::context& ctx): game::state::base(ctx) { - debug::log::push_task("Entering graphics menu state"); + debug::log::trace("Entering graphics menu state..."); // Construct menu item texts scene::text* fullscreen_name_text = new scene::text(); @@ -46,10 +50,8 @@ graphics_menu::graphics_menu(game::context& ctx): scene::text* v_sync_value_text = new scene::text(); scene::text* aa_method_name_text = new scene::text(); scene::text* aa_method_value_text = new scene::text(); - scene::text* bloom_name_text = new scene::text(); - scene::text* bloom_value_text = new scene::text(); - scene::text* font_size_name_text = new scene::text(); - scene::text* font_size_value_text = new scene::text(); + scene::text* font_scale_name_text = new scene::text(); + scene::text* font_scale_value_text = new scene::text(); scene::text* dyslexia_font_name_text = new scene::text(); scene::text* dyslexia_font_value_text = new scene::text(); scene::text* back_text = new scene::text(); @@ -59,20 +61,18 @@ graphics_menu::graphics_menu(game::context& ctx): ctx.menu_item_texts.push_back({resolution_name_text, resolution_value_text}); ctx.menu_item_texts.push_back({v_sync_name_text, v_sync_value_text}); ctx.menu_item_texts.push_back({aa_method_name_text, aa_method_value_text}); - ctx.menu_item_texts.push_back({bloom_name_text, bloom_value_text}); - ctx.menu_item_texts.push_back({font_size_name_text, font_size_value_text}); + ctx.menu_item_texts.push_back({font_scale_name_text, font_scale_value_text}); ctx.menu_item_texts.push_back({dyslexia_font_name_text, dyslexia_font_value_text}); ctx.menu_item_texts.push_back({back_text, nullptr}); // Set content of menu item texts - fullscreen_name_text->set_content((*ctx.strings)["graphics_menu_fullscreen"]); - resolution_name_text->set_content((*ctx.strings)["graphics_menu_resolution"]); - v_sync_name_text->set_content((*ctx.strings)["graphics_menu_v_sync"]); - aa_method_name_text->set_content((*ctx.strings)["graphics_menu_aa_method"]); - bloom_name_text->set_content((*ctx.strings)["graphics_menu_bloom"]); - font_size_name_text->set_content((*ctx.strings)["graphics_menu_font_size"]); - dyslexia_font_name_text->set_content((*ctx.strings)["graphics_menu_dyslexia_font"]); - back_text->set_content((*ctx.strings)["back"]); + fullscreen_name_text->set_content(get_string(ctx, "graphics_menu_fullscreen"_fnv1a32)); + resolution_name_text->set_content(get_string(ctx, "graphics_menu_resolution"_fnv1a32)); + v_sync_name_text->set_content(get_string(ctx, "graphics_menu_v_sync"_fnv1a32)); + aa_method_name_text->set_content(get_string(ctx, "graphics_menu_aa_method"_fnv1a32)); + font_scale_name_text->set_content(get_string(ctx, "graphics_menu_font_scale"_fnv1a32)); + dyslexia_font_name_text->set_content(get_string(ctx, "graphics_menu_dyslexia_font"_fnv1a32)); + back_text->set_content(get_string(ctx, "back"_fnv1a32)); update_value_text_content(); // Init menu item index @@ -92,21 +92,13 @@ graphics_menu::graphics_menu(game::context& ctx): ctx.app->set_fullscreen(fullscreen); - if (!fullscreen) - { - int2 resolution; - resolution.x() = (*ctx.config)["windowed_resolution"][0].get(); - resolution.y() = (*ctx.config)["windowed_resolution"][1].get(); - - ctx.app->resize_window(resolution.x(), resolution.y()); - } this->update_value_text_content(); game::menu::align_text(ctx); game::menu::update_text_tweens(ctx); - // Save display mode config - (*ctx.config)["fullscreen"] = fullscreen; + // Update fullscreen settings + (*ctx.settings)["fullscreen"_fnv1a32] = fullscreen; }; auto increase_resolution_callback = [this, &ctx]() @@ -121,6 +113,9 @@ graphics_menu::graphics_menu(game::context& ctx): if (ctx.render_scale > 2.0f) ctx.render_scale = 2.0f; + // Update render scale setting + (*ctx.settings)["render_scale"_fnv1a32] = ctx.render_scale; + // Resize framebuffers game::graphics::change_render_resolution(ctx, ctx.render_scale); @@ -128,9 +123,6 @@ graphics_menu::graphics_menu(game::context& ctx): this->update_value_text_content(); game::menu::align_text(ctx); game::menu::update_text_tweens(ctx); - - // Update config - (*ctx.config)["render_scale"] = ctx.render_scale; }; auto decrease_resolution_callback = [this, &ctx]() @@ -145,6 +137,9 @@ graphics_menu::graphics_menu(game::context& ctx): if (ctx.render_scale < 0.25f) ctx.render_scale = 0.25f; + // Update render scale setting + (*ctx.settings)["render_scale"_fnv1a32] = ctx.render_scale; + // Resize framebuffers game::graphics::change_render_resolution(ctx, ctx.render_scale); @@ -152,23 +147,20 @@ graphics_menu::graphics_menu(game::context& ctx): this->update_value_text_content(); game::menu::align_text(ctx); game::menu::update_text_tweens(ctx); - - // Update config - (*ctx.config)["render_scale"] = ctx.render_scale; }; auto toggle_v_sync_callback = [this, &ctx]() { bool v_sync = !ctx.app->get_v_sync(); + // Update v-sync setting + (*ctx.settings)["v_sync"_fnv1a32] = v_sync; + ctx.app->set_v_sync(v_sync); this->update_value_text_content(); game::menu::align_text(ctx); game::menu::update_text_tweens(ctx); - - // Save v-sync config - (*ctx.config)["v_sync"] = v_sync; }; auto next_aa_method_callback = [this, &ctx]() @@ -177,15 +169,16 @@ graphics_menu::graphics_menu(game::context& ctx): { case render::anti_aliasing_method::none: ctx.anti_aliasing_method = render::anti_aliasing_method::fxaa; - (*ctx.config)["anti_aliasing_method"] = "fxaa"; break; case render::anti_aliasing_method::fxaa: ctx.anti_aliasing_method = render::anti_aliasing_method::none; - (*ctx.config)["anti_aliasing_method"] = "none"; break; } + // Update anti-aliasing method setting + (*ctx.settings)["anti_aliasing_method"_fnv1a32] = std::to_underlying(ctx.anti_aliasing_method); + game::graphics::select_anti_aliasing_method(ctx, ctx.anti_aliasing_method); // Update value text @@ -203,71 +196,49 @@ graphics_menu::graphics_menu(game::context& ctx): { case render::anti_aliasing_method::none: ctx.anti_aliasing_method = render::anti_aliasing_method::fxaa; - (*ctx.config)["anti_aliasing_method"] = "fxaa"; break; case render::anti_aliasing_method::fxaa: ctx.anti_aliasing_method = render::anti_aliasing_method::none; - (*ctx.config)["anti_aliasing_method"] = "none"; break; } - game::graphics::select_anti_aliasing_method(ctx, ctx.anti_aliasing_method); - - // Update value text - this->update_value_text_content(); + // Update anti-aliasing method setting + (*ctx.settings)["anti_aliasing_method"_fnv1a32] = std::to_underlying(ctx.anti_aliasing_method); - // Refresh and realign text - game::menu::refresh_text(ctx); - game::menu::align_text(ctx); - game::menu::update_text_tweens(ctx); - }; - - auto toggle_bloom_callback = [this, &ctx]() - { - game::graphics::toggle_bloom(ctx, !ctx.bloom_enabled); + game::graphics::select_anti_aliasing_method(ctx, ctx.anti_aliasing_method); // Update value text this->update_value_text_content(); - // Update config - (*ctx.config)["bloom_enabled"] = ctx.bloom_enabled; - // Refresh and realign text game::menu::refresh_text(ctx); game::menu::align_text(ctx); game::menu::update_text_tweens(ctx); }; - auto increase_font_size_callback = [this, &ctx]() + auto increase_font_scale_callback = [this, &ctx]() { - // Increase font size + // Increase font scale if (ctx.menu_modifier_control.is_active()) - ctx.font_size += 0.01f; + ctx.font_scale += 0.01f; else - ctx.font_size += 0.1f; + ctx.font_scale += 0.1f; + + // Limit font scale + if (ctx.font_scale > 2.0f) + ctx.font_scale = 2.0f; - // Limit font size - if (ctx.font_size > 2.0f) - ctx.font_size = 2.0f; + // Update font scale setting + (*ctx.settings)["font_scale"_fnv1a32] = ctx.font_scale; // Update value text this->update_value_text_content(); - // Update config - (*ctx.config)["font_size"] = ctx.font_size; - // Reload fonts - debug::log::push_task("Reloading fonts"); - try - { - game::load_fonts(ctx); - } - catch (...) - { - debug::log::pop_task(EXIT_FAILURE); - } - debug::log::pop_task(EXIT_SUCCESS); + debug::log::trace("Reloading fonts..."); + game::load_fonts(ctx); + debug::log::trace("Reloaded fonts"); // Refresh and realign text game::menu::refresh_text(ctx); @@ -275,35 +246,28 @@ graphics_menu::graphics_menu(game::context& ctx): game::menu::update_text_tweens(ctx); }; - auto decrease_font_size_callback = [this, &ctx]() + auto decrease_font_scale_callback = [this, &ctx]() { - // Decrease font size + // Decrease font scale if (ctx.menu_modifier_control.is_active()) - ctx.font_size -= 0.01f; + ctx.font_scale -= 0.01f; else - ctx.font_size -= 0.1f; + ctx.font_scale -= 0.1f; - // Limit font size - if (ctx.font_size < 0.1f) - ctx.font_size = 0.1f; + // Limit font scale + if (ctx.font_scale < 0.1f) + ctx.font_scale = 0.1f; + + // Update font scale setting + (*ctx.settings)["font_scale"_fnv1a32] = ctx.font_scale; // Update value text this->update_value_text_content(); - // Update config - (*ctx.config)["font_size"] = ctx.font_size; - // Reload fonts - debug::log::push_task("Reloading fonts"); - try - { - game::load_fonts(ctx); - } - catch (...) - { - debug::log::pop_task(EXIT_FAILURE); - } - debug::log::pop_task(EXIT_SUCCESS); + debug::log::trace("Reloading fonts..."); + game::load_fonts(ctx); + debug::log::trace("Reloaded fonts"); // Refresh and realign text game::menu::refresh_text(ctx); @@ -318,20 +282,13 @@ graphics_menu::graphics_menu(game::context& ctx): // Update value text this->update_value_text_content(); - // Save dyslexia font config - (*ctx.config)["dyslexia_font"] = ctx.dyslexia_font; + // Save dyslexia font setting + (*ctx.settings)["dyslexia_font"_fnv1a32] = ctx.dyslexia_font; // Reload fonts - debug::log::push_task("Reloading fonts"); - try - { - game::load_fonts(ctx); - } - catch (...) - { - debug::log::pop_task(EXIT_FAILURE); - } - debug::log::pop_task(EXIT_SUCCESS); + debug::log::trace("Reloading fonts..."); + game::load_fonts(ctx); + debug::log::trace("Reloaded fonts"); // Refresh and realign text game::menu::refresh_text(ctx); @@ -366,8 +323,7 @@ graphics_menu::graphics_menu(game::context& ctx): ctx.menu_select_callbacks.push_back(increase_resolution_callback); ctx.menu_select_callbacks.push_back(toggle_v_sync_callback); ctx.menu_select_callbacks.push_back(next_aa_method_callback); - ctx.menu_select_callbacks.push_back(toggle_bloom_callback); - ctx.menu_select_callbacks.push_back(increase_font_size_callback); + ctx.menu_select_callbacks.push_back(increase_font_scale_callback); ctx.menu_select_callbacks.push_back(toggle_dyslexia_font_callback); ctx.menu_select_callbacks.push_back(select_back_callback); @@ -376,8 +332,7 @@ graphics_menu::graphics_menu(game::context& ctx): ctx.menu_left_callbacks.push_back(decrease_resolution_callback); ctx.menu_left_callbacks.push_back(toggle_v_sync_callback); ctx.menu_left_callbacks.push_back(previous_aa_method_callback); - ctx.menu_left_callbacks.push_back(toggle_bloom_callback); - ctx.menu_left_callbacks.push_back(decrease_font_size_callback); + ctx.menu_left_callbacks.push_back(decrease_font_scale_callback); ctx.menu_left_callbacks.push_back(toggle_dyslexia_font_callback); ctx.menu_left_callbacks.push_back(nullptr); @@ -386,8 +341,7 @@ graphics_menu::graphics_menu(game::context& ctx): ctx.menu_right_callbacks.push_back(increase_resolution_callback); ctx.menu_right_callbacks.push_back(toggle_v_sync_callback); ctx.menu_right_callbacks.push_back(next_aa_method_callback); - ctx.menu_right_callbacks.push_back(toggle_bloom_callback); - ctx.menu_right_callbacks.push_back(increase_font_size_callback); + ctx.menu_right_callbacks.push_back(increase_font_scale_callback); ctx.menu_right_callbacks.push_back(toggle_dyslexia_font_callback); ctx.menu_right_callbacks.push_back(nullptr); @@ -400,12 +354,12 @@ graphics_menu::graphics_menu(game::context& ctx): // Fade in menu game::menu::fade_in(ctx, nullptr); - debug::log::pop_task(EXIT_SUCCESS); + debug::log::trace("Entered graphics menu state"); } graphics_menu::~graphics_menu() { - debug::log::push_task("Exiting graphics menu state"); + debug::log::trace("Exiting graphics menu state..."); // Destruct menu game::menu::clear_controls(ctx); @@ -414,7 +368,7 @@ graphics_menu::~graphics_menu() game::menu::remove_text_from_ui(ctx); game::menu::delete_text(ctx); - debug::log::pop_task(EXIT_SUCCESS); + debug::log::trace("Exited graphics menu state"); } void graphics_menu::update_value_text_content() @@ -423,12 +377,11 @@ void graphics_menu::update_value_text_content() const float render_scale = ctx.render_scale; const bool v_sync = ctx.app->get_v_sync(); const int aa_method_index = static_cast(ctx.anti_aliasing_method); - const bool bloom_enabled = ctx.bloom_enabled; - const float font_size = ctx.font_size; + const float font_scale = ctx.font_scale; const bool dyslexia_font = ctx.dyslexia_font; - const std::string string_on = (*ctx.strings)["on"]; - const std::string string_off = (*ctx.strings)["off"]; + const std::string string_on = get_string(ctx, "on"_fnv1a32); + const std::string string_off = get_string(ctx, "off"_fnv1a32); /* const std::string string_quality[4] = @@ -442,16 +395,15 @@ void graphics_menu::update_value_text_content() const std::string string_aa_methods[2] = { - (*ctx.strings)["graphics_menu_aa_method_none"], - (*ctx.strings)["graphics_menu_aa_method_fxaa"] + get_string(ctx, "graphics_menu_aa_method_none"_fnv1a32), + get_string(ctx, "graphics_menu_aa_method_fxaa"_fnv1a32) }; std::get<1>(ctx.menu_item_texts[0])->set_content((fullscreen) ? string_on : string_off); std::get<1>(ctx.menu_item_texts[1])->set_content(std::to_string(static_cast(std::round(render_scale * 100.0f))) + "%"); std::get<1>(ctx.menu_item_texts[2])->set_content((v_sync) ? string_on : string_off); std::get<1>(ctx.menu_item_texts[3])->set_content(string_aa_methods[aa_method_index]); - std::get<1>(ctx.menu_item_texts[4])->set_content((bloom_enabled) ? string_on : string_off); - std::get<1>(ctx.menu_item_texts[5])->set_content(std::to_string(static_cast(std::round(font_size * 100.0f))) + "%"); + std::get<1>(ctx.menu_item_texts[5])->set_content(std::to_string(static_cast(std::round(font_scale * 100.0f))) + "%"); std::get<1>(ctx.menu_item_texts[6])->set_content((dyslexia_font) ? string_on : string_off); } diff --git a/src/game/state/keyboard-config-menu.cpp b/src/game/state/keyboard-config-menu.cpp index 8e2bacf..3a73e89 100644 --- a/src/game/state/keyboard-config-menu.cpp +++ b/src/game/state/keyboard-config-menu.cpp @@ -25,6 +25,10 @@ #include "resources/resource-manager.hpp" #include "game/menu.hpp" #include "game/controls.hpp" +#include "game/strings.hpp" +#include "utility/hash/fnv1a.hpp" + +using namespace hash::literals; namespace game { namespace state { @@ -32,7 +36,7 @@ namespace state { keyboard_config_menu::keyboard_config_menu(game::context& ctx): game::state::base(ctx) { - debug::log::push_task("Entering keyboard config menu state"); + debug::log::trace("Entering keyboard config menu state..."); // Add camera control menu items add_control_item("move_forward"); @@ -53,7 +57,7 @@ keyboard_config_menu::keyboard_config_menu(game::context& ctx): ctx.menu_item_texts.push_back({back_text, nullptr}); // Set content of menu item texts - back_text->set_content((*ctx.strings)["back"]); + back_text->set_content(get_string(ctx, "back"_fnv1a32)); // Init menu item index game::menu::init_menu_item_index(ctx, "keyboard_config"); @@ -107,12 +111,12 @@ keyboard_config_menu::keyboard_config_menu(game::context& ctx): // Fade in menu game::menu::fade_in(ctx, nullptr); - debug::log::pop_task(EXIT_SUCCESS); + debug::log::trace("Entered keyboard config menu state"); } keyboard_config_menu::~keyboard_config_menu() { - debug::log::push_task("Exiting keyboard config menu state"); + debug::log::trace("Exiting keyboard config menu state..."); // Destruct menu game::menu::clear_controls(ctx); @@ -124,7 +128,7 @@ keyboard_config_menu::~keyboard_config_menu() // Save control profile game::save_control_profile(ctx); - debug::log::pop_task(EXIT_SUCCESS); + debug::log::trace("Exited keyboard config menu state..."); } std::string keyboard_config_menu::get_binding_string(input::control* control) @@ -234,11 +238,7 @@ void keyboard_config_menu::add_control_item(const std::string& control_name) ctx.menu_item_texts.push_back({name_text, value_text}); // Set content of name text - std::string string_name = "control_" + control_name; - if (auto it = ctx.strings->find(string_name); it != ctx.strings->end()) - name_text->set_content(it->second); - else - name_text->set_content(control_name); + name_text->set_content(control_name); // Set content of value text //value_text->set_content(get_binding_string( control)); @@ -246,7 +246,7 @@ void keyboard_config_menu::add_control_item(const std::string& control_name) auto select_callback = [this, &ctx = this->ctx, value_text]() { // Clear binding string from value text - value_text->set_content((*ctx.strings)["ellipsis"]); + value_text->set_content(get_string(ctx, "ellipsis"_fnv1a32)); game::menu::align_text(ctx); game::menu::update_text_tweens(ctx); diff --git a/src/game/state/language-menu.cpp b/src/game/state/language-menu.cpp index e070e8e..3cc4682 100644 --- a/src/game/state/language-menu.cpp +++ b/src/game/state/language-menu.cpp @@ -24,6 +24,10 @@ #include "debug/log.hpp" #include "game/fonts.hpp" #include "game/menu.hpp" +#include "game/strings.hpp" +#include "utility/hash/fnv1a.hpp" + +using namespace hash::literals; namespace game { namespace state { @@ -31,7 +35,7 @@ namespace state { language_menu::language_menu(game::context& ctx): game::state::base(ctx) { - debug::log::push_task("Entering language menu state"); + debug::log::trace("Entering language menu state..."); // Construct menu item texts scene::text* language_name_text = new scene::text(); @@ -59,30 +63,24 @@ language_menu::language_menu(game::context& ctx): auto next_language_callback = [this, &ctx]() { // Increment language index - ++ctx.language_index; - if (ctx.language_index >= ctx.language_count) - ctx.language_index = 0; + ctx.language_index = (ctx.language_index + 1) % ctx.language_count; - // Find corresponding language code and strings - ctx.language_code = (*ctx.string_table)[0][ctx.language_index + 2]; - ctx.strings = &ctx.string_table_map[ctx.language_code]; + // Update language index setting + (*ctx.settings)["language_index"_fnv1a32] = ctx.language_index; - // Update language in config - (*ctx.config)["language"] = ctx.language_code; + // Build string map for current language if not built + if (ctx.string_maps[ctx.language_index].empty()) + { + i18n::build_string_map(*ctx.string_table, 0, ctx.language_index + 2, ctx.string_maps[ctx.language_index]); + } - debug::log::info("Language changed to \"" + ctx.language_code + "\""); + // Log language change + debug::log::info("Language index: {}; code: {}", ctx.language_index, get_string(ctx, "language_code"_fnv1a32)); // Reload fonts - debug::log::push_task("Reloading fonts"); - try - { - game::load_fonts(ctx); - } - catch (...) - { - debug::log::pop_task(EXIT_FAILURE); - } - debug::log::pop_task(EXIT_SUCCESS); + debug::log::trace("Reloading fonts..."); + game::load_fonts(ctx); + debug::log::trace("Reloaded fonts"); game::menu::update_text_font(ctx); this->update_text_content(); @@ -92,31 +90,32 @@ language_menu::language_menu(game::context& ctx): }; auto previous_language_callback = [this, &ctx]() { - // Increment language index - --ctx.language_index; - if (ctx.language_index < 0) + // Decrement language index + if (ctx.language_index > 0) + { + --ctx.language_index; + } + else + { ctx.language_index = ctx.language_count - 1; + } - // Find corresponding language code and strings - ctx.language_code = (*ctx.string_table)[0][ctx.language_index + 2]; - ctx.strings = &ctx.string_table_map[ctx.language_code]; + // Update language index setting + (*ctx.settings)["language_index"_fnv1a32] = ctx.language_index; - // Update language in config - (*ctx.config)["language"] = ctx.language_code; + // Build string map for current language if not built + if (ctx.string_maps[ctx.language_index].empty()) + { + i18n::build_string_map(*ctx.string_table, 0, ctx.language_index + 2, ctx.string_maps[ctx.language_index]); + } - debug::log::info("Language changed to \"" + ctx.language_code + "\""); + // Log language change + debug::log::info("Language index: {}; code: {}", ctx.language_index, get_string(ctx, "language_code"_fnv1a32)); // Reload fonts - debug::log::push_task("Reloading fonts"); - try - { - game::load_fonts(ctx); - } - catch (...) - { - debug::log::pop_task(EXIT_FAILURE); - } - debug::log::pop_task(EXIT_SUCCESS); + debug::log::trace("Reloading fonts..."); + game::load_fonts(ctx); + debug::log::trace("Reloaded fonts"); game::menu::update_text_font(ctx); this->update_text_content(); @@ -168,12 +167,12 @@ language_menu::language_menu(game::context& ctx): // Fade in menu game::menu::fade_in(ctx, nullptr); - debug::log::pop_task(EXIT_SUCCESS); + debug::log::trace("Entered language menu state"); } language_menu::~language_menu() { - debug::log::push_task("Exiting language menu state"); + debug::log::trace("Exiting language menu state..."); // Destruct menu game::menu::clear_controls(ctx); @@ -182,7 +181,7 @@ language_menu::~language_menu() game::menu::remove_text_from_ui(ctx); game::menu::delete_text(ctx); - debug::log::pop_task(EXIT_SUCCESS); + debug::log::trace("Exited language menu state..."); } void language_menu::update_text_content() @@ -190,9 +189,9 @@ void language_menu::update_text_content() auto [language_name, language_value] = ctx.menu_item_texts[0]; auto [back_name, back_value] = ctx.menu_item_texts[1]; - language_name->set_content((*ctx.strings)["language_menu_language"]); - language_value->set_content((*ctx.strings)["language_name"]); - back_name->set_content((*ctx.strings)["back"]); + language_name->set_content(get_string(ctx, "language_menu_language"_fnv1a32)); + language_value->set_content(get_string(ctx, "language_name"_fnv1a32)); + back_name->set_content(get_string(ctx, "back"_fnv1a32)); } } // namespace state diff --git a/src/game/state/main-menu.cpp b/src/game/state/main-menu.cpp index 6644028..fba3c28 100644 --- a/src/game/state/main-menu.cpp +++ b/src/game/state/main-menu.cpp @@ -22,7 +22,6 @@ #include "game/state/extras-menu.hpp" #include "game/state/nuptial-flight.hpp" #include "game/world.hpp" -#include "game/load.hpp" #include "game/menu.hpp" #include "game/ecoregion.hpp" #include "game/ant/swarm.hpp" @@ -43,6 +42,10 @@ #include "game/component/transform.hpp" #include "math/projection.hpp" #include +#include "game/strings.hpp" +#include "utility/hash/fnv1a.hpp" + +using namespace hash::literals; namespace game { namespace state { @@ -50,7 +53,7 @@ namespace state { main_menu::main_menu(game::context& ctx, bool fade_in): game::state::base(ctx) { - debug::log::push_task("Entering main menu state"); + debug::log::trace("Entering main menu state..."); ctx.ui_clear_pass->set_cleared_buffers(true, true, false); @@ -58,13 +61,13 @@ main_menu::main_menu(game::context& ctx, bool fade_in): title_text.set_material(&ctx.title_font_material); title_text.set_color({1.0f, 1.0f, 1.0f, (fade_in) ? 1.0f : 0.0f}); title_text.set_font(&ctx.title_font); - title_text.set_content((*ctx.strings)["title_antkeeper"]); + title_text.set_content(get_string(ctx, "title_antkeeper"_fnv1a32)); // Align title text const auto& title_aabb = static_cast&>(title_text.get_local_bounds()); float title_w = title_aabb.max_point.x() - title_aabb.min_point.x(); float title_h = title_aabb.max_point.y() - title_aabb.min_point.y(); - title_text.set_translation({std::round(-title_w * 0.5f), std::round(-title_h * 0.5f + (ctx.app->get_viewport_dimensions().y() / 3.0f) / 2.0f), 0.0f}); + title_text.set_translation({std::round(-title_w * 0.5f), std::round(-title_h * 0.5f + (ctx.app->get_viewport_size().y() / 3.0f) / 2.0f), 0.0f}); title_text.update_tweens(); // Add title text to UI @@ -97,17 +100,17 @@ main_menu::main_menu(game::context& ctx, bool fade_in): ctx.menu_item_texts.push_back({quit_text, nullptr}); // Set content of menu item texts - start_text->set_content((*ctx.strings)["main_menu_start"]); - options_text->set_content((*ctx.strings)["main_menu_options"]); - extras_text->set_content((*ctx.strings)["main_menu_extras"]); - quit_text->set_content((*ctx.strings)["main_menu_quit"]); + start_text->set_content(get_string(ctx, "main_menu_start"_fnv1a32)); + options_text->set_content(get_string(ctx, "main_menu_options"_fnv1a32)); + extras_text->set_content(get_string(ctx, "main_menu_extras"_fnv1a32)); + quit_text->set_content(get_string(ctx, "main_menu_quit"_fnv1a32)); // Init menu item index game::menu::init_menu_item_index(ctx, "main"); game::menu::update_text_color(ctx); game::menu::update_text_font(ctx); - game::menu::align_text(ctx, true, false, (-ctx.app->get_viewport_dimensions().y() / 3.0f) / 2.0f); + game::menu::align_text(ctx, true, false, (-ctx.app->get_viewport_size().y() / 3.0f) / 2.0f); game::menu::update_text_tweens(ctx); game::menu::add_text_to_ui(ctx); game::menu::setup_animations(ctx); @@ -248,7 +251,7 @@ main_menu::main_menu(game::context& ctx, bool fade_in): { game::world::cosmogenesis(ctx); game::world::create_observer(ctx); - game::world::enter_ecoregion(ctx, *ctx.resource_manager->load("seedy-scrub.eco")); + //game::world::enter_ecoregion(ctx, *ctx.resource_manager->load("seedy-scrub.eco")); } // Set world time @@ -261,12 +264,10 @@ main_menu::main_menu(game::context& ctx, bool fade_in): const float ev100_sunny16 = physics::light::ev::from_settings(16.0f, 1.0f / 100.0f, 100.0f); ctx.surface_camera->set_exposure(ev100_sunny16); - const auto& viewport_dimensions = ctx.app->get_viewport_dimensions(); - const float aspect_ratio = static_cast(viewport_dimensions[0]) / static_cast(viewport_dimensions[1]); + const auto& viewport_size = ctx.app->get_viewport_size(); + const float aspect_ratio = static_cast(viewport_size[0]) / static_cast(viewport_size[1]); float fov = math::vertical_fov(math::radians(100.0f), aspect_ratio); - if (ctx.config->contains("near_fov")) - fov = math::vertical_fov(math::radians((*ctx.config)["near_fov"].get()), aspect_ratio); ctx.surface_camera->look_at({0, 2.0f, 0}, {0, 0, 0}, {0, 0, 1}); ctx.surface_camera->set_perspective(fov, ctx.surface_camera->get_aspect_ratio(), ctx.surface_camera->get_clip_near(), ctx.surface_camera->get_clip_far()); @@ -282,12 +283,12 @@ main_menu::main_menu(game::context& ctx, bool fade_in): //if (!ctx.menu_bg_billboard->is_active()) // game::menu::fade_in_bg(ctx); - debug::log::pop_task(EXIT_SUCCESS); + debug::log::trace("Entered main menu state"); } main_menu::~main_menu() { - debug::log::push_task("Exiting main menu state"); + debug::log::trace("Exiting main menu state..."); // Destruct menu game::menu::clear_controls(ctx); @@ -305,7 +306,7 @@ main_menu::~main_menu() // Destruct title text ctx.ui_scene->remove_object(&title_text); - debug::log::pop_task(EXIT_SUCCESS); + debug::log::trace("Exited main menu state"); } void main_menu::fade_in_title() diff --git a/src/game/state/nest-selection.cpp b/src/game/state/nest-selection.cpp index a001111..7b4fba4 100644 --- a/src/game/state/nest-selection.cpp +++ b/src/game/state/nest-selection.cpp @@ -45,7 +45,6 @@ #include "render/passes/ground-pass.hpp" #include "state-machine.hpp" #include "config.hpp" -#include "game/load.hpp" #include "math/interpolation.hpp" #include "physics/light/exposure.hpp" #include "application.hpp" @@ -66,20 +65,20 @@ namespace state { nest_selection::nest_selection(game::context& ctx): game::state::base(ctx) { - debug::log::push_task("Entering nest selection state"); + debug::log::trace("Entering nest selection state..."); - debug::log::push_task("Generating genome"); + debug::log::trace("Generating genome..."); std::random_device rng; ant::genome* genome = ant::cladogenesis(ctx.active_ecoregion->gene_pools[0], rng); - debug::log::pop_task(EXIT_SUCCESS); + debug::log::trace("Generated genome"); - debug::log::push_task("Building worker phenome"); + debug::log::trace("Building worker phenome..."); ant::phenome worker_phenome = ant::phenome(*genome, ant::caste::worker); - debug::log::pop_task(EXIT_SUCCESS); + debug::log::trace("Built worker phenome..."); - debug::log::push_task("Generating worker model"); + debug::log::trace("Generating worker model..."); render::model* worker_model = ant::morphogenesis(worker_phenome); - debug::log::pop_task(EXIT_SUCCESS); + debug::log::trace("Generated worker model"); // Create worker entity(s) entity::id worker_eid = ctx.entity_registry->create(); @@ -110,11 +109,7 @@ nest_selection::nest_selection(game::context& ctx): } // Init time scale - double time_scale = 1.0; - - // Read time scale settings - if (ctx.config->contains("time_scale")) - time_scale = (*ctx.config)["time_scale"].get(); + double time_scale = 60.0; // Set time scale game::world::set_time_scale(ctx, time_scale); @@ -131,8 +126,8 @@ nest_selection::nest_selection(game::context& ctx): const float ev100_sunny16 = physics::light::ev::from_settings(16.0f, 1.0f / 100.0f, 100.0f); ctx.surface_camera->set_exposure(ev100_sunny16); - const auto& viewport_dimensions = ctx.app->get_viewport_dimensions(); - const float aspect_ratio = static_cast(viewport_dimensions[0]) / static_cast(viewport_dimensions[1]); + const auto& viewport_size = ctx.app->get_viewport_size(); + const float aspect_ratio = static_cast(viewport_size[0]) / static_cast(viewport_size[1]); // Init first person camera rig parameters first_person_camera_rig_translation_spring_angular_frequency = period_to_rads(0.125f); @@ -147,16 +142,6 @@ nest_selection::nest_selection(game::context& ctx): first_person_camera_rig_pedestal_speed = 2.0f; first_person_camera_rig_pedestal = 0.0f; - // Read first person camera rig settings - if (ctx.config->contains("standing_eye_height")) - first_person_camera_rig_max_elevation = (*ctx.config)["standing_eye_height"].get(); - if (ctx.config->contains("walking_speed")) - first_person_camera_far_speed = (*ctx.config)["walking_speed"].get(); - if (ctx.config->contains("near_fov")) - first_person_camera_near_fov = math::vertical_fov(math::radians((*ctx.config)["near_fov"].get()), aspect_ratio); - if (ctx.config->contains("far_fov")) - first_person_camera_far_fov = math::vertical_fov(math::radians((*ctx.config)["far_fov"].get()), aspect_ratio); - // Create first person camera rig create_first_person_camera_rig(); @@ -198,16 +183,16 @@ nest_selection::nest_selection(game::context& ctx): // Queue control setup ctx.function_queue.push(std::bind(&nest_selection::enable_controls, this)); - debug::log::pop_task(EXIT_SUCCESS); + debug::log::trace("Entered nest selection state"); } nest_selection::~nest_selection() { - debug::log::push_task("Exiting nest selection state"); + debug::log::trace("Exiting nest selection state..."); destroy_first_person_camera_rig(); - debug::log::pop_task(EXIT_SUCCESS); + debug::log::trace("Exited nest selection state"); } void nest_selection::create_first_person_camera_rig() diff --git a/src/game/state/nuptial-flight.cpp b/src/game/state/nuptial-flight.cpp index 3dc22ff..88ca82e 100644 --- a/src/game/state/nuptial-flight.cpp +++ b/src/game/state/nuptial-flight.cpp @@ -48,7 +48,6 @@ #include "render/passes/ground-pass.hpp" #include "state-machine.hpp" #include "config.hpp" -#include "game/load.hpp" #include "math/interpolation.hpp" #include "physics/light/exposure.hpp" #include "color/color.hpp" @@ -61,7 +60,7 @@ namespace state { nuptial_flight::nuptial_flight(game::context& ctx): game::state::base(ctx) { - debug::log::push_task("Entering nuptial flight state"); + debug::log::trace("Entering nuptial flight state..."); // Init selected picking flag selected_picking_flag = std::uint32_t{1} << (sizeof(std::uint32_t) * 8 - 1); @@ -101,8 +100,8 @@ nuptial_flight::nuptial_flight(game::context& ctx): const float ev100_sunny16 = physics::light::ev::from_settings(16.0f, 1.0f / 100.0f, 100.0f); ctx.surface_camera->set_exposure(ev100_sunny16); - const auto& viewport_dimensions = ctx.app->get_viewport_dimensions(); - const float aspect_ratio = static_cast(viewport_dimensions[0]) / static_cast(viewport_dimensions[1]); + const auto& viewport_size = ctx.app->get_viewport_size(); + const float aspect_ratio = static_cast(viewport_size[0]) / static_cast(viewport_size[1]); // Init camera rig params camera_rig_near_distance = 1.0f; @@ -115,12 +114,6 @@ nuptial_flight::nuptial_flight(game::context& ctx): camera_rig_fov_spring_angular_frequency = period_to_rads(0.125f); camera_rig_focus_ease_to_duration = 1.0f; - // Read camera rig settingss - if (ctx.config->contains("near_fov")) - camera_rig_near_fov = math::vertical_fov(math::radians((*ctx.config)["near_fov"].get()), aspect_ratio); - if (ctx.config->contains("far_fov")) - camera_rig_far_fov = math::vertical_fov(math::radians((*ctx.config)["far_fov"].get()), aspect_ratio); - // Create camera rig create_camera_rig(); @@ -143,12 +136,12 @@ nuptial_flight::nuptial_flight(game::context& ctx): // Queue control setup ctx.function_queue.push(std::bind(&nuptial_flight::enable_controls, this)); - debug::log::pop_task(EXIT_SUCCESS); + debug::log::trace("Entered nuptial flight state"); } nuptial_flight::~nuptial_flight() { - debug::log::push_task("Exiting nuptial flight state"); + debug::log::trace("Exiting nuptial flight state..."); // Deselect selected entity select_entity(entt::null); @@ -156,7 +149,7 @@ nuptial_flight::~nuptial_flight() destroy_camera_rig(); game::ant::destroy_swarm(ctx, swarm_eid); - debug::log::pop_task(EXIT_SUCCESS); + debug::log::trace("Exited nuptial flight state"); } void nuptial_flight::create_camera_rig() @@ -619,14 +612,14 @@ void nuptial_flight::enable_controls() // Get window-space mouse coordinates auto [mouse_x, mouse_y] = ctx.app->get_mouse()->get_current_position(); - // Get window viewport dimensions - const auto viewport_dimensions = ctx.app->get_viewport_dimensions(); + // Get window viewport size + const auto viewport_size = ctx.app->viewport_size(); // Transform mouse coordinates from window space to NDC space const float2 mouse_ndc = { - static_cast(mouse_x) / static_cast(viewport_dimensions[0] - 1) * 2.0f - 1.0f, - (1.0f - static_cast(mouse_y) / static_cast(viewport_dimensions[1] - 1)) * 2.0f - 1.0f + static_cast(mouse_x) / static_cast(viewport_size[0] - 1) * 2.0f - 1.0f, + (1.0f - static_cast(mouse_y) / static_cast(viewport_size[1] - 1)) * 2.0f - 1.0f }; // Get picking ray from camera diff --git a/src/game/state/options-menu.cpp b/src/game/state/options-menu.cpp index 738322e..3f482d9 100644 --- a/src/game/state/options-menu.cpp +++ b/src/game/state/options-menu.cpp @@ -24,7 +24,6 @@ #include "game/state/sound-menu.hpp" #include "game/state/language-menu.hpp" #include "game/state/pause-menu.hpp" -#include "game/save.hpp" #include "game/menu.hpp" #include "animation/ease.hpp" #include "animation/animation.hpp" @@ -32,6 +31,10 @@ #include "application.hpp" #include "scene/text.hpp" #include "debug/log.hpp" +#include "game/strings.hpp" +#include "utility/hash/fnv1a.hpp" + +using namespace hash::literals; namespace game { namespace state { @@ -39,7 +42,7 @@ namespace state { options_menu::options_menu(game::context& ctx): game::state::base(ctx) { - debug::log::push_task("Entering options menu state"); + debug::log::trace("Entering options menu state..."); // Construct menu item texts scene::text* controls_text = new scene::text(); @@ -49,11 +52,11 @@ options_menu::options_menu(game::context& ctx): scene::text* back_text = new scene::text(); // Set content of menu item texts - controls_text->set_content((*ctx.strings)["options_menu_controls"]); - graphics_text->set_content((*ctx.strings)["options_menu_graphics"]); - sound_text->set_content((*ctx.strings)["options_menu_sound"]); - language_text->set_content((*ctx.strings)["options_menu_language"]); - back_text->set_content((*ctx.strings)["back"]); + controls_text->set_content(get_string(ctx, "options_menu_controls"_fnv1a32)); + graphics_text->set_content(get_string(ctx, "options_menu_graphics"_fnv1a32)); + sound_text->set_content(get_string(ctx, "options_menu_sound"_fnv1a32)); + language_text->set_content(get_string(ctx, "options_menu_language"_fnv1a32)); + back_text->set_content(get_string(ctx, "back"_fnv1a32)); // Build list of menu item texts ctx.menu_item_texts.push_back({controls_text, nullptr}); @@ -171,7 +174,7 @@ options_menu::options_menu(game::context& ctx): game::menu::clear_controls(ctx); // Save config - game::save::config(ctx); + //game::save::config(ctx); game::menu::fade_out ( @@ -216,12 +219,12 @@ options_menu::options_menu(game::context& ctx): // Fade in menu game::menu::fade_in(ctx, nullptr); - debug::log::pop_task(EXIT_SUCCESS); + debug::log::trace("Entered options menu state"); } options_menu::~options_menu() { - debug::log::push_task("Exiting options menu state"); + debug::log::trace("Exiting options menu state..."); // Destruct menu game::menu::clear_controls(ctx); @@ -230,7 +233,7 @@ options_menu::~options_menu() game::menu::remove_text_from_ui(ctx); game::menu::delete_text(ctx); - debug::log::pop_task(EXIT_SUCCESS); + debug::log::trace("Exited options menu state"); } } // namespace state diff --git a/src/game/state/pause-menu.cpp b/src/game/state/pause-menu.cpp index 93e9ef9..5e295a4 100644 --- a/src/game/state/pause-menu.cpp +++ b/src/game/state/pause-menu.cpp @@ -30,7 +30,10 @@ #include "debug/log.hpp" #include "animation/screen-transition.hpp" #include "config.hpp" -#include "game/save.hpp" +#include "game/strings.hpp" +#include "utility/hash/fnv1a.hpp" + +using namespace hash::literals; namespace game { namespace state { @@ -38,7 +41,7 @@ namespace state { pause_menu::pause_menu(game::context& ctx): game::state::base(ctx) { - debug::log::push_task("Entering pause menu state"); + debug::log::trace("Entering pause menu state..."); // Construct menu item texts scene::text* resume_text = new scene::text(); @@ -47,10 +50,10 @@ pause_menu::pause_menu(game::context& ctx): scene::text* quit_text = new scene::text(); // Set content of menu item texts - resume_text->set_content((*ctx.strings)["pause_menu_resume"]); - options_text->set_content((*ctx.strings)["pause_menu_options"]); - main_menu_text->set_content((*ctx.strings)["pause_menu_main_menu"]); - quit_text->set_content((*ctx.strings)["pause_menu_quit"]); + resume_text->set_content(get_string(ctx, "pause_menu_resume"_fnv1a32)); + options_text->set_content(get_string(ctx, "pause_menu_options"_fnv1a32)); + main_menu_text->set_content(get_string(ctx, "pause_menu_main_menu"_fnv1a32)); + quit_text->set_content(get_string(ctx, "pause_menu_quit"_fnv1a32)); // Build list of menu item texts ctx.menu_item_texts.push_back({resume_text, nullptr}); @@ -205,14 +208,14 @@ pause_menu::pause_menu(game::context& ctx): game::menu::fade_in_bg(ctx); // Save colony - game::save::colony(ctx); + //game::save::colony(ctx); - debug::log::pop_task(EXIT_SUCCESS); + debug::log::trace("Entered pause menu state"); } pause_menu::~pause_menu() { - debug::log::push_task("Exiting pause menu state"); + debug::log::trace("Exiting pause menu state..."); // Destruct menu game::menu::clear_controls(ctx); @@ -221,7 +224,7 @@ pause_menu::~pause_menu() game::menu::remove_text_from_ui(ctx); game::menu::delete_text(ctx); - debug::log::pop_task(EXIT_SUCCESS); + debug::log::trace("Exited pause menu state"); } } // namespace state diff --git a/src/game/state/sound-menu.cpp b/src/game/state/sound-menu.cpp index b6f00f5..405bb76 100644 --- a/src/game/state/sound-menu.cpp +++ b/src/game/state/sound-menu.cpp @@ -23,6 +23,10 @@ #include "scene/text.hpp" #include "debug/log.hpp" #include "game/menu.hpp" +#include "game/strings.hpp" +#include "utility/hash/fnv1a.hpp" + +using namespace hash::literals; namespace game { namespace state { @@ -30,7 +34,7 @@ namespace state { sound_menu::sound_menu(game::context& ctx): game::state::base(ctx) { - debug::log::push_task("Entering sound menu state"); + debug::log::trace("Entering sound menu state..."); // Construct menu item texts scene::text* master_volume_name_text = new scene::text(); @@ -57,13 +61,13 @@ sound_menu::sound_menu(game::context& ctx): ctx.menu_item_texts.push_back({back_text, nullptr}); // Set content of menu item texts - master_volume_name_text->set_content((*ctx.strings)["sound_menu_master_volume"]); - ambience_volume_name_text->set_content((*ctx.strings)["sound_menu_ambience_volume"]); - effects_volume_name_text->set_content((*ctx.strings)["sound_menu_effects_volume"]); - mono_audio_name_text->set_content((*ctx.strings)["sound_menu_mono_audio"]); - captions_name_text->set_content((*ctx.strings)["sound_menu_captions"]); - captions_size_name_text->set_content((*ctx.strings)["sound_menu_captions_size"]); - back_text->set_content((*ctx.strings)["back"]); + master_volume_name_text->set_content(get_string(ctx, "sound_menu_master_volume"_fnv1a32)); + ambience_volume_name_text->set_content(get_string(ctx, "sound_menu_ambience_volume"_fnv1a32)); + effects_volume_name_text->set_content(get_string(ctx, "sound_menu_effects_volume"_fnv1a32)); + mono_audio_name_text->set_content(get_string(ctx, "sound_menu_mono_audio"_fnv1a32)); + captions_name_text->set_content(get_string(ctx, "sound_menu_captions"_fnv1a32)); + captions_size_name_text->set_content(get_string(ctx, "sound_menu_captions_size"_fnv1a32)); + back_text->set_content(get_string(ctx, "back"_fnv1a32)); update_value_text_content(); // Init menu item index @@ -220,12 +224,12 @@ sound_menu::sound_menu(game::context& ctx): // Fade in menu game::menu::fade_in(ctx, nullptr); - debug::log::pop_task(EXIT_SUCCESS); + debug::log::trace("Entered sound menu state"); } sound_menu::~sound_menu() { - debug::log::push_task("Exiting sound menu state"); + debug::log::trace("Exiting sound menu state..."); // Destruct menu game::menu::clear_controls(ctx); @@ -234,21 +238,13 @@ sound_menu::~sound_menu() game::menu::remove_text_from_ui(ctx); game::menu::delete_text(ctx); - // Update config - (*ctx.config)["master_volume"] = ctx.master_volume; - (*ctx.config)["ambience_volume"] = ctx.ambience_volume; - (*ctx.config)["effects_volume"] = ctx.effects_volume; - (*ctx.config)["mono_audio"] = ctx.mono_audio; - (*ctx.config)["captions"] = ctx.captions; - (*ctx.config)["captions_size"] = ctx.captions_size; - - debug::log::pop_task(EXIT_SUCCESS); + debug::log::trace("Exited sound menu state"); } void sound_menu::update_value_text_content() { - const std::string string_on = (*ctx.strings)["on"]; - const std::string string_off = (*ctx.strings)["off"]; + const std::string string_on = get_string(ctx, "on"_fnv1a32); + const std::string string_off = get_string(ctx, "off"_fnv1a32); std::get<1>(ctx.menu_item_texts[0])->set_content(std::to_string(static_cast(std::round(ctx.master_volume * 100.0f))) + "%"); std::get<1>(ctx.menu_item_texts[1])->set_content(std::to_string(static_cast(std::round(ctx.ambience_volume * 100.0f))) + "%"); diff --git a/src/game/state/splash.cpp b/src/game/state/splash.cpp index 413db38..e262e97 100644 --- a/src/game/state/splash.cpp +++ b/src/game/state/splash.cpp @@ -38,7 +38,7 @@ splash::splash(game::context& ctx): game::state::base(ctx), skipped(false) { - debug::log::push_task("Entering splash state"); + debug::log::trace("Entering splash state..."); // Enable color buffer clearing in UI pass ctx.ui_clear_pass->set_cleared_buffers(true, true, false); @@ -67,15 +67,9 @@ splash::splash(game::context& ctx): ctx.ui_scene->add_object(&splash_billboard); // Load animation timing configuration - double splash_fade_in_duration = 0.0; - double splash_duration = 0.0; - double splash_fade_out_duration = 0.0; - if (ctx.config->contains("splash_fade_in_duration")) - splash_fade_in_duration = (*ctx.config)["splash_fade_in_duration"].get(); - if (ctx.config->contains("splash_duration")) - splash_duration = (*ctx.config)["splash_duration"].get(); - if (ctx.config->contains("splash_fade_out_duration")) - splash_fade_out_duration = (*ctx.config)["splash_fade_out_duration"].get(); + const double splash_fade_in_duration = 0.5; + const double splash_duration = 2.0; + const double splash_fade_out_duration = 0.5; // Construct splash fade in animation splash_fade_in_animation.set_interpolator(ease::out_cubic); @@ -164,12 +158,12 @@ splash::splash(game::context& ctx): ); ctx.input_mapper.connect(ctx.app->get_device_manager().get_event_queue()); - debug::log::pop_task(EXIT_SUCCESS); + debug::log::trace("Entered splash state"); } splash::~splash() { - debug::log::push_task("Exiting splash state"); + debug::log::trace("Exiting splash state..."); // Disable splash skipper ctx.input_mapper.disconnect(); @@ -187,7 +181,7 @@ splash::~splash() // Disable color buffer clearing in UI pass ctx.ui_clear_pass->set_cleared_buffers(false, true, false); - debug::log::pop_task(EXIT_SUCCESS); + debug::log::trace("Exited splash state"); } } // namespace state diff --git a/src/game/save.hpp b/src/game/strings.cpp similarity index 72% rename from src/game/save.hpp rename to src/game/strings.cpp index c96cf36..532712a 100644 --- a/src/game/save.hpp +++ b/src/game/strings.cpp @@ -17,25 +17,21 @@ * along with Antkeeper source code. If not, see . */ -#ifndef ANTKEEPER_GAME_SAVE_HPP -#define ANTKEEPER_GAME_SAVE_HPP - -#include "game/context.hpp" +#include "game/strings.hpp" +#include namespace game { -namespace save { -/** - * Saves the current ant colony. - */ -void colony(game::context& ctx); +std::string get_string(const game::context& ctx, std::uint32_t key) +{ + const auto& string_map = ctx.string_maps[ctx.language_index]; + + if (auto i = string_map.find(key); i != string_map.end()) + { + return i->second; + } + + return std::format("${:x}", key); +} -/** - * Saves the current configuration. - */ -void config(game::context& ctx); - -} // namespace save } // namespace game - -#endif // ANTKEEPER_GAME_SAVE_HPP diff --git a/src/game/strings.hpp b/src/game/strings.hpp new file mode 100644 index 0000000..ff7bdf5 --- /dev/null +++ b/src/game/strings.hpp @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2023 Christopher J. Howard + * + * This file is part of Antkeeper source code. + * + * Antkeeper source code is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Antkeeper source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Antkeeper source code. If not, see . + */ + +#ifndef ANTKEEPER_GAME_STRINGS_HPP +#define ANTKEEPER_GAME_STRINGS_HPP + +#include "game/context.hpp" +#include +#include + +namespace game { + +/** + * Returns a localized string. + * + * @param[in] ctx Game context. + * @param[in] key String key. + * @param[out] string String value. + * + * @return `true` if the string was found, `false` otherwise. + */ +[[nodiscard]] std::string get_string(const game::context& ctx, std::uint32_t key); + +} // namespace game + +#endif // ANTKEEPER_GAME_STRINGS_HPP diff --git a/src/game/system/subterrain.cpp b/src/game/system/subterrain.cpp index f820d59..17e7964 100644 --- a/src/game/system/subterrain.cpp +++ b/src/game/system/subterrain.cpp @@ -195,22 +195,22 @@ subterrain::subterrain(entity::registry& registry, ::resource_manager* resource_ { // Load subterrain materials - subterrain_inside_material = resource_manager->load<::render::material>("subterrain-inside.mtl"); - subterrain_inside_material = resource_manager->load<::render::material>("subterrain-outside.mtl"); + subterrain_inside_material = nullptr;//resource_manager->load<::render::material>("subterrain-inside.mtl"); + subterrain_outside_material = nullptr;//resource_manager->load<::render::material>("subterrain-outside.mtl"); // Allocate subterrain model subterrain_model = new ::render::model(); // Create inside model group subterrain_inside_group = subterrain_model->add_group("inside"); - subterrain_inside_group->set_material(resource_manager->load<::render::material>("subterrain-inside.mtl")); + subterrain_inside_group->set_material(subterrain_inside_material); subterrain_inside_group->set_drawing_mode(gl::drawing_mode::triangles); subterrain_inside_group->set_start_index(0); subterrain_inside_group->set_index_count(0); // Create outside model group subterrain_outside_group = subterrain_model->add_group("outside"); - subterrain_outside_group->set_material(resource_manager->load<::render::material>("subterrain-outside.mtl")); + subterrain_outside_group->set_material(subterrain_outside_material); subterrain_outside_group->set_drawing_mode(gl::drawing_mode::triangles); subterrain_outside_group->set_start_index(0); subterrain_outside_group->set_index_count(0); diff --git a/src/game/world.cpp b/src/game/world.cpp index 235be3f..b38cd75 100644 --- a/src/game/world.cpp +++ b/src/game/world.cpp @@ -65,6 +65,7 @@ #include "scene/ambient-light.hpp" #include "scene/directional-light.hpp" #include "scene/text.hpp" +#include "i18n/string-table.hpp" #include #include #include @@ -93,21 +94,20 @@ static void create_moon(game::context& ctx); void cosmogenesis(game::context& ctx) { - debug::log::push_task("Generating cosmos"); + debug::log::trace("Generating cosmos..."); load_ephemeris(ctx); create_stars(ctx); create_sun(ctx); create_earth_moon_system(ctx); - debug::log::pop_task(EXIT_SUCCESS); + debug::log::trace("Generated cosmos"); } void create_observer(game::context& ctx) { - debug::log::push_task("Creating observer"); + debug::log::trace("Creating observer..."); - try { // Create observer entity entity::id observer_eid = ctx.entity_registry->create(); @@ -136,13 +136,8 @@ void create_observer(game::context& ctx) // Set astronomy system observer ctx.astronomy_system->set_observer(observer_eid); } - catch (const std::exception&) - { - debug::log::pop_task(EXIT_FAILURE); - return; - } - debug::log::pop_task(EXIT_SUCCESS); + debug::log::trace("Created observer"); } void set_location(game::context& ctx, double elevation, double latitude, double longitude) @@ -170,18 +165,17 @@ void set_location(game::context& ctx, double elevation, double latitude, double void set_time(game::context& ctx, double t) { - debug::log::push_task("Setting time to UT1 " + std::to_string(t)); try { ctx.astronomy_system->set_time(t); ctx.orbit_system->set_time(t); + + debug::log::info("Set time to UT1 {}", t); } - catch (const std::exception&) + catch (const std::exception& e) { - debug::log::pop_task(EXIT_FAILURE); - return; + debug::log::error("Failed to set time to UT1 {}: {}", t, e.what()); } - debug::log::pop_task(EXIT_SUCCESS); } void set_time(game::context& ctx, int year, int month, int day, int hour, int minute, double second) @@ -220,60 +214,16 @@ void set_time_scale(game::context& ctx, double scale) void load_ephemeris(game::context& ctx) { - debug::log::push_task("Loading ephemeris"); - - try - { - std::string ephemeris_filename; - if (ctx.config->contains("ephemeris")) - { - ephemeris_filename = (*ctx.config)["ephemeris"].get(); - } - else - { - debug::log::warning("No ephemeris set in config"); - debug::log::pop_task(EXIT_FAILURE); - return; - } - - ctx.orbit_system->set_ephemeris(ctx.resource_manager->load>(ephemeris_filename)); - } - catch (const std::exception&) - { - debug::log::pop_task(EXIT_FAILURE); - return; - } - - debug::log::pop_task(EXIT_SUCCESS); + ctx.orbit_system->set_ephemeris(ctx.resource_manager->load>("de421.eph")); } void create_stars(game::context& ctx) { - debug::log::push_task("Generating fixed stars"); + debug::log::trace("Generating fixed stars..."); // Load star catalog - string_table* star_catalog = nullptr; - try - { - std::string star_catalog_filename; - if (ctx.config->contains("star_catalog")) - { - star_catalog_filename = (*ctx.config)["star_catalog"].get(); - } - else - { - debug::log::warning("No star catalog set in config"); - debug::log::pop_task(EXIT_FAILURE); - return; - } - - star_catalog = ctx.resource_manager->load(star_catalog_filename); - } - catch (const std::exception&) - { - debug::log::pop_task(EXIT_FAILURE); - return; - } + i18n::string_table* star_catalog = nullptr; + star_catalog = ctx.resource_manager->load("hipparcos-7.tsv"); // Allocate star catalog vertex data std::size_t star_count = 0; @@ -290,23 +240,24 @@ void create_stars(game::context& ctx) // Build star catalog vertex data for (std::size_t i = 1; i < star_catalog->size(); ++i) { - const string_table_row& catalog_row = (*star_catalog)[i]; + const i18n::string_table_row& catalog_row = (*star_catalog)[i]; // Parse star catalog item - double ra = 0.0; - double dec = 0.0; - double vmag = 0.0; - double bv = 0.0; + float ra = 0.0; + float dec = 0.0; + float vmag = 0.0; + float bv = 0.0; try { - ra = std::stod(catalog_row[1]); - dec = std::stod(catalog_row[2]); - vmag = std::stod(catalog_row[3]); - bv = std::stod(catalog_row[4]); + ra = std::stof(catalog_row[1]); + dec = std::stof(catalog_row[2]); + vmag = std::stof(catalog_row[3]); + bv = std::stof(catalog_row[4]); } catch (const std::exception&) { - debug::log::warning("Invalid star catalog item on row " + std::to_string(i)); + debug::log::warning("Invalid star catalog item on row {}", i); + continue; } // Convert right ascension and declination from degrees to radians @@ -314,38 +265,38 @@ void create_stars(game::context& ctx) dec = math::wrap_radians(math::radians(dec)); // Convert ICRF coordinates from spherical to Cartesian - double3 position = physics::orbit::frame::bci::cartesian(double3{1.0, dec, ra}); + float3 position = physics::orbit::frame::bci::cartesian(float3{1.0f, dec, ra}); // Convert color index to color temperature - double cct = color::index::bv_to_cct(bv); + float cct = color::index::bv_to_cct(bv); // Calculate XYZ color from color temperature - double3 color_xyz = color::cct::to_xyz(cct); + float3 color_xyz = color::cct::to_xyz(cct); // Transform XYZ color to ACEScg colorspace - double3 color_acescg = color::aces::ap1.from_xyz * color_xyz; + float3 color_acescg = color::aces::ap1.from_xyz * color_xyz; // Convert apparent magnitude to brightness factor relative to a 0th magnitude star - double brightness = physics::light::vmag::to_brightness(vmag); + float brightness = physics::light::vmag::to_brightness(vmag); // Build vertex - *(star_vertex++) = static_cast(position.x()); - *(star_vertex++) = static_cast(position.y()); - *(star_vertex++) = static_cast(position.z()); - *(star_vertex++) = static_cast(color_acescg.x()); - *(star_vertex++) = static_cast(color_acescg.y()); - *(star_vertex++) = static_cast(color_acescg.z()); - *(star_vertex++) = static_cast(brightness); + *(star_vertex++) = position.x(); + *(star_vertex++) = position.y(); + *(star_vertex++) = position.z(); + *(star_vertex++) = color_acescg.x(); + *(star_vertex++) = color_acescg.y(); + *(star_vertex++) = color_acescg.z(); + *(star_vertex++) = brightness; // Calculate spectral illuminance - double3 illuminance = color_acescg * physics::light::vmag::to_illuminance(vmag); + double3 illuminance = double3(color_acescg * physics::light::vmag::to_illuminance(vmag)); // Add spectral illuminance to total starlight illuminance starlight_illuminance += illuminance; } // Unload star catalog - ctx.resource_manager->unload("stars.csv"); + ctx.resource_manager->unload("hipparcos-7.tsv"); // Allocate stars model render::model* stars_model = new render::model(); @@ -400,14 +351,13 @@ void create_stars(game::context& ctx) // Pass starlight illuminance to astronomy system ctx.astronomy_system->set_starlight_illuminance(starlight_illuminance); - debug::log::pop_task(EXIT_SUCCESS); + debug::log::trace("Generated fixed stars"); } void create_sun(game::context& ctx) { - debug::log::push_task("Generating Sun"); + debug::log::trace("Generating Sun..."); - try { // Create sun entity entity::archetype* sun_archetype = ctx.resource_manager->load("sun.ent"); @@ -446,20 +396,14 @@ void create_sun(game::context& ctx) ctx.astronomy_system->set_sky_light(sky_light); ctx.astronomy_system->set_bounce_light(bounce_light); } - catch (const std::exception&) - { - debug::log::pop_task(EXIT_FAILURE); - return; - } - debug::log::pop_task(EXIT_SUCCESS); + debug::log::trace("Generated Sun"); } void create_earth_moon_system(game::context& ctx) { - debug::log::push_task("Generating Earth-Moon system"); + debug::log::trace("Generating Earth-Moon system..."); - try { // Create Earth-Moon barycenter entity entity::archetype* em_bary_archetype = ctx.resource_manager->load("em-bary.ent"); @@ -472,20 +416,14 @@ void create_earth_moon_system(game::context& ctx) // Create Moon create_moon(ctx); } - catch (const std::exception&) - { - debug::log::pop_task(EXIT_FAILURE); - return; - } - debug::log::pop_task(EXIT_SUCCESS); + debug::log::trace("Generated Earth-Moon system"); } void create_earth(game::context& ctx) { - debug::log::push_task("Generating Earth"); + debug::log::trace("Generating Earth..."); - try { // Create earth entity entity::archetype* earth_archetype = ctx.resource_manager->load("earth.ent"); @@ -495,20 +433,14 @@ void create_earth(game::context& ctx) // Assign orbital parent ctx.entity_registry->get(earth_eid).parent = ctx.entities["em_bary"]; } - catch (const std::exception&) - { - debug::log::pop_task(EXIT_FAILURE); - return; - } - debug::log::pop_task(EXIT_SUCCESS); + debug::log::trace("Generated Earth"); } void create_moon(game::context& ctx) { - debug::log::push_task("Generating Moon"); + debug::log::trace("Generating Moon..."); - try { // Create lunar entity entity::archetype* moon_archetype = ctx.resource_manager->load("moon.ent"); @@ -532,13 +464,8 @@ void create_moon(game::context& ctx) // Pass moon light scene object to astronomy system ctx.astronomy_system->set_moon_light(moon_light); } - catch (const std::exception&) - { - debug::log::pop_task(EXIT_FAILURE); - return; - } - debug::log::pop_task(EXIT_SUCCESS); + debug::log::trace("Generated Moon"); } void enter_ecoregion(game::context& ctx, const ecoregion& ecoregion) @@ -595,12 +522,11 @@ void enter_ecoregion(game::context& ctx, const ecoregion& ecoregion) ); stbi_flip_vertically_on_write(1); - stbi_write_tga((ctx.config_path / "gallery" / "voronoi-f1-400-nc8-2k.tga").string().c_str(), img.get_width(), img.get_height(), img.get_channel_count(), img.data()); + stbi_write_tga((ctx.screenshots_path / "voronoi-f1-400-nc8-2k.tga").string().c_str(), img.get_width(), img.get_height(), img.get_channel_count(), img.data()); */ - debug::log::push_task("Entering ecoregion " + ecoregion.name); - try + debug::log::trace("Entering ecoregion {}...", ecoregion.name); { // Set active ecoregion ctx.active_ecoregion = &ecoregion; @@ -641,11 +567,8 @@ void enter_ecoregion(game::context& ctx, const ecoregion& ecoregion) ); ctx.astronomy_system->set_bounce_albedo(double3(ecoregion.terrain_albedo)); } - catch (...) - { - debug::log::pop_task(EXIT_FAILURE); - } - debug::log::pop_task(EXIT_SUCCESS); + + debug::log::trace("Entered ecoregion {}", ecoregion.name); } } // namespace world diff --git a/src/gl/gl.hpp b/src/gl/gl.hpp index fcb64e8..a6d41bc 100644 --- a/src/gl/gl.hpp +++ b/src/gl/gl.hpp @@ -20,28 +20,28 @@ #ifndef ANTKEEPER_GL_HPP #define ANTKEEPER_GL_HPP -/// Graphics library (GL) is a cross-platform GPU interface. +/// Graphics library interface. namespace gl {} -#include "buffer-usage.hpp" -#include "color-space.hpp" -#include "drawing-mode.hpp" -#include "element-array-type.hpp" -#include "framebuffer.hpp" -#include "pixel-format.hpp" -#include "pixel-type.hpp" -#include "rasterizer.hpp" -#include "shader-input.hpp" -#include "shader-object.hpp" -#include "shader-program.hpp" -#include "shader-stage.hpp" -#include "shader-variable-type.hpp" -#include "texture-2d.hpp" -#include "texture-cube.hpp" -#include "texture-filter.hpp" -#include "texture-wrapping.hpp" -#include "vertex-array.hpp" -#include "vertex-attribute.hpp" -#include "vertex-buffer.hpp" +#include "gl/buffer-usage.hpp" +#include "gl/color-space.hpp" +#include "gl/drawing-mode.hpp" +#include "gl/element-array-type.hpp" +#include "gl/framebuffer.hpp" +#include "gl/pixel-format.hpp" +#include "gl/pixel-type.hpp" +#include "gl/rasterizer.hpp" +#include "gl/shader-input.hpp" +#include "gl/shader-object.hpp" +#include "gl/shader-program.hpp" +#include "gl/shader-stage.hpp" +#include "gl/shader-variable-type.hpp" +#include "gl/texture-2d.hpp" +#include "gl/texture-cube.hpp" +#include "gl/texture-filter.hpp" +#include "gl/texture-wrapping.hpp" +#include "gl/vertex-array.hpp" +#include "gl/vertex-attribute.hpp" +#include "gl/vertex-buffer.hpp" #endif // ANTKEEPER_GL_HPP diff --git a/src/i18n/i18n.hpp b/src/i18n/i18n.hpp new file mode 100644 index 0000000..946d3a2 --- /dev/null +++ b/src/i18n/i18n.hpp @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2023 Christopher J. Howard + * + * This file is part of Antkeeper source code. + * + * Antkeeper source code is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Antkeeper source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Antkeeper source code. If not, see . + */ + +#ifndef ANTKEEPER_I18N_HPP +#define ANTKEEPER_I18N_HPP + +/// Internationalization and localization. +namespace i18n {} + +#endif // ANTKEEPER_I18N_HPP diff --git a/src/i18n/string-map.cpp b/src/i18n/string-map.cpp new file mode 100644 index 0000000..15361f3 --- /dev/null +++ b/src/i18n/string-map.cpp @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2023 Christopher J. Howard + * + * This file is part of Antkeeper source code. + * + * Antkeeper source code is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Antkeeper source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Antkeeper source code. If not, see . + */ + +#include "i18n/string-map.hpp" +#include "utility/hash/fnv1a.hpp" +#include + +namespace i18n { + +void build_string_map(const string_table& table, std::size_t key_column, std::size_t value_column, string_map& map) +{ + map.clear(); + + const std::size_t max_column = std::max(key_column, value_column); + + for (const auto& row: table) + { + if (row.size() <= max_column) + { + continue; + } + + const auto& value_string = row[value_column]; + if (value_string.empty()) + { + continue; + } + + const auto& key_string = row[key_column]; + const std::uint32_t key_hash = hash::fnv1a32(key_string.c_str(), key_string.length()); + map[key_hash] = row[value_column]; + } +} + +} // namespace i18n diff --git a/src/resources/string-table.hpp b/src/i18n/string-map.hpp similarity index 50% rename from src/resources/string-table.hpp rename to src/i18n/string-map.hpp index 91c1429..5c8d538 100644 --- a/src/resources/string-table.hpp +++ b/src/i18n/string-map.hpp @@ -17,38 +17,30 @@ * along with Antkeeper source code. If not, see . */ -#ifndef ANTKEEPER_STRING_TABLE_HPP -#define ANTKEEPER_STRING_TABLE_HPP +#ifndef ANTKEEPER_I18N_STRING_MAP_HPP +#define ANTKEEPER_I18N_STRING_MAP_HPP +#include "i18n/string-table.hpp" #include #include -#include -/** - * A single row in a string table. - */ -typedef std::vector string_table_row; +namespace i18n { /** - * A table of strings. + * Maps 32-bit keys to strings. */ -typedef std::vector string_table; - -/** - * An index for finding elements in a string table. - */ -typedef std::unordered_map string_table_index; - -typedef std::unordered_map> string_table_map; - -void build_string_table_map(string_table_map* map, const string_table& table); +typedef std::unordered_map string_map; /** - * Creates an index for a string table using strings in the first column as keys. + * Builds a string map from a string table. Keys are generated with the 32-bit FNV-1a hash function. * - * @param table Table for which an index will be created. + * @param[in] table String table from which the string map will be built. + * @param[in] key_column Column containing key strings. + * @param[in] value_column Column containing value strings. + * @param[out] String map to build. */ -string_table_index index_string_table(const string_table& table); +void build_string_map(const string_table& table, std::size_t key_column, std::size_t value_column, string_map& map); -#endif // ANTKEEPER_STRING_TABLE_HPP +} // namespace i18n +#endif // ANTKEEPER_I18N_STRING_MAP_HPP diff --git a/src/utility/type-id.hpp b/src/i18n/string-table.hpp similarity index 63% rename from src/utility/type-id.hpp rename to src/i18n/string-table.hpp index 5d53dcd..6b2562e 100644 --- a/src/utility/type-id.hpp +++ b/src/i18n/string-table.hpp @@ -17,26 +17,24 @@ * along with Antkeeper source code. If not, see . */ -#ifndef ANTKEEPER_UTILITY_TYPE_ID_HPP -#define ANTKEEPER_UTILITY_TYPE_ID_HPP +#ifndef ANTKEEPER_I18N_STRING_TABLE_HPP +#define ANTKEEPER_I18N_STRING_TABLE_HPP + +#include +#include + +namespace i18n { /** - * Provides unique type IDs at compile-time via the templated addresses of this function. - * - * @tparam T Type. + * A single row in a string table. */ -template -void type_id_generator() noexcept {} - -/// Type ID type. -using type_id_t = void(*)(void); +typedef std::vector string_table_row; /** - * Compile-time constant type ID. - * - * @tparam T Type. + * A table of strings. */ -template -constexpr type_id_t type_id = &type_id_generator; +typedef std::vector string_table; + +} // namespace i18n -#endif // ANTKEEPER_UTILITY_TYPE_ID_HPP +#endif // ANTKEEPER_I18N_STRING_TABLE_HPP diff --git a/src/input/event.hpp b/src/input/event.hpp index 5b57135..c555841 100644 --- a/src/input/event.hpp +++ b/src/input/event.hpp @@ -237,15 +237,22 @@ struct mouse_scrolled }; /** - * Event generated when a window has been closed. + * Event generated when a window has been requested to close. */ -struct window_closed {}; +struct window_closed +{ + /// Pointer to the window that has been requested to close. + void* window; +}; /** * Event generated when a window has gained or lost focus. */ struct window_focus_changed { + /// Pointer to the window that has gained or lost focus. + void* window; + /// `true` if the window is in focus, `false` otherwise. bool in_focus; }; @@ -255,8 +262,44 @@ struct window_focus_changed */ struct window_moved { + /// Pointer to the window that has been moved. + void* window; + /// Position of the window, in pixels. math::vector position; + + /// `true` if the window is maximized, `false` otherwise. + bool maximized; + + /// `true` if the window is fullscreen, `false` otherwise. + bool fullscreen; +}; + +/** + * Event generated when a window has been maximized. + */ +struct window_maximized +{ + /// Pointer to the window that has been maximized. + void* window; +}; + +/** + * Event generated when a window has been minimized. + */ +struct window_minimized +{ + /// Pointer to the window that has been minimized. + void* window; +}; + +/** + * Event generated when a window has been restored. + */ +struct window_restored +{ + /// Pointer to the window that has been restored. + void* window; }; /** @@ -264,17 +307,20 @@ struct window_moved */ struct window_resized { - /// Window width, in pixels. - std::int32_t window_width; + /// Pointer to the window that has been resized. + void* window; + + /// Window size, in display units. + math::vector size; - /// Window height, in pixels. - std::int32_t window_height; + /// `true` if the window is maximized, `false` otherwise. + bool maximized; - /// Viewport width, in pixels. - std::int32_t viewport_width; + /// `true` if the window is fullscreen, `false` otherwise. + bool fullscreen; - /// Viewport height, in pixels. - std::int32_t viewport_height; + /// Window viewport size, in pixels. + math::vector viewport_size; }; } // namespace event diff --git a/src/main.cpp b/src/main.cpp index 8a22637..a7d86c4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -24,21 +24,26 @@ #include "game/state/boot.hpp" #include "utility/ansi.hpp" #include "utility/paths.hpp" +#include #include #include +#include #include #include #include int main(int argc, char* argv[]) { + // Get time at which the application launched + const auto launch_time = std::chrono::system_clock::now(); + // Enable VT100 sequences in console for colored text debug::console::enable_vt100(); // Subscribe log to cout function to message logged events auto log_to_cout_subscription = debug::log::default_logger().get_message_logged_channel().subscribe ( - [&](const auto& event) + [&launch_time](const auto& event) { static const char* severities[] = { @@ -62,7 +67,8 @@ int main(int argc, char* argv[]) std::osyncstream(std::cout) << std::format ( - "{}:{}:{}: {}{}: {}{}\n", + "{:8.03f} {}:{}:{}: {}{}: {}{}\n", + std::chrono::duration(event.time - launch_time).count(), std::filesystem::path(event.location.file_name()).filename().string(), event.location.line(), event.location.column(), @@ -75,7 +81,7 @@ int main(int argc, char* argv[]) ); // Determine path to log archive - const std::filesystem::path log_archive_path = get_config_path(config::application_name) / "logs"; + const std::filesystem::path log_archive_path = get_shared_config_path() / config::application_name / "logs"; // Set up log archive bool log_archive_exists = false; @@ -130,14 +136,15 @@ int main(int argc, char* argv[]) // Set up logging to file std::shared_ptr log_to_file_subscription; + std::filesystem::path log_filepath; if (config::debug_log_archive_capacity && log_archive_exists) { // Determine log filename - const auto time = std::chrono::floor(std::chrono::system_clock::now()); + const auto time = std::chrono::floor(launch_time); const std::string log_filename = std::format("{0}-{1:%Y%m%d}T{1:%H%M%S}Z.log", config::application_name, time); // Open log file - std::filesystem::path log_filepath = log_archive_path / log_filename; + log_filepath = log_archive_path / log_filename; const std::string log_filepath_string = log_filepath.string(); auto log_filestream = std::make_shared(log_filepath); @@ -153,12 +160,12 @@ int main(int argc, char* argv[]) // Subscribe log to file function to message logged events log_to_file_subscription = debug::log::default_logger().get_message_logged_channel().subscribe ( - [log_filestream](const auto& event) + [&launch_time, log_filestream](const auto& event) { std::osyncstream(*log_filestream) << std::format ( - "\n{0:%Y%m%d}T{0:%H%M%S}Z\t{1}\t{2}\t{3}\t{4}\t{5}", - std::chrono::floor(event.time), + "\n{:.03f}\t{}\t{}\t{}\t{}\t{}", + std::chrono::duration(event.time - launch_time).count(), std::filesystem::path(event.location.file_name()).filename().string(), event.location.line(), event.location.column(), @@ -184,8 +191,11 @@ int main(int argc, char* argv[]) } } - // Log application name and version string - debug::log::info("{} v{}", config::application_name, config::application_version_string); + // Log application name and version string, followed by launch time + debug::log::info("{0} {1}; {2:%Y%m%d}T{2:%H%M%S}Z", config::application_name, config::application_version_string, std::chrono::floor(launch_time)); + + // Start marker + debug::log::debug("Hi! 🐜"); try { @@ -198,8 +208,16 @@ int main(int argc, char* argv[]) catch (const std::exception& e) { debug::log::fatal("Unhandled exception: {}", e.what()); + + #if defined(NDEBUG) + SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Error", std::format("Unhandled exception: {}", e.what()).c_str(), nullptr); + #endif + return EXIT_FAILURE; } + // Clean exit marker + debug::log::debug("Bye! 🐜"); + return EXIT_SUCCESS; } diff --git a/src/render/anti-aliasing-method.hpp b/src/render/anti-aliasing-method.hpp index 78129fa..44206b4 100644 --- a/src/render/anti-aliasing-method.hpp +++ b/src/render/anti-aliasing-method.hpp @@ -20,12 +20,14 @@ #ifndef ANTKEEPER_RENDER_ANTI_ALIASING_METHOD_HPP #define ANTKEEPER_RENDER_ANTI_ALIASING_METHOD_HPP +#include + namespace render { /** * Anti-aliasing methods. */ -enum class anti_aliasing_method +enum class anti_aliasing_method: std::uint8_t { /// No anti-aliasing. none, diff --git a/src/render/passes/fxaa-pass.cpp b/src/render/passes/fxaa-pass.cpp index e69d075..d715984 100644 --- a/src/render/passes/fxaa-pass.cpp +++ b/src/render/passes/fxaa-pass.cpp @@ -30,6 +30,7 @@ #include "gl/texture-2d.hpp" #include "render/vertex-attribute.hpp" #include "render/context.hpp" +#include "debug/log.hpp" #include namespace render { diff --git a/src/render/passes/ground-pass.cpp b/src/render/passes/ground-pass.cpp index 6eddb7b..c874a67 100644 --- a/src/render/passes/ground-pass.cpp +++ b/src/render/passes/ground-pass.cpp @@ -19,7 +19,6 @@ #include "render/passes/ground-pass.hpp" #include "resources/resource-manager.hpp" -#include "resources/string-table.hpp" #include "gl/rasterizer.hpp" #include "gl/framebuffer.hpp" #include "gl/shader-program.hpp" diff --git a/src/render/passes/material-pass.cpp b/src/render/passes/material-pass.cpp index dcd6865..e68e29f 100644 --- a/src/render/passes/material-pass.cpp +++ b/src/render/passes/material-pass.cpp @@ -591,8 +591,6 @@ const material_pass::parameter_set* material_pass::load_parameter_set(const gl:: bool operation_compare(const render::operation& a, const render::operation& b) { - /// @TODO: something is wrong with this compare op, assertion fails - if (!a.material) return false; else if (!b.material) diff --git a/src/render/passes/sky-pass.cpp b/src/render/passes/sky-pass.cpp index 7af5638..459a8f2 100644 --- a/src/render/passes/sky-pass.cpp +++ b/src/render/passes/sky-pass.cpp @@ -19,7 +19,6 @@ #include "render/passes/sky-pass.hpp" #include "resources/resource-manager.hpp" -#include "resources/string-table.hpp" #include "gl/rasterizer.hpp" #include "gl/framebuffer.hpp" #include "gl/shader-program.hpp" diff --git a/src/resources/deserialize-context.cpp b/src/resources/deserialize-context.cpp new file mode 100644 index 0000000..d22c62f --- /dev/null +++ b/src/resources/deserialize-context.cpp @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2023 Christopher J. Howard + * + * This file is part of Antkeeper source code. + * + * Antkeeper source code is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Antkeeper source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Antkeeper source code. If not, see . + */ + +#include "resources/deserialize-context.hpp" +#include "resources/deserialize-error.hpp" +#include + +deserialize_context::deserialize_context(void* handle): + handle(handle), + m_eof(false), + m_error(false) +{} + +std::size_t deserialize_context::read8(std::byte* data, std::size_t count) +{ + const PHYSFS_sint64 status = PHYSFS_readBytes(reinterpret_cast(handle), data, count); + + if (status < 0) + { + m_error = true; + throw deserialize_error(PHYSFS_getLastError()); + //return 0; + } + + if (status != count) + { + m_eof = true; + m_error = true; + throw deserialize_error(PHYSFS_getLastError()); + //return static_cast(count); + } + + return count; +} + +std::size_t deserialize_context::read16_le(std::byte* data, std::size_t count) +{ + PHYSFS_File* file = reinterpret_cast(handle); + PHYSFS_uint16* data16 = reinterpret_cast(data); + + for (std::size_t i = 0; i < count; ++i) + { + if (!PHYSFS_readULE16(file, data16)) + { + m_eof = (PHYSFS_eof(file) != 0); + m_error = true; + throw deserialize_error(PHYSFS_getLastError()); + //return i; + } + + ++data16; + } + + return count; +} + +std::size_t deserialize_context::read16_be(std::byte* data, std::size_t count) +{ + PHYSFS_File* file = reinterpret_cast(handle); + PHYSFS_uint16* data16 = reinterpret_cast(data); + + for (std::size_t i = 0; i < count; ++i) + { + if (!PHYSFS_readUBE16(file, data16)) + { + m_eof = (PHYSFS_eof(file) != 0); + m_error = true; + throw deserialize_error(PHYSFS_getLastError()); + //return i; + } + + ++data16; + } + + return count; +} + +std::size_t deserialize_context::read32_le(std::byte* data, std::size_t count) +{ + PHYSFS_File* file = reinterpret_cast(handle); + PHYSFS_uint32* data32 = reinterpret_cast(data); + + for (std::size_t i = 0; i < count; ++i) + { + if (!PHYSFS_readULE32(file, data32)) + { + m_eof = (PHYSFS_eof(file) != 0); + m_error = true; + throw deserialize_error(PHYSFS_getLastError()); + //return i; + } + + ++data32; + } + + return count; +} + +std::size_t deserialize_context::read32_be(std::byte* data, std::size_t count) +{ + PHYSFS_File* file = reinterpret_cast(handle); + PHYSFS_uint32* data32 = reinterpret_cast(data); + + for (std::size_t i = 0; i < count; ++i) + { + if (!PHYSFS_readUBE32(file, data32)) + { + m_eof = (PHYSFS_eof(file) != 0); + m_error = true; + throw deserialize_error(PHYSFS_getLastError()); + //return i; + } + + ++data32; + } + + return count; +} + +std::size_t deserialize_context::read64_le(std::byte* data, std::size_t count) +{ + PHYSFS_File* file = reinterpret_cast(handle); + PHYSFS_uint64* data64 = reinterpret_cast(data); + + for (std::size_t i = 0; i < count; ++i) + { + if (!PHYSFS_readULE64(file, data64)) + { + m_eof = (PHYSFS_eof(file) != 0); + m_error = true; + throw deserialize_error(PHYSFS_getLastError()); + //return i; + } + + ++data64; + } + + return count; +} + +std::size_t deserialize_context::read64_be(std::byte* data, std::size_t count) +{ + PHYSFS_File* file = reinterpret_cast(handle); + PHYSFS_uint64* data64 = reinterpret_cast(data); + + for (std::size_t i = 0; i < count; ++i) + { + if (!PHYSFS_readUBE64(file, data64)) + { + m_eof = (PHYSFS_eof(file) != 0); + m_error = true; + throw deserialize_error(PHYSFS_getLastError()); + //return i; + } + + ++data64; + } + + return count; +} diff --git a/src/resources/deserialize-context.hpp b/src/resources/deserialize-context.hpp new file mode 100644 index 0000000..7e00cc4 --- /dev/null +++ b/src/resources/deserialize-context.hpp @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2023 Christopher J. Howard + * + * This file is part of Antkeeper source code. + * + * Antkeeper source code is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Antkeeper source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Antkeeper source code. If not, see . + */ + +#ifndef ANTKEEPER_RESOURCES_DESERIALIZE_CONTEXT_HPP +#define ANTKEEPER_RESOURCES_DESERIALIZE_CONTEXT_HPP + +#include +#include + +/** + * Provides access to a deserialization state. + */ +struct deserialize_context +{ +public: + /** + * Reads 8-bit (byte) data. + * + * @param data Pointer to data source. + * @param count Number of bytes to read. + * + * @return Number of bytes read. + * + * @throw deserialize_error Read error. + */ + std::size_t read8(std::byte* data, std::size_t count) noexcept(false); + + /** + * Reads 16-bit (word) data. + * + * @tparam Endian Endianness of the read operation. + * + * @param data Pointer to data destination. + * @param count Number of words to read. + * + * @return Number of words read. + * + * @throw deserialize_error Read error. + */ + template + inline std::size_t read16(std::byte* data, std::size_t count) noexcept(false) + { + if constexpr (Endian == std::endian::little) + { + return read16_le(data, count); + } + else + { + return read16_be(data, count); + } + } + + /** + * Reads 32-bit (double word) data. + * + * @tparam Endian Endianness of the read operation. + * + * @param data Pointer to data destination. + * @param count Number of double words to read. + * + * @return Number of double words read. + * + * @throw deserialize_error Read error. + */ + template + inline std::size_t read32(std::byte* data, std::size_t count) noexcept(false) + { + if constexpr (Endian == std::endian::little) + { + return read32_le(data, count); + } + else + { + return read32_be(data, count); + } + } + + /** + * Reads 64-bit (quad word) data. + * + * @tparam Endian Endianness of the read operation. + * + * @param data Pointer to data destination. + * @param count Number of quad words to read. + * + * @return Number of quad words read. + * + * @throw deserialize_error Read error. + */ + template + inline std::size_t read64(std::byte* data, std::size_t count) noexcept(false) + { + if constexpr (Endian == std::endian::little) + { + return read64_le(data, count); + } + else + { + return read64_be(data, count); + } + } + + /** + * Returns `true` if the end of a file was reached. + */ + inline bool eof() const noexcept + { + return m_eof; + } + + /** + * Returns `true` if an error occured during a read operation, `false` otherwise. + */ + inline bool error() const noexcept + { + return m_error; + } + +private: + template + friend class resource_loader; + + deserialize_context(void* handle); + std::size_t read16_le(std::byte* data, std::size_t count) noexcept(false); + std::size_t read16_be(std::byte* data, std::size_t count) noexcept(false); + std::size_t read32_le(std::byte* data, std::size_t count) noexcept(false); + std::size_t read32_be(std::byte* data, std::size_t count) noexcept(false); + std::size_t read64_le(std::byte* data, std::size_t count) noexcept(false); + std::size_t read64_be(std::byte* data, std::size_t count) noexcept(false); + + void* handle; + bool m_eof; + bool m_error; +}; + +#endif // ANTKEEPER_RESOURCES_DESERIALIZE_CONTEXT_HPP diff --git a/src/resources/string-table.cpp b/src/resources/deserialize-error.hpp similarity index 57% rename from src/resources/string-table.cpp rename to src/resources/deserialize-error.hpp index 24acf2e..37cb5e2 100644 --- a/src/resources/string-table.cpp +++ b/src/resources/deserialize-error.hpp @@ -17,30 +17,17 @@ * along with Antkeeper source code. If not, see . */ -#include "resources/string-table.hpp" +#ifndef ANTKEEPER_RESOURCES_DESERIALIZE_ERROR_HPP +#define ANTKEEPER_RESOURCES_DESERIALIZE_ERROR_HPP -void build_string_table_map(string_table_map* map, const string_table& table) -{ - map->clear(); - - for (std::size_t i = 0; i < table.size(); ++i) - { - for (std::size_t j = 2; j < table[i].size(); ++j) - { - const std::string& string = table[i][j]; - (*map)[table[0][j]][table[i][0]] = string.empty() ? "# MISSING STRING #" : string; - } - } -} +#include -string_table_index index_string_table(const string_table& table) +/** + * An exception of this type is thrown when an error occurs during deserialization. + */ +class deserialize_error: public std::runtime_error { - string_table_index index; + using std::runtime_error::runtime_error; +}; - for (std::size_t i = 0; i < table.size(); ++i) - { - index[table[i][0]] = i; - } - - return index; -} +#endif // ANTKEEPER_RESOURCES_DESERIALIZE_ERROR_HPP diff --git a/src/resources/deserializer.cpp b/src/resources/deserializer.cpp new file mode 100644 index 0000000..3727b9e --- /dev/null +++ b/src/resources/deserializer.cpp @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2023 Christopher J. Howard + * + * This file is part of Antkeeper source code. + * + * Antkeeper source code is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Antkeeper source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Antkeeper source code. If not, see . + */ + +#include "resources/deserializer.hpp" +#include + +template <> +void deserializer::deserialize(bool& value, deserialize_context& ctx) +{ + std::uint8_t temp; + ctx.read8(reinterpret_cast(&temp), 1); + value = (temp) ? true : false; +}; + +template <> +void deserializer::deserialize(std::uint8_t& value, deserialize_context& ctx) +{ + ctx.read8(reinterpret_cast(&value), 1); +}; + +template <> +void deserializer::deserialize(std::uint16_t& value, deserialize_context& ctx) +{ + ctx.read16(reinterpret_cast(&value), 1); +}; + +template <> +void deserializer::deserialize(std::uint32_t& value, deserialize_context& ctx) +{ + ctx.read32(reinterpret_cast(&value), 1); +}; + +template <> +void deserializer::deserialize(std::uint64_t& value, deserialize_context& ctx) +{ + ctx.read64(reinterpret_cast(&value), 1); +}; + +template <> +void deserializer::deserialize(std::int8_t& value, deserialize_context& ctx) +{ + ctx.read8(reinterpret_cast(&value), 1); +}; + +template <> +void deserializer::deserialize(std::int16_t& value, deserialize_context& ctx) +{ + ctx.read16(reinterpret_cast(&value), 1); +}; + +template <> +void deserializer::deserialize(std::int32_t& value, deserialize_context& ctx) +{ + ctx.read32(reinterpret_cast(&value), 1); +}; + +template <> +void deserializer::deserialize(std::int64_t& value, deserialize_context& ctx) +{ + ctx.read64(reinterpret_cast(&value), 1); +}; + +template <> +void deserializer::deserialize(float& value, deserialize_context& ctx) +{ + ctx.read32(reinterpret_cast(&value), 1); +}; + +template <> +void deserializer::deserialize(double& value, deserialize_context& ctx) +{ + ctx.read64(reinterpret_cast(&value), 1); +}; + +template <> +void deserializer::deserialize(std::string& value, deserialize_context& ctx) +{ + std::uint64_t length = 0; + ctx.read64(reinterpret_cast(&length), 1); + value.resize(static_cast(length)); + ctx.read8(reinterpret_cast(value.data()), static_cast(length)); +}; + +template <> +void deserializer::deserialize(std::u8string& value, deserialize_context& ctx) +{ + std::uint64_t length = 0; + ctx.read64(reinterpret_cast(&length), 1); + value.resize(static_cast(length)); + ctx.read8(reinterpret_cast(value.data()), static_cast(length)); +}; + +template <> +void deserializer::deserialize(std::u16string& value, deserialize_context& ctx) +{ + std::uint64_t length = 0; + ctx.read64(reinterpret_cast(&length), 1); + value.resize(static_cast(length)); + ctx.read16(reinterpret_cast(value.data()), static_cast(length)); +}; + +template <> +void deserializer::deserialize(std::u32string& value, deserialize_context& ctx) +{ + std::uint64_t length = 0; + ctx.read64(reinterpret_cast(&length), 1); + value.resize(static_cast(length)); + ctx.read32(reinterpret_cast(value.data()), static_cast(length)); +}; diff --git a/src/resources/deserializer.hpp b/src/resources/deserializer.hpp new file mode 100644 index 0000000..6d013f9 --- /dev/null +++ b/src/resources/deserializer.hpp @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2023 Christopher J. Howard + * + * This file is part of Antkeeper source code. + * + * Antkeeper source code is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Antkeeper source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Antkeeper source code. If not, see . + */ + +#ifndef ANTKEEPER_RESOURCES_DESERIALIZER_HPP +#define ANTKEEPER_RESOURCES_DESERIALIZER_HPP + +#include "resources/deserialize-context.hpp" + +/** + * Specializations of deserializer define the deserialization process for a given type. + * + * @tparam T Deserializable type. + */ +template +struct deserializer +{ + /** + * Deserializes a value. + * + * @param value Value to deserialize. + * @param ctx Deserialize context. + */ + void deserialize(T& value, deserialize_context& ctx); +}; + +#endif // ANTKEEPER_RESOURCES_DESERIALIZER_HPP diff --git a/src/resources/dict-loader.cpp b/src/resources/dict-loader.cpp new file mode 100644 index 0000000..ac3f614 --- /dev/null +++ b/src/resources/dict-loader.cpp @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2023 Christopher J. Howard + * + * This file is part of Antkeeper source code. + * + * Antkeeper source code is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Antkeeper source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Antkeeper source code. If not, see . + */ + +#include "resources/resource-loader.hpp" +#include "resources/serializer.hpp" +#include "resources/deserializer.hpp" +#include "utility/dict.hpp" +#include +#include + +template <> +dict* resource_loader>::load(resource_manager* resource_manager, PHYSFS_File* file, const std::filesystem::path& path) +{ + dict* dict = new ::dict(); + + deserialize_context ctx(file); + deserializer<::dict>().deserialize(*dict, ctx); + + return dict; +} + +template <> +void resource_loader>::save(resource_manager* resource_manager, PHYSFS_File* file, const std::filesystem::path& path, const dict* dict) +{ + serialize_context ctx(file); + serializer<::dict>().serialize(*dict, ctx); +} diff --git a/src/resources/resource-loader.cpp b/src/resources/resource-loader.cpp index de9890f..1c8fa75 100644 --- a/src/resources/resource-loader.cpp +++ b/src/resources/resource-loader.cpp @@ -17,27 +17,45 @@ * along with Antkeeper source code. If not, see . */ -#include "resource-loader.hpp" +#include "resources/resource-loader.hpp" +#include "resources/deserialize-error.hpp" #include void physfs_getline(PHYSFS_File* file, std::string& line) { - PHYSFS_sint64 bytes; - char c; - line.clear(); - do + for (;;) { - bytes = PHYSFS_readBytes(file, &c, 1); - - if (bytes != 1 || c == '\n') - break; - - if (c == '\r') - continue; + char c; + const PHYSFS_sint64 status = PHYSFS_readBytes(file, &c, 1); - line.append(1, c); + if (status == 1) + { + if (c == '\r') + { + continue; + } + else if (c == '\n') + { + break; + } + else + { + line.append(1, c); + } + + } + else + { + if (PHYSFS_eof(file)) + { + break; + } + else + { + throw deserialize_error(PHYSFS_getLastError()); + } + } } - while (!PHYSFS_eof(file)); } diff --git a/src/resources/resource-manager.cpp b/src/resources/resource-manager.cpp index 771dc58..c6f1938 100644 --- a/src/resources/resource-manager.cpp +++ b/src/resources/resource-manager.cpp @@ -18,14 +18,33 @@ */ #include "resources/resource-manager.hpp" +#include "debug/log.hpp" +#include resource_manager::resource_manager() { + // Log PhysicsFS info + // PHYSFS_Version physfs_compiled_version; + // PHYSFS_Version physfs_linked_version; + // PHYSFS_VERSION(&physfs_compiled_version); + // PHYSFS_getLinkedVersion(&physfs_linked_version); + // debug::log::info + // ( + // "PhysicsFS compiled version: {}.{}.{}; linked version: {}.{}.{}", + // physfs_compiled_version.major, + // physfs_compiled_version.minor, + // physfs_compiled_version.patch, + // physfs_linked_version.major, + // physfs_linked_version.minor, + // physfs_linked_version.patch + // ); + // Init PhysicsFS debug::log::trace("Initializing PhysicsFS..."); if (!PHYSFS_init(nullptr)) { debug::log::error("Failed to initialize PhysicsFS: {}", PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode())); + throw std::runtime_error("Failed to initialize PhysicsFS"); } else { @@ -72,6 +91,18 @@ bool resource_manager::mount(const std::filesystem::path& path) } } +void resource_manager::set_write_dir(const std::filesystem::path& path) +{ + if (!PHYSFS_setWriteDir(path.string().c_str())) + { + debug::log::error("Failed set write directory to \"{}\": {}", path.string(), PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode())); + } + else + { + debug::log::trace("Set write directory to \"{}\"", path.string()); + } +} + void resource_manager::unload(const std::filesystem::path& path) { // Check if resource is in the cache diff --git a/src/resources/resource-manager.hpp b/src/resources/resource-manager.hpp index 0621ddb..441aafb 100644 --- a/src/resources/resource-manager.hpp +++ b/src/resources/resource-manager.hpp @@ -47,6 +47,8 @@ public: ~resource_manager(); bool mount(const std::filesystem::path& path); + + void set_write_dir(const std::filesystem::path& path); /** * Adds a path to be searched when a resource is requested. diff --git a/src/resources/serialize-context.cpp b/src/resources/serialize-context.cpp new file mode 100644 index 0000000..750837d --- /dev/null +++ b/src/resources/serialize-context.cpp @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2023 Christopher J. Howard + * + * This file is part of Antkeeper source code. + * + * Antkeeper source code is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Antkeeper source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Antkeeper source code. If not, see . + */ + +#include "resources/serialize-context.hpp" +#include "resources/serialize-error.hpp" +#include + +serialize_context::serialize_context(void* handle): + handle(handle), + m_error(false) +{} + +std::size_t serialize_context::write8(const std::byte* data, std::size_t count) +{ + const PHYSFS_sint64 status = PHYSFS_writeBytes(reinterpret_cast(handle), data, count); + + if (status < 0) + { + m_error = true; + throw serialize_error(PHYSFS_getLastError()); + //return 0; + } + + if (status != count) + { + m_error = true; + throw serialize_error(PHYSFS_getLastError()); + //return static_cast(count); + } + + return count; +} + +std::size_t serialize_context::write16(const std::byte* data, std::size_t count) +{ + PHYSFS_File* file = reinterpret_cast(handle); + const PHYSFS_uint16* data16 = reinterpret_cast(data); + + for (std::size_t i = 0; i < count; ++i) + { + if constexpr (serialize_context::endian == std::endian::little) + { + if (!PHYSFS_writeULE16(file, *data16)) + { + m_error = true; + throw serialize_error(PHYSFS_getLastError()); + //return i; + } + } + else + { + if (!PHYSFS_writeUBE16(file, *data16)) + { + m_error = true; + throw serialize_error(PHYSFS_getLastError()); + //return i; + } + } + + ++data16; + } + + return count; +} + +std::size_t serialize_context::write32(const std::byte* data, std::size_t count) +{ + PHYSFS_File* file = reinterpret_cast(handle); + const PHYSFS_uint32* data32 = reinterpret_cast(data); + + for (std::size_t i = 0; i < count; ++i) + { + if constexpr (serialize_context::endian == std::endian::little) + { + if (!PHYSFS_writeULE32(file, *data32)) + { + m_error = true; + throw serialize_error(PHYSFS_getLastError()); + //return i; + } + } + else + { + if (!PHYSFS_writeUBE32(file, *data32)) + { + m_error = true; + throw serialize_error(PHYSFS_getLastError()); + //return i; + } + } + + ++data32; + } + + return count; +} + +std::size_t serialize_context::write64(const std::byte* data, std::size_t count) +{ + PHYSFS_File* file = reinterpret_cast(handle); + const PHYSFS_uint64* data64 = reinterpret_cast(data); + + for (std::size_t i = 0; i < count; ++i) + { + if constexpr (serialize_context::endian == std::endian::little) + { + if (!PHYSFS_writeULE64(file, *data64)) + { + m_error = true; + throw serialize_error(PHYSFS_getLastError()); + //return i; + } + } + else + { + if (!PHYSFS_writeUBE64(file, *data64)) + { + m_error = true; + throw serialize_error(PHYSFS_getLastError()); + //return i; + } + } + + ++data64; + } + + return count; +} diff --git a/src/resources/serialize-context.hpp b/src/resources/serialize-context.hpp new file mode 100644 index 0000000..d0ab774 --- /dev/null +++ b/src/resources/serialize-context.hpp @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2023 Christopher J. Howard + * + * This file is part of Antkeeper source code. + * + * Antkeeper source code is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Antkeeper source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Antkeeper source code. If not, see . + */ + +#ifndef ANTKEEPER_RESOURCES_SERIALIZE_CONTEXT_HPP +#define ANTKEEPER_RESOURCES_SERIALIZE_CONTEXT_HPP + +#include +#include + +/** + * Provides access to a serialization state. + */ +struct serialize_context +{ +public: + static inline constexpr std::endian endian = std::endian::little; + + /** + * Writes 8-bit (byte) data. + * + * @param data Pointer to data source. + * @param count Number of bytes to write. + * + * @return Number of bytes written. + * + * @throw serialize_error Write error. + */ + std::size_t write8(const std::byte* data, std::size_t count) noexcept(false); + + /** + * Writes 16-bit (word) data. + * + * @param data Pointer to data source. + * @param count Number of words to write. + * + * @return Number of words written. + * + * @throw serialize_error Write error. + */ + std::size_t write16(const std::byte* data, std::size_t count) noexcept(false); + + /** + * Writes 32-bit (double word) data. + * + * @param data Pointer to data source. + * @param count Number of double words to write. + * + * @return Number of double words written. + * + * @throw serialize_error Write error. + */ + std::size_t write32(const std::byte* data, std::size_t count) noexcept(false); + + /** + * Writes 64-bit (quad word) data. + * + * @param data Pointer to data source. + * @param count Number of quad words to write. + * + * @return Number of quad words written. + * + * @throw serialize_error Write error. + */ + std::size_t write64(const std::byte* data, std::size_t count) noexcept(false); + + /** + * Returns `true` if an error occured during a write operation, `false` otherwise. + */ + inline bool error() const noexcept + { + return m_error; + } + +private: + template + friend class resource_loader; + + serialize_context(void* handle); + + void* handle; + bool m_error; +}; + +#endif // ANTKEEPER_RESOURCES_SERIALIZE_CONTEXT_HPP diff --git a/src/resources/serialize-error.hpp b/src/resources/serialize-error.hpp new file mode 100644 index 0000000..ba20207 --- /dev/null +++ b/src/resources/serialize-error.hpp @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2023 Christopher J. Howard + * + * This file is part of Antkeeper source code. + * + * Antkeeper source code is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Antkeeper source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Antkeeper source code. If not, see . + */ + +#ifndef ANTKEEPER_RESOURCES_SERIALIZE_ERROR_HPP +#define ANTKEEPER_RESOURCES_SERIALIZE_ERROR_HPP + +#include + +/** + * An exception of this type is thrown when an error occurs during serialization. + */ +class serialize_error: public std::runtime_error +{ + using std::runtime_error::runtime_error; +}; + +#endif // ANTKEEPER_RESOURCES_SERIALIZE_ERROR_HPP diff --git a/src/resources/serializer.cpp b/src/resources/serializer.cpp new file mode 100644 index 0000000..1bd7b15 --- /dev/null +++ b/src/resources/serializer.cpp @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2023 Christopher J. Howard + * + * This file is part of Antkeeper source code. + * + * Antkeeper source code is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Antkeeper source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Antkeeper source code. If not, see . + */ + +#include "resources/serializer.hpp" +#include + +template <> +void serializer::serialize(const bool& value, serialize_context& ctx) +{ + const std::uint8_t temp = (value) ? 1 : 0; + ctx.write8(reinterpret_cast(&temp), 1); +}; + +template <> +void serializer::serialize(const std::uint8_t& value, serialize_context& ctx) +{ + ctx.write8(reinterpret_cast(&value), 1); +}; + +template <> +void serializer::serialize(const std::uint16_t& value, serialize_context& ctx) +{ + ctx.write16(reinterpret_cast(&value), 1); +}; + +template <> +void serializer::serialize(const std::uint32_t& value, serialize_context& ctx) +{ + ctx.write32(reinterpret_cast(&value), 1); +}; + +template <> +void serializer::serialize(const std::uint64_t& value, serialize_context& ctx) +{ + ctx.write64(reinterpret_cast(&value), 1); +}; + +template <> +void serializer::serialize(const std::int8_t& value, serialize_context& ctx) +{ + ctx.write8(reinterpret_cast(&value), 1); +}; + +template <> +void serializer::serialize(const std::int16_t& value, serialize_context& ctx) +{ + ctx.write16(reinterpret_cast(&value), 1); +}; + +template <> +void serializer::serialize(const std::int32_t& value, serialize_context& ctx) +{ + ctx.write32(reinterpret_cast(&value), 1); +}; + +template <> +void serializer::serialize(const std::int64_t& value, serialize_context& ctx) +{ + ctx.write64(reinterpret_cast(&value), 1); +}; + +template <> +void serializer::serialize(const float& value, serialize_context& ctx) +{ + ctx.write32(reinterpret_cast(&value), 1); +}; + +template <> +void serializer::serialize(const double& value, serialize_context& ctx) +{ + ctx.write64(reinterpret_cast(&value), 1); +}; + +template <> +void serializer::serialize(const std::string& value, serialize_context& ctx) +{ + const std::uint64_t length = static_cast(value.length()); + ctx.write64(reinterpret_cast(&length), 1); + ctx.write8(reinterpret_cast(value.data()), static_cast(length)); +}; + +template <> +void serializer::serialize(const std::u8string& value, serialize_context& ctx) +{ + const std::uint64_t length = static_cast(value.length()); + ctx.write64(reinterpret_cast(&length), 1); + ctx.write8(reinterpret_cast(value.data()), static_cast(length)); +}; + +template <> +void serializer::serialize(const std::u16string& value, serialize_context& ctx) +{ + const std::uint64_t length = static_cast(value.length()); + ctx.write64(reinterpret_cast(&length), 1); + ctx.write16(reinterpret_cast(value.data()), static_cast(length)); +}; + +template <> +void serializer::serialize(const std::u32string& value, serialize_context& ctx) +{ + const std::uint64_t length = static_cast(value.length()); + ctx.write64(reinterpret_cast(&length), 1); + ctx.write32(reinterpret_cast(value.data()), static_cast(length)); +}; diff --git a/src/resources/serializer.hpp b/src/resources/serializer.hpp new file mode 100644 index 0000000..2482c70 --- /dev/null +++ b/src/resources/serializer.hpp @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2023 Christopher J. Howard + * + * This file is part of Antkeeper source code. + * + * Antkeeper source code is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Antkeeper source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Antkeeper source code. If not, see . + */ + +#ifndef ANTKEEPER_RESOURCES_SERIALIZER_HPP +#define ANTKEEPER_RESOURCES_SERIALIZER_HPP + +#include "resources/serialize-context.hpp" + +/** + * Specializations of serializer define the serialization process for a given type. + * + * @tparam T Serializable type. + */ +template +struct serializer +{ + /** + * Serializes a value. + * + * @param value Value to serialize. + * @param ctx Serialize context. + */ + void serialize(const T& value, serialize_context& ctx); +}; + +#endif // ANTKEEPER_RESOURCES_SERIALIZER_HPP diff --git a/src/resources/string-table-loader.cpp b/src/resources/string-table-loader.cpp index 36ce5f5..564487f 100644 --- a/src/resources/string-table-loader.cpp +++ b/src/resources/string-table-loader.cpp @@ -17,121 +17,63 @@ * along with Antkeeper source code. If not, see . */ -#include "resource-loader.hpp" -#include "string-table.hpp" +#include "resources/resource-loader.hpp" +#include "i18n/string-table.hpp" +#include "resources/deserialize-error.hpp" #include -static string_table_row parse_row(const std::string& line) +template <> +i18n::string_table* resource_loader::load(resource_manager* resource_manager, PHYSFS_File* file, const std::filesystem::path& path) { - std::vector row; - std::string column; - bool quoted = false; - bool escape = false; - - for (char c: line) + i18n::string_table* table = new i18n::string_table(); + + i18n::string_table_row row; + std::string entry; + + for (;;) { - if (escape) + char c; + const PHYSFS_sint64 status = PHYSFS_readBytes(file, &c, 1); + + if (status == 1) { - switch (c) + if (c == '\t') { - case 'n': - column.push_back('\n'); - break; - - case 't': - column.push_back('\t'); - break; - - default: - column.push_back(c); - break; + row.push_back(entry); + entry.clear(); + } + else if (c == '\n') + { + row.push_back(entry); + entry.clear(); + table->push_back(row); + row.clear(); + } + else if (c != '\r') + { + entry.push_back(c); } - - escape = false; } else { - switch (c) + if (PHYSFS_eof(file)) { - case '\\': - escape = true; - break; - - case ',': - if (quoted) - { - column.push_back(c); - } - else - { - row.push_back(column); - column.clear(); - } - break; - - case '"': - if (!quoted) - { - quoted = true; - } - else - { - quoted = false; - } - break; - - default: - column.push_back(c); - break; + if (!entry.empty()) + { + row.push_back(entry); + } + if (!row.empty()) + { + table->push_back(row); + } + break; } - } - } - - row.push_back(column); - - return row; -} - -template <> -string_table* resource_loader::load(resource_manager* resource_manager, PHYSFS_File* file, const std::filesystem::path& path) -{ - string_table* table = new string_table(); - std::string line; - - while (!PHYSFS_eof(file)) - { - physfs_getline(file, line); - table->push_back(parse_row(line)); - } - - return table; -} - -template <> -void resource_loader::save(resource_manager* resource_manager, PHYSFS_File* file, const std::filesystem::path& path, const string_table* table) -{ - const char* delimeter = ","; - const char* newline = "\n"; - - for (std::size_t i = 0; i < table->size(); ++i) - { - const string_table_row& row = (*table)[i]; - - for (std::size_t j = 0; j < row.size(); ++j) - { - const std::string& column = row[j]; - - PHYSFS_writeBytes(file, column.data(), column.length()); - - if (j < row.size() - 1) + else { - PHYSFS_writeBytes(file, delimeter, 1); + throw deserialize_error(PHYSFS_getLastError()); } } - - if (i < table->size() - 1) - { - PHYSFS_writeBytes(file, newline, 1); - } } + + return table; } diff --git a/src/utility/dict.cpp b/src/utility/dict.cpp new file mode 100644 index 0000000..ed6a5f8 --- /dev/null +++ b/src/utility/dict.cpp @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2023 Christopher J. Howard + * + * This file is part of Antkeeper source code. + * + * Antkeeper source code is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Antkeeper source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Antkeeper source code. If not, see . + */ + +#include "utility/dict.hpp" +#include "resources/serializer.hpp" +#include "resources/serialize-error.hpp" +#include "resources/deserializer.hpp" +#include "resources/deserialize-error.hpp" +#include "utility/hash/fnv1a.hpp" +#include +#include +#include +#include +#include + +using namespace hash::literals; + +template +static void serialize_any(const std::any& any, serialize_context& ctx) +{ + serializer().serialize(std::any_cast(any), ctx); +} + +template +static void deserialize_any(std::any& any, deserialize_context& ctx) +{ + T value; + deserializer().deserialize(value, ctx); + any = std::move(value); +} + +/** + * Serializes a dict with an unsigned 32-bit key. + * + * @throw serialize_error Write error. + * @throw serialize_error Unsupported dict value type. + */ +template <> +void serializer>::serialize(const dict& dict, serialize_context& ctx) +{ + // Map type indices to tuples containing a type hash and serialize function pointer + static const std::unordered_map + < + std::type_index, + std::tuple + < + std::uint32_t, + void (*)(const std::any&, serialize_context&) + > + > type_map + { + {std::type_index(typeid(bool)), {"bool"_fnv1a32, &serialize_any}}, + {std::type_index(typeid(std::uint8_t)), {"uint8"_fnv1a32, &serialize_any}}, + {std::type_index(typeid(std::uint16_t)), {"uint16"_fnv1a32, &serialize_any}}, + {std::type_index(typeid(std::uint32_t)), {"uint32"_fnv1a32, &serialize_any}}, + {std::type_index(typeid(std::uint64_t)), {"uint64"_fnv1a32, &serialize_any}}, + {std::type_index(typeid(std::int8_t)), {"int8"_fnv1a32, &serialize_any}}, + {std::type_index(typeid(std::int16_t)), {"int16"_fnv1a32, &serialize_any}}, + {std::type_index(typeid(std::int32_t)), {"int32"_fnv1a32, &serialize_any}}, + {std::type_index(typeid(std::int64_t)), {"int64"_fnv1a32, &serialize_any}}, + {std::type_index(typeid(float)), {"float"_fnv1a32, &serialize_any}}, + {std::type_index(typeid(double)), {"double"_fnv1a32, &serialize_any}}, + {std::type_index(typeid(std::string)), {"string"_fnv1a32, &serialize_any}}, + {std::type_index(typeid(std::u8string)), {"u8string"_fnv1a32, &serialize_any}}, + {std::type_index(typeid(std::u16string)), {"u16string"_fnv1a32, &serialize_any}}, + {std::type_index(typeid(std::u32string)), {"u32string"_fnv1a32, &serialize_any}} + }; + + // Write dict size + std::uint64_t size = static_cast(dict.size()); + ctx.write64(reinterpret_cast(&size), 1); + + // Write dict entries + for (const auto& [key, value]: dict) + { + if (auto i = type_map.find(value.type()); i != type_map.end()) + { + const auto& [type_hash, type_serializer] = i->second; + + // Write entry type hash and key + ctx.write32(reinterpret_cast(&type_hash), 1); + ctx.write32(reinterpret_cast(&key), 1); + + // Serialize entry value + type_serializer(value, ctx); + } + else + { + throw serialize_error("Unsupported dict value type"); + } + } +}; + +/** + * Deserializes a dict with an unsigned 32-bit key. + * + * @throw deserialize_error Write error. + * @throw deserialize_error Unsupported dict value type. + */ +template <> +void deserializer>::deserialize(dict& dict, deserialize_context& ctx) +{ + // Map type hashes to deserialize function pointers + static const std::unordered_map + < + std::uint32_t, + void (*)(std::any&, deserialize_context&) + > type_map + { + {"bool"_fnv1a32, &deserialize_any}, + {"uint8"_fnv1a32, &deserialize_any}, + {"uint16"_fnv1a32, &deserialize_any}, + {"uint32"_fnv1a32, &deserialize_any}, + {"uint64"_fnv1a32, &deserialize_any}, + {"int8"_fnv1a32, &deserialize_any}, + {"int16"_fnv1a32, &deserialize_any}, + {"int32"_fnv1a32, &deserialize_any}, + {"int64"_fnv1a32, &deserialize_any}, + {"float"_fnv1a32, &deserialize_any}, + {"double"_fnv1a32, &deserialize_any}, + {"string"_fnv1a32, &deserialize_any}, + {"u8string"_fnv1a32, &deserialize_any}, + {"u16string"_fnv1a32, &deserialize_any}, + {"u32string"_fnv1a32, &deserialize_any} + }; + + // Read dict size + std::uint64_t size = 0; + ctx.read64(reinterpret_cast(&size), 1); + + // Read dict entries + for (std::size_t i = 0; i < size; ++i) + { + // Read entry type hash + std::uint32_t type_hash = 0; + ctx.read32(reinterpret_cast(&type_hash), 1); + + if (auto i = type_map.find(type_hash); i != type_map.end()) + { + // Read entry key + std::uint32_t key = 0; + ctx.read32(reinterpret_cast(&key), 1); + + // Deserialize entry value + i->second(dict[key], ctx); + } + else + { + throw deserialize_error("Unsupported dict value type"); + } + } +}; diff --git a/src/utility/dict.hpp b/src/utility/dict.hpp new file mode 100644 index 0000000..d8a5dcc --- /dev/null +++ b/src/utility/dict.hpp @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2023 Christopher J. Howard + * + * This file is part of Antkeeper source code. + * + * Antkeeper source code is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Antkeeper source code is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Antkeeper source code. If not, see . + */ + +#ifndef ANTKEEPER_UTILITY_DICT_HPP +#define ANTKEEPER_UTILITY_DICT_HPP + +#include +#include + +/** + * Unordered dictionary type. + * + * @tparam Key Key type. + */ +template +using dict = std::unordered_map; + +#endif // ANTKEEPER_UTILITY_DICT_HPP diff --git a/src/utility/paths.cpp b/src/utility/paths.cpp index 3cab130..0f67264 100644 --- a/src/utility/paths.cpp +++ b/src/utility/paths.cpp @@ -17,7 +17,7 @@ * along with Antkeeper source code. If not, see . */ -#include "paths.hpp" +#include "utility/paths.hpp" #include #include #include @@ -32,33 +32,16 @@ #include #endif -#if defined(_WIN32) - std::string narrow(const std::wstring& wstring) - { - std::string string(WideCharToMultiByte(CP_UTF8, 0, &wstring[0], static_cast(wstring.size()), nullptr, 0, nullptr, nullptr), '\0'); - WideCharToMultiByte(CP_UTF8, 0, &wstring[0], static_cast(wstring.size()), &string[0], static_cast(string.size()), nullptr, nullptr); - return string; - } - - std::wstring widen(const std::string& string) - { - std::wstring wstring(MultiByteToWideChar(CP_UTF8, 0, &string[0], static_cast(string.size()), nullptr, 0), L'\0'); - MultiByteToWideChar(CP_UTF8, 0, &string[0], static_cast(string.size()), &wstring[0], static_cast(wstring.size())); - return wstring; - } -#endif - std::filesystem::path get_executable_path() { std::filesystem::path executable_path; #if defined(_WIN32) // Get executable path on Windows - HMODULE hModule = GetModuleHandleW(nullptr); - std::wstring wpath(MAX_PATH, L'\0'); - GetModuleFileNameW(hModule, &wpath[0], MAX_PATH); - wpath.erase(std::find(wpath.begin(), wpath.end(), L'\0'), wpath.end()); - executable_path = narrow(wpath); + std::wstring path(MAX_PATH, L'\0'); + GetModuleFileNameW(GetModuleHandleW(nullptr), path.data(), MAX_PATH); + path.erase(std::find(path.begin(), path.end(), L'\0'), path.end()); + executable_path = path; #else // Get executable path on Linux char path[PATH_MAX]; @@ -73,43 +56,79 @@ std::filesystem::path get_executable_path() return executable_path; } -std::filesystem::path get_data_path(const std::string& application_name) +std::filesystem::path get_executable_data_path() { #if defined(_WIN32) return get_executable_path().parent_path(); #else - return get_executable_path().parent_path().parent_path() / "share" / application_name; + return get_executable_path().parent_path().parent_path() / "share"; #endif } -std::filesystem::path get_config_path(const std::string& application_name) +std::filesystem::path get_local_config_path() { - std::filesystem::path config_path; + std::filesystem::path local_config_path; #if defined(_WIN32) - std::wstring wpath(MAX_PATH, L'\0'); - if (SHGetSpecialFolderPathW(nullptr, &wpath[0], CSIDL_LOCAL_APPDATA, FALSE)) + + std::wstring path(MAX_PATH, L'\0'); + if (SHGetFolderPathW(nullptr, CSIDL_LOCAL_APPDATA, nullptr, SHGFP_TYPE_CURRENT, path.data()) == S_OK) { - wpath.erase(std::find(wpath.begin(), wpath.end(), L'\0'), wpath.end()); - config_path = std::filesystem::path(narrow(wpath)) / application_name; + path.erase(std::find(path.begin(), path.end(), L'\0'), path.end()); + local_config_path = path; } + + // Windows Vista+ + // wchar_t* path_buffer = nullptr; + // if (SHGetKnownFolderPath(FOLDERID_LocalAppData, KF_FLAG_DEFAULT, nullptr, &path_buffer) == S_OK) + // { + // local_config_path = std::filesystem::path(path_buffer); + // CoTaskMemFree(static_cast(path_buffer)); + // } + #else // Determine home path std::filesystem::path home_path = getpwuid(getuid())->pw_dir; // Determine config path - char* xdgConfigHome = std::getenv("XDG_CONFIG_HOME"); - if (!xdgConfigHome) + char* xdg_config_home = std::getenv("XDG_CONFIG_HOME"); + if (!xdg_config_home) { // Default to $HOME/.config/ as per: // https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html#variables - config_path = home_path / ".config/" / application_name; + local_config_path = home_path / ".config/"; } else { - config_path = std::filesystem::path(xdgConfigHome) / application_name; + local_config_path = xdg_config_home; } #endif - return config_path; + return local_config_path; +} + +std::filesystem::path get_shared_config_path() +{ + #if defined(_WIN32) + std::filesystem::path shared_config_path; + + std::wstring path(MAX_PATH, L'\0'); + if (SHGetFolderPathW(nullptr, CSIDL_APPDATA, nullptr, SHGFP_TYPE_CURRENT, path.data()) == S_OK) + { + path.erase(std::find(path.begin(), path.end(), L'\0'), path.end()); + shared_config_path = path; + } + + // Windows Vista+ + // wchar_t* path_buffer = nullptr; + // if (SHGetKnownFolderPath(FOLDERID_RoamingAppData, KF_FLAG_DEFAULT, nullptr, &path_buffer) == S_OK) + // { + // shared_config_path = path_buffer; + // CoTaskMemFree(static_cast(path_buffer)); + // } + + return shared_config_path; + #else + return get_local_config_path(); + #endif } diff --git a/src/utility/paths.hpp b/src/utility/paths.hpp index d0b989c..fe9d3e0 100644 --- a/src/utility/paths.hpp +++ b/src/utility/paths.hpp @@ -17,11 +17,10 @@ * along with Antkeeper source code. If not, see . */ -#ifndef ANTKEEPER_PATHS_HPP -#define ANTKEEPER_PATHS_HPP +#ifndef ANTKEEPER_UTILITY_PATHS_HPP +#define ANTKEEPER_UTILITY_PATHS_HPP #include -#include /** * Returns the absolute path to the current executable. @@ -33,24 +32,31 @@ /** * Returns the absolute path to the directory containing application data. * - * Windows: executable_directory - * GNU/Linux: executable_directory/../share/applicationName/ + * Windows: ` get_executable_path()` + * GNU/Linux: ` get_executable_path()/../share/` * - * @param application_name Name of the application. * @return Path to the application's data directory. */ -[[nodiscard]] std::filesystem::path get_data_path(const std::string& application_name); +[[nodiscard]] std::filesystem::path get_executable_data_path(); /** - * Returns the absolute path to the directory containing user-specific application data. + * Returns the absolute path to the directory containing user-specific, device-specific application data. * - * Windows: %LOCALAPPDATA%\application_name\ - * GNU/Linux: $XDG_CONFIG_HOME/application_name/ or ~/.config/application_name/ if $XDG_CONFIG_HOME is not set. + * Windows: `%LOCALAPPDATA%` + * GNU/Linux: `$XDG_CONFIG_HOME` or `~/.config/` if `$XDG_CONFIG_HOME` is not set. * - * @param application_name Name of the application. - * @return Path to the application's config directory. + * @return Path to the local config directory. */ -[[nodiscard]] std::filesystem::path get_config_path(const std::string& application_name); +[[nodiscard]] std::filesystem::path get_local_config_path(); -#endif // ANTKEEPER_PATHS_HPP +/** + * Returns the absolute path to the directory containing user-specific application data that may be shared across devices. + * + * Windows: `%APPDATA%` + * GNU/Linux: `$XDG_CONFIG_HOME` or `~/.config/` if `$XDG_CONFIG_HOME` is not set. + * + * @return Path to the shared config directory. + */ +[[nodiscard]] std::filesystem::path get_shared_config_path(); +#endif // ANTKEEPER_UTILITY_PATHS_HPP