diff --git a/assets/schema/oopetris.config.schema.json b/assets/schema/oopetris.config.schema.json new file mode 100644 index 00000000..531f2bb8 --- /dev/null +++ b/assets/schema/oopetris.config.schema.json @@ -0,0 +1,133 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://git.uibk.ac.at/csba1761/bsc/-/blob/main/backend/assets/environments.schema.json", + "$ref": "#/$defs/Root", + "$defs": { + "Root": { + "type": "object", + "properties": { + "controls": { + "$ref": "#/$defs/Controls" + }, + "volume": { + "type": "number", + "minimum": 0.0, + "maximum": 1.0 + }, + "discord": { + "type": "boolean" + }, + "api_url": { + "type": "string" + } + }, + "required": [ + "volume" + ], + "additionalProperties": false + }, + "Controls": { + "type": "object", + "properties": { + "selected": { + "anyOf": [ + { + "type": "number", + "minimum": 0, + "multipleOf": 1 + }, + { + "type": "null" + } + ] + }, + "inputs": { + "$ref": "#/$defs/Inputs" + } + }, + "required": [], + "additionalProperties": false + }, + "Inputs": { + "type": "array", + "items": { + "$ref": "#/$defs/Input" + }, + "additionalItems": false, + "minItems": 0, + "default": [] + }, + "Input": { + "description": "TODO: this isn't fully specified", + "anyOf": [ + { + "$ref": "#/$defs/KeyboardInput" + }, + { + "type": "object", + "additionalItems": true + } + ] + }, + "KeyboardInput": { + "type": "object", + "properties": { + "type": { + "const": "keyboard" + }, + "drop": { + "$ref": "#/$defs/KeyboardInputKey" + }, + "hold": { + "$ref": "#/$defs/KeyboardInputKey" + }, + "move_down": { + "$ref": "#/$defs/KeyboardInputKey" + }, + "move_left": { + "$ref": "#/$defs/KeyboardInputKey" + }, + "move_right": { + "$ref": "#/$defs/KeyboardInputKey" + }, + "rotate_left": { + "$ref": "#/$defs/KeyboardInputKey" + }, + "rotate_right": { + "$ref": "#/$defs/KeyboardInputKey" + }, + "menu": { + "type": "object", + "properties": { + "pause": { + "$ref": "#/$defs/KeyboardInputKey" + }, + "open_settings": { + "$ref": "#/$defs/KeyboardInputKey" + } + }, + "additionalItems": false, + "required": [ + "pause", + "open_settings" + ] + } + }, + "required": [ + "menu", + "rotate_right", + "rotate_left", + "move_right", + "rotate_right", + "move_left", + "move_down", + "hold", + "drop", + "type" + ] + } + }, + "KeyboardInputKey": { + "type": "string" + } +} diff --git a/platforms/android/app/jni/Android.mk b/platforms/android/app/jni/Android.mk index f049a98a..af51c87e 100644 --- a/platforms/android/app/jni/Android.mk +++ b/platforms/android/app/jni/Android.mk @@ -15,82 +15,88 @@ include $(PREBUILT_SHARED_LIBRARY) include $(CLEAR_VARS) -LOCAL_MODULE := libpng +LOCAL_MODULE := png16 LOCAL_SRC_FILES := $(shell find "${SUBPROJECTS_PATH}" -name libpng16.so) include $(PREBUILT_SHARED_LIBRARY) include $(CLEAR_VARS) -LOCAL_MODULE := SDL2_ttf +LOCAL_MODULE := sdl2_ttf LOCAL_SRC_FILES := $(shell find "${SUBPROJECTS_PATH}" -name libsdl2_ttf.so) include $(PREBUILT_SHARED_LIBRARY) include $(CLEAR_VARS) -LOCAL_MODULE := SDL2_mixer +LOCAL_MODULE := sdl2_mixer LOCAL_SRC_FILES := $(shell find "${SUBPROJECTS_PATH}" -name libsdl2mixer.so) include $(PREBUILT_SHARED_LIBRARY) include $(CLEAR_VARS) -LOCAL_MODULE := libvorbis +LOCAL_MODULE := vorbis LOCAL_SRC_FILES := $(shell find "${SUBPROJECTS_PATH}" -name libvorbis.so) include $(PREBUILT_SHARED_LIBRARY) include $(CLEAR_VARS) -LOCAL_MODULE := libvorbisfile +LOCAL_MODULE := vorbisfile LOCAL_SRC_FILES := $(shell find "${SUBPROJECTS_PATH}" -name libvorbisfile.so) include $(PREBUILT_SHARED_LIBRARY) include $(CLEAR_VARS) -LOCAL_MODULE := libogg +LOCAL_MODULE := ogg LOCAL_SRC_FILES := $(shell find "${SUBPROJECTS_PATH}" -name libogg.so) include $(PREBUILT_SHARED_LIBRARY) include $(CLEAR_VARS) -LOCAL_MODULE := libflac +LOCAL_MODULE := flac LOCAL_SRC_FILES := $(shell find "${SUBPROJECTS_PATH}" -name libFLAC.so) include $(PREBUILT_SHARED_LIBRARY) include $(CLEAR_VARS) -LOCAL_MODULE := SDL2_image +LOCAL_MODULE := sdl2_image LOCAL_SRC_FILES := $(shell find "${SUBPROJECTS_PATH}" -name libsdl2image.so) include $(PREBUILT_SHARED_LIBRARY) include $(CLEAR_VARS) -LOCAL_MODULE := libfmt +LOCAL_MODULE := fmt LOCAL_SRC_FILES := $(shell find "${SUBPROJECTS_PATH}" -name libfmt.so) include $(PREBUILT_SHARED_LIBRARY) include $(CLEAR_VARS) -LOCAL_MODULE := liboopetris_core +LOCAL_MODULE := keyutils +LOCAL_SRC_FILES := $(shell find "${SUBPROJECTS_PATH}" -name libkeyutils.so) +include $(PREBUILT_SHARED_LIBRARY) + + +include $(CLEAR_VARS) +LOCAL_MODULE := oopetris_core LIB_PATH := $(BUILD_PATH)/src/libs/core LOCAL_SRC_FILES := $(LIB_PATH)/liboopetris_core.so include $(PREBUILT_SHARED_LIBRARY) include $(CLEAR_VARS) -LOCAL_MODULE := liboopetris_recordings +LOCAL_MODULE := oopetris_recordings LIB_PATH := $(BUILD_PATH)/src/libs/recordings LOCAL_SRC_FILES := $(LIB_PATH)/liboopetris_recordings.so include $(PREBUILT_SHARED_LIBRARY) include $(CLEAR_VARS) -LOCAL_MODULE := liboopetris_graphics +LOCAL_MODULE := oopetris_graphics LIB_PATH := $(BUILD_PATH)/src LOCAL_SRC_FILES := $(LIB_PATH)/liboopetris_graphics.so include $(PREBUILT_SHARED_LIBRARY) include $(CLEAR_VARS) -LOCAL_MODULE := liboopetris +LOCAL_MODULE := oopetris LIB_PATH := $(BUILD_PATH)/src/executables LOCAL_SRC_FILES := $(LIB_PATH)/liboopetris.so include $(PREBUILT_SHARED_LIBRARY) @@ -99,7 +105,7 @@ include $(PREBUILT_SHARED_LIBRARY) include $(CLEAR_VARS) LOCAL_MODULE := main -LOCAL_SHARED_LIBRARIES := SDL2 SDL2_ttf freetype libpng SDL2_mixer libvorbis libvorbisfile libogg libflac SDL2_image libfmt liboopetris_core liboopetris_recordings liboopetris_graphics liboopetris +LOCAL_SHARED_LIBRARIES := SDL2 sdl2_ttf freetype png16 sdl2_mixer vorbis vorbisfile ogg flac sdl2_image fmt keyutils oopetris_core oopetris_recordings oopetris_graphics oopetris LOCAL_LDLIBS := -ldl -lGLESv1_CM -lGLESv2 -lOpenSLES -llog -landroid LOCAL_LDFLAGS := -Wl,--no-undefined include $(BUILD_SHARED_LIBRARY) diff --git a/platforms/android/app/src/main/java/org/libsdl/app/SDLActivity.java b/platforms/android/app/src/main/java/org/libsdl/app/SDLActivity.java index 62e58142..205fe929 100644 --- a/platforms/android/app/src/main/java/org/libsdl/app/SDLActivity.java +++ b/platforms/android/app/src/main/java/org/libsdl/app/SDLActivity.java @@ -269,7 +269,7 @@ protected String getMainFunction() { */ protected String[] getLibraries() { return new String[] { - "sdl2", + "SDL2", "sdl2image", "sdl2mixer", // "SDL2_net", diff --git a/settings.json b/settings.json index 34c9bdd6..5ab6e5ba 100644 --- a/settings.json +++ b/settings.json @@ -1,4 +1,5 @@ { + "$schema": "https://raw.githubusercontent.com/OpenBrickProtocolFoundation/oopetris/refs/heads/main/assets/schema/oopetris.config.schema.json", "controls": { "selected": null, "inputs": [ @@ -75,6 +76,7 @@ } ] }, - "volume": 0.2, - "discord": false + "volume": 0.0, + "discord": false, + "api_url": "https://oopetris.totto.lt/api/" } diff --git a/src/discord/core.cpp b/src/discord/core.cpp index 97200f26..6a5e32c6 100644 --- a/src/discord/core.cpp +++ b/src/discord/core.cpp @@ -146,6 +146,7 @@ void DiscordInstance::clear_activity(bool wait) { DiscordActivityWrapper::DiscordActivityWrapper(const std::string& details, discord::ActivityType type) { m_activity.SetDetails(details.c_str()); m_activity.SetType(type); + m_activity.SetSupportedPlatforms(constants::discord::supported_platforms); } diff --git a/src/discord/core.hpp b/src/discord/core.hpp index 21423b00..d9e5a347 100644 --- a/src/discord/core.hpp +++ b/src/discord/core.hpp @@ -23,7 +23,7 @@ namespace constants::discord { //TODO(Totto): this isn't correct for all platforms and needs to be tested #if defined(__ANDROID__) - constexpr const char* platform_dependent_launch_arguments = ""; +#error "Not supported" #elif defined(__CONSOLE__) #error "Not supported" #elif defined(FLATPAK_BUILD) @@ -39,6 +39,19 @@ namespace constants::discord { #error "Unsupported platform" #endif + +#if defined(__ANDROID__) + constexpr const std::uint32_t supported_platforms = DiscordActivitySupportedPlatformFlags_Android; +#elif defined(__CONSOLE__) +#error "Not supported" +#elif defined(FLATPAK_BUILD) || defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__) \ + || defined(__APPLE__) || defined(__linux__) + constexpr const std::uint32_t supported_platforms = DiscordActivitySupportedPlatformFlags_Desktop; +#else +#error "Unsupported platform" +#endif + + // manually synchronized to https://discord.com/developers/applications/1220147916371394650/rich-presence/assets enum class ArtAsset : u8 { Logo }; diff --git a/src/executables/game/application.cpp b/src/executables/game/application.cpp index d0fee17d..6d67b577 100644 --- a/src/executables/game/application.cpp +++ b/src/executables/game/application.cpp @@ -263,14 +263,21 @@ void Application::initialize() { const auto start_time = SDL_GetTicks64(); const std::future load_everything = std::async(std::launch::async, [this] { + this->m_settings_manager = std::make_unique(); + + this->m_settings_manager->add_callback([this](const auto& settings) { this->reload_api(settings); }); + + const auto current_settings = this->m_settings_manager->settings(); + this->m_music_manager = std::make_unique(this, num_audio_channels); + this->m_music_manager->set_volume(current_settings.volume, true, true); this->m_input_manager = std::make_shared(this->m_window); - this->m_settings_manager = std::make_unique(this); - this->m_font_manager = std::make_unique(); + this->reload_api(current_settings); + this->load_resources(); #if !defined(NDEBUG) @@ -283,7 +290,7 @@ void Application::initialize() { #endif #if defined(_HAVE_DISCORD_SDK) - if (m_settings_manager->settings().discord) { + if (current_settings.discord) { auto discord_instance = DiscordInstance::initialize(); if (not discord_instance.has_value()) { spdlog::warn( @@ -413,5 +420,20 @@ void Application::load_resources() { return m_discord_instance; } - #endif + + +void Application::reload_api(const settings::Settings& settings) { + + if (auto api_url = settings.api_url; api_url.has_value()) { + auto maybe_api = lobby::API::get_api(api_url.value()); + if (maybe_api.has_value()) { + //TODO(Totto): do this somehow asynchronous + m_api = std::make_unique(std::move(maybe_api.value())); + } else { + spdlog::error("Error in connecting to lobby API: {}", maybe_api.error()); + } + } else { + spdlog::info("No lobby API provided"); + } +} diff --git a/src/executables/game/application.hpp b/src/executables/game/application.hpp index 69b1a981..53597acd 100644 --- a/src/executables/game/application.hpp +++ b/src/executables/game/application.hpp @@ -5,6 +5,7 @@ #include "graphics/renderer.hpp" #include "graphics/window.hpp" #include "input/input.hpp" +#include "lobby/api.hpp" #include "manager/event_dispatcher.hpp" #include "manager/event_listener.hpp" #include "manager/music_manager.hpp" @@ -28,10 +29,11 @@ struct Application final : public EventListener, public ServiceProvider { std::optional m_target_framerate; // these fields are initalized asynchronously in a separate thread + std::unique_ptr m_settings_manager; std::unique_ptr m_music_manager; std::shared_ptr m_input_manager; - std::unique_ptr m_settings_manager; std::unique_ptr m_font_manager; + std::unique_ptr m_api; #if !defined(NDEBUG) @@ -127,6 +129,10 @@ struct Application final : public EventListener, public ServiceProvider { return *m_input_manager; } + [[nodiscard]] const std::unique_ptr& api() const override { + return m_api; + } + #if defined(_HAVE_DISCORD_SDK) @@ -140,4 +146,5 @@ struct Application final : public EventListener, public ServiceProvider { private: void initialize(); void load_resources(); + void reload_api(const settings::Settings& settings); }; diff --git a/src/helper/message_box.hpp b/src/helper/message_box.hpp index 61dc8da7..7b53c3c7 100644 --- a/src/helper/message_box.hpp +++ b/src/helper/message_box.hpp @@ -5,6 +5,8 @@ #include #include +#include + #include "./windows.hpp" namespace helper { @@ -19,7 +21,7 @@ namespace helper { struct MessageBox { - enum class Type { Error, Warning, Information }; + enum class Type : u8 { Error, Warning, Information }; /*** * \brief This blocks the current thread and should be called on the thread, the parent windows was created (if that is nullptr, it may be on another thread, but not otherwise) diff --git a/src/input/controller_input.hpp b/src/input/controller_input.hpp index cd0a8ef9..ebb82f21 100644 --- a/src/input/controller_input.hpp +++ b/src/input/controller_input.hpp @@ -149,20 +149,21 @@ namespace nlohmann { } static void to_json(json& obj, const input::ControllerSettings& settings) { - obj = nlohmann::json{ - { "rotate_left", settings.rotate_left.to_string() }, - { "rotate_right", settings.rotate_right.to_string() }, - { "move_left", settings.move_left.to_string() }, - { "move_right", settings.move_right.to_string() }, - { "move_down", settings.move_down.to_string() }, - { "drop", settings.drop.to_string() }, - { "hold", settings.hold.to_string() }, - { - "menu", nlohmann::json{ - { "pause", settings.pause.to_string() }, - { "open_settings", settings.open_settings.to_string() }, - }, } - }; + obj = nlohmann::json::object({ + { "rotate_left", settings.rotate_left.to_string() }, + { "rotate_right", settings.rotate_right.to_string() }, + { "move_left", settings.move_left.to_string() }, + { "move_right", settings.move_right.to_string() }, + { "move_down", settings.move_down.to_string() }, + { "drop", settings.drop.to_string() }, + { "hold", settings.hold.to_string() }, + { + "menu", nlohmann::json::object({ + { "pause", settings.pause.to_string() }, + { "open_settings", settings.open_settings.to_string() }, + }), + } + }); } }; } // namespace nlohmann diff --git a/src/input/joystick_input.hpp b/src/input/joystick_input.hpp index dc464097..116bbbec 100644 --- a/src/input/joystick_input.hpp +++ b/src/input/joystick_input.hpp @@ -377,9 +377,10 @@ namespace nlohmann { } static void to_json(json& obj, const input::JoystickIdentification& identification) { - obj = nlohmann::json{ - { "guid", identification.guid.to_string(), { "name", identification.name } }, - }; + obj = nlohmann::json::object({ + { "guid", identification.guid.to_string() }, + { "name", identification.name } + }); } }; @@ -431,24 +432,21 @@ namespace nlohmann { static void to_json(json& obj, const input::JoystickSettings& settings) { - auto identification = nlohmann::json{}; + auto identification = nlohmann::json::object(); adl_serializer::to_json(identification, settings.identification); - obj = nlohmann::json{ - { "identification", identification }, - { "rotate_left", settings.rotate_left }, - { "rotate_right", settings.rotate_right }, - { "move_left", settings.move_left }, - { "move_right", settings.move_right }, - { "move_down", settings.move_down }, - { "drop", settings.drop }, - { "hold", settings.hold }, - { - "menu", nlohmann::json{ - { "pause", settings.pause }, - { "open_settings", settings.open_settings }, - }, } - }; + obj = nlohmann::json::object({ + { "identification", identification }, + { "rotate_left", settings.rotate_left }, + { "rotate_right", settings.rotate_right }, + { "move_left", settings.move_left }, + { "move_right", settings.move_right }, + { "move_down", settings.move_down }, + { "drop", settings.drop }, + { "hold", settings.hold }, + { "menu", nlohmann::json::object({ { "pause", settings.pause }, + { "open_settings", settings.open_settings } }) } + }); } }; } // namespace nlohmann diff --git a/src/input/keyboard_input.hpp b/src/input/keyboard_input.hpp index 1c0c354c..88a3d545 100644 --- a/src/input/keyboard_input.hpp +++ b/src/input/keyboard_input.hpp @@ -147,20 +147,17 @@ namespace nlohmann { } static void to_json(json& obj, const input::KeyboardSettings& settings) { - obj = nlohmann::json{ - { "rotate_left", settings.rotate_left.to_string() }, - { "rotate_right", settings.rotate_right.to_string() }, - { "move_left", settings.move_left.to_string() }, - { "move_right", settings.move_right.to_string() }, - { "move_down", settings.move_down.to_string() }, - { "drop", settings.drop.to_string() }, - { "hold", settings.hold.to_string() }, - { - "menu", nlohmann::json{ - { "pause", settings.pause.to_string() }, - { "open_settings", settings.open_settings.to_string() }, - }, } - }; + obj = nlohmann::json::object({ + { "rotate_left", settings.rotate_left.to_string() }, + { "rotate_right", settings.rotate_right.to_string() }, + { "move_left", settings.move_left.to_string() }, + { "move_right", settings.move_right.to_string() }, + { "move_down", settings.move_down.to_string() }, + { "drop", settings.drop.to_string() }, + { "hold", settings.hold.to_string() }, + { "menu", nlohmann::json::object({ { "pause", settings.pause.to_string() }, + { "open_settings", settings.open_settings.to_string() } }) } + }); } }; } // namespace nlohmann diff --git a/src/input/touch_input.hpp b/src/input/touch_input.hpp index f6f85cbf..06414a53 100644 --- a/src/input/touch_input.hpp +++ b/src/input/touch_input.hpp @@ -214,12 +214,12 @@ namespace nlohmann { static void to_json(json& obj, const input::TouchSettings& settings) { - obj = nlohmann::json{ - { "move_x_threshold", settings.move_x_threshold }, - { "move_y_threshold", settings.move_y_threshold }, - { "rotation_duration_threshold", settings.rotation_duration_threshold }, - { "drop_duration_threshold", settings.drop_duration_threshold }, - }; + obj = nlohmann::json::object({ + { "move_x_threshold", settings.move_x_threshold }, + { "move_y_threshold", settings.move_y_threshold }, + { "rotation_duration_threshold", settings.rotation_duration_threshold }, + { "drop_duration_threshold", settings.drop_duration_threshold } + }); } }; } // namespace nlohmann diff --git a/src/libs/core/helper/errors.hpp b/src/libs/core/helper/errors.hpp index 24a8c26b..91ffc084 100644 --- a/src/libs/core/helper/errors.hpp +++ b/src/libs/core/helper/errors.hpp @@ -2,6 +2,7 @@ #pragma once +#include "./types.hpp" #include "./windows.hpp" #include @@ -10,7 +11,7 @@ namespace helper { namespace error { - enum class Severity { Fatal, Major, Minor }; + enum class Severity : u8 { Fatal, Major, Minor }; } struct GeneralError : std::exception { diff --git a/src/libs/core/helper/parse_json.cpp b/src/libs/core/helper/parse_json.cpp index 1a05b99d..34b5f6ba 100644 --- a/src/libs/core/helper/parse_json.cpp +++ b/src/libs/core/helper/parse_json.cpp @@ -34,6 +34,10 @@ std::string json::get_json_type(const nlohmann::json::value_t& type) { } } +bool json::is_meta_key(const std::string& key) { + return key.starts_with("$"); +} + void json::check_for_no_additional_keys(const nlohmann::json& obj, const std::vector& keys) { if (not obj.is_object()) { @@ -46,6 +50,9 @@ void json::check_for_no_additional_keys(const nlohmann::json& obj, const std::ve for (const auto& [key, _] : object) { + if (is_meta_key(key)) { + continue; + } if (std::ranges::find(keys, key) == keys.cend()) { throw nlohmann::json::type_error::create( 302, fmt::format("object may only contain expected keys, but contained '{}'", key), &obj diff --git a/src/libs/core/helper/parse_json.hpp b/src/libs/core/helper/parse_json.hpp index 7070ecd1..23b9117f 100644 --- a/src/libs/core/helper/parse_json.hpp +++ b/src/libs/core/helper/parse_json.hpp @@ -5,9 +5,10 @@ // better json error messages, see https://json.nlohmann.me/api/macros/json_diagnostics/ #define JSON_DIAGNOSTICS 1 // NOLINT(cppcoreguidelines-macro-usage) #endif - #include +#include + #include "./expected.hpp" #include "./windows.hpp" @@ -49,6 +50,12 @@ NLOHMANN_JSON_NAMESPACE_END namespace json { + enum class ParseError : u8 { + OpenError, + ReadError, + FormatError, + }; + template [[nodiscard]] helper::expected try_parse_json(const std::string& content) noexcept { @@ -68,24 +75,44 @@ namespace json { } template - [[nodiscard]] helper::expected try_parse_json_file(const std::filesystem::path& file) noexcept { + [[nodiscard]] helper::expected> try_parse_json_file( + const std::filesystem::path& file + ) noexcept { if (not std::filesystem::exists(file)) { - return helper::unexpected{ fmt::format("File '{}' doesn't exist", file.string()) }; + return helper::unexpected>{ std::make_pair( + fmt::format("File '{}' doesn't exist", file.string()), ParseError::OpenError + ) }; } std::ifstream file_stream{ file }; - if (!file_stream.is_open()) { - return helper::unexpected{ fmt::format("File '{}' couldn't be opened!", file.string()) }; + if (not file_stream.is_open()) { + return helper::unexpected>{ std::make_pair( + fmt::format("File '{}' couldn't be opened!", file.string()), ParseError::OpenError + ) }; } std::stringstream result; result << file_stream.rdbuf(); - return try_parse_json(result.str()); - } + file_stream.close(); + + if (file_stream.fail()) { + return helper::unexpected>{ std::make_pair( + fmt::format("Couldn't read from file '{}'", file.string()), ParseError::ReadError + ) }; + } + + auto parse_result = try_parse_json(result.str()); + if (not parse_result.has_value()) { + return helper::unexpected>{ + std::make_pair(std::move(parse_result.error()), ParseError::FormatError) + }; + } + return parse_result.value(); + } template [[nodiscard]] helper::expected try_convert_to_json(const T& input) noexcept { @@ -129,8 +156,40 @@ namespace json { } } + + template + std::optional + try_write_json_to_file(const std::filesystem::path& file, const T& value, const bool pretty = false) { + + + const auto result = json::try_json_to_string(value, pretty); + + if (not result.has_value()) { + return fmt::format("unable to convert settings to json: {}", result.error()); + } + + std::ofstream file_stream{ file }; + + if (not file_stream.is_open()) { + return fmt::format("File '{}' couldn't be opened!", file.string()); + } + + file_stream << result.value(); + + file_stream.close(); + + if (file_stream.fail()) { + return fmt::format("Couldn't write to file '{}' ", file.string()); + } + + return std::nullopt; + } + + OOPETRIS_CORE_EXPORTED std::string get_json_type(const nlohmann::json::value_t& type); + OOPETRIS_CORE_EXPORTED [[nodiscard]] bool is_meta_key(const std::string& key); + OOPETRIS_CORE_EXPORTED void check_for_no_additional_keys(const nlohmann::json& obj, const std::vector& keys); diff --git a/src/libs/recordings/utility/recording_json_wrapper.hpp b/src/libs/recordings/utility/recording_json_wrapper.hpp index f13babb3..277da647 100644 --- a/src/libs/recordings/utility/recording_json_wrapper.hpp +++ b/src/libs/recordings/utility/recording_json_wrapper.hpp @@ -64,10 +64,10 @@ namespace nlohmann { } static void to_json(json& obj, const recorder::TetrionHeader& tetrion_header) { - obj = nlohmann::json{ - { "seed", tetrion_header.seed }, - { "starting_level", tetrion_header.starting_level } - }; + obj = nlohmann::json::object({ + { "seed", tetrion_header.seed }, + { "starting_level", tetrion_header.starting_level } + }); } }; @@ -93,11 +93,11 @@ namespace nlohmann { static void to_json(json& obj, const recorder::Record& record) { - obj = nlohmann::json{ - { "tetrion_index", record.tetrion_index }, - { "simulation_step_index", record.simulation_step_index }, - { "event", record.event } - }; + obj = nlohmann::json::object({ + { "tetrion_index", record.tetrion_index }, + { "simulation_step_index", record.simulation_step_index }, + { "event", record.event } + }); } }; @@ -110,10 +110,10 @@ namespace nlohmann { } static void to_json(json& obj, const shapes::AbstractPoint& point) { - obj = nlohmann::json{ - { "x", point.x }, - { "y", point.y } - }; + obj = nlohmann::json::object({ + { "x", point.x }, + { "y", point.y } + }); } }; @@ -139,10 +139,10 @@ namespace nlohmann { } static void to_json(json& obj, const Mino& mino) { - obj = nlohmann::json{ - { "position", mino.position() }, - { "type", mino.type() } - }; + obj = nlohmann::json::object({ + { "position", mino.position() }, + { "type", mino.type() } + }); } }; @@ -160,14 +160,14 @@ namespace nlohmann { nlohmann::adl_serializer>::to_json(mino_stack_json, snapshot.mino_stack().minos()); - obj = nlohmann::json{ - { "tetrion_index", snapshot.tetrion_index() }, - { "level", snapshot.level() }, - { "score", snapshot.score() }, - { "lines_cleared", snapshot.lines_cleared() }, - { "simulation_step_index", snapshot.simulation_step_index() }, - { "mino_stack", mino_stack_json } - }; + obj = nlohmann::json::object({ + { "tetrion_index", snapshot.tetrion_index() }, + { "level", snapshot.level() }, + { "score", snapshot.score() }, + { "lines_cleared", snapshot.lines_cleared() }, + { "simulation_step_index", snapshot.simulation_step_index() }, + { "mino_stack", mino_stack_json } + }); } }; @@ -199,13 +199,13 @@ namespace nlohmann { snapshots_json, recording_reader.snapshots() ); - obj = nlohmann::json{ - { "version", recorder::Recording::current_supported_version_number }, - { "information", information_json }, - { "tetrion_headers", tetrion_headers_json }, - { "records", records_json }, - { "snapshots", snapshots_json }, - }; + obj = nlohmann::json::object({ + { "version", recorder::Recording::current_supported_version_number }, + { "information", information_json }, + { "tetrion_headers", tetrion_headers_json }, + { "records", records_json }, + { "snapshots", snapshots_json }, + }); } }; } // namespace nlohmann diff --git a/src/lobby/api.cpp b/src/lobby/api.cpp index 29248eb3..77d10d89 100644 --- a/src/lobby/api.cpp +++ b/src/lobby/api.cpp @@ -2,15 +2,22 @@ #include "api.hpp" +#include #include - #if defined(_OOPETRIS_ONLINE_USE_CURL) #include "./curl_client.hpp" #else #include "./httplib_client.hpp" #endif +namespace { + namespace constants { + + constexpr const char* api_token_key = "API_TOKEN_save"; + } +} // namespace + helper::expected lobby::API::check_compatibility() { const auto server_version = get_version(); @@ -46,7 +53,19 @@ helper::expected lobby::API::check_reachability() { } lobby::API::API(const std::string& api_url) - : m_client{ std::make_unique(api_url) } { } + : m_client{ std::make_unique(api_url) }, + m_secret_storage{ std::make_unique(secret::KeyringType::User) } { + + auto value = m_secret_storage->load(constants::api_token_key); + + if (value.has_value()) { + if (not this->setup_authentication(value.value().as_string())) { + throw std::runtime_error("Couldn't setup authentication"); + } + } else { + spdlog::info("API: Key not found, so probably not logged in already: {}", value.error()); + } +} helper::expected lobby::API::get_version() { auto res = m_client->Get("/version"); @@ -57,23 +76,39 @@ helper::expected lobby::API::get_version() { lobby::API::API(API&& other) noexcept : m_client{ std::move(other.m_client) }, - m_authentication_token{ std::move(other.m_authentication_token) } { } + m_authentication_token{ std::move(other.m_authentication_token) }, + m_secret_storage{ std::move(other.m_secret_storage) } { } lobby::API::~API() = default; helper::expected lobby::API::get_api(const std::string& url) { - API api{ url }; + try { + + API api{ url }; - const auto reachable = api.check_reachability(); + const auto reachable = api.check_reachability(); - if (not reachable.has_value()) { - return helper::unexpected{ reachable.error() }; + if (not reachable.has_value()) { + return helper::unexpected{ reachable.error() }; + } + + //TODO(Totto): once version is standard, check here if the version is supported + + return api; + } catch (const std::exception& error) { + return helper::unexpected{ error.what() }; } +} - //TODO(Totto): once version is standard, check here if the version is supported +void lobby::API::check_url(const std::string& url, std::function&& callback) { - return api; + //TODO(Totto): is this doen correctly + std::ignore = std::async(std::launch::async, [url, callback = std::move(callback)] { + auto result = lobby::API::get_api(url); + + callback(result.has_value()); + }); } @@ -107,11 +142,15 @@ bool lobby::API::authenticate(const Credentials& credentials) { return false; } - m_authentication_token = result.value().jwt; + return this->setup_authentication(result.value().jwt); +} - m_client->SetBearerAuth(m_authentication_token.value()); +void lobby::API::logout() { + m_client->ResetBearerAuth(); - return true; + if (auto result = m_secret_storage->remove(constants::api_token_key); result.has_value()) { + spdlog::error("API: {}", result.value()); + } } helper::expected, std::string> lobby::API::get_lobbies() { @@ -230,3 +269,15 @@ helper::expected lobby::API::register_user(const RegisterRequ return is_request_ok(res, 204); } + +bool lobby::API::setup_authentication(const std::string& token) { + + m_authentication_token = token; + + m_client->SetBearerAuth(token); + if (auto result = m_secret_storage->store(constants::api_token_key, secret::Buffer{ token }); result.has_value()) { + spdlog::error("API {}", result.value()); + } + + return true; +} diff --git a/src/lobby/api.hpp b/src/lobby/api.hpp index 80ba02ba..1ce9fefa 100644 --- a/src/lobby/api.hpp +++ b/src/lobby/api.hpp @@ -4,6 +4,7 @@ #include "./client.hpp" +#include "./credentials/secret.hpp" #include "./types.hpp" #include "helper/windows.hpp" @@ -16,6 +17,7 @@ namespace lobby { private: std::unique_ptr m_client; std::optional m_authentication_token; + std::unique_ptr m_secret_storage; // lobby commit used: https://github.com/OpenBrickProtocolFoundation/lobby/commit/2e0c8d05592f4e4d08437e6cb754a30f02c4e97c static constexpr StaticString supported_version{ "0.1.0" }; @@ -30,7 +32,6 @@ namespace lobby { helper::expected login(const lobby::Credentials& credentials); - public: OOPETRIS_GRAPHICS_EXPORTED API(API&& other) noexcept; OOPETRIS_GRAPHICS_EXPORTED API& operator=(API&& other) noexcept = delete; @@ -43,11 +44,15 @@ namespace lobby { OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] helper::expected static get_api(const std::string& url); + OOPETRIS_GRAPHICS_EXPORTED + void static check_url(const std::string& url, std::function&& callback); OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] bool is_authenticated(); OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] bool authenticate(const Credentials& credentials); + OOPETRIS_GRAPHICS_EXPORTED void logout(); + OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] helper::expected, std::string> get_lobbies(); OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] helper::expected join_lobby(int lobby_id); @@ -71,6 +76,9 @@ namespace lobby { OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] helper::expected register_user( const RegisterRequest& register_request ); + + private: + OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] bool setup_authentication(const std::string& token); }; diff --git a/src/lobby/client.hpp b/src/lobby/client.hpp index e05f0bab..5e3f470f 100644 --- a/src/lobby/client.hpp +++ b/src/lobby/client.hpp @@ -59,6 +59,8 @@ namespace oopetris::http { OOPETRIS_GRAPHICS_EXPORTED virtual void SetBearerAuth( //NOLINT(readability-identifier-naming) const std::string& token ) = 0; + + OOPETRIS_GRAPHICS_EXPORTED virtual void ResetBearerAuth() = 0; //NOLINT(readability-identifier-naming) }; OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] std::string status_message(int status); diff --git a/src/lobby/credentials/buffer.cpp b/src/lobby/credentials/buffer.cpp new file mode 100644 index 00000000..8424876a --- /dev/null +++ b/src/lobby/credentials/buffer.cpp @@ -0,0 +1,52 @@ + + + +#include "./buffer.hpp" + + +secret::Buffer::Buffer(const std::string& data) : m_size{ data.size() } { + if (m_size == 0) { + return; + } + + m_data = new std::byte[m_size]; //NOLINT(cppcoreguidelines-owning-memory) + std::memcpy(this->m_data, data.data(), m_size); +} + +secret::Buffer::Buffer(std::byte* data, std::size_t size) : m_size{ size } { + if (m_size == 0) { + return; + } + + m_data = new std::byte[m_size]; //NOLINT(cppcoreguidelines-owning-memory) + std::memcpy(this->m_data, data, m_size); +} + +secret::Buffer::~Buffer() { + if (m_data != nullptr && m_size != 0) { + delete[] m_data; + } +} + +secret::Buffer::Buffer(Buffer&& other) noexcept : m_data{ other.m_data }, m_size{ other.m_size } { + other.m_size = 0; + other.m_data = nullptr; +} + + +[[nodiscard]] std::string secret::Buffer::as_string() const { + return std::string{ + reinterpret_cast(m_data), //NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + reinterpret_cast(m_data //NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + ) + m_size //NOLINT(cppcoreguidelines-pro-bounds-pointer-arithmetic,coreguidelines-pro-type-reinterpret-cast) + }; +} + + +[[nodiscard]] std::byte* secret::Buffer::data() const { + return m_data; +} + +[[nodiscard]] std::size_t secret::Buffer::size() const { + return m_size; +} diff --git a/src/lobby/credentials/buffer.hpp b/src/lobby/credentials/buffer.hpp new file mode 100644 index 00000000..76d386cb --- /dev/null +++ b/src/lobby/credentials/buffer.hpp @@ -0,0 +1,44 @@ + + +#pragma once + +#include +#include + +#include "helper/windows.hpp" + +namespace secret { + struct Buffer { + private: + std::byte* m_data{ nullptr }; + std::size_t m_size; + + public: + OOPETRIS_GRAPHICS_EXPORTED explicit Buffer(const std::string& data); + OOPETRIS_GRAPHICS_EXPORTED explicit Buffer(std::byte* data, std::size_t size); + template + explicit Buffer(std::array data) : m_size{ data.size() } { + if (m_size == 0) { + return; + } + + m_data = new std::byte[m_size]; //NOLINT(cppcoreguidelines-owning-memory) + std::memcpy(this->m_data, data, m_size); + } + + OOPETRIS_GRAPHICS_EXPORTED ~Buffer(); + + OOPETRIS_GRAPHICS_EXPORTED Buffer(const Buffer& other) = delete; + OOPETRIS_GRAPHICS_EXPORTED Buffer& operator=(const Buffer& other) = delete; + + OOPETRIS_GRAPHICS_EXPORTED Buffer(Buffer&& other) noexcept; + OOPETRIS_GRAPHICS_EXPORTED Buffer& operator=(Buffer&& other) noexcept = delete; + + [[nodiscard]] std::string as_string() const; + + [[nodiscard]] std::byte* data() const; + + [[nodiscard]] std::size_t size() const; + }; + +} // namespace secret diff --git a/src/lobby/credentials/meson.build b/src/lobby/credentials/meson.build new file mode 100644 index 00000000..614c441b --- /dev/null +++ b/src/lobby/credentials/meson.build @@ -0,0 +1,6 @@ +graphics_src_files += files( + 'buffer.cpp', + 'buffer.hpp', + 'secret.cpp', + 'secret.hpp', +) diff --git a/src/lobby/credentials/secret.cpp b/src/lobby/credentials/secret.cpp new file mode 100644 index 00000000..301e19cb --- /dev/null +++ b/src/lobby/credentials/secret.cpp @@ -0,0 +1,471 @@ + +#include "./secret.hpp" + +#if defined(__linux__) || defined(__ANDROID__) + +#include +#include +namespace { + + namespace constants { + constexpr const char* key_type_user = "user"; + + constexpr const char* key_name_prefix = "OOPetris_key__"; + } // namespace constants + + std::string get_key_name(const std::string& key) { + return constants::key_name_prefix + key; + } + + i64 get_id_from_name(key_serial_t keyring_id, const std::string& key) { + + const std::string full_key = get_key_name(key); + + // 0 stands for: do not create a link to another keyring + return keyctl_search(keyring_id, constants::key_type_user, full_key.c_str(), 0); + } + + +} // namespace + + +secret::SecretStorage::SecretStorage(KeyringType type) : m_type{ type } { + + key_serial_t key_type{}; + switch (m_type) { + case secret::KeyringType::User: + key_type = KEY_SPEC_USER_KEYRING; + break; + case secret::KeyringType::Session: + key_type = KEY_SPEC_SESSION_KEYRING; + break; + case secret::KeyringType::Persistent: { + // -1 stands for current uid, 0 stands for: do not create a link to another keyring + auto result = keyctl_get_persistent(-1, 0); + if (result < 0) { + throw std::runtime_error(fmt::format("Error while getting the persistent keyring: {}", strerror(errno)) + ); + } + + this->m_ring_id = static_cast(result); + + return; + } + default: + UNREACHABLE(); + } + + // 1 stands for, create if not exists, as a bool + this->m_ring_id = keyctl_get_keyring_ID(key_type, 1); + + if (this->m_ring_id < 0) { + throw std::runtime_error(fmt::format("Error while getting the requested keyring: {}", strerror(errno))); + } +} + +secret::SecretStorage::~SecretStorage() = default; //NOLINT(performance-trivially-destructible) + +secret::SecretStorage::SecretStorage(SecretStorage&& other) noexcept + : m_type{ other.m_type }, + m_ring_id{ other.m_ring_id } { } + +[[nodiscard]] helper::expected secret::SecretStorage::load(const std::string& key) const { + + auto key_id = get_id_from_name(m_ring_id, key); + + if (key_id < 0) { + return helper::unexpected{ + fmt::format("Error while loading a key, can't find key by name: {}", strerror(errno)) + }; + } + + + void* buffer = nullptr; + + auto result = keyctl_read_alloc(static_cast(key_id), &buffer); + + if (result < 0) { + return helper::unexpected{ + fmt::format("Error while loading a key, can't read the value: {}", strerror(errno)) + }; + } + + + auto result_buffer = + Buffer{ reinterpret_cast(buffer), //NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + static_cast(result) }; + + free(buffer); // NOLINT(cppcoreguidelines-no-malloc,cppcoreguidelines-owning-memory) + return result_buffer; +} + +[[nodiscard]] std::optional +secret::SecretStorage::store(const std::string& key, const Buffer& value, bool update) const { + + auto key_id = get_id_from_name(m_ring_id, key); + + + if (key_id > 0 && not update) { + return "Error while storing a key, it already exists and we can't update it"; + } + + auto full_key = get_key_name(key); + + auto new_id = add_key(constants::key_type_user, full_key.c_str(), value.data(), value.size(), m_ring_id); + + if (new_id < 0) { + return fmt::format("Error while storing a key, can't add the key: {}", strerror(errno)); + } + + + auto result = keyctl_link(new_id, m_ring_id); + + if (result < 0) { + return fmt::format("Error while storing a key, can't link the key to the keyring: {}", strerror(errno)); + } + + return std::nullopt; +} + + +[[nodiscard]] std::optional secret::SecretStorage::remove(const std::string& key) const { + + auto key_id = get_id_from_name(m_ring_id, key); + + if (key_id < 0) { + return fmt::format("Error while removing a key, can't find key by name: {}", strerror(errno)); + } + + auto result = keyctl_unlink(static_cast(key_id), m_ring_id); + + if (result < 0) { + return fmt::format("Error while removing a key, can't unlink key: {}", strerror(errno)); + } + + return std::nullopt; +} + + +#elif defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__) + +#include +#include + +#ifndef NOMINMAX +#define NOMINMAX +#endif +#ifndef _WINSOCKAPI_ +#define _WINSOCKAPI_ +#endif +#include +#include + + +namespace { + + namespace constants { + constexpr const wchar_t* property_name = L"OOPetris Payload"; + + constexpr const wchar_t* key_name_prefix = L"OOPetris_key__"; + + constexpr const wchar_t* used_algorithm = BCRYPT_AES_ALGORITHM; + } // namespace constants + + std::wstring get_key_name(const std::string& key) { + std::wstring result{ constants::key_name_prefix }; + for (auto& normal_char : key) { + result += normal_char; + } + return result; + } + + helper::expected + get_handle_from_name(secret::KeyringType type, NCRYPT_PROV_HANDLE phProvider, const std::string& key) { + + + NCRYPT_KEY_HANDLE key_handle{}; + + auto key_name = get_key_name(key); + + // the key is of type user, if not specified otherwise, session mode is not supported + DWORD flags = (type == secret::KeyringType::Persistent ? NCRYPT_MACHINE_KEY_FLAG : 0); + + // 0 means no dwLegacyKeySpec + auto result = NCryptOpenKey(phProvider, &key_handle, key_name.c_str(), 0, flags); + + + if (result != ERROR_SUCCESS) { + return helper::unexpected{ fmt::format("Error while opening a key: {}", result) }; + } + + return key_handle; + } + +} // namespace + + +secret::SecretStorage::SecretStorage(KeyringType type) : m_type{ type }, m_phProvider{} { + + if (type == KeyringType::Session) { + spdlog::warn("KeyringType Session is not supported, using KeyringType User"); + m_type = KeyringType::User; + } + + // there are no flags available, so using 0 + auto result = NCryptOpenStorageProvider(&this->m_phProvider, MS_KEY_STORAGE_PROVIDER, 0); + + if (result != ERROR_SUCCESS) { + throw std::runtime_error(fmt::format("Error while getting a storage provider handle: {}", result)); + } +} + +secret::SecretStorage::~SecretStorage() { + if (m_phProvider) { + // ignore return code, as it only indicates, if we passed a valid or invalid handle + NCryptFreeObject(m_phProvider); + } +} + +secret::SecretStorage::SecretStorage(SecretStorage&& other) noexcept + : m_type{ other.m_type }, + m_phProvider{ other.m_phProvider } { + other.m_phProvider = 0; +} + + +[[nodiscard]] helper::expected secret::SecretStorage::load(const std::string& key) const { + + auto maybe_key_handle = get_handle_from_name(m_type, m_phProvider, key); + + if (not maybe_key_handle.has_value()) { + return helper::unexpected{ maybe_key_handle.error() }; + } + + auto key_handle = maybe_key_handle.value(); + + DWORD bytes_needed{}; + + // no flags needed, so using 0 + auto result = NCryptGetProperty(key_handle, constants::property_name, nullptr, 0, &bytes_needed, 0); + + if (result != ERROR_SUCCESS) { + NCryptFreeObject(key_handle); + return helper::unexpected{ + fmt::format("Error while loading a key, getting the property size failed: {}", result) + }; + } + + PBYTE buffer = new BYTE[bytes_needed]; + + DWORD bytes_written{}; + + auto result2 = NCryptGetProperty(key_handle, constants::property_name, buffer, bytes_needed, &bytes_written, 0); + + if (result2 != ERROR_SUCCESS) { + NCryptFreeObject(key_handle); + delete[] buffer; + return helper::unexpected{ + fmt::format("Error while loading a key, getting the property failed: {}", result2) + }; + } + + if (bytes_written != bytes_needed) { + NCryptFreeObject(key_handle); + delete[] buffer; + return helper::unexpected{ + fmt::format("Error while loading a key, getting the property failed: mismatching sizes reported") + }; + } + + NCryptFreeObject(key_handle); + + auto result_buffer = + Buffer{ reinterpret_cast(buffer), //NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + static_cast(bytes_needed) }; + + + delete[] buffer; + + return result_buffer; +} + +[[nodiscard]] std::optional +secret::SecretStorage::store(const std::string& key, const Buffer& value, bool update) const { + + NCRYPT_KEY_HANDLE key_handle{}; + + auto key_name = get_key_name(key); + + // the key is of type user, if not specified otherwise, session mode is not supported, also prefer VBS, but not require it, take the update flag also in consideration + DWORD flags = (m_type == secret::KeyringType::Persistent ? NCRYPT_MACHINE_KEY_FLAG : 0) + | (update ? NCRYPT_OVERWRITE_KEY_FLAG : 0) +#ifdef NCRYPT_PREFER_VBS_FLAG + | NCRYPT_PREFER_VBS_FLAG; +#else + ; +#endif + + // 0 means no dwLegacyKeySpec + auto result = NCryptCreatePersistedKey( + this->m_phProvider, &key_handle, constants::used_algorithm, key_name.c_str(), 0, flags + ); + + if (result != ERROR_SUCCESS) { + return fmt::format("Error while storing a key, creating a key failed: {}", result); + } + + DWORD flags2 = m_type == KeyringType::Persistent ? NCRYPT_PERSIST_FLAG : 0; + + PBYTE buffer = new BYTE[value.size()]; + + std::memcpy(buffer, value.data(), value.size()); + + auto result2 = + NCryptSetProperty(key_handle, constants::property_name, buffer, static_cast(value.size()), flags2); + + delete[] buffer; + + if (result2 != ERROR_SUCCESS) { + NCryptFreeObject(key_handle); + return fmt::format("Error while storing a key, setting the key data failed: {}", result); + } + + // no specific flags are needed, so using 0 + auto result3 = NCryptFinalizeKey(key_handle, 0); + + + if (result3 != ERROR_SUCCESS) { + NCryptFreeObject(key_handle); + return fmt::format("Error while storing a key, finalizing the key failed: {}", result); + } + + // ignoring return value + NCryptFreeObject(key_handle); + return std::nullopt; +} + + +[[nodiscard]] std::optional secret::SecretStorage::remove(const std::string& key) const { + + auto maybe_key_handle = get_handle_from_name(m_type, m_phProvider, key); + + if (not maybe_key_handle.has_value()) { + return maybe_key_handle.error(); + } + + auto key_handle = maybe_key_handle.value(); + + // no flags needed, so using 0 + auto result = NCryptDeleteKey(key_handle, 0); + + if (result != ERROR_SUCCESS) { + NCryptFreeObject(key_handle); + return fmt::format("Error while removing a key, deleting the key failed: {}", result); + } + + return std::nullopt; +} + +#elif defined(__CONSOLE__) || defined(__APPLE__) + +#include "helper/graphic_utils.hpp" +#include + +namespace { + + namespace secrets_constants { + constexpr const char* store_file_name = ".secret_key_storage"; + } // namespace secrets_constants + + helper::expected get_json_from_file(const std::string& file) { + auto result = json::try_parse_json_file(file); + + if (not result.has_value()) { + auto [error, _] = result.error(); + return helper::unexpected{ error }; + } + + return result.value(); + } + + +} // namespace + + +// This is a dummy fallback, but good enough for this platforms +secret::SecretStorage::SecretStorage(KeyringType type) : m_type{ type } { + + m_file_path = utils::get_root_folder() / secrets_constants::store_file_name; +} + +secret::SecretStorage::~SecretStorage() = default; + +secret::SecretStorage::SecretStorage(SecretStorage&& other) noexcept + : m_type{ other.m_type }, + m_file_path{ other.m_file_path } { } + + +[[nodiscard]] helper::expected secret::SecretStorage::load(const std::string& key) const { + + auto maybe_json_value = get_json_from_file(m_file_path); + + if (not maybe_json_value.has_value()) { + return helper::unexpected{ maybe_json_value.error() }; + } + + auto json_value = maybe_json_value.value(); + + if (not json_value.contains(key)) { + return helper::unexpected{ fmt::format("Couldn't find key: {}", key) }; + } + + std::string result{}; + + json_value.at(key).get_to(result); + + auto result_buffer = Buffer{ result }; + + return result_buffer; +} + +[[nodiscard]] std::optional +secret::SecretStorage::store(const std::string& key, const Buffer& value, bool update) const { + + auto maybe_json_value = get_json_from_file(m_file_path); + + if (not maybe_json_value.has_value()) { + return maybe_json_value.error(); + } + + auto json_value = maybe_json_value.value(); + + if (json_value.contains(key) && not update) { + return "Error while storing a key, it already exists and we can't update it"; + } + + + json_value.at(key) = value.as_string(); + + return json::try_write_json_to_file(m_file_path, json_value, true); +} + + +[[nodiscard]] std::optional secret::SecretStorage::remove(const std::string& key) const { + + auto maybe_json_value = get_json_from_file(m_file_path); + + if (not maybe_json_value.has_value()) { + return maybe_json_value.error(); + } + + auto json_value = maybe_json_value.value(); + + json_value.erase(key); + + return json::try_write_json_to_file(m_file_path, json_value, true); +} + + +#else +#error "Unsupported platform" +#endif diff --git a/src/lobby/credentials/secret.hpp b/src/lobby/credentials/secret.hpp new file mode 100644 index 00000000..c2a91a93 --- /dev/null +++ b/src/lobby/credentials/secret.hpp @@ -0,0 +1,63 @@ + +#pragma once + +#include +#include + +#include +#include + +#include "./buffer.hpp" +#include "helper/windows.hpp" + + +#if defined(__linux__) + +#include +#elif defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__) + +#include +namespace oopetris::secret::details { + using NCRYPT_PROV_HANDLE = ULONG_PTR; +} // namespace oopetris::secret::details + +#endif + +namespace secret { + + enum class KeyringType : u8 { User, Session, Persistent }; + + struct SecretStorage { + private: + KeyringType m_type; + +#if defined(__linux__) || defined(__ANDROID__) + key_serial_t m_ring_id; +#elif defined(__CONSOLE__) || defined(__APPLE__) + std::string m_file_path; +#elif defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__) + oopetris::secret::details::NCRYPT_PROV_HANDLE m_phProvider; +#endif + + + public: + OOPETRIS_GRAPHICS_EXPORTED explicit SecretStorage(KeyringType type); + + OOPETRIS_GRAPHICS_EXPORTED ~SecretStorage(); //NOLINT(performance-trivially-destructible) + + OOPETRIS_GRAPHICS_EXPORTED SecretStorage(const SecretStorage& other) = delete; + OOPETRIS_GRAPHICS_EXPORTED SecretStorage& operator=(const SecretStorage& other) = delete; + + OOPETRIS_GRAPHICS_EXPORTED SecretStorage(SecretStorage&& other) noexcept; + OOPETRIS_GRAPHICS_EXPORTED SecretStorage& operator=(SecretStorage&& other) noexcept = delete; + + OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] helper::expected load(const std::string& key + ) const; + + OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] std::optional + store(const std::string& key, const Buffer& value, bool update = true) const; + + OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] std::optional remove(const std::string& key) const; + }; + +} // namespace secret diff --git a/src/lobby/curl_client.cpp b/src/lobby/curl_client.cpp index bc5e7a32..1936d4d5 100644 --- a/src/lobby/curl_client.cpp +++ b/src/lobby/curl_client.cpp @@ -133,3 +133,14 @@ void oopetris::http::implementation::ActualClient::SetBearerAuth(const std::stri m_session->SetHeader(cpr::Header{ "Authorization", fmt::format("Bearer {}", token) }); #endif } + + +void oopetris::http::implementation::ActualClient::ResetBearerAuth() { + + +#if CPR_LIBCURL_VERSION_NUM >= 0x073D00 + m_session->SetBearer(std::string{}); +#else + m_session->SetHeader(); +#endif +} diff --git a/src/lobby/curl_client.hpp b/src/lobby/curl_client.hpp index 1c9abce5..64c6c4da 100644 --- a/src/lobby/curl_client.hpp +++ b/src/lobby/curl_client.hpp @@ -76,6 +76,8 @@ namespace oopetris::http::implementation { Put(const std::string& url, const std::optional>& payload) override; OOPETRIS_GRAPHICS_EXPORTED void SetBearerAuth(const std::string& token) override; + + OOPETRIS_GRAPHICS_EXPORTED void ResetBearerAuth() override; }; diff --git a/src/lobby/httplib_client.cpp b/src/lobby/httplib_client.cpp index a394d3b0..560162ab 100644 --- a/src/lobby/httplib_client.cpp +++ b/src/lobby/httplib_client.cpp @@ -115,3 +115,8 @@ void oopetris::http::implementation::ActualClient::SetBearerAuth(const std::stri m_client.set_bearer_token_auth(token); } + +void oopetris::http::implementation::ActualClient::ResetBearerAuth() { + + m_client.set_bearer_token_auth(""); +} diff --git a/src/lobby/httplib_client.hpp b/src/lobby/httplib_client.hpp index c8d1858a..894e05e2 100644 --- a/src/lobby/httplib_client.hpp +++ b/src/lobby/httplib_client.hpp @@ -76,6 +76,8 @@ namespace oopetris::http::implementation { Put(const std::string& url, const std::optional>& payload) override; OOPETRIS_GRAPHICS_EXPORTED void SetBearerAuth(const std::string& token) override; + + OOPETRIS_GRAPHICS_EXPORTED void ResetBearerAuth() override; }; diff --git a/src/lobby/meson.build b/src/lobby/meson.build index e8632334..90779ffd 100644 --- a/src/lobby/meson.build +++ b/src/lobby/meson.build @@ -18,3 +18,5 @@ graphics_src_files += files( 'constants.hpp', 'types.hpp', ) + +subdir('credentials') diff --git a/src/manager/event_dispatcher.hpp b/src/manager/event_dispatcher.hpp index 2f00636e..a66dfed1 100644 --- a/src/manager/event_dispatcher.hpp +++ b/src/manager/event_dispatcher.hpp @@ -20,10 +20,13 @@ struct EventDispatcher final { sdl::Key{ SDLK_RETURN }, sdl::Key{ SDLK_BACKSPACE }, sdl::Key{ SDLK_BACKSPACE, { sdl::Modifier::CTRL } }, + sdl::Key{ SDLK_BACKSPACE, { sdl::Modifier::SHIFT } }, sdl::Key{ SDLK_DOWN }, sdl::Key{ SDLK_UP }, sdl::Key{ SDLK_LEFT }, sdl::Key{ SDLK_RIGHT }, + sdl::Key{ SDLK_LEFT, { sdl::Modifier::CTRL } }, + sdl::Key{ SDLK_RIGHT, { sdl::Modifier::CTRL } }, sdl::Key{ SDLK_ESCAPE }, sdl::Key{ SDLK_TAB }, sdl::Key{ SDLK_c, { sdl::Modifier::CTRL } }, diff --git a/src/manager/meson.build b/src/manager/meson.build index 8258817b..f85e11fc 100644 --- a/src/manager/meson.build +++ b/src/manager/meson.build @@ -9,6 +9,8 @@ graphics_src_files += files( 'sdl_controller_key.hpp', 'sdl_key.cpp', 'sdl_key.hpp', + 'settings.cpp', + 'settings.hpp', 'settings_manager.cpp', 'settings_manager.hpp', ) diff --git a/src/manager/music_manager.cpp b/src/manager/music_manager.cpp index ecd4201e..b7865bbb 100644 --- a/src/manager/music_manager.cpp +++ b/src/manager/music_manager.cpp @@ -360,7 +360,7 @@ std::optional MusicManager::change_volume(const std::int8_t steps) { new_volume = std::nullopt; } else { - new_volume = current_volume.value() + MusicManager::step_width * static_cast(steps); + new_volume = current_volume.value() + (MusicManager::step_width * static_cast(steps)); if (new_volume <= 0.0F) { diff --git a/src/manager/sdl_key.cpp b/src/manager/sdl_key.cpp index 4a755720..a5e6e20d 100644 --- a/src/manager/sdl_key.cpp +++ b/src/manager/sdl_key.cpp @@ -316,7 +316,6 @@ helper::expected sdl::Key::from_string(const std::string& [[nodiscard]] bool sdl::Key::has_modifier(const Modifier& modifier) const { const auto sdl_modifier = to_sdl_modifier(modifier); - ; return (m_modifiers & sdl_modifier) != 0; } @@ -397,9 +396,17 @@ helper::expected sdl::Key::from_string(const std::string& for (const auto& modifier : multiple) { const auto sdl_modifier = to_sdl_modifier(modifier); - if (((other.m_modifiers & sdl_modifier) & (this->m_modifiers & sdl_modifier) //NOLINT(misc-redundant-expression) - ) - == 0) { + + const auto mask_1 = other.m_modifiers & sdl_modifier; + const auto mask_2 = this->m_modifiers & sdl_modifier; + + // if none of the has this modifier, just check the next modifier + if (mask_1 == 0 && mask_2 == 0) { + continue; + } + + // if only one of them has the modifier, it is not equal + if ((mask_1 & mask_2) == 0) { return false; } } @@ -408,10 +415,25 @@ helper::expected sdl::Key::from_string(const std::string& return true; } - return std::ranges::all_of(special, [this, &other](const auto& modifier) { + for (const auto& modifier : special) { //NOLINT(readability-use-anyofallof) const auto sdl_modifier = to_sdl_modifier(modifier); - return ((other.m_modifiers & sdl_modifier) == (m_modifiers & sdl_modifier)); - }); + + const auto mask_1 = other.m_modifiers & sdl_modifier; + const auto mask_2 = this->m_modifiers & sdl_modifier; + + // if none of the has this modifier, just check the next modifier + if (mask_1 == 0 && mask_2 == 0) { + continue; + } + + // if only one of them has the modifier, it is not equal + if ((mask_1 & mask_2) == 0) { + return false; + } + } + + + return true; } [[nodiscard]] std::string sdl::Key::to_string() const { diff --git a/src/manager/sdl_key.hpp b/src/manager/sdl_key.hpp index 8c67d8f4..330889a4 100644 --- a/src/manager/sdl_key.hpp +++ b/src/manager/sdl_key.hpp @@ -71,8 +71,33 @@ namespace sdl { */ OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] bool has_modifier_exact(const Modifier& modifier) const; + /** + * @brief Shorthand for is_equal(key, true); + * + * @param other + * @return bool + */ OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] bool operator==(const Key& other) const; + /** + * @brief compare two keys with another, this IS symmetrical. The result is true when these conditions are met: + * 1: the key identifier are the same + * 2: all "selected modifiers" have the same value, "same" means here, that either both have the modifier (as defined by has_modifier) or none of them has it + * 3: selected modifiers => if ignore_special_modifiers is true, only the modifiers specified by the category "multiple" are selected. those are: + * - sdl::Modifier::CTRL + * - sdl::Modifier::SHIFT + * - sdl::Modifier::ALT + * - sdl::Modifier::GUI + * + * if ignore_special_modifiers is false also the modifiers of teh category "special" are selected, these are: + * - sdl::Modifier::NUM, + * - sdl::Modifier::CAPS, + * - sdl::Modifier::MODE, + * - sdl::Modifier::SCROLL, + * @param other + * @param ignore_special_modifiers + * @return bool + */ OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] bool is_equal(const Key& other, bool ignore_special_modifiers = true) const; diff --git a/src/manager/service_provider.hpp b/src/manager/service_provider.hpp index e72ff38a..3dc956ab 100644 --- a/src/manager/service_provider.hpp +++ b/src/manager/service_provider.hpp @@ -28,6 +28,11 @@ namespace scenes { struct Scene; } +namespace lobby { + struct API; + +} + struct ServiceProvider { ServiceProvider() = default; ServiceProvider(const ServiceProvider&) = delete; @@ -53,6 +58,8 @@ struct ServiceProvider { [[nodiscard]] virtual input::InputManager& input_manager() = 0; [[nodiscard]] virtual const input::InputManager& input_manager() const = 0; + [[nodiscard]] virtual const std::unique_ptr& api() const = 0; + #if defined(_HAVE_DISCORD_SDK) && !defined(_OOPETRIS_RECORDING_UTILITY) [[nodiscard]] virtual std::optional& discord_instance() = 0; diff --git a/src/manager/settings.cpp b/src/manager/settings.cpp new file mode 100644 index 00000000..a7d9ac8f --- /dev/null +++ b/src/manager/settings.cpp @@ -0,0 +1,34 @@ +#include "settings.hpp" +#include "helper/graphic_utils.hpp" +#include "input/keyboard_input.hpp" +#include "input/touch_input.hpp" + + +#include + + +void settings::to_json(nlohmann::json& obj, const settings::Settings& settings) { + obj = nlohmann::json::object({ + { "controls", + nlohmann::json::object({ { "inputs", settings.controls }, { "selected", settings.selected } }) }, + { "volume", settings.volume }, + { "discord", settings.discord }, + { "api_url", settings.api_url } + }); +} + +void settings::from_json(const nlohmann::json& obj, settings::Settings& settings) { + + ::json::check_for_no_additional_keys(obj, { "controls", "volume", "discord", "api_url" }); + + obj.at("volume").get_to(settings.volume); + obj.at("api_url").get_to(settings.api_url); + obj.at("discord").get_to(settings.discord); + + const auto& controls = obj.at("controls"); + + ::json::check_for_no_additional_keys(controls, { "inputs", "selected" }); + + controls.at("inputs").get_to(settings.controls); + controls.at("selected").get_to(settings.selected); +} diff --git a/src/manager/settings.hpp b/src/manager/settings.hpp new file mode 100644 index 00000000..0a83415b --- /dev/null +++ b/src/manager/settings.hpp @@ -0,0 +1,83 @@ +#pragma once + + +#include "helper/windows.hpp" +#include "input/controller_input.hpp" +#include "input/joystick_input.hpp" +#include "input/keyboard_input.hpp" +#include "input/touch_input.hpp" +#include "manager/service_provider.hpp" + +#include +#include + +using Controls = + std::variant; + +namespace nlohmann { + template<> + struct adl_serializer { + static Controls from_json(const json& obj) { + const auto& type = obj.at("type"); + + if (type == "keyboard") { + return Controls{ nlohmann::adl_serializer::from_json(obj) }; + } + + if (type == "joystick") { + return Controls{ nlohmann::adl_serializer::from_json(obj) }; + } + + if (type == "controller") { + return Controls{ nlohmann::adl_serializer::from_json(obj) }; + } + + if (type == "touch") { + return Controls{ nlohmann::adl_serializer::from_json(obj) }; + } + + throw std::runtime_error{ fmt::format("unsupported control type '{}'", to_string(type)) }; + } + + static void to_json(json& obj, Controls controls) { // NOLINT(misc-no-recursion) + std::visit( + helper::overloaded{ + [&](const input::KeyboardSettings& keyboard_settings) { // NOLINT(misc-no-recursion) + nlohmann::adl_serializer::to_json(obj, keyboard_settings); + obj["type"] = "keyboard"; + }, + [&](const input::JoystickSettings& joystick_settings) { // NOLINT(misc-no-recursion) + nlohmann::adl_serializer::to_json(obj, joystick_settings); + obj["type"] = "joystick"; + }, + [&](const input::ControllerSettings& controller_settings) { // NOLINT(misc-no-recursion) + nlohmann::adl_serializer::to_json(obj, controller_settings); + obj["type"] = "controller"; + }, + [&](const input::TouchSettings& touch_settings) { // NOLINT(misc-no-recursion) + nlohmann::adl_serializer::to_json(obj, touch_settings); + obj["type"] = "touch"; + } }, + controls + ); + } + }; +} // namespace nlohmann + + +namespace settings { + + struct Settings { + std::vector controls; + std::optional selected; + float volume{ 0.2F }; + std::optional discord; //changing this requires a restart + std::optional api_url; + }; + + + OOPETRIS_GRAPHICS_EXPORTED void to_json(nlohmann::json& obj, const Settings& settings); + + OOPETRIS_GRAPHICS_EXPORTED void from_json(const nlohmann::json& obj, Settings& settings); + +} // namespace settings diff --git a/src/manager/settings_manager.cpp b/src/manager/settings_manager.cpp index 883655e1..6115da53 100644 --- a/src/manager/settings_manager.cpp +++ b/src/manager/settings_manager.cpp @@ -3,54 +3,70 @@ #include "input/keyboard_input.hpp" #include "input/touch_input.hpp" +namespace { + constexpr const auto settings_filename = "settings.json"; + +} -#include -SettingsManager::SettingsManager(ServiceProvider* service_provider) : m_service_provider{ service_provider } { - const std::filesystem::path settings_file = utils::get_root_folder() / detail::settings_filename; +SettingsManager::SettingsManager() { + const std::filesystem::path settings_file = utils::get_root_folder() / settings_filename; - const auto result = json::try_parse_json_file(settings_file); + const auto result = json::try_parse_json_file(settings_file); if (result.has_value()) { m_settings = result.value(); } else { - spdlog::warn("unable to load settings from \"{}\": {}", detail::settings_filename, result.error()); + auto [error, error_type] = result.error(); + + spdlog::error("unable to load settings from \"{}\": {}", settings_filename, error); spdlog::warn("applying default settings"); m_settings = { - detail::Settings{ .controls = {}, .selected = std::nullopt, .volume = 1.0, .discord = false } + settings::Settings{ .controls = {}, + .selected = std::nullopt, + .volume = 1.0, + .discord = false, + .api_url = std::nullopt } }; - //TODO(Totto): save the file, if it doesn't exist, if it has an error, just leave it there + //save the default file, only if it doesn't exist, if it has an error, just leave it there + if (error_type == json::ParseError::OpenError) { + this->save(); + } } } -[[nodiscard]] const detail::Settings& SettingsManager::settings() const { +[[nodiscard]] const settings::Settings& SettingsManager::settings() const { return m_settings; } -void detail::to_json(nlohmann::json& obj, const detail::Settings& settings) { - obj = nlohmann::json{ - { "controls", - nlohmann::json{ { "inputs", settings.controls }, { "selected", settings.selected } }, - { "volume", settings.volume }, - { "discord", settings.discord } } - }; +void SettingsManager::add_callback(Callback&& callback) { + m_callbacks.emplace_back(std::move(callback)); } -void detail::from_json(const nlohmann::json& obj, detail::Settings& settings) { +void SettingsManager::save() const { + const std::filesystem::path settings_file = utils::get_root_folder() / settings_filename; - ::json::check_for_no_additional_keys(obj, { "controls", "volume", "discord" }); + const auto result = json::try_write_json_to_file(settings_file, m_settings, true); - obj.at("volume").get_to(settings.volume); - obj.at("discord").get_to(settings.discord); + if (result.has_value()) { + spdlog::error("unable to save settings to \"{}\": {}", settings_filename, result.value()); + return; + } - const auto& controls = obj.at("controls"); + this->fire_callbacks(); +} - ::json::check_for_no_additional_keys(controls, { "inputs", "selected" }); +void SettingsManager::save(const settings::Settings& new_settings) { + this->m_settings = new_settings; + this->save(); +} - controls.at("inputs").get_to(settings.controls); - controls.at("selected").get_to(settings.selected); +void SettingsManager::fire_callbacks() const { + for (const auto& callback : m_callbacks) { + callback(m_settings); + } } diff --git a/src/manager/settings_manager.hpp b/src/manager/settings_manager.hpp index 5d1f81df..71bae702 100644 --- a/src/manager/settings_manager.hpp +++ b/src/manager/settings_manager.hpp @@ -8,90 +8,28 @@ #include "input/touch_input.hpp" #include "manager/service_provider.hpp" -#include -#include +#include "./settings.hpp" -using Controls = - std::variant; - -namespace nlohmann { - template<> - struct adl_serializer { - static Controls from_json(const json& obj) { - const auto& type = obj.at("type"); - - if (type == "keyboard") { - return Controls{ nlohmann::adl_serializer::from_json(obj) }; - } - - if (type == "joystick") { - return Controls{ nlohmann::adl_serializer::from_json(obj) }; - } - - if (type == "controller") { - return Controls{ nlohmann::adl_serializer::from_json(obj) }; - } - - if (type == "touch") { - return Controls{ nlohmann::adl_serializer::from_json(obj) }; - } - - throw std::runtime_error{ fmt::format("unsupported control type '{}'", to_string(type)) }; - } - - static void to_json(json& obj, Controls controls) { // NOLINT(misc-no-recursion) - std::visit( - helper::overloaded{ - [&](const input::KeyboardSettings& keyboard_settings) { // NOLINT(misc-no-recursion) - nlohmann::adl_serializer::to_json(obj, keyboard_settings); - obj["type"] = "keyboard"; - }, - [&](const input::JoystickSettings& joystick_settings) { // NOLINT(misc-no-recursion) - nlohmann::adl_serializer::to_json(obj, joystick_settings); - obj["type"] = "joystick"; - }, - [&](const input::ControllerSettings& controller_settings) { // NOLINT(misc-no-recursion) - nlohmann::adl_serializer::to_json(obj, controller_settings); - obj["type"] = "controller"; - }, - [&](const input::TouchSettings& touch_settings) { // NOLINT(misc-no-recursion) - nlohmann::adl_serializer::to_json(obj, touch_settings); - obj["type"] = "touch"; - } }, - controls - ); - } - }; -} // namespace nlohmann - - -namespace detail { - - static constexpr auto settings_filename = "settings.json"; +struct SettingsManager { +public: + using Callback = std::function; - struct Settings { - std::vector controls; - std::optional selected; - float volume{ 0.2F }; - bool discord{ false }; //changing this requires a restart - }; +private: + settings::Settings m_settings; + std::vector m_callbacks; +public: + OOPETRIS_GRAPHICS_EXPORTED explicit SettingsManager(); - OOPETRIS_GRAPHICS_EXPORTED void to_json(nlohmann::json& obj, const Settings& settings); + OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] const settings::Settings& settings() const; - OOPETRIS_GRAPHICS_EXPORTED void from_json(const nlohmann::json& obj, Settings& settings); + OOPETRIS_GRAPHICS_EXPORTED void add_callback(Callback&& callback); -} // namespace detail + OOPETRIS_GRAPHICS_EXPORTED void save() const; + OOPETRIS_GRAPHICS_EXPORTED void save(const settings::Settings& new_settings); -struct SettingsManager { private: - ServiceProvider* m_service_provider; - detail::Settings m_settings; - -public: - OOPETRIS_GRAPHICS_EXPORTED explicit SettingsManager(ServiceProvider* service_provider); - - OOPETRIS_GRAPHICS_EXPORTED [[nodiscard]] const detail::Settings& settings() const; + void fire_callbacks() const; }; diff --git a/src/scenes/online_lobby/online_lobby.cpp b/src/scenes/online_lobby/online_lobby.cpp index 97f6670e..abbc16d8 100644 --- a/src/scenes/online_lobby/online_lobby.cpp +++ b/src/scenes/online_lobby/online_lobby.cpp @@ -27,16 +27,6 @@ namespace scenes { std::pair{ 0.05, 0.03 }, layout } { - - //TODO(Totto): after the settings have been reworked, make this url changeable! - auto maybe_api = lobby::API::get_api("http://127.0.0.1:5000"); - if (maybe_api.has_value()) { - m_api = std::make_unique(std::move(maybe_api.value())); - } else { - spdlog::error("Error in connecting to lobby API: {}", maybe_api.error()); - m_api = nullptr; - } - auto focus_helper = ui::FocusHelper{ 1 }; m_main_layout.add( diff --git a/src/scenes/online_lobby/online_lobby.hpp b/src/scenes/online_lobby/online_lobby.hpp index 6f31428b..91fa4caa 100644 --- a/src/scenes/online_lobby/online_lobby.hpp +++ b/src/scenes/online_lobby/online_lobby.hpp @@ -1,7 +1,5 @@ #pragma once -#include "lobby/api.hpp" - #include "scenes/scene.hpp" #include "ui/components/label.hpp" #include "ui/components/text_button.hpp" @@ -17,7 +15,6 @@ namespace scenes { ui::TileLayout m_main_layout; std::optional m_next_command; - std::unique_ptr m_api; public: OOPETRIS_GRAPHICS_EXPORTED explicit OnlineLobby(ServiceProvider* service_provider, const ui::Layout& layout); diff --git a/src/scenes/settings_menu/color_setting_row.cpp b/src/scenes/settings_menu/color_setting_row.cpp index c6a6316a..e0f42c5f 100644 --- a/src/scenes/settings_menu/color_setting_row.cpp +++ b/src/scenes/settings_menu/color_setting_row.cpp @@ -114,17 +114,19 @@ bool detail::ColorPickerScene::handle_event( const SDL_Event& event ) { + const auto result = m_color_picker.handle_event(input_manager, event); + if (result) { + return result; + } + const auto navigation_event = input_manager->get_navigation_event(event); + if (navigation_event == input::NavigationEvent::BACK) { m_should_exit = true; return true; } - const auto result = m_color_picker.handle_event(input_manager, event); - if (result) { - return result; - } // swallow all events return true; diff --git a/src/scenes/settings_menu/settings_menu.cpp b/src/scenes/settings_menu/settings_menu.cpp index ecfdeee1..8be0bf90 100644 --- a/src/scenes/settings_menu/settings_menu.cpp +++ b/src/scenes/settings_menu/settings_menu.cpp @@ -2,8 +2,10 @@ #include #include "color_setting_row.hpp" +#include "lobby/api.hpp" #include "manager/music_manager.hpp" #include "manager/resource_manager.hpp" +#include "manager/settings_manager.hpp" #include "settings_details.hpp" #include "settings_menu.hpp" #include "ui/components/label.hpp" @@ -36,7 +38,7 @@ namespace scenes { std::pair{ 0.05, 0.03 }, layout }, - m_colors{COLOR_LITERAL("#FF33FF"), COLOR_LITERAL("hsv(281.71, 0.70085, 0.45882)"), COLOR_LITERAL("rgb(246, 255, 61)"),COLOR_LITERAL("hsv(103.12, 0.39024, 0.32157)")},m_game_input{game_input} + m_colors{COLOR_LITERAL("#FF33FF"), COLOR_LITERAL("hsv(281.71, 0.70085, 0.45882)"), COLOR_LITERAL("rgb(246, 255, 61)"),COLOR_LITERAL("hsv(103.12, 0.39024, 0.32157)")},m_game_input{game_input},m_settings{m_service_provider->settings_manager().settings()} { auto focus_helper = ui::FocusHelper{ 1 }; @@ -68,9 +70,13 @@ namespace scenes { const auto value = service_provider->music_manager().get_volume(); return value.has_value() ? value.value() : 0.0F; }, - [service_provider](double amount) { + [service_provider, this](double amount) { const auto mapped_amount = amount <= 0.0F ? std::nullopt : std::optional{ amount }; service_provider->music_manager().set_volume(mapped_amount, false, false); + + this->m_settings.volume = static_cast(amount); + + this->m_did_change_settings = true; }, 0.05F, std::pair{ 0.6, 1.0 }, ui::Alignment{ ui::AlignmentHorizontal::Middle, ui::AlignmentVertical::Center } @@ -81,9 +87,39 @@ namespace scenes { [this, scroll_layout_index, slider_index](std::optional) { auto* scroll_layout = this->m_main_layout.get(scroll_layout_index); scroll_layout->get(slider_index)->on_change(); + + if (auto volume = this->m_service_provider->music_manager().get_volume(); volume.has_value()) { + this->m_settings.volume = static_cast(volume.value()); + + } else { + this->m_settings.volume = 0.0; + } + + this->m_did_change_settings = true; } ); + + scroll_layout->add( + ui::RelativeItemSize{ scroll_layout->layout(), 0.2 }, service_provider, "Lobby", + service_provider->font_manager().get(FontId::Default), Color::white(), + std::pair{ 0.1, 0.3 }, + ui::Alignment{ ui::AlignmentHorizontal::Middle, ui::AlignmentVertical::Bottom } + ); + + auto api_url_input_index = scroll_layout->add( + ui::RelativeItemSize{ scroll_layout->layout(), 0.2 }, service_provider, + service_provider->font_manager().get(FontId::Default), Color::white(), focus_helper.focus_id(), + std::pair{ 0.8, 0.6 }, + ui::Alignment{ ui::AlignmentHorizontal::Middle, ui::AlignmentVertical::Center }, + ui::TextInputMode::Scale + ); + + if (auto api_url = m_settings.api_url; api_url.has_value()) { + scroll_layout->get(api_url_input_index)->set_text(api_url.value()); + } + + scroll_layout->add( ui::RelativeItemSize{ scroll_layout->layout(), 0.2 }, service_provider, "Colors", service_provider->font_manager().get(FontId::Default), Color::white(), @@ -104,14 +140,36 @@ namespace scenes { ); } - m_main_layout.add( - service_provider, "Return", service_provider->font_manager().get(FontId::Default), Color::white(), - focus_helper.focus_id(), + + auto return_layout_index = m_main_layout.add( + focus_helper.focus_id(), 2, ui::Direction::Horizontal, ui::AbsolutMargin{ 0 }, + std::pair{ 0.2, 0.1 } + + ); + + auto* return_layout = m_main_layout.get(return_layout_index); + + + return_layout->add( + service_provider, "Return and Save", service_provider->font_manager().get(FontId::Default), + Color::white(), focus_helper.focus_id(), [this](const ui::TextButton&) -> bool { - m_next_command = Command{ Return{} }; + m_next_command = Command{ Return{ ReturnType::Save } }; return false; }, - std::pair{ 0.15, 0.85 }, + std::pair{ 0.8, 0.85 }, + ui::Alignment{ ui::AlignmentHorizontal::Middle, ui::AlignmentVertical::Center }, + std::pair{ 0.1, 0.1 } + ); + + return_layout->add( + service_provider, "Return and Cancel", service_provider->font_manager().get(FontId::Default), + Color::white(), focus_helper.focus_id(), + [this](const ui::TextButton&) -> bool { + m_next_command = Command{ Return{ ReturnType::Cancel } }; + return false; + }, + std::pair{ 0.8, 0.85 }, ui::Alignment{ ui::AlignmentHorizontal::Middle, ui::AlignmentVertical::Center }, std::pair{ 0.1, 0.1 } ); @@ -123,7 +181,13 @@ namespace scenes { if (m_next_command.has_value()) { return std::visit( helper::overloaded{ - [this](const Return&) { + [this](const Return& ret) { + const auto return_type = ret.type; + + if (return_type == ReturnType::Save && m_did_change_settings) { + m_service_provider->settings_manager().save(m_settings); + } + m_service_provider->music_manager().remove_volume_listener(listener_name); return UpdateResult{ SceneUpdate::StopUpdating, Scene::Pop{} }; }, @@ -140,6 +204,25 @@ namespace scenes { return UpdateResult{ SceneUpdate::StopUpdating, std::move(change_scene) }; } + if (auto text_input = utils::is_child_class(action.widget); + text_input.has_value()) { + + const auto api_url = text_input.value()->get_text(); + + this->m_status = Status::Loading; + //TODO(Totto): do this somehow asynchronous + lobby::API::check_url(api_url, [this, api_url](bool success) { + this->m_status = success ? Status::Ok : Status::Error; + this->m_settings.api_url = api_url; + this->m_did_change_settings = true; + }); + + + m_next_command = std::nullopt; + + return UpdateResult{ SceneUpdate::StopUpdating, std::nullopt }; + } + throw std::runtime_error("Requested action on unknown widget, this is a fatal error"); } }, diff --git a/src/scenes/settings_menu/settings_menu.hpp b/src/scenes/settings_menu/settings_menu.hpp index a5177e02..831dbe34 100644 --- a/src/scenes/settings_menu/settings_menu.hpp +++ b/src/scenes/settings_menu/settings_menu.hpp @@ -2,6 +2,7 @@ #include +#include "manager/settings.hpp" #include "scenes/scene.hpp" #include "ui/layouts/tile_layout.hpp" #include "ui/widget.hpp" @@ -10,7 +11,11 @@ namespace details::settings::menu { - struct Return { }; + enum class ReturnType : u8 { Save, Cancel }; + + struct Return { + ReturnType type; + }; struct Action { ui::Widget* widget; @@ -33,12 +38,18 @@ namespace scenes { struct SettingsMenu : public Scene { private: + enum class Status : u8 { Loading, Ok, Error }; + std::optional m_next_command{ std::nullopt }; ui::TileLayout m_main_layout; //todo migrate to settings state std::vector m_colors; std::optional> m_game_input; + settings::Settings m_settings; + bool m_did_change_settings{ false }; + Status m_status{ Status::Ok }; + const std::string listener_name = "settings_menu"; explicit SettingsMenu( diff --git a/src/ui/components/text_button.cpp b/src/ui/components/text_button.cpp index be5b8e90..e4c4ae82 100644 --- a/src/ui/components/text_button.cpp +++ b/src/ui/components/text_button.cpp @@ -19,8 +19,8 @@ ui::TextButton::TextButton( text, font, text_color, { fill_rect.top_left.x + static_cast(margin.first), fill_rect.top_left.y + static_cast(margin.second), - fill_rect.width() - 2 * static_cast(margin.first), - fill_rect.height() - 2 * static_cast(margin.second) } }, + fill_rect.width() - (2 * static_cast(margin.first)), + fill_rect.height() - (2 * static_cast(margin.second)) } }, focus_id, std::move(callback), fill_rect, diff --git a/src/ui/components/textinput.cpp b/src/ui/components/textinput.cpp index debd2271..2223980b 100644 --- a/src/ui/components/textinput.cpp +++ b/src/ui/components/textinput.cpp @@ -6,6 +6,10 @@ #include "manager/event_dispatcher.hpp" #include "textinput.hpp" +#if defined(_HAVE_ICU_DEP) +#include +#endif + using namespace std::chrono_literals; @@ -129,18 +133,30 @@ ui::TextInput::handle_event( //NOLINT(readability-function-cognitive-complexity) { EventHandleType::RequestAction, this } }; } + //TODO(Totto): in some cases this is caught before that, and never triggered case SDLK_BACKSPACE: { - const auto remove_all = (event.key.keysym.mod & KMOD_CTRL) != 0; + const auto sdl_key = sdl::Key{ event.key.keysym }; + + const auto ctrl_pressed = sdl_key.has_modifier(sdl::Modifier::CTRL); + + const auto shift_pressed = sdl_key.has_modifier(sdl::Modifier::SHIFT); if (not m_text.empty()) { - if (remove_all) { - m_text = ""; - m_cursor_position = 0; + // NOTE: if both modifiers are pressed, we prioritize ctrl + if (ctrl_pressed) { + remove_at_cursor(RemoveMode::All); + recalculate_textures(true); + return true; + } + + if (shift_pressed) { + remove_at_cursor(RemoveMode::LastWord); recalculate_textures(true); return true; } - remove_at_cursor(); + + remove_at_cursor(RemoveMode::OneChar); recalculate_textures(true); } @@ -148,7 +164,12 @@ ui::TextInput::handle_event( //NOLINT(readability-function-cognitive-complexity) } case SDLK_LEFT: { if (m_cursor_position != 0) { - if ((event.key.keysym.mod & KMOD_CTRL) != 0) { + + const auto sdl_key = sdl::Key{ event.key.keysym }; + + const auto ctrl_pressed = sdl_key.has_modifier(sdl::Modifier::CTRL); + + if (ctrl_pressed) { m_cursor_position = 0; } else { --m_cursor_position; @@ -161,7 +182,12 @@ ui::TextInput::handle_event( //NOLINT(readability-function-cognitive-complexity) case SDLK_RIGHT: { const u32 current_string_length = static_cast(utf8::distance(m_text.cbegin(), m_text.cend())); if (m_cursor_position < current_string_length) { - if ((event.key.keysym.mod & KMOD_CTRL) != 0) { + + const auto sdl_key = sdl::Key{ event.key.keysym }; + + const auto ctrl_pressed = sdl_key.has_modifier(sdl::Modifier::CTRL); + + if (ctrl_pressed) { m_cursor_position = current_string_length; } else { @@ -173,7 +199,12 @@ ui::TextInput::handle_event( //NOLINT(readability-function-cognitive-complexity) return true; } case SDLK_v: { - if ((event.key.keysym.mod & KMOD_CTRL) != 0) { + + const auto sdl_key = sdl::Key{ event.key.keysym }; + + const auto ctrl_pressed = sdl_key.has_modifier(sdl::Modifier::CTRL); + + if (ctrl_pressed) { if (SDL_HasClipboardText() != SDL_FALSE) { char* text = SDL_GetClipboardText(); @@ -193,7 +224,11 @@ ui::TextInput::handle_event( //NOLINT(readability-function-cognitive-complexity) return false; } case SDLK_c: { - if ((event.key.keysym.mod & KMOD_CTRL) != 0) { + const auto sdl_key = sdl::Key{ event.key.keysym }; + + const auto ctrl_pressed = sdl_key.has_modifier(sdl::Modifier::CTRL); + + if (ctrl_pressed) { const int result = SDL_SetClipboardText(m_text.c_str()); if (result != 0) { throw helper::MinorError{ @@ -407,12 +442,20 @@ bool ui::TextInput::add_string(const std::string& add) { return true; } -bool ui::TextInput::remove_at_cursor() { + +bool ui::TextInput::remove_at_cursor(RemoveMode remove_mode) { if (m_cursor_position == 0) { return false; } + if (remove_mode == RemoveMode::All) { + m_text = ""; + m_cursor_position = 0; + return true; + } + + const u32 current_string_length = static_cast(utf8::distance(m_text.cbegin(), m_text.cend())); // cursor_position is the range [0, length] (inclusively !) @@ -420,14 +463,80 @@ bool ui::TextInput::remove_at_cursor() { throw std::runtime_error("cursor_postion is invalid!"); } - auto start = m_text.begin(); - utf8::advance(start, m_cursor_position - 1, m_text.end()); - auto end = start; - utf8::next(end, m_text.end()); - m_text.erase(start, end); - --m_cursor_position; - return true; + if (remove_mode == RemoveMode::OneChar) { + + auto start = m_text.begin(); + utf8::advance(start, m_cursor_position - 1, m_text.end()); + auto end = start; + utf8::next(end, m_text.end()); + m_text.erase(start, end); + + --m_cursor_position; + return true; + } + + if (remove_mode == RemoveMode::LastWord) { + + auto get_char_cat = [](u32 code_point) -> int { +#if defined(_HAVE_ICU_DEP) + // see: https://en.wikipedia.org/wiki/Unicode_character_property#General_Category + return u_charType(code_point); +#else + if (code_point >= 0x80) { + return 1; + } + + int int_code_point = static_cast(code_point); + + if (isalnum(int_code_point)) { + return 2; + } + + if (isblank(int_code_point)) { + return 3; + } + + if (iscntrl(int_code_point)) { + return 4; + } + + return 5; + +#endif + }; + + auto start = m_text.begin(); + utf8::advance(start, m_cursor_position, m_text.end()); + auto end = start; + u32 remove_amount = 0; + int char_cat = -1; + + while (true) { + auto temp = start; + auto code_point = utf8::prior(temp, m_text.begin()); + + if (char_cat < 0) { + char_cat = get_char_cat(code_point); + } else if (char_cat != get_char_cat(code_point)) { + break; + } + + remove_amount++; + + start = temp; + + if (temp == m_text.begin()) { + break; + } + } + m_text.erase(start, end); + + m_cursor_position -= remove_amount; + return true; + } + + utils::unreachable(); } void ui::TextInput::on_focus() { diff --git a/src/ui/components/textinput.hpp b/src/ui/components/textinput.hpp index ec1b079e..f494e9fa 100644 --- a/src/ui/components/textinput.hpp +++ b/src/ui/components/textinput.hpp @@ -13,7 +13,7 @@ namespace ui { - enum class TextInputMode { Scroll, Scale }; + enum class TextInputMode : u8 { Scroll, Scale }; struct TextInput final : public Widget, public Focusable, public Hoverable { private: @@ -42,6 +42,8 @@ namespace ui { bool is_top_level ); + enum class RemoveMode : u8 { LastWord, OneChar, All }; + public: OOPETRIS_GRAPHICS_EXPORTED explicit TextInput( ServiceProvider* service_provider, @@ -75,7 +77,7 @@ namespace ui { bool add_string(const std::string& add); - bool remove_at_cursor(); + bool remove_at_cursor(RemoveMode remove_mode); void on_focus() override; diff --git a/src/ui/layouts/grid_layout.cpp b/src/ui/layouts/grid_layout.cpp index b3023a77..f351e4b0 100644 --- a/src/ui/layouts/grid_layout.cpp +++ b/src/ui/layouts/grid_layout.cpp @@ -44,6 +44,8 @@ void ui::GridLayout::render(const ServiceProvider& service_provider) const { if (direction == Direction::Horizontal) { const u32 total_margin = this->size <= 1 ? 0 : (this->size - 1) * gap.get_margin(); + assert(layout().get_rect().width() > (total_margin + (margin.first * 2)) + && "width has to be greater than the margins"); width = (layout().get_rect().width() - total_margin - (margin.first * 2)) / this->size; const u32 margin_x = index * gap.get_margin(); @@ -51,6 +53,8 @@ void ui::GridLayout::render(const ServiceProvider& service_provider) const { x_pos += margin_x + total_width; } else { const u32 total_margin = this->size <= 1 ? 0 : (this->size - 1) * gap.get_margin(); + assert(layout().get_rect().height() > (total_margin - (margin.second * 2)) + && "height has to be greater than the margins"); height = (layout().get_rect().height() - total_margin - (margin.second * 2)) / this->size; const u32 margin_y = index * gap.get_margin(); diff --git a/subprojects/icu.wrap b/subprojects/icu.wrap new file mode 100644 index 00000000..982b9a5b --- /dev/null +++ b/subprojects/icu.wrap @@ -0,0 +1,16 @@ +[wrap-file] +directory = icu +source_url = https://github.com/unicode-org/icu/releases/download/release-73-2/icu4c-73_2-src.tgz +source_filename = icu4c-73_2-src.tgz +source_hash = 818a80712ed3caacd9b652305e01afc7fa167e6f2e94996da44b90c2ab604ce1 +patch_filename = icu_73.2-2_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/icu_73.2-2/get_patch +patch_hash = 218a5f20b58b6b2372e636c2eb2d611a898fdc11be17d6c4f35a3cd54d472010 +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/icu_73.2-2/icu4c-73_2-src.tgz +wrapdb_version = 73.2-2 + +[provide] +icu-uc = icuuc_dep +icu-io = icuio_dep +icu-i18n = icui18n_dep +program_names = genbrk, genccode, gencmn diff --git a/subprojects/keyutils.wrap b/subprojects/keyutils.wrap new file mode 100644 index 00000000..dec7a2e5 --- /dev/null +++ b/subprojects/keyutils.wrap @@ -0,0 +1,9 @@ +[wrap-git] +url = https://git.kernel.org/pub/scm/linux/kernel/git/dhowells/keyutils.git +revision = master +depth = 1 +patch_directory = keyutils +diff_files = keyutils_warnings_fix.diff + +[provide] +keyutils = keyutils_dep diff --git a/subprojects/packagefiles/keyutils/meson.build b/subprojects/packagefiles/keyutils/meson.build new file mode 100644 index 00000000..89d611c0 --- /dev/null +++ b/subprojects/packagefiles/keyutils/meson.build @@ -0,0 +1,57 @@ +project( + 'keyutils', + 'c', + meson_version: '>=1.3.0', + version: '1.6.3', + license: 'GPL-2.0', + default_options: { + 'optimization': '3', + 'warning_level': '2', + 'werror': true, + 'strip': true, + 'cpp_std': ['c++23', 'c++latest', 'vc++latest', 'c++20'], + 'b_ndebug': 'if-release', + }, +) + +inc_dirs = include_directories('.') + +src_files = files( + 'keyutils.c', +) + +API_VERSON = '1.10' + +sh = find_program('sh') + +current_date = run_command( + sh, + '-c', 'date -u +%F', + check: true, +).stdout().strip() + +compilation_flags = [ + '-DPKGBUILD="' + current_date + '"', + '-DPKGVERSION="keyutils-' + meson.project_version() + '"', + '-DAPIVERSION="libkeyutils-' + API_VERSON + '"', +] + +if get_option('no_glibc_keyerr') + compilation_flags += '-DNO_GLIBC_KEYERR' +endif + +keyutils_lib = library( + 'keyutils', + src_files, + include_directories: inc_dirs, + install: true, + c_args: compilation_flags, +) + +keyutils_dep = declare_dependency( + include_directories: inc_dirs, + version: meson.project_version(), + link_with: keyutils_lib, +) + +meson.override_dependency('keyutils', keyutils_dep) diff --git a/subprojects/packagefiles/keyutils/meson.options b/subprojects/packagefiles/keyutils/meson.options new file mode 100644 index 00000000..8ffabfc9 --- /dev/null +++ b/subprojects/packagefiles/keyutils/meson.options @@ -0,0 +1,6 @@ +option( + 'no_glibc_keyerr', + type: 'boolean', + value: false, + description: 'don\'t have glibc keyerr constants', +) diff --git a/subprojects/packagefiles/keyutils_warnings_fix.diff b/subprojects/packagefiles/keyutils_warnings_fix.diff new file mode 100644 index 00000000..3e951ab5 --- /dev/null +++ b/subprojects/packagefiles/keyutils_warnings_fix.diff @@ -0,0 +1,13 @@ +diff --git a/keyutils.c b/keyutils.c +index 48b779e..beeeb9b 100644 +--- a/keyutils.c ++++ b/keyutils.c +@@ -672,7 +672,7 @@ key_serial_t find_key_by_type_and_desc(const char *type, const char *desc, + if (errno == ENOMEM) + break; + } +- if (n >= sizeof(rdesc) - 1) ++ if ((unsigned long)n >= sizeof(rdesc) - 1) + continue; + rdesc[n] = '\0'; + diff --git a/tests/graphics/sdl_key.cpp b/tests/graphics/sdl_key.cpp index 91d29d7f..45229c81 100644 --- a/tests/graphics/sdl_key.cpp +++ b/tests/graphics/sdl_key.cpp @@ -68,6 +68,48 @@ TEST(SDLKey, FromString) { } +TEST(SDLKey, ModifierTests) { + const auto key1 = sdl::Key{ SDLK_k, { sdl::Modifier::CTRL } }; + + ASSERT_EQ(key1.has_modifier(sdl::Modifier::ALT), false); + + ASSERT_EQ(key1.has_modifier(sdl::Modifier::LCTRL), true); + ASSERT_EQ(key1.has_modifier(sdl::Modifier::RCTRL), true); + ASSERT_EQ(key1.has_modifier(sdl::Modifier::CTRL), true); + + ASSERT_EQ(key1.has_modifier_exact(sdl::Modifier::LCTRL), false); + ASSERT_EQ(key1.has_modifier_exact(sdl::Modifier::RCTRL), false); + ASSERT_EQ(key1.has_modifier_exact(sdl::Modifier::CTRL), true); +} + + +TEST(SDLKey, SymmetryOfEqual) { + + const std::vector> keys{ + { sdl::Key{ SDLK_ESCAPE, { sdl::Modifier::LCTRL } }, sdl::Key{ SDLK_ESCAPE, { sdl::Modifier::LCTRL } }, true }, + { sdl::Key{ SDLK_ESCAPE, { sdl::Modifier::LCTRL } }, sdl::Key{ SDLK_ESCAPE, { sdl::Modifier::CTRL } }, true }, + { sdl::Key{ SDLK_ESCAPE, { sdl::Modifier::RCTRL } }, sdl::Key{ SDLK_ESCAPE, { sdl::Modifier::CTRL } }, true }, + { sdl::Key{ SDLK_ESCAPE, { sdl::Modifier::RCTRL } }, sdl::Key{ SDLK_ESCAPE, { sdl::Modifier::LCTRL } }, false }, + { sdl::Key{ SDLK_ESCAPE, { sdl::Modifier::CTRL } }, sdl::Key{ SDLK_ESCAPE, { sdl::Modifier::CTRL } }, true }, + { sdl::Key{ SDLK_ESCAPE, { sdl::Modifier::CTRL } }, sdl::Key{ SDLK_ESCAPE }, false }, + { sdl::Key{ SDLK_ESCAPE, { sdl::Modifier::LALT } }, sdl::Key{ SDLK_ESCAPE, { sdl::Modifier::CTRL } }, false }, + { sdl::Key{ SDLK_ESCAPE, { sdl::Modifier::RALT } }, sdl::Key{ SDLK_ESCAPE, { sdl::Modifier::CTRL } }, false }, + { sdl::Key{ SDLK_ESCAPE, { sdl::Modifier::LGUI } }, sdl::Key{ SDLK_ESCAPE, { sdl::Modifier::CTRL } }, false }, + }; + + for (const auto& [key, other_key, expected] : keys) { + + const auto are_equal = key == other_key; + + ASSERT_EQ(are_equal, expected) << "wrong result for: " << key << " == " << other_key; + + const auto are_equal_2 = key == other_key; + + ASSERT_EQ(are_equal_2, expected) << "wrong result for: " << other_key << " == " << key; + } +} + + TEST(SDLKey, ToString) { const std::vector keys{ @@ -95,3 +137,29 @@ TEST(SDLKey, ToString) { ASSERT_EQ(parsed.value(), key); } } + + +TEST(SDLKey, ComplexComparison) { + + const auto to_compare = sdl::Key{ SDLK_ESCAPE, { sdl::Modifier::CTRL } }; + + const std::vector> keys{ + { sdl::Key{ SDLK_1, { sdl::Modifier::LCTRL } }, false }, + { sdl::Key{ SDLK_ESCAPE, { sdl::Modifier::LCTRL } }, true }, + { sdl::Key{ SDLK_ESCAPE, { sdl::Modifier::RCTRL } }, true }, + { sdl::Key{ SDLK_ESCAPE, { sdl::Modifier::CTRL } }, true }, + { sdl::Key{ SDLK_ESCAPE }, false }, + { sdl::Key{ SDLK_ESCAPE, { sdl::Modifier::CTRL, sdl::Modifier::GUI } }, false }, + { sdl::Key{ SDLK_ESCAPE, { sdl::Modifier::LALT } }, false }, + { sdl::Key{ SDLK_ESCAPE, { sdl::Modifier::RALT } }, false }, + { sdl::Key{ SDLK_ESCAPE, { sdl::Modifier::LGUI } }, false }, + }; + + + for (const auto& [key, expected] : keys) { + + const auto are_equal = to_compare == key; + + ASSERT_EQ(are_equal, expected) << "wrong result for: " << to_compare << " == " << key; + } +} diff --git a/tools/dependencies/meson.build b/tools/dependencies/meson.build index 2e333dac..d621887e 100644 --- a/tools/dependencies/meson.build +++ b/tools/dependencies/meson.build @@ -119,7 +119,22 @@ utf8cpp_dep = dependency( required: true, version: '>=4.0.0', ) -core_lib += {'deps': [core_lib.get('deps'), utf8cpp_dep]} +graphics_lib += {'deps': [graphics_lib.get('deps'), utf8cpp_dep]} + +icu_dep = dependency( + 'icu-uc', + required: false, +) + +if icu_dep.found() + graphics_lib += { + 'compile_args': [ + graphics_lib.get('compile_args'), + '-D_HAVE_ICU_DEP', + ], + 'deps': [graphics_lib.get('deps'), icu_dep], + } +endif is_flatpak_build = false @@ -265,6 +280,16 @@ if build_application endif + if host_machine.system() == 'linux' or (meson.is_cross_build() and host_machine.system() == 'android') + keyutils_dep = dependency('keyutils', required: true, allow_fallback: true) + graphics_lib += {'deps': [graphics_lib.get('deps'), keyutils_dep]} + elif host_machine.system() == 'windows' + c = meson.get_compiler('c') + + ncrypt_dep = c.find_library('ncrypt') + graphics_lib += {'deps': [graphics_lib.get('deps'), ncrypt_dep]} + endif + build_installer = get_option('build_installer') is_flatpak_build = false diff --git a/tools/install/meson.build b/tools/install/meson.build index 3ed51689..b141b05e 100644 --- a/tools/install/meson.build +++ b/tools/install/meson.build @@ -10,11 +10,11 @@ if build_application install_tag: 'assets', exclude_files: [ 'oopetris.desktop.in', - flatpak_app_name+'.metainfo.xml.in', + flatpak_app_name + '.metainfo.xml.in', 'OOPetris.svg', 'recordings.magic', ], - exclude_directories: ['icon'], + exclude_directories: ['icon', 'schema'], ) app_name = 'oopetris' @@ -89,7 +89,7 @@ if build_application flaptak_conf.set('APP_VERSION', meson.project_version()) metainfo_file = configure_file( - input: meson.project_source_root() / 'assets' / (flatpak_app_name+'.metainfo.xml.in'), + input: meson.project_source_root() / 'assets' / (flatpak_app_name + '.metainfo.xml.in'), output: app_name + '.metainfo.xml', configuration: flaptak_conf, install: true,