diff --git a/lib/libimhex/include/hex/api/achievement_manager.hpp b/lib/libimhex/include/hex/api/achievement_manager.hpp index ff32cda9c..8847cfd12 100644 --- a/lib/libimhex/include/hex/api/achievement_manager.hpp +++ b/lib/libimhex/include/hex/api/achievement_manager.hpp @@ -20,68 +20,127 @@ namespace hex { public: explicit Achievement(std::string unlocalizedCategory, std::string unlocalizedName) : m_unlocalizedCategory(std::move(unlocalizedCategory)), m_unlocalizedName(std::move(unlocalizedName)) { } + /** + * @brief Returns the unlocalized name of the achievement + * @return Unlocalized name of the achievement + */ [[nodiscard]] const std::string &getUnlocalizedName() const { return this->m_unlocalizedName; } + /** + * @brief Returns the unlocalized category of the achievement + * @return Unlocalized category of the achievement + */ [[nodiscard]] const std::string &getUnlocalizedCategory() const { return this->m_unlocalizedCategory; } + /** + * @brief Returns whether the achievement is unlocked + * @return Whether the achievement is unlocked + */ [[nodiscard]] bool isUnlocked() const { return this->m_progress == this->m_maxProgress; } + /** + * @brief Sets the description of the achievement + * @param description Description of the achievement + * @return Reference to the achievement + */ Achievement& setDescription(std::string description) { this->m_unlocalizedDescription = std::move(description); return *this; } + /** + * @brief Adds a requirement to the achievement. The achievement will only be unlockable if all requirements are unlocked. + * @param requirement Unlocalized name of the requirement + * @return Reference to the achievement + */ Achievement& addRequirement(std::string requirement) { this->m_requirements.emplace_back(std::move(requirement)); return *this; } + /** + * @brief Adds a visibility requirement to the achievement. The achievement will only be visible if all requirements are unlocked. + * @param requirement Unlocalized name of the requirement + * @return Reference to the achievement + */ Achievement& addVisibilityRequirement(std::string requirement) { this->m_visibilityRequirements.emplace_back(std::move(requirement)); return *this; } + /** + * @brief Marks the achievement as blacked. Blacked achievements are visible but their name and description are hidden. + * @return Reference to the achievement + */ Achievement& setBlacked() { this->m_blacked = true; return *this; } + /** + * @brief Marks the achievement as invisible. Invisible achievements are not visible at all. + * @return Reference to the achievement + */ Achievement& setInvisible() { this->m_invisible = true; return *this; } + /** + * @brief Returns whether the achievement is blacked + * @return Whether the achievement is blacked + */ [[nodiscard]] bool isBlacked() const { return this->m_blacked; } + /** + * @brief Returns whether the achievement is invisible + * @return Whether the achievement is invisible + */ [[nodiscard]] bool isInvisible() const { return this->m_invisible; } + /** + * @brief Returns the list of requirements of the achievement + * @return List of requirements of the achievement + */ [[nodiscard]] const std::vector &getRequirements() const { return this->m_requirements; } + /** + * @brief Returns the list of visibility requirements of the achievement + * @return List of visibility requirements of the achievement + */ [[nodiscard]] const std::vector &getVisibilityRequirements() const { return this->m_visibilityRequirements; } + /** + * @brief Returns the unlocalized description of the achievement + * @return Unlocalized description of the achievement + */ [[nodiscard]] const std::string &getUnlocalizedDescription() const { return this->m_unlocalizedDescription; } + /** + * @brief Returns the icon of the achievement + * @return Icon of the achievement + */ [[nodiscard]] const ImGui::Texture &getIcon() const { if (this->m_iconData.empty()) return this->m_icon; @@ -94,6 +153,11 @@ namespace hex { return this->m_icon; } + /** + * @brief Sets the icon of the achievement + * @param data Icon data + * @return Reference to the achievement + */ Achievement& setIcon(std::span data) { this->m_iconData.reserve(data.size()); for (auto &byte : data) @@ -102,19 +166,34 @@ namespace hex { return *this; } + /** + * @brief Sets the icon of the achievement + * @param data Icon data + * @return Reference to the achievement + */ Achievement& setIcon(std::span data) { this->m_iconData.assign(data.begin(), data.end()); return *this; } + /** + * @brief Sets the icon of the achievement + * @param data Icon data + * @return Reference to the achievement + */ Achievement& setIcon(std::vector data) { this->m_iconData = std::move(data); return *this; } - Achievement& setIcon(std::vector data) { + /** + * @brief Sets the icon of the achievement + * @param data Icon data + * @return Reference to the achievement + */ + Achievement& setIcon(const std::vector &data) { this->m_iconData.reserve(data.size()); for (auto &byte : data) this->m_iconData.emplace_back(static_cast(byte)); @@ -122,32 +201,61 @@ namespace hex { return *this; } + /** + * @brief Specifies the required progress to unlock the achievement. This is the number of times this achievement has to be triggered to unlock it. The default is 1. + * @param progress Required progress + * @return Reference to the achievement + */ Achievement& setRequiredProgress(u32 progress) { this->m_maxProgress = progress; return *this; } + /** + * @brief Returns the required progress to unlock the achievement + * @return Required progress to unlock the achievement + */ [[nodiscard]] u32 getRequiredProgress() const { return this->m_maxProgress; } + /** + * @brief Returns the current progress of the achievement + * @return Current progress of the achievement + */ [[nodiscard]] u32 getProgress() const { return this->m_progress; } + /** + * @brief Sets the callback to call when the achievement is clicked + * @param callback Callback to call when the achievement is clicked + */ void setClickCallback(const std::function &callback) { this->m_clickCallback = callback; } + /** + * @brief Returns the callback to call when the achievement is clicked + * @return Callback to call when the achievement is clicked + */ [[nodiscard]] const std::function &getClickCallback() const { return this->m_clickCallback; } + /** + * @brief Returns whether the achievement is temporary. Temporary achievements have been added by challenge projects for example and will be removed when the project is closed. + * @return Whether the achievement is temporary + */ [[nodiscard]] bool isTemporary() const { return this->m_temporary; } + /** + * @brief Sets whether the achievement is unlocked + * @param unlocked Whether the achievement is unlocked + */ void setUnlocked(bool unlocked) { if (unlocked) { if (this->m_progress < this->m_maxProgress) @@ -210,6 +318,12 @@ namespace hex { } }; + /** + * @brief Adds a new achievement + * @tparam T Type of the achievement + * @param args Arguments to pass to the constructor of the achievement + * @return Reference to the achievement + */ template T = Achievement> static Achievement& addAchievement(auto && ... args) { auto newAchievement = std::make_unique(std::forward(args)...); @@ -228,6 +342,12 @@ namespace hex { return *achievement; } + /** + * @brief Adds a new temporary achievement + * @tparam T Type of the achievement + * @param args Arguments to pass to the constructor of the achievement + * @return Reference to the achievement + */ template T = Achievement> static Achievement& addTemporaryAchievement(auto && ... args) { auto &achievement = addAchievement(std::forward(args)...); @@ -237,17 +357,52 @@ namespace hex { return achievement; } + /** + * @brief Unlocks an achievement + * @param unlocalizedCategory Unlocalized category of the achievement + * @param unlocalizedName Unlocalized name of the achievement + */ static void unlockAchievement(const std::string &unlocalizedCategory, const std::string &unlocalizedName); + /** + * @brief Returns all registered achievements + * @return All achievements + */ static std::unordered_map>>& getAchievements(); + /** + * @brief Returns all achievement start nodes + * @note Start nodes are all nodes that don't have any parents + * @param rebuild Whether to rebuild the list of start nodes + * @return All achievement start nodes + */ static std::unordered_map>& getAchievementStartNodes(bool rebuild = true); + + /** + * @brief Returns all achievement nodes + * @param rebuild Whether to rebuild the list of nodes + * @return All achievement nodes + */ static std::unordered_map>& getAchievementNodes(bool rebuild = true); + /** + * @brief Loads the progress of all achievements from the achievements save file + */ static void loadProgress(); + + /** + * @brief Stores the progress of all achievements to the achievements save file + */ static void storeProgress(); + /** + * @brief Removes all registered achievements from the tree + */ static void clear(); + + /** + * @brief Removes all temporary achievements from the tree + */ static void clearTemporary(); private: diff --git a/lib/libimhex/include/hex/api/event.hpp b/lib/libimhex/include/hex/api/event.hpp index 5aa027e9e..99511cddf 100644 --- a/lib/libimhex/include/hex/api/event.hpp +++ b/lib/libimhex/include/hex/api/event.hpp @@ -43,7 +43,9 @@ namespace hex { } } - constexpr bool operator==(const EventId &rhs) const = default; + constexpr bool operator==(const EventId &other) const { + return this->m_hash == other.m_hash; + } private: u32 m_hash; @@ -67,6 +69,9 @@ namespace hex { Callback m_func; }; + template + concept EventType = std::derived_from; + } @@ -84,7 +89,7 @@ namespace hex { * @param function Function to call when the event is posted * @return Token to unsubscribe from the event */ - template + template static EventList::iterator subscribe(typename E::Callback function) { auto &events = getEvents(); return events.insert(events.end(), std::make_pair(E::Id, std::make_unique(function))); @@ -96,7 +101,7 @@ namespace hex { * @param token Unique token to register the event to. Later required to unsubscribe again * @param function Function to call when the event is posted */ - template + template static void subscribe(void *token, typename E::Callback function) { getTokenStore().insert(std::make_pair(token, subscribe(function))); } @@ -114,7 +119,7 @@ namespace hex { * @tparam E Event * @param token Token passed to subscribe */ - template + template static void unsubscribe(void *token) noexcept { auto &tokenStore = getTokenStore(); auto iter = std::find_if(tokenStore.begin(), tokenStore.end(), [&](auto &item) { @@ -133,7 +138,7 @@ namespace hex { * @tparam E Event * @param args Arguments to pass to the event */ - template + template static void post(auto &&...args) noexcept { for (const auto &[id, event] : getEvents()) { if (id == E::Id) { diff --git a/lib/libimhex/include/hex/api/layout_manager.hpp b/lib/libimhex/include/hex/api/layout_manager.hpp index 3f5a07228..fd5c01914 100644 --- a/lib/libimhex/include/hex/api/layout_manager.hpp +++ b/lib/libimhex/include/hex/api/layout_manager.hpp @@ -18,14 +18,44 @@ namespace hex { std::fs::path path; }; + /** + * @brief Save the current layout + * @param name Name of the layout + */ static void save(const std::string &name); + + /** + * @brief Load a layout from a file + * @param path Path to the layout file + */ static void load(const std::fs::path &path); + + /** + * @brief Load a layout from a string + * @param content Layout string + */ static void loadString(const std::string &content); + /** + * @brief Get a list of all layouts + * @return List of all added layouts + */ static std::vector getLayouts(); + /** + * @brief Handles loading of layouts if needed + * @note This function should only be called by ImHex + */ static void process(); + + /** + * @brief Reload all layouts + */ static void reload(); + + /** + * @brief Reset the layout manager + */ static void reset(); private: diff --git a/lib/libimhex/include/hex/api/task.hpp b/lib/libimhex/include/hex/api/task.hpp index eb8d6c42d..a5fc2eca5 100644 --- a/lib/libimhex/include/hex/api/task.hpp +++ b/lib/libimhex/include/hex/api/task.hpp @@ -115,6 +115,11 @@ namespace hex { std::weak_ptr m_task; }; + struct Timer { + std::chrono::time_point elapseTime; + std::function callback; + }; + /** * @brief The Task Manager is responsible for running and managing asynchronous tasks */ @@ -144,7 +149,6 @@ namespace hex { */ static TaskHolder createBackgroundTask(std::string name, std::function function); - /** * @brief Creates a new synchronous task that will execute the given function at the start of the next frame * @param function Function to be executed @@ -157,6 +161,12 @@ namespace hex { */ static void runWhenTasksFinished(const std::function &function); + /** + * @brief Creates a callback that will be executed after the given time + * @param duration Time to wait + * @param function Function to be executed + */ + static void doAfter(std::chrono::duration duration, const std::function &function); static void collectGarbage(); @@ -164,6 +174,7 @@ namespace hex { static size_t getRunningBackgroundTaskCount(); static std::list> &getRunningTasks(); + static std::list &getTimers(); static void runDeferredCalls(); private: diff --git a/lib/libimhex/include/hex/ui/imgui_imhex_extensions.h b/lib/libimhex/include/hex/ui/imgui_imhex_extensions.h index c871cd210..7c052021f 100644 --- a/lib/libimhex/include/hex/ui/imgui_imhex_extensions.h +++ b/lib/libimhex/include/hex/ui/imgui_imhex_extensions.h @@ -48,6 +48,7 @@ namespace ImGui { public: Texture() = default; Texture(const ImU8 *buffer, int size, int width = 0, int height = 0); + Texture(std::span bytes, int width = 0, int height = 0); explicit Texture(const char *path); Texture(unsigned int texture, int width, int height); Texture(const Texture&) = delete; diff --git a/lib/libimhex/source/api/task.cpp b/lib/libimhex/source/api/task.cpp index d970b992f..14cc59149 100644 --- a/lib/libimhex/source/api/task.cpp +++ b/lib/libimhex/source/api/task.cpp @@ -19,6 +19,7 @@ namespace hex { std::mutex s_deferredCallsMutex, s_tasksFinishedMutex; std::list> s_tasks, s_taskQueue; + std::list s_timers; std::list> s_deferredCalls; std::list> s_tasksFinishedCallbacks; @@ -300,12 +301,17 @@ namespace hex { call(); s_tasksFinishedCallbacks.clear(); } + } std::list> &TaskManager::getRunningTasks() { return s_tasks; } + std::list &TaskManager::getTimers() { + return s_timers; + } + size_t TaskManager::getRunningTaskCount() { std::unique_lock lock(s_queueMutex); @@ -336,6 +342,12 @@ namespace hex { call(); s_deferredCalls.clear(); + + for (const auto &timer : s_timers) { + if (timer.elapseTime >= std::chrono::steady_clock::now()) { + timer.callback(); + } + } } void TaskManager::runWhenTasksFinished(const std::function &function) { @@ -344,4 +356,8 @@ namespace hex { s_tasksFinishedCallbacks.push_back(function); } + void TaskManager::doAfter(std::chrono::duration duration, const std::function &function) { + s_timers.push_back({ std::chrono::steady_clock::now() + duration, function }); + } + } diff --git a/lib/libimhex/source/ui/imgui_imhex_extensions.cpp b/lib/libimhex/source/ui/imgui_imhex_extensions.cpp index a7a414525..014c35a24 100644 --- a/lib/libimhex/source/ui/imgui_imhex_extensions.cpp +++ b/lib/libimhex/source/ui/imgui_imhex_extensions.cpp @@ -48,6 +48,8 @@ namespace ImGui { this->m_textureId = reinterpret_cast(static_cast(texture)); } + Texture::Texture(std::span bytes, int width, int height) : Texture(reinterpret_cast(bytes.data()), bytes.size(), width, height) { } + Texture::Texture(const char *path) { unsigned char *imageData = stbi_load(path, &this->m_width, &this->m_height, nullptr, 4); if (imageData == nullptr) diff --git a/plugins/builtin/source/content/views/view_about.cpp b/plugins/builtin/source/content/views/view_about.cpp index 4ba28c9d4..59a919935 100644 --- a/plugins/builtin/source/content/views/view_about.cpp +++ b/plugins/builtin/source/content/views/view_about.cpp @@ -15,6 +15,7 @@ namespace hex::plugin::builtin { ViewAbout::ViewAbout() : View("hex.builtin.view.help.about.name") { + // Add "About" menu item to the help menu ContentRegistry::Interface::addMenuItem({ "hex.builtin.menu.help", "hex.builtin.view.help.about.name" }, 1000, Shortcut::None, [this] { TaskManager::doLater([] { ImGui::OpenPopup(View::toWindowName("hex.builtin.view.help.about.name").c_str()); }); this->m_aboutWindowOpen = true; @@ -23,6 +24,7 @@ namespace hex::plugin::builtin { ContentRegistry::Interface::addMenuItemSeparator({ "hex.builtin.menu.help" }, 2000); + // Add documentation links to the help menu ContentRegistry::Interface::addMenuItem({ "hex.builtin.menu.help", "hex.builtin.view.help.documentation" }, 3000, Shortcut::None, [] { hex::openWebpage("https://docs.werwolv.net/imhex"); AchievementManager::unlockAchievement("hex.builtin.achievement.starting_out", "hex.builtin.achievement.starting_out.docs.name"); @@ -31,18 +33,23 @@ namespace hex::plugin::builtin { ContentRegistry::Interface::addMenuItem({ "hex.builtin.menu.help", "hex.builtin.menu.help.ask_for_help" }, 4000, CTRLCMD + SHIFT + Keys::D, [] { PopupDocsQuestion::open(); }); + + this->m_logoTexture = ImGui::Texture(romfs::get("assets/common/logo.png").span()); } static void link(const std::string &name, const std::string &author, const std::string &url) { + // Draw the hyperlink and open the URL if clicked if (ImGui::BulletHyperlink(name.c_str())) hex::openWebpage(url); + // Show the URL as a tooltip if (ImGui::IsItemHovered()) { ImGui::BeginTooltip(); ImGui::TextFormatted("{}", url); ImGui::EndTooltip(); } + // Show the author if there is one if (!author.empty()) { ImGui::SameLine(0, 0); ImGui::TextFormatted("by {}", author); @@ -50,28 +57,33 @@ namespace hex::plugin::builtin { } void ViewAbout::drawAboutMainPage() { + // Draw main about table if (ImGui::BeginTable("about_table", 2, ImGuiTableFlags_SizingFixedFit)) { ImGui::TableNextRow(); ImGui::TableNextColumn(); - if (!this->m_logoTexture.isValid()) { - auto logo = romfs::get("assets/common/logo.png"); - this->m_logoTexture = ImGui::Texture(reinterpret_cast(logo.data()), logo.size()); - } - + // Draw the ImHex icon ImGui::Image(this->m_logoTexture, scaled({ 64, 64 })); ImGui::TableNextColumn(); + // Draw basic information about ImHex and its version ImGui::TextFormatted("ImHex Hex Editor v{} by WerWolv - " ICON_FA_CODE_BRANCH, ImHexApi::System::getImHexVersion()); ImGui::SameLine(); + + // Draw clickable link to the current commit if (ImGui::Hyperlink(hex::format("{0}@{1}", ImHexApi::System::getCommitBranch(), ImHexApi::System::getCommitHash()).c_str())) hex::openWebpage("https://github.com/WerWolv/ImHex/commit/" + ImHexApi::System::getCommitHash(true)); + // Draw the author of the current translation ImGui::TextUnformatted("hex.builtin.view.help.about.translator"_lang); + // Draw information about the open-source nature of ImHex ImGui::TextUnformatted("hex.builtin.view.help.about.source"_lang); + ImGui::SameLine(); + + //Draw clickable link to the GitHub repository if (ImGui::Hyperlink("WerWolv/ImHex")) hex::openWebpage("https://github.com/WerWolv/ImHex"); @@ -80,6 +92,7 @@ namespace hex::plugin::builtin { ImGui::NewLine(); + // Draw donation links ImGui::TextUnformatted("hex.builtin.view.help.about.donations"_lang); ImGui::Separator(); @@ -99,6 +112,13 @@ namespace hex::plugin::builtin { ImGui::TextFormattedWrapped("These amazing people have contributed to ImHex in the past. If you'd like to become part of them, please submit a PR to the GitHub Repository!"); ImGui::NewLine(); + // Draw main ImHex contributors + link("iTrooz for a huge amount of help maintaining ImHex and the CI", "", "https://github.com/iTrooz"); + link("jumanji144 for a ton of help with the Pattern Language, API and usage stats", "", "https://github.com/Nowilltolife"); + + ImGui::NewLine(); + + // Draw additional contributors link("Mary for porting ImHex to MacOS", "", "https://github.com/Thog"); link("Roblabla for adding the MSI Windows installer", "", "https://github.com/roblabla"); link("jam1garner for adding support for Rust plugins", "", "https://github.com/jam1garner"); @@ -111,6 +131,7 @@ namespace hex::plugin::builtin { void ViewAbout::drawLibraryCreditsPage() { ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.2F, 0.2F, 0.2F, 0.3F)); + // Draw ImGui dependencies link("ImGui", "ocornut", "https://github.com/ocornut/imgui/"); link("imgui_club", "ocornut", "https://github.com/ocornut/imgui_club/"); link("imnodes", "Nelarius", "https://github.com/Nelarius/imnodes/"); @@ -119,6 +140,7 @@ namespace hex::plugin::builtin { ImGui::NewLine(); + // Draw dependencies maintained by individual people link("capstone", "aquynh", "https://github.com/aquynh/capstone/"); link("JSON for Modern C++", "nlohmann", "https://github.com/nlohmann/json/"); link("YARA", "VirusTotal", "https://github.com/VirusTotal/yara/"); @@ -127,12 +149,13 @@ namespace hex::plugin::builtin { link("microtar", "rxi", "https://github.com/rxi/microtar/"); link("xdgpp", "danyspin97", "https://sr.ht/~danyspin97/xdgpp/"); link("FreeType", "David Turner", "https://gitlab.freedesktop.org/freetype/freetype/"); - link("Mbed TLS", "ARM", "https://github.com/ARMmbed/mbedtls/"); + link("mbedTLS", "ARM", "https://github.com/ARMmbed/mbedtls/"); link("libcurl", "Daniel Stenberg", "https://curl.se/"); link("libfmt", "vitaut", "https://fmt.dev/"); ImGui::NewLine(); + // Draw dependencies maintained by groups link("GNU libmagic", "", "https://www.darwinsys.com/file/"); link("GLFW3", "", "https://github.com/glfw/glfw/"); link("LLVM", "", "https://github.com/llvm/llvm-project/"); @@ -148,7 +171,8 @@ namespace hex::plugin::builtin { ImGui::TableSetupColumn("Type"); ImGui::TableSetupColumn("Paths"); - constexpr static std::array, u32(fs::ImHexPath::END)-1> PathTypes = { + // Specify the types of paths to display + constexpr static std::array, size_t(fs::ImHexPath::END) - 1U> PathTypes = { { { "Patterns", fs::ImHexPath::Patterns }, { "Patterns Includes", fs::ImHexPath::PatternsInclude }, @@ -169,6 +193,7 @@ namespace hex::plugin::builtin { } }; + // Draw the table ImGui::TableHeadersRow(); for (const auto &[name, type] : PathTypes) { ImGui::TableNextRow(); @@ -177,6 +202,7 @@ namespace hex::plugin::builtin { ImGui::TableNextColumn(); for (auto &path : fs::getDefaultPaths(type, true)){ + // Draw hyperlink to paths that exist or red text if they don't if (wolv::io::fs::isDirectory(path)){ if (ImGui::Hyperlink(wolv::util::toUTF8String(path).c_str())) { fs::openFolderExternal(path); @@ -198,11 +224,13 @@ namespace hex::plugin::builtin { void ViewAbout::drawAboutPopup() { if (ImGui::BeginPopupModal(View::toWindowName("hex.builtin.view.help.about.name").c_str(), &this->m_aboutWindowOpen)) { + // Allow window to be closed by pressing ESC if (ImGui::IsKeyDown(ImGui::GetKeyIndex(ImGuiKey_Escape))) ImGui::CloseCurrentPopup(); if (ImGui::BeginTabBar("about_tab_bar")) { + // Draw main ImHex tab if (ImGui::BeginTabItem("ImHex")) { if (ImGui::BeginChild(1)) { this->drawAboutMainPage(); @@ -211,6 +239,7 @@ namespace hex::plugin::builtin { ImGui::EndTabItem(); } + // Draw contributors tab if (ImGui::BeginTabItem("hex.builtin.view.help.about.contributor"_lang)) { ImGui::NewLine(); if (ImGui::BeginChild(1)) { @@ -220,6 +249,7 @@ namespace hex::plugin::builtin { ImGui::EndTabItem(); } + // Draw libraries tab if (ImGui::BeginTabItem("hex.builtin.view.help.about.libs"_lang)) { ImGui::NewLine(); if (ImGui::BeginChild(1)) { @@ -229,6 +259,7 @@ namespace hex::plugin::builtin { ImGui::EndTabItem(); } + // Draw paths tab if (ImGui::BeginTabItem("hex.builtin.view.help.about.paths"_lang)) { ImGui::NewLine(); if (ImGui::BeginChild(1)) { @@ -238,6 +269,7 @@ namespace hex::plugin::builtin { ImGui::EndTabItem(); } + // Draw license tab if (ImGui::BeginTabItem("hex.builtin.view.help.about.license"_lang)) { ImGui::NewLine(); if (ImGui::BeginChild(1)) { diff --git a/plugins/builtin/source/content/views/view_achievements.cpp b/plugins/builtin/source/content/views/view_achievements.cpp index 9f111535e..1e7592af4 100644 --- a/plugins/builtin/source/content/views/view_achievements.cpp +++ b/plugins/builtin/source/content/views/view_achievements.cpp @@ -1,8 +1,7 @@ #include "content/views/view_achievements.hpp" #include - -#include +#include #include