diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 96f8a5384..5210cef40 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -614,7 +614,7 @@ jobs: # Fedora cmake build (in imhex.spec) - name: 📦 Build RPM run: | - fedpkg --path $GITHUB_WORKSPACE --release ${{ matrix.mock_release }} mockbuild --enable-network -N --root $GITHUB_WORKSPACE/mock.cfg + fedpkg --path $GITHUB_WORKSPACE --release ${{ matrix.mock_release }} mockbuild --enable-network -N --root $GITHUB_WORKSPACE/mock.cfg extra_args -- -v - name: 🟩 Move and rename finished RPM run: | diff --git a/lib/libimhex/CMakeLists.txt b/lib/libimhex/CMakeLists.txt index 5a4a9ecbb..3b0f915f5 100644 --- a/lib/libimhex/CMakeLists.txt +++ b/lib/libimhex/CMakeLists.txt @@ -15,6 +15,7 @@ set(LIBIMHEX_SOURCES source/api/project_file_manager.cpp source/api/theme_manager.cpp source/api/layout_manager.cpp + source/api/achievement_manager.cpp source/data_processor/attribute.cpp source/data_processor/link.cpp diff --git a/lib/libimhex/include/hex/api/achievement_manager.hpp b/lib/libimhex/include/hex/api/achievement_manager.hpp new file mode 100644 index 000000000..ff32cda9c --- /dev/null +++ b/lib/libimhex/include/hex/api/achievement_manager.hpp @@ -0,0 +1,257 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace hex { + + class AchievementManager; + + class Achievement { + public: + explicit Achievement(std::string unlocalizedCategory, std::string unlocalizedName) : m_unlocalizedCategory(std::move(unlocalizedCategory)), m_unlocalizedName(std::move(unlocalizedName)) { } + + [[nodiscard]] const std::string &getUnlocalizedName() const { + return this->m_unlocalizedName; + } + + [[nodiscard]] const std::string &getUnlocalizedCategory() const { + return this->m_unlocalizedCategory; + } + + [[nodiscard]] bool isUnlocked() const { + return this->m_progress == this->m_maxProgress; + } + + Achievement& setDescription(std::string description) { + this->m_unlocalizedDescription = std::move(description); + + return *this; + } + + Achievement& addRequirement(std::string requirement) { + this->m_requirements.emplace_back(std::move(requirement)); + + return *this; + } + + Achievement& addVisibilityRequirement(std::string requirement) { + this->m_visibilityRequirements.emplace_back(std::move(requirement)); + + return *this; + } + + Achievement& setBlacked() { + this->m_blacked = true; + + return *this; + } + + Achievement& setInvisible() { + this->m_invisible = true; + + return *this; + } + + [[nodiscard]] bool isBlacked() const { + return this->m_blacked; + } + + [[nodiscard]] bool isInvisible() const { + return this->m_invisible; + } + + [[nodiscard]] const std::vector &getRequirements() const { + return this->m_requirements; + } + + [[nodiscard]] const std::vector &getVisibilityRequirements() const { + return this->m_visibilityRequirements; + } + + [[nodiscard]] const std::string &getUnlocalizedDescription() const { + return this->m_unlocalizedDescription; + } + + [[nodiscard]] const ImGui::Texture &getIcon() const { + if (this->m_iconData.empty()) + return this->m_icon; + + if (this->m_icon.isValid()) + return m_icon; + + this->m_icon = ImGui::Texture(reinterpret_cast(this->m_iconData.data()), this->m_iconData.size()); + + return this->m_icon; + } + + Achievement& setIcon(std::span data) { + this->m_iconData.reserve(data.size()); + for (auto &byte : data) + this->m_iconData.emplace_back(static_cast(byte)); + + return *this; + } + + Achievement& setIcon(std::span data) { + this->m_iconData.assign(data.begin(), data.end()); + + return *this; + } + + Achievement& setIcon(std::vector data) { + this->m_iconData = std::move(data); + + return *this; + } + + Achievement& setIcon(std::vector data) { + this->m_iconData.reserve(data.size()); + for (auto &byte : data) + this->m_iconData.emplace_back(static_cast(byte)); + + return *this; + } + + Achievement& setRequiredProgress(u32 progress) { + this->m_maxProgress = progress; + + return *this; + } + + [[nodiscard]] u32 getRequiredProgress() const { + return this->m_maxProgress; + } + + [[nodiscard]] u32 getProgress() const { + return this->m_progress; + } + + void setClickCallback(const std::function &callback) { + this->m_clickCallback = callback; + } + + [[nodiscard]] const std::function &getClickCallback() const { + return this->m_clickCallback; + } + + [[nodiscard]] bool isTemporary() const { + return this->m_temporary; + } + + void setUnlocked(bool unlocked) { + if (unlocked) { + if (this->m_progress < this->m_maxProgress) + this->m_progress++; + } else { + this->m_progress = 0; + } + } + + protected: + void setProgress(u32 progress) { + this->m_progress = progress; + } + + private: + std::string m_unlocalizedCategory, m_unlocalizedName; + std::string m_unlocalizedDescription; + + bool m_blacked = false; + bool m_invisible = false; + std::vector m_requirements, m_visibilityRequirements; + + std::function m_clickCallback; + + std::vector m_iconData; + mutable ImGui::Texture m_icon; + + u32 m_progress = 0; + u32 m_maxProgress = 1; + + bool m_temporary = false; + + friend class AchievementManager; + }; + + class AchievementManager { + public: + AchievementManager() = delete; + + struct AchievementNode { + Achievement *achievement; + std::vector children, parents; + std::vector visibilityParents; + ImVec2 position; + + [[nodiscard]] bool hasParents() const { + return !this->parents.empty(); + } + + [[nodiscard]] bool isUnlockable() const { + return std::all_of(this->parents.begin(), this->parents.end(), [](auto &parent) { return parent->achievement->isUnlocked(); }); + } + + [[nodiscard]] bool isVisible() const { + return std::all_of(this->visibilityParents.begin(), this->visibilityParents.end(), [](auto &parent) { return parent->achievement->isUnlocked(); }); + } + + [[nodiscard]] bool isUnlocked() const { + return this->achievement->isUnlocked(); + } + }; + + template T = Achievement> + static Achievement& addAchievement(auto && ... args) { + auto newAchievement = std::make_unique(std::forward(args)...); + + const auto &category = newAchievement->getUnlocalizedCategory(); + const auto &name = newAchievement->getUnlocalizedName(); + + auto [categoryIter, categoryInserted] = getAchievements().insert({ category, std::unordered_map>{} }); + auto &[categoryKey, achievements] = *categoryIter; + + auto [achievementIter, achievementInserted] = achievements.insert({ name, std::move(newAchievement) }); + auto &[achievementKey, achievement] = *achievementIter; + + achievementAdded(); + + return *achievement; + } + + template T = Achievement> + static Achievement& addTemporaryAchievement(auto && ... args) { + auto &achievement = addAchievement(std::forward(args)...); + + achievement.m_temporary = true; + + return achievement; + } + + static void unlockAchievement(const std::string &unlocalizedCategory, const std::string &unlocalizedName); + + static std::unordered_map>>& getAchievements(); + + static std::unordered_map>& getAchievementStartNodes(bool rebuild = true); + static std::unordered_map>& getAchievementNodes(bool rebuild = true); + + static void loadProgress(); + static void storeProgress(); + + static void clear(); + static void clearTemporary(); + + private: + static void achievementAdded(); + }; + +} \ No newline at end of file diff --git a/lib/libimhex/include/hex/api/event.hpp b/lib/libimhex/include/hex/api/event.hpp index decee0cc6..24237c815 100644 --- a/lib/libimhex/include/hex/api/event.hpp +++ b/lib/libimhex/include/hex/api/event.hpp @@ -23,7 +23,11 @@ #define EVENT_DEF(event_name, ...) EVENT_DEF_IMPL(event_name, #event_name, true, __VA_ARGS__) #define EVENT_DEF_NO_LOG(event_name, ...) EVENT_DEF_IMPL(event_name, #event_name, false, __VA_ARGS__) + +/* Forward declarations */ struct GLFWwindow; +namespace hex { class Achievement; } + namespace hex { @@ -202,6 +206,7 @@ namespace hex { EVENT_DEF(EventStoreContentDownloaded, const std::fs::path&); EVENT_DEF(EventStoreContentRemoved, const std::fs::path&); EVENT_DEF(EventImHexClosing); + EVENT_DEF(EventAchievementUnlocked, const Achievement&); /** * @brief Called when a project has been loaded diff --git a/lib/libimhex/source/api/achievement_manager.cpp b/lib/libimhex/source/api/achievement_manager.cpp new file mode 100644 index 000000000..92b179d95 --- /dev/null +++ b/lib/libimhex/source/api/achievement_manager.cpp @@ -0,0 +1,223 @@ +#include + +#include + +namespace hex { + + std::unordered_map>> &AchievementManager::getAchievements() { + static std::unordered_map>> achievements; + + return achievements; + } + + std::unordered_map>& AchievementManager::getAchievementNodes(bool rebuild) { + static std::unordered_map> nodeCategoryStorage; + + if (!nodeCategoryStorage.empty() || !rebuild) + return nodeCategoryStorage; + + nodeCategoryStorage.clear(); + + // Add all achievements to the node storage + for (auto &[categoryName, achievements] : getAchievements()) { + auto &nodes = nodeCategoryStorage[categoryName]; + + for (auto &[achievementName, achievement] : achievements) { + nodes.emplace_back(achievement.get()); + } + } + + return nodeCategoryStorage; + } + + std::unordered_map>& AchievementManager::getAchievementStartNodes(bool rebuild) { + static std::unordered_map> startNodes; + + if (!startNodes.empty() || !rebuild) + return startNodes; + + auto &nodeCategoryStorage = getAchievementNodes(); + + startNodes.clear(); + + // Add all parents and children to the nodes + for (auto &[categoryName, achievements] : nodeCategoryStorage) { + for (auto &achievementNode : achievements) { + for (auto &requirement : achievementNode.achievement->getRequirements()) { + for (auto &[requirementCategoryName, requirementAchievements] : nodeCategoryStorage) { + auto iter = std::find_if(requirementAchievements.begin(), requirementAchievements.end(), [&requirement](auto &node) { + return node.achievement->getUnlocalizedName() == requirement; + }); + + if (iter != requirementAchievements.end()) { + achievementNode.parents.emplace_back(&*iter); + iter->children.emplace_back(&achievementNode); + } + } + } + + for (auto &requirement : achievementNode.achievement->getVisibilityRequirements()) { + for (auto &[requirementCategoryName, requirementAchievements] : nodeCategoryStorage) { + auto iter = std::find_if(requirementAchievements.begin(), requirementAchievements.end(), [&requirement](auto &node) { + return node.achievement->getUnlocalizedName() == requirement; + }); + + if (iter != requirementAchievements.end()) { + achievementNode.visibilityParents.emplace_back(&*iter); + } + } + } + } + } + + for (auto &[categoryName, achievements] : nodeCategoryStorage) { + for (auto &achievementNode : achievements) { + if (!achievementNode.hasParents()) { + startNodes[categoryName].emplace_back(&achievementNode); + } + + for (const auto &parent : achievementNode.parents) { + if (parent->achievement->getUnlocalizedCategory() != achievementNode.achievement->getUnlocalizedCategory()) + startNodes[categoryName].emplace_back(&achievementNode); + } + } + } + + return startNodes; + } + + void AchievementManager::unlockAchievement(const std::string &unlocalizedCategory, const std::string &unlocalizedName) { + auto &categories = getAchievements(); + + auto categoryIter = categories.find(unlocalizedCategory); + if (categoryIter == categories.end()) { + return; + } + + auto &[categoryName, achievements] = *categoryIter; + + auto achievementIter = achievements.find(unlocalizedName); + + if (achievementIter == achievements.end()) { + return; + } + + auto &nodes = getAchievementNodes()[categoryName]; + + for (const auto &node : nodes) { + auto &achievement = node.achievement; + + if (achievement->getUnlocalizedCategory() != unlocalizedCategory) { + continue; + } + if (achievement->getUnlocalizedName() != unlocalizedName) { + continue; + } + + if (node.achievement->isUnlocked()) { + return; + } + + for (const auto &requirement : node.parents) { + if (!requirement->achievement->isUnlocked()) { + return; + } + } + + achievement->setUnlocked(true); + + if (achievement->isUnlocked()) + EventManager::post(*achievement); + } + } + + void AchievementManager::clear() { + getAchievements().clear(); + getAchievementStartNodes(false).clear(); + getAchievementNodes(false).clear(); + } + + void AchievementManager::clearTemporary() { + auto &categories = getAchievements(); + for (auto &[categoryName, achievements] : categories) { + std::erase_if(achievements, [](auto &data) { + auto &[achievementName, achievement] = data; + return achievement->isTemporary(); + }); + } + + std::erase_if(categories, [](auto &data) { + auto &[categoryName, achievements] = data; + return achievements.empty(); + }); + + getAchievementStartNodes(false).clear(); + getAchievementNodes(false).clear(); + } + + void AchievementManager::achievementAdded() { + getAchievementStartNodes(false).clear(); + getAchievementNodes(false).clear(); + } + + constexpr static auto AchievementsFile = "achievements.json"; + + void AchievementManager::loadProgress() { + for (const auto &directory : fs::getDefaultPaths(fs::ImHexPath::Config)) { + auto path = directory / AchievementsFile; + + if (!wolv::io::fs::exists(path)) { + continue; + } + + wolv::io::File file(path, wolv::io::File::Mode::Read); + + if (!file.isValid()) { + continue; + } + + try { + auto json = nlohmann::json::parse(file.readString()); + + for (const auto &[categoryName, achievements] : getAchievements()) { + for (const auto &[achievementName, achievement] : achievements) { + try { + achievement->setProgress(json[categoryName][achievementName]); + } catch (const std::exception &e) { + log::warn("Failed to load achievement progress for '{}::{}': {}", categoryName, achievementName, e.what()); + } + } + } + } catch (const std::exception &e) { + log::error("Failed to load achievements: {}", e.what()); + } + + } + } + + void AchievementManager::storeProgress() { + for (const auto &directory : fs::getDefaultPaths(fs::ImHexPath::Config)) { + auto path = directory / AchievementsFile; + + wolv::io::File file(path, wolv::io::File::Mode::Create); + + if (!file.isValid()) { + continue; + } + + nlohmann::json json; + + for (const auto &[categoryName, achievements] : getAchievements()) { + json[categoryName] = nlohmann::json::object(); + + for (const auto &[achievementName, achievement] : achievements) { + json[categoryName][achievementName] = achievement->getProgress(); + } + } + + file.writeString(json.dump(4)); + break; + } + } + +} \ No newline at end of file diff --git a/lib/libimhex/source/api/plugin_manager.cpp b/lib/libimhex/source/api/plugin_manager.cpp index e5be30a0a..47a438cdb 100644 --- a/lib/libimhex/source/api/plugin_manager.cpp +++ b/lib/libimhex/source/api/plugin_manager.cpp @@ -93,8 +93,10 @@ namespace hex { this->m_initializePluginFunction(); } catch (const std::exception &e) { log::error("Plugin '{}' threw an exception on init: {}", pluginName, e.what()); + return false; } catch (...) { log::error("Plugin '{}' threw an exception on init", pluginName); + return false; } } else { return false; diff --git a/lib/libimhex/source/helpers/tar.cpp b/lib/libimhex/source/helpers/tar.cpp index e0c1e0212..75d75563b 100644 --- a/lib/libimhex/source/helpers/tar.cpp +++ b/lib/libimhex/source/helpers/tar.cpp @@ -88,7 +88,13 @@ namespace hex { bool Tar::contains(const std::fs::path &path) { mtar_header_t header; - return mtar_find(&this->m_ctx, path.string().c_str(), &header) == MTAR_ESUCCESS; + + auto fixedPath = path.string(); + #if defined(OS_WINDOWS) + std::replace(fixedPath.begin(), fixedPath.end(), '\\', '/'); + #endif + + return mtar_find(&this->m_ctx, fixedPath.c_str(), &header) == MTAR_ESUCCESS; } std::string Tar::getOpenErrorString(){ diff --git a/lib/libimhex/source/ui/imgui_imhex_extensions.cpp b/lib/libimhex/source/ui/imgui_imhex_extensions.cpp index 8939d280b..a7a414525 100644 --- a/lib/libimhex/source/ui/imgui_imhex_extensions.cpp +++ b/lib/libimhex/source/ui/imgui_imhex_extensions.cpp @@ -35,8 +35,8 @@ namespace ImGui { glGenTextures(1, &texture); glBindTexture(GL_TEXTURE_2D, texture); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); #if defined(GL_UNPACK_ROW_LENGTH) glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); diff --git a/main/source/init/tasks.cpp b/main/source/init/tasks.cpp index 8f67142e2..9bf3b9335 100644 --- a/main/source/init/tasks.cpp +++ b/main/source/init/tasks.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -402,6 +403,8 @@ namespace hex::init { ThemeManager::reset(); + AchievementManager::getAchievements().clear(); + ProjectFile::getHandlers().clear(); ProjectFile::getProviderHandlers().clear(); ProjectFile::setProjectFunctions(nullptr, nullptr); @@ -454,13 +457,14 @@ namespace hex::init { } // Make sure there's only one built-in plugin - builtinPlugins++; if (builtinPlugins > 1) continue; // Initialize the plugin if (!plugin.initializePlugin()) { log::error("Failed to initialize plugin {}", wolv::util::toUTF8String(plugin.getPath().filename())); loadErrors++; + } else { + builtinPlugins++; } } diff --git a/plugins/builtin/CMakeLists.txt b/plugins/builtin/CMakeLists.txt index 3c83b513f..03cfdab89 100644 --- a/plugins/builtin/CMakeLists.txt +++ b/plugins/builtin/CMakeLists.txt @@ -34,6 +34,7 @@ add_imhex_plugin( source/content/recent.cpp source/content/file_handlers.cpp source/content/project.cpp + source/content/achievements.cpp source/content/providers/file_provider.cpp source/content/providers/gdb_provider.cpp @@ -64,6 +65,7 @@ add_imhex_plugin( source/content/views/view_find.cpp source/content/views/view_theme_manager.cpp source/content/views/view_logs.cpp + source/content/views/view_achievements.cpp source/content/helpers/math_evaluator.cpp source/content/helpers/notification.cpp diff --git a/plugins/builtin/include/content/popups/popup_text_input.hpp b/plugins/builtin/include/content/popups/popup_text_input.hpp index 41f5543c1..e4ccb5809 100644 --- a/plugins/builtin/include/content/popups/popup_text_input.hpp +++ b/plugins/builtin/include/content/popups/popup_text_input.hpp @@ -5,6 +5,8 @@ #include #include +#include + namespace hex::plugin::builtin { class PopupTextInput : public Popup { diff --git a/plugins/builtin/include/content/views/view_achievements.hpp b/plugins/builtin/include/content/views/view_achievements.hpp new file mode 100644 index 000000000..db744f75e --- /dev/null +++ b/plugins/builtin/include/content/views/view_achievements.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include + +namespace hex::plugin::builtin { + + class ViewAchievements : public View { + public: + ViewAchievements(); + ~ViewAchievements() override; + + void drawContent() override; + void drawAlwaysVisible() override; + + [[nodiscard]] bool isAvailable() const override { return true; } + [[nodiscard]] bool hasViewMenuItemEntry() const override { return false; } + + [[nodiscard]] ImVec2 getMinSize() const override { + return scaled({ 800, 600 }); + } + + [[nodiscard]] ImVec2 getMaxSize() const override { + return scaled({ 1600, 1200 }); + } + + private: + ImVec2 drawAchievementTree(ImDrawList *drawList, const AchievementManager::AchievementNode * prevNode, const std::vector &nodes, ImVec2 position); + + private: + bool m_viewOpen = false; + + std::list m_achievementUnlockQueue; + const Achievement *m_currAchievement = nullptr; + const Achievement *m_achievementToGoto = nullptr; + float m_achievementUnlockQueueTimer = -1; + bool m_showPopup = true; + + ImVec2 m_offset; + }; + +} \ No newline at end of file diff --git a/plugins/builtin/romfs/assets/achievements/abacus.png b/plugins/builtin/romfs/assets/achievements/abacus.png new file mode 100644 index 000000000..c53423d7a Binary files /dev/null and b/plugins/builtin/romfs/assets/achievements/abacus.png differ diff --git a/plugins/builtin/romfs/assets/achievements/adhesive-bandage.png b/plugins/builtin/romfs/assets/achievements/adhesive-bandage.png new file mode 100644 index 000000000..0ee4a6f66 Binary files /dev/null and b/plugins/builtin/romfs/assets/achievements/adhesive-bandage.png differ diff --git a/plugins/builtin/romfs/assets/achievements/black-question-mark-ornament.png b/plugins/builtin/romfs/assets/achievements/black-question-mark-ornament.png new file mode 100644 index 000000000..05e22b4c3 Binary files /dev/null and b/plugins/builtin/romfs/assets/achievements/black-question-mark-ornament.png differ diff --git a/plugins/builtin/romfs/assets/achievements/bookmark-tabs.png b/plugins/builtin/romfs/assets/achievements/bookmark-tabs.png new file mode 100644 index 000000000..7da28a92c Binary files /dev/null and b/plugins/builtin/romfs/assets/achievements/bookmark-tabs.png differ diff --git a/plugins/builtin/romfs/assets/achievements/bookmark.png b/plugins/builtin/romfs/assets/achievements/bookmark.png new file mode 100644 index 000000000..de6161860 Binary files /dev/null and b/plugins/builtin/romfs/assets/achievements/bookmark.png differ diff --git a/plugins/builtin/romfs/assets/achievements/brain.png b/plugins/builtin/romfs/assets/achievements/brain.png new file mode 100644 index 000000000..c4cf6a6f0 Binary files /dev/null and b/plugins/builtin/romfs/assets/achievements/brain.png differ diff --git a/plugins/builtin/romfs/assets/achievements/briefcase.png b/plugins/builtin/romfs/assets/achievements/briefcase.png new file mode 100644 index 000000000..8e22dc79e Binary files /dev/null and b/plugins/builtin/romfs/assets/achievements/briefcase.png differ diff --git a/plugins/builtin/romfs/assets/achievements/card-index-dividers.png b/plugins/builtin/romfs/assets/achievements/card-index-dividers.png new file mode 100644 index 000000000..ec1a4ce3b Binary files /dev/null and b/plugins/builtin/romfs/assets/achievements/card-index-dividers.png differ diff --git a/plugins/builtin/romfs/assets/achievements/clipboard.png b/plugins/builtin/romfs/assets/achievements/clipboard.png new file mode 100644 index 000000000..85aa0d11a Binary files /dev/null and b/plugins/builtin/romfs/assets/achievements/clipboard.png differ diff --git a/plugins/builtin/romfs/assets/achievements/cloud.png b/plugins/builtin/romfs/assets/achievements/cloud.png new file mode 100644 index 000000000..9c4b19355 Binary files /dev/null and b/plugins/builtin/romfs/assets/achievements/cloud.png differ diff --git a/plugins/builtin/romfs/assets/achievements/collision-symbol.png b/plugins/builtin/romfs/assets/achievements/collision-symbol.png new file mode 100644 index 000000000..86f772157 Binary files /dev/null and b/plugins/builtin/romfs/assets/achievements/collision-symbol.png differ diff --git a/plugins/builtin/romfs/assets/achievements/copy.png b/plugins/builtin/romfs/assets/achievements/copy.png new file mode 100644 index 000000000..103bcecff Binary files /dev/null and b/plugins/builtin/romfs/assets/achievements/copy.png differ diff --git a/plugins/builtin/romfs/assets/achievements/eye-in-speech-bubble.png b/plugins/builtin/romfs/assets/achievements/eye-in-speech-bubble.png new file mode 100644 index 000000000..5f40ed4c7 Binary files /dev/null and b/plugins/builtin/romfs/assets/achievements/eye-in-speech-bubble.png differ diff --git a/plugins/builtin/romfs/assets/achievements/fortune-cookie.png b/plugins/builtin/romfs/assets/achievements/fortune-cookie.png new file mode 100644 index 000000000..b6d0edf47 Binary files /dev/null and b/plugins/builtin/romfs/assets/achievements/fortune-cookie.png differ diff --git a/plugins/builtin/romfs/assets/achievements/frame-with-picture.png b/plugins/builtin/romfs/assets/achievements/frame-with-picture.png new file mode 100644 index 000000000..7e11ffe16 Binary files /dev/null and b/plugins/builtin/romfs/assets/achievements/frame-with-picture.png differ diff --git a/plugins/builtin/romfs/assets/achievements/hammer-and-pick.png b/plugins/builtin/romfs/assets/achievements/hammer-and-pick.png new file mode 100644 index 000000000..e143aa8b7 Binary files /dev/null and b/plugins/builtin/romfs/assets/achievements/hammer-and-pick.png differ diff --git a/plugins/builtin/romfs/assets/achievements/hammer-and-wrench.png b/plugins/builtin/romfs/assets/achievements/hammer-and-wrench.png new file mode 100644 index 000000000..4ee0673d6 Binary files /dev/null and b/plugins/builtin/romfs/assets/achievements/hammer-and-wrench.png differ diff --git a/plugins/builtin/romfs/assets/achievements/hammer.png b/plugins/builtin/romfs/assets/achievements/hammer.png new file mode 100644 index 000000000..36adfb7d4 Binary files /dev/null and b/plugins/builtin/romfs/assets/achievements/hammer.png differ diff --git a/plugins/builtin/romfs/assets/achievements/hourglass.png b/plugins/builtin/romfs/assets/achievements/hourglass.png new file mode 100644 index 000000000..106bd0917 Binary files /dev/null and b/plugins/builtin/romfs/assets/achievements/hourglass.png differ diff --git a/plugins/builtin/romfs/assets/achievements/linked-paperclips.png b/plugins/builtin/romfs/assets/achievements/linked-paperclips.png new file mode 100644 index 000000000..43791695f Binary files /dev/null and b/plugins/builtin/romfs/assets/achievements/linked-paperclips.png differ diff --git a/plugins/builtin/romfs/assets/achievements/open-book.png b/plugins/builtin/romfs/assets/achievements/open-book.png new file mode 100644 index 000000000..5ec8681c5 Binary files /dev/null and b/plugins/builtin/romfs/assets/achievements/open-book.png differ diff --git a/plugins/builtin/romfs/assets/achievements/package.png b/plugins/builtin/romfs/assets/achievements/package.png new file mode 100644 index 000000000..0b2662bd3 Binary files /dev/null and b/plugins/builtin/romfs/assets/achievements/package.png differ diff --git a/plugins/builtin/romfs/assets/achievements/page-facing-up.png b/plugins/builtin/romfs/assets/achievements/page-facing-up.png new file mode 100644 index 000000000..18f21e470 Binary files /dev/null and b/plugins/builtin/romfs/assets/achievements/page-facing-up.png differ diff --git a/plugins/builtin/romfs/assets/achievements/pencil.png b/plugins/builtin/romfs/assets/achievements/pencil.png new file mode 100644 index 000000000..08adf1ed8 Binary files /dev/null and b/plugins/builtin/romfs/assets/achievements/pencil.png differ diff --git a/plugins/builtin/romfs/assets/achievements/right-pointing-magnifying-glass.png b/plugins/builtin/romfs/assets/achievements/right-pointing-magnifying-glass.png new file mode 100644 index 000000000..8c11b507a Binary files /dev/null and b/plugins/builtin/romfs/assets/achievements/right-pointing-magnifying-glass.png differ diff --git a/plugins/builtin/romfs/assets/achievements/ring.png b/plugins/builtin/romfs/assets/achievements/ring.png new file mode 100644 index 000000000..34adc5468 Binary files /dev/null and b/plugins/builtin/romfs/assets/achievements/ring.png differ diff --git a/plugins/builtin/romfs/assets/achievements/water-wave.png b/plugins/builtin/romfs/assets/achievements/water-wave.png new file mode 100644 index 000000000..257eeb0c5 Binary files /dev/null and b/plugins/builtin/romfs/assets/achievements/water-wave.png differ diff --git a/plugins/builtin/romfs/assets/achievements/wrench.png b/plugins/builtin/romfs/assets/achievements/wrench.png new file mode 100644 index 000000000..dd2571ce0 Binary files /dev/null and b/plugins/builtin/romfs/assets/achievements/wrench.png differ diff --git a/plugins/builtin/romfs/lang/en_US.json b/plugins/builtin/romfs/lang/en_US.json index 489c0a5fd..c4d0a7874 100644 --- a/plugins/builtin/romfs/lang/en_US.json +++ b/plugins/builtin/romfs/lang/en_US.json @@ -4,6 +4,61 @@ "country": "United States", "fallback": true, "translations": { + "hex.builtin.achievement.starting_out": "Starting out", + "hex.builtin.achievement.starting_out.docs.name": "RTFM", + "hex.builtin.achievement.starting_out.docs.desc": "Open the documentation by selecting Help -> Documentation from the menu bar.", + "hex.builtin.achievement.starting_out.open_file.name": "The inner workings", + "hex.builtin.achievement.starting_out.open_file.desc": "Open a file by dragging it onto ImHex or by selecting File -> Open from the menu bar.", + "hex.builtin.achievement.starting_out.save_project.name": "Keeping this", + "hex.builtin.achievement.starting_out.save_project.desc": "Save a project by selecting File -> Save Project from the menu bar.", + "hex.builtin.achievement.hex_editor": "Hex Editor", + "hex.builtin.achievement.hex_editor.select_byte.name": "You and you and you", + "hex.builtin.achievement.hex_editor.select_byte.desc": "Select multiple bytes in the Hex Editor by clicking and dragging over them.", + "hex.builtin.achievement.hex_editor.create_bookmark.name": "Building a library", + "hex.builtin.achievement.hex_editor.create_bookmark.desc": "Create a bookmark by right-clicking on a byte and selecting Bookmark from the context menu.", + "hex.builtin.achievement.hex_editor.open_new_view.name": "Seeing double", + "hex.builtin.achievement.hex_editor.open_new_view.desc": "Open a new view by clicking on the 'Open new view' button in a bookmark.", + "hex.builtin.achievement.hex_editor.modify_byte.name": "Edit the hex", + "hex.builtin.achievement.hex_editor.modify_byte.desc": "Modify a byte by double-clicking on it and then entering the new value.", + "hex.builtin.achievement.hex_editor.copy_as.name": "Copy that", + "hex.builtin.achievement.hex_editor.copy_as.desc": "Copy bytes as a C++ array by selecting Copy As -> C++ Array from the context menu.", + "hex.builtin.achievement.hex_editor.create_patch.name": "ROM Hacks", + "hex.builtin.achievement.hex_editor.create_patch.desc": "Create a IPS patch for the use in other tools by selecting the Export option in the File menu.", + "hex.builtin.achievement.hex_editor.fill.name": "Flood fill", + "hex.builtin.achievement.hex_editor.fill.desc": "Fill a region with a byte by selecting Fill from the context menu.", + "hex.builtin.achievement.patterns": "Patterns", + "hex.builtin.achievement.patterns.place_menu.name": "Easy Patterns", + "hex.builtin.achievement.patterns.place_menu.desc": "Place a pattern of any built-in type in your data by right-clicking on a byte and using the 'Place pattern' option.", + "hex.builtin.achievement.patterns.load_existing.name": "Hey, I know this one", + "hex.builtin.achievement.patterns.load_existing.desc": "Load a pattern that has been created by someone else by using the 'File -> Import' menu.", + "hex.builtin.achievement.patterns.modify_data.name": "Edit the pattern", + "hex.builtin.achievement.patterns.modify_data.desc": "Modify the underlying bytes of a pattern by double-clicking its value in the pattern data view and entering a new value.", + "hex.builtin.achievement.patterns.data_inspector.name": "Inspector Gadget", + "hex.builtin.achievement.patterns.data_inspector.desc": "Create a custom data inspector entry using the pattern language. You can find how to do that in the documentation.", + "hex.builtin.achievement.find": "Finding", + "hex.builtin.achievement.find.find_strings.name": "One Ring to find them", + "hex.builtin.achievement.find.find_strings.desc": "Locate all strings in your file by using the Find view in 'Strings' mode.", + "hex.builtin.achievement.find.find_specific_string.name": "Too Many Items", + "hex.builtin.achievement.find.find_specific_string.desc": "Refine your search by searching for occurrences of a specific string by using the 'Sequences' mode.", + "hex.builtin.achievement.find.find_numeric.name": "About ... that much", + "hex.builtin.achievement.find.find_numeric.desc": "Search for numeric values between 250 and 1000 by using the 'Numeric Value' mode.", + "hex.builtin.achievement.data_processor": "Data Processor", + "hex.builtin.achievement.data_processor.place_node.name": "Look at all these nodes", + "hex.builtin.achievement.data_processor.place_node.desc": "Place any built-in node in the data processor by right-clicking on the workspace and selecting a node from the context menu.", + "hex.builtin.achievement.data_processor.create_connection.name": "I feel a connection here", + "hex.builtin.achievement.data_processor.create_connection.desc": "Connect two nodes by dragging a connection from one node to another.", + "hex.builtin.achievement.data_processor.modify_data.name": "Decode this", + "hex.builtin.achievement.data_processor.modify_data.desc": "Preprocess the displayed bytes by using the built-in Read and Write Data Access nodes.", + "hex.builtin.achievement.data_processor.custom_node.name": "Building my own!", + "hex.builtin.achievement.data_processor.custom_node.desc": "Create a custom node by selecting 'Custom -> New Node' from the context menu and simplify your existing pattern by moving nodes into it.", + "hex.builtin.achievement.misc": "Miscellaneous", + "hex.builtin.achievement.misc.analyze_file.name": "owo wat dis?", + "hex.builtin.achievement.misc.analyze_file.desc": "Analyze the bytes of your data by using the 'Analyze' option in the Data Information view.", + "hex.builtin.achievement.misc.download_from_store.name": "There's an app for that", + "hex.builtin.achievement.misc.download_from_store.desc": "Download any item from the Content Store", + "hex.builtin.achievement.misc.create_hash.name": "Hash browns", + "hex.builtin.achievement.misc.create_hash.desc": "Create a new hash function in the Hash view by selecting the type, giving it a name and clicking on the Plus button next to it.", + "hex.builtin.command.calc.desc": "Calculator", "hex.builtin.command.cmd.desc": "Command", "hex.builtin.command.cmd.result": "Run command '{0}'", @@ -603,6 +658,10 @@ "hex.builtin.tools.wiki_explain.invalid_response": "Invalid response from Wikipedia!", "hex.builtin.tools.wiki_explain.results": "Results", "hex.builtin.tools.wiki_explain.search": "Search", + "hex.builtin.view.achievements.name": "Achievements", + "hex.builtin.view.achievements.unlocked": "Achievement Unlocked!", + "hex.builtin.view.achievements.unlocked_count": "Unlocked", + "hex.builtin.view.achievements.click": "Click here", "hex.builtin.view.bookmarks.address": "0x{0:02X} - 0x{1:02X}", "hex.builtin.view.bookmarks.button.jump": "Jump to", "hex.builtin.view.bookmarks.button.remove": "Remove", @@ -961,9 +1020,12 @@ "hex.builtin.welcome.learn.latest.desc": "Read ImHex's current changelog", "hex.builtin.welcome.learn.latest.link": "https://github.com/WerWolv/ImHex/releases/latest", "hex.builtin.welcome.learn.latest.title": "Latest Release", - "hex.builtin.welcome.learn.pattern.desc": "Learn how to write ImHex patterns with our extensive documentation", - "hex.builtin.welcome.learn.pattern.link": "https://imhex.werwolv.net/docs", + "hex.builtin.welcome.learn.pattern.desc": "Learn how to write ImHex patterns", + "hex.builtin.welcome.learn.pattern.link": "https://docs.werwolv.net/pattern-language/", "hex.builtin.welcome.learn.pattern.title": "Pattern Language Documentation", + "hex.builtin.welcome.learn.imhex.desc": "Learn the basics of ImHex with our extensive documentation", + "hex.builtin.welcome.learn.imhex.link": "https://docs.werwolv.net/imhex/", + "hex.builtin.welcome.learn.imhex.title": "ImHex Documentation", "hex.builtin.welcome.learn.plugins.desc": "Extend ImHex with additional features using plugins", "hex.builtin.welcome.learn.plugins.link": "https://github.com/WerWolv/ImHex/wiki/Plugins-Development-Guide", "hex.builtin.welcome.learn.plugins.title": "Plugins API", diff --git a/plugins/builtin/source/content/achievements.cpp b/plugins/builtin/source/content/achievements.cpp new file mode 100644 index 000000000..508b14cf7 --- /dev/null +++ b/plugins/builtin/source/content/achievements.cpp @@ -0,0 +1,348 @@ +#include +#include + +#include + +#include +#include + +#include +#include + +namespace hex::plugin::builtin { + + namespace { + + class AchievementStartingOut : public Achievement { + public: + explicit AchievementStartingOut(std::string unlocalizedName) : Achievement("hex.builtin.achievement.starting_out", std::move(unlocalizedName)) { } + }; + + class AchievementHexEditor : public Achievement { + public: + explicit AchievementHexEditor(std::string unlocalizedName) : Achievement("hex.builtin.achievement.hex_editor", std::move(unlocalizedName)) { } + }; + + class AchievementPatterns : public Achievement { + public: + explicit AchievementPatterns(std::string unlocalizedName) : Achievement("hex.builtin.achievement.patterns", std::move(unlocalizedName)) { } + }; + + class AchievementDataProcessor : public Achievement { + public: + explicit AchievementDataProcessor(std::string unlocalizedName) : Achievement("hex.builtin.achievement.data_processor", std::move(unlocalizedName)) { } + }; + + class AchievementFind : public Achievement { + public: + explicit AchievementFind(std::string unlocalizedName) : Achievement("hex.builtin.achievement.find", std::move(unlocalizedName)) { } + }; + + class AchievementMisc : public Achievement { + public: + explicit AchievementMisc(std::string unlocalizedName) : Achievement("hex.builtin.achievement.misc", std::move(unlocalizedName)) { } + }; + + void registerGettingStartedAchievements() { + AchievementManager::addAchievement("hex.builtin.achievement.starting_out.docs.name") + .setDescription("hex.builtin.achievement.starting_out.docs.desc") + .setIcon(romfs::get("assets/achievements/open-book.png").span()); + + AchievementManager::addAchievement("hex.builtin.achievement.starting_out.open_file.name") + .setDescription("hex.builtin.achievement.starting_out.open_file.desc") + .setIcon(romfs::get("assets/achievements/page-facing-up.png").span()); + + AchievementManager::addAchievement("hex.builtin.achievement.starting_out.save_project.name") + .setDescription("hex.builtin.achievement.starting_out.save_project.desc") + .setIcon(romfs::get("assets/achievements/card-index-dividers.png").span()) + .addRequirement("hex.builtin.achievement.starting_out.open_file.name"); + + + AchievementManager::addAchievement("hex.builtin.achievement.starting_out.crash.name") + .setDescription("hex.builtin.achievement.starting_out.crash.desc") + .setIcon(romfs::get("assets/achievements/collision-symbol.png").span()) + .setInvisible(); + } + + void registerHexEditorAchievements() { + AchievementManager::addAchievement("hex.builtin.achievement.hex_editor.select_byte.name") + .setDescription("hex.builtin.achievement.hex_editor.select_byte.desc") + .setIcon(romfs::get("assets/achievements/bookmark-tabs.png").span()) + .addRequirement("hex.builtin.achievement.starting_out.open_file.name"); + + AchievementManager::addAchievement("hex.builtin.achievement.hex_editor.open_new_view.name") + .setDescription("hex.builtin.achievement.hex_editor.open_new_view.desc") + .setIcon(romfs::get("assets/achievements/frame-with-picture.png").span()) + .addRequirement("hex.builtin.achievement.hex_editor.create_bookmark.name"); + + AchievementManager::addAchievement("hex.builtin.achievement.hex_editor.modify_byte.name") + .setDescription("hex.builtin.achievement.hex_editor.modify_byte.desc") + .setIcon(romfs::get("assets/achievements/pencil.png").span()) + .addRequirement("hex.builtin.achievement.hex_editor.select_byte.name") + .addVisibilityRequirement("hex.builtin.achievement.hex_editor.select_byte.name"); + + AchievementManager::addAchievement("hex.builtin.achievement.hex_editor.copy_as.name") + .setDescription("hex.builtin.achievement.hex_editor.copy_as.desc") + .setIcon(romfs::get("assets/achievements/copy.png").span()) + .addRequirement("hex.builtin.achievement.hex_editor.modify_byte.name"); + + AchievementManager::addAchievement("hex.builtin.achievement.hex_editor.create_patch.name") + .setDescription("hex.builtin.achievement.hex_editor.create_patch.desc") + .setIcon(romfs::get("assets/achievements/adhesive-bandage.png").span()) + .addRequirement("hex.builtin.achievement.hex_editor.modify_byte.name"); + + AchievementManager::addAchievement("hex.builtin.achievement.hex_editor.fill.name") + .setDescription("hex.builtin.achievement.hex_editor.fill.desc") + .setIcon(romfs::get("assets/achievements/water-wave.png").span()) + .addRequirement("hex.builtin.achievement.hex_editor.select_byte.name") + .addVisibilityRequirement("hex.builtin.achievement.hex_editor.select_byte.name"); + + AchievementManager::addAchievement("hex.builtin.achievement.hex_editor.create_bookmark.name") + .setDescription("hex.builtin.achievement.hex_editor.create_bookmark.desc") + .setIcon(romfs::get("assets/achievements/bookmark.png").span()) + .addRequirement("hex.builtin.achievement.hex_editor.select_byte.name") + .addVisibilityRequirement("hex.builtin.achievement.hex_editor.select_byte.name"); + } + + void registerPatternsAchievements() { + AchievementManager::addAchievement("hex.builtin.achievement.patterns.place_menu.name") + .setDescription("hex.builtin.achievement.patterns.place_menu.desc") + .setIcon(romfs::get("assets/achievements/clipboard.png").span()) + .addRequirement("hex.builtin.achievement.hex_editor.select_byte.name"); + + AchievementManager::addAchievement("hex.builtin.achievement.patterns.load_existing.name") + .setDescription("hex.builtin.achievement.patterns.load_existing.desc") + .setIcon(romfs::get("assets/achievements/hourglass.png").span()) + .addRequirement("hex.builtin.achievement.patterns.place_menu.name"); + + AchievementManager::addAchievement("hex.builtin.achievement.patterns.modify_data.name") + .setDescription("hex.builtin.achievement.patterns.modify_data.desc") + .setIcon(romfs::get("assets/achievements/hammer.png").span()) + .addRequirement("hex.builtin.achievement.patterns.place_menu.name"); + + + AchievementManager::addAchievement("hex.builtin.achievement.patterns.data_inspector.name") + .setDescription("hex.builtin.achievement.patterns.data_inspector.desc") + .setIcon(romfs::get("assets/achievements/eye-in-speech-bubble.png").span()) + .addRequirement("hex.builtin.achievement.hex_editor.select_byte.name"); + } + + + void registerFindAchievements() { + AchievementManager::addAchievement("hex.builtin.achievement.find.find_strings.name") + .setDescription("hex.builtin.achievement.find.find_strings.desc") + .setIcon(romfs::get("assets/achievements/ring.png").span()) + .addRequirement("hex.builtin.achievement.starting_out.open_file.name"); + + AchievementManager::addAchievement("hex.builtin.achievement.find.find_specific_string.name") + .setDescription("hex.builtin.achievement.find.find_specific_string.desc") + .setIcon(romfs::get("assets/achievements/right-pointing-magnifying-glass.png").span()) + .addRequirement("hex.builtin.achievement.find.find_strings.name"); + + AchievementManager::addAchievement("hex.builtin.achievement.find.find_numeric.name") + .setDescription("hex.builtin.achievement.find.find_numeric.desc") + .setIcon(romfs::get("assets/achievements/abacus.png").span()) + .addRequirement("hex.builtin.achievement.find.find_strings.name"); + } + + void registerDataProcessorAchievements() { + AchievementManager::addAchievement("hex.builtin.achievement.data_processor.place_node.name") + .setDescription("hex.builtin.achievement.data_processor.place_node.desc") + .setIcon(romfs::get("assets/achievements/cloud.png").span()) + .addRequirement("hex.builtin.achievement.starting_out.open_file.name"); + + AchievementManager::addAchievement("hex.builtin.achievement.data_processor.create_connection.name") + .setDescription("hex.builtin.achievement.data_processor.create_connection.desc") + .setIcon(romfs::get("assets/achievements/linked-paperclips.png").span()) + .addRequirement("hex.builtin.achievement.data_processor.place_node.name"); + + AchievementManager::addAchievement("hex.builtin.achievement.data_processor.modify_data.name") + .setDescription("hex.builtin.achievement.data_processor.modify_data.desc") + .setIcon(romfs::get("assets/achievements/hammer-and-pick.png").span()) + .addRequirement("hex.builtin.achievement.data_processor.create_connection.name"); + + AchievementManager::addAchievement("hex.builtin.achievement.data_processor.custom_node.name") + .setDescription("hex.builtin.achievement.data_processor.custom_node.desc") + .setIcon(romfs::get("assets/achievements/wrench.png").span()) + .addRequirement("hex.builtin.achievement.data_processor.create_connection.name"); + } + + void registerMiscAchievements() { + AchievementManager::addAchievement("hex.builtin.achievement.misc.analyze_file.name") + .setDescription("hex.builtin.achievement.misc.analyze_file.desc") + .setIcon(romfs::get("assets/achievements/brain.png").span()) + .addRequirement("hex.builtin.achievement.starting_out.open_file.name"); + + AchievementManager::addAchievement("hex.builtin.achievement.misc.download_from_store.name") + .setDescription("hex.builtin.achievement.misc.download_from_store.desc") + .setIcon(romfs::get("assets/achievements/package.png").span()) + .addRequirement("hex.builtin.achievement.starting_out.open_file.name"); + + AchievementManager::addAchievement("hex.builtin.achievement.misc.create_hash.name") + .setDescription("hex.builtin.achievement.misc.create_hash.desc") + .setIcon(romfs::get("assets/achievements/fortune-cookie.png").span()) + .addRequirement("hex.builtin.achievement.starting_out.open_file.name"); + } + + + void registerEvents() { + EventManager::subscribe([](const auto ®ion) { + if (region.getSize() > 1) + AchievementManager::unlockAchievement("hex.builtin.achievement.hex_editor", "hex.builtin.achievement.hex_editor.select_byte.name"); + }); + + EventManager::subscribe([](const auto&) { + AchievementManager::unlockAchievement("hex.builtin.achievement.hex_editor", "hex.builtin.achievement.hex_editor.create_bookmark.name"); + }); + + EventManager::subscribe([](u64, u8, u8) { + AchievementManager::unlockAchievement("hex.builtin.achievement.hex_editor", "hex.builtin.achievement.hex_editor.modify_byte.name"); + }); + + + EventManager::subscribe(AchievementManager::loadProgress); + EventManager::subscribe(AchievementManager::storeProgress); + + // Clear temporary achievements when last provider is closed + EventManager::subscribe([](hex::prv::Provider *oldProvider, hex::prv::Provider *newProvider) { + hex::unused(oldProvider); + if (newProvider == nullptr) { + AchievementManager::clearTemporary(); + } + }); + } + + void registerChallengeAchievementHandlers() { + static std::string challengeAchievement; + static std::string challengeDescription; + + static std::map> icons; + + ProjectFile::registerHandler({ + .basePath = "challenge", + .required = false, + .load = [](const std::fs::path &basePath, Tar &tar) { + if (!tar.contains(basePath / "achievements.json") || !tar.contains(basePath / "description.txt")) + return true; + + challengeAchievement = tar.readString(basePath / "achievements.json"); + challengeDescription = tar.readString(basePath / "description.txt"); + + nlohmann::json unlockedJson; + if (tar.contains(basePath / "unlocked.json")) { + unlockedJson = nlohmann::json::parse(tar.readString(basePath / "unlocked.json")); + } + + try { + auto json = nlohmann::json::parse(challengeAchievement); + + if (json.contains("achievements")) { + for (const auto &achievement : json["achievements"]) { + auto &newAchievement = AchievementManager::addTemporaryAchievement("hex.builtin.achievement.challenge", achievement["name"]) + .setDescription(achievement["description"]); + + if (achievement.contains("icon")) { + if (const auto &icon = achievement["icon"]; icon.is_string() && !icon.is_null()) { + auto iconPath = icon.get(); + + auto data = tar.readVector(basePath / iconPath); + newAchievement.setIcon(data); + icons[iconPath] = std::move(data); + } + } + + if (achievement.contains("requirements")) { + if (const auto &requirements = achievement["requirements"]; requirements.is_array()) { + for (const auto &requirement : requirements) { + newAchievement.addRequirement(requirement.get()); + } + } + } + + if (achievement.contains("visibility_requirements")) { + if (const auto &requirements = achievement["visibility_requirements"]; requirements.is_array()) { + for (const auto &requirement : requirements) { + newAchievement.addVisibilityRequirement(requirement.get()); + } + } + } + + if (achievement.contains("password")) { + if (const auto &password = achievement["password"]; password.is_string() && !password.is_null()) { + newAchievement.setClickCallback([password = password.get()](Achievement &achievement) { + if (password.empty()) + achievement.setUnlocked(true); + else + PopupTextInput::open("Enter Password", "Enter the password to unlock this achievement", [password, &achievement](const std::string &input) { + if (input == password) + achievement.setUnlocked(true); + else + PopupInfo::open("The password you entered was incorrect."); + }); + }); + + if (unlockedJson.contains("achievements") && unlockedJson["achievements"].is_array()) { + for (const auto &unlockedAchievement : unlockedJson["achievements"]) { + if (unlockedAchievement.is_string() && unlockedAchievement.get() == achievement["name"].get()) { + newAchievement.setUnlocked(true); + break; + } + } + } + } + } + } + } + } catch (const nlohmann::json::exception &e) { + log::error("Failed to load challenge project: {}", e.what()); + return false; + } + + PopupInfo::open(challengeDescription); + + + return true; + }, + .store = [](const std::fs::path &basePath, Tar &tar) { + if (!challengeAchievement.empty()) + tar.writeString(basePath / "achievements.json", challengeAchievement); + if (!challengeDescription.empty()) + tar.writeString(basePath / "description.txt", challengeDescription); + + for (const auto &[iconPath, data] : icons) { + tar.writeVector(basePath / iconPath, data); + } + + nlohmann::json unlockedJson; + + unlockedJson["achievements"] = nlohmann::json::array(); + for (const auto &[categoryName, achievements] : AchievementManager::getAchievements()) { + for (const auto &[achievementName, achievement] : achievements) { + if (achievement->isTemporary() && achievement->isUnlocked()) { + unlockedJson["achievements"].push_back(achievementName); + } + } + } + + tar.writeString(basePath / "unlocked.json", unlockedJson.dump(4)); + + return true; + } + }); + } + + } + + void registerAchievements() { + registerGettingStartedAchievements(); + registerHexEditorAchievements(); + registerPatternsAchievements(); + registerFindAchievements(); + registerDataProcessorAchievements(); + registerMiscAchievements(); + + registerEvents(); + registerChallengeAchievementHandlers(); + } + +} \ No newline at end of file diff --git a/plugins/builtin/source/content/data_formatters.cpp b/plugins/builtin/source/content/data_formatters.cpp index 4fff9aaf0..7503f3805 100644 --- a/plugins/builtin/source/content/data_formatters.cpp +++ b/plugins/builtin/source/content/data_formatters.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -48,6 +49,8 @@ namespace hex::plugin::builtin { }); ContentRegistry::DataFormatter::add("hex.builtin.view.hex_editor.copy.cpp", [](prv::Provider *provider, u64 offset, size_t size) { + AchievementManager::unlockAchievement("hex.builtin.achievement.hex_editor", "hex.builtin.achievement.hex_editor.copy_as.name"); + return formatLanguageArray(provider, offset, size, hex::format("constexpr std::array data = {{", size), "0x{0:02X}, ", "};"); }); diff --git a/plugins/builtin/source/content/data_processor_nodes.cpp b/plugins/builtin/source/content/data_processor_nodes.cpp index bca5c65b7..2866fc25a 100644 --- a/plugins/builtin/source/content/data_processor_nodes.cpp +++ b/plugins/builtin/source/content/data_processor_nodes.cpp @@ -1,7 +1,9 @@ #include +#include +#include + #include -#include #include #include #include @@ -467,6 +469,10 @@ namespace hex::plugin::builtin { const auto &address = this->getIntegerOnInput(0); const auto &data = this->getBufferOnInput(1); + if (!data.empty()) { + AchievementManager::unlockAchievement("hex.builtin.achievement.data_processor", "hex.builtin.achievement.data_processor.modify_data.name"); + } + this->setOverlayData(address, data); } }; diff --git a/plugins/builtin/source/content/project.cpp b/plugins/builtin/source/content/project.cpp index c6421c6e9..4c160f189 100644 --- a/plugins/builtin/source/content/project.cpp +++ b/plugins/builtin/source/content/project.cpp @@ -5,6 +5,8 @@ #include #include +#include + #include #include @@ -160,6 +162,9 @@ namespace hex::plugin::builtin { resetPath.release(); } + AchievementManager::unlockAchievement("hex.builtin.achievement.starting_out", "hex.builtin.achievement.starting_out.save_project.name"); + + return result; } diff --git a/plugins/builtin/source/content/providers/file_provider.cpp b/plugins/builtin/source/content/providers/file_provider.cpp index 88718bbc8..0b795667e 100644 --- a/plugins/builtin/source/content/providers/file_provider.cpp +++ b/plugins/builtin/source/content/providers/file_provider.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -247,6 +248,8 @@ namespace hex::plugin::builtin { this->m_file.close(); + AchievementManager::unlockAchievement("hex.builtin.achievement.starting_out", "hex.builtin.achievement.starting_out.open_file.name"); + return true; } diff --git a/plugins/builtin/source/content/views.cpp b/plugins/builtin/source/content/views.cpp index fc59586ff..4301613b5 100644 --- a/plugins/builtin/source/content/views.cpp +++ b/plugins/builtin/source/content/views.cpp @@ -20,6 +20,7 @@ #include "content/views/view_find.hpp" #include "content/views/view_theme_manager.hpp" #include "content/views/view_logs.hpp" +#include "content/views/view_achievements.hpp" namespace hex::plugin::builtin { @@ -46,6 +47,7 @@ namespace hex::plugin::builtin { ContentRegistry::Views::add(); ContentRegistry::Views::add(); ContentRegistry::Views::add(); + ContentRegistry::Views::add(); } } \ No newline at end of file diff --git a/plugins/builtin/source/content/views/view_about.cpp b/plugins/builtin/source/content/views/view_about.cpp index 897e9dd90..4ba28c9d4 100644 --- a/plugins/builtin/source/content/views/view_about.cpp +++ b/plugins/builtin/source/content/views/view_about.cpp @@ -1,6 +1,7 @@ #include "content/views/view_about.hpp" #include +#include #include #include @@ -23,7 +24,8 @@ namespace hex::plugin::builtin { ContentRegistry::Interface::addMenuItemSeparator({ "hex.builtin.menu.help" }, 2000); ContentRegistry::Interface::addMenuItem({ "hex.builtin.menu.help", "hex.builtin.view.help.documentation" }, 3000, Shortcut::None, [] { - hex::openWebpage("https://imhex.werwolv.net/docs"); + hex::openWebpage("https://docs.werwolv.net/imhex"); + AchievementManager::unlockAchievement("hex.builtin.achievement.starting_out", "hex.builtin.achievement.starting_out.docs.name"); }); ContentRegistry::Interface::addMenuItem({ "hex.builtin.menu.help", "hex.builtin.menu.help.ask_for_help" }, 4000, CTRLCMD + SHIFT + Keys::D, [] { diff --git a/plugins/builtin/source/content/views/view_achievements.cpp b/plugins/builtin/source/content/views/view_achievements.cpp new file mode 100644 index 000000000..9f111535e --- /dev/null +++ b/plugins/builtin/source/content/views/view_achievements.cpp @@ -0,0 +1,378 @@ +#include "content/views/view_achievements.hpp" + +#include + +#include + +#include + +#include + +namespace hex::plugin::builtin { + + ViewAchievements::ViewAchievements() : View("hex.builtin.view.achievements.name") { + ContentRegistry::Interface::addMenuItem({ "hex.builtin.menu.extras", "hex.builtin.view.achievements.name" }, 2600, Shortcut::None, [&, this] { + this->m_viewOpen = true; + this->getWindowOpenState() = true; + }); + + EventManager::subscribe(this, [this](const Achievement &achievement) { + this->m_achievementUnlockQueue.push_back(&achievement); + }); + + this->m_showPopup = bool(ContentRegistry::Settings::read("hex.builtin.setting.interface", "hex.builtin.setting.interface.achievement_popup", 1)); + } + + ViewAchievements::~ViewAchievements() { + EventManager::unsubscribe(this); + } + + void drawAchievement(ImDrawList *drawList, const AchievementManager::AchievementNode *node, ImVec2 position) { + const auto achievementSize = scaled({ 50, 50 }); + + auto &achievement = *node->achievement; + + const auto borderColor = [&] { + if (achievement.isUnlocked()) + return ImGui::GetCustomColorU32(ImGuiCustomCol_ToolbarYellow, 1.0F); + else if (node->isUnlockable()) + return ImGui::GetColorU32(ImGuiCol_Button, 1.0F); + else + return ImGui::GetColorU32(ImGuiCol_PlotLines, 1.0F); + }(); + + const auto fillColor = [&] { + if (achievement.isUnlocked()) + return ImGui::GetColorU32(ImGuiCol_FrameBg, 1.0F) | 0xFF000000; + else if (node->isUnlockable()) + return (u32(ImColor(ImLerp(ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled), ImGui::GetStyleColorVec4(ImGuiCol_Text), sinf(ImGui::GetTime() * 6.0F) * 0.5F + 0.5F))) & 0x00FFFFFF) | 0x80000000; + else + return ImGui::GetColorU32(ImGuiCol_TextDisabled, 0.5F); + }(); + + if (achievement.isUnlocked()) { + drawList->AddRectFilled(position, position + achievementSize, fillColor, 5_scaled, 0); + drawList->AddRect(position, position + achievementSize, borderColor, 5_scaled, 0, 2_scaled); + } else { + drawList->AddRectFilled(position, position + achievementSize, ImGui::GetColorU32(ImGuiCol_WindowBg) | 0xFF000000, 5_scaled, 0); + } + + if (const auto &icon = achievement.getIcon(); icon.isValid()) { + ImVec2 iconSize; + if (icon.getSize().x > icon.getSize().y) { + iconSize.x = achievementSize.x; + iconSize.y = iconSize.x / icon.getSize().x * icon.getSize().y; + } else { + iconSize.y = achievementSize.y; + iconSize.x = iconSize.y / icon.getSize().y * icon.getSize().x; + } + + iconSize *= 0.7F; + + ImVec2 margin = (achievementSize - iconSize) / 2.0F; + drawList->AddImage(icon, position + margin, position + margin + iconSize); + } + + if (!achievement.isUnlocked()) { + drawList->AddRectFilled(position, position + achievementSize, fillColor, 5_scaled, 0); + drawList->AddRect(position, position + achievementSize, borderColor, 5_scaled, 0, 2_scaled); + } + + auto tooltipPos = position + ImVec2(achievementSize.x, 0); + auto tooltipSize = achievementSize * ImVec2(4, 0); + + if (ImGui::IsMouseHoveringRect(position, position + achievementSize)) { + ImGui::SetNextWindowPos(tooltipPos); + ImGui::SetNextWindowSize(tooltipSize); + if (ImGui::BeginTooltip()) { + if (achievement.isBlacked() && !achievement.isUnlocked()) { + ImGui::TextUnformatted("[ ??? ]"); + } else { + ImGui::BeginDisabled(!achievement.isUnlocked()); + ImGui::TextUnformatted(LangEntry(achievement.getUnlocalizedName())); + + if (auto requiredProgress = achievement.getRequiredProgress(); requiredProgress > 1) { + ImGui::ProgressBar(float(achievement.getProgress()) / float(requiredProgress + 1), ImVec2(achievementSize.x * 4, 5_scaled), ""); + } + + bool separator = false; + + if (achievement.getClickCallback() && !achievement.isUnlocked()) { + ImGui::Separator(); + separator = true; + + ImGui::TextFormattedColored(ImGui::GetCustomColorVec4(ImGuiCustomCol_ToolbarYellow), "[ {} ]", LangEntry("hex.builtin.view.achievements.click")); + } + + if (const auto &desc = achievement.getUnlocalizedDescription(); !desc.empty()) { + if (!separator) + ImGui::Separator(); + else + ImGui::NewLine(); + + ImGui::TextFormattedWrapped("{}", LangEntry(desc)); + } + ImGui::EndDisabled(); + } + + ImGui::EndTooltip(); + } + + if (!achievement.isUnlocked() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { + if (ImGui::GetIO().KeyShift) { + #if defined (DEBUG) + AchievementManager::unlockAchievement(node->achievement->getUnlocalizedCategory(), node->achievement->getUnlocalizedName()); + #endif + } else { + if (auto clickCallback = achievement.getClickCallback(); clickCallback) + clickCallback(achievement); + } + } + } + } + + void drawOverlay(ImDrawList *drawList, ImVec2 windowMin, ImVec2 windowMax, const std::string &currCategory) { + auto &achievements = AchievementManager::getAchievements()[currCategory]; + auto unlockedCount = std::count_if(achievements.begin(), achievements.end(), [](const auto &entry) { + const auto &[name, achievement] = entry; + return achievement->isUnlocked(); + }); + + auto invisibleCount = std::count_if(achievements.begin(), achievements.end(), [](const auto &entry) { + const auto &[name, achievement] = entry; + return achievement->isInvisible(); + }); + + auto unlockedText = hex::format("{}: {} / {}{}", "hex.builtin.view.achievements.unlocked_count"_lang, unlockedCount, achievements.size() - invisibleCount, invisibleCount > 0 ? "+" : " "); + + auto &style = ImGui::GetStyle(); + auto overlaySize = ImGui::CalcTextSize(unlockedText.c_str()) + style.ItemSpacing + style.WindowPadding * 2.0F; + auto padding = scaled({ 10, 10 }); + + auto overlayPos = ImVec2(windowMax.x - overlaySize.x - padding.x, windowMin.y + padding.y); + + drawList->AddRectFilled(overlayPos, overlayPos + overlaySize, ImGui::GetColorU32(ImGuiCol_WindowBg, 0.8F)); + drawList->AddRect(overlayPos, overlayPos + overlaySize, ImGui::GetColorU32(ImGuiCol_Border)); + + ImGui::SetCursorScreenPos(overlayPos + padding); + ImGui::BeginGroup(); + + ImGui::TextUnformatted(unlockedText.c_str()); + + ImGui::EndGroup(); + } + + void drawBackground(ImDrawList *drawList, ImVec2 min, ImVec2 max, ImVec2 offset) { + const auto patternSize = scaled({ 10, 10 }); + + const auto darkColor = ImGui::GetColorU32(ImGuiCol_TableRowBg); + const auto lightColor = ImGui::GetColorU32(ImGuiCol_TableRowBgAlt); + + drawList->AddRect(min, max, ImGui::GetColorU32(ImGuiCol_Border), 0.0F, 0, 1.0_scaled); + + bool light = false; + bool prevStart = false; + for (float x = min.x + offset.x; x < max.x; x += i32(patternSize.x)) { + if (prevStart == light) + light = !light; + prevStart = light; + + for (float y = min.y + offset.y; y < max.y; y += i32(patternSize.y)) { + drawList->AddRectFilled({ x, y }, { x + patternSize.x, y + patternSize.y }, light ? lightColor : darkColor); + light = !light; + } + } + } + + ImVec2 ViewAchievements::drawAchievementTree(ImDrawList *drawList, const AchievementManager::AchievementNode * prevNode, const std::vector &nodes, ImVec2 position) { + ImVec2 maxPos = position; + for (auto &node : nodes) { + if (node->achievement->isInvisible() && !node->achievement->isUnlocked()) + continue; + + if (!node->visibilityParents.empty()) { + bool visible = true; + for (auto &parent : node->visibilityParents) { + if (!parent->achievement->isUnlocked()) { + visible = false; + break; + } + } + + if (!visible) + continue; + } + + drawList->ChannelsSetCurrent(1); + + if (prevNode != nullptr) { + if (prevNode->achievement->getUnlocalizedCategory() != node->achievement->getUnlocalizedCategory()) + continue; + + auto start = prevNode->position + scaled({ 25, 25 }); + auto end = position + scaled({ 25, 25 }); + auto middle = ((start + end) / 2.0F) - scaled({ 50, 0 }); + + const auto color = [prevNode]{ + if (prevNode->achievement->isUnlocked()) + return ImGui::GetColorU32(ImGuiCol_Text) | 0xFF000000; + else + return ImGui::GetColorU32(ImGuiCol_TextDisabled) | 0xFF000000; + }(); + + drawList->AddBezierQuadratic(start, middle, end, color, 2_scaled); + + if (this->m_achievementToGoto != nullptr) { + if (this->m_achievementToGoto == node->achievement) { + this->m_offset = position - scaled({ 100, 100 }); + } + } + } + + drawList->ChannelsSetCurrent(2); + + drawAchievement(drawList, node, position); + + node->position = position; + auto newMaxPos = drawAchievementTree(drawList, node, node->children, position + scaled({ 150, 0 })); + if (newMaxPos.x > maxPos.x) + maxPos.x = newMaxPos.x; + if (newMaxPos.y > maxPos.y) + maxPos.y = newMaxPos.y; + + position.y = maxPos.y + 100_scaled; + } + + return maxPos; + } + + void ViewAchievements::drawContent() { + if (ImGui::Begin(View::toWindowName("hex.builtin.view.achievements.name").c_str(), &this->m_viewOpen, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoDocking)) { + if (ImGui::BeginTabBar("##achievement_categories")) { + auto &startNodes = AchievementManager::getAchievementStartNodes(); + + std::vector categories; + for (const auto &[categoryName, achievements] : startNodes) { + categories.push_back(categoryName); + } + + std::reverse(categories.begin(), categories.end()); + + for (const auto &categoryName : categories) { + const auto &achievements = startNodes[categoryName]; + + bool visible = false; + for (const auto &achievement : achievements) { + if (achievement->isUnlocked() || achievement->isUnlockable()) { + visible = true; + break; + } + } + + if (!visible) + continue; + + ImGuiTabItemFlags flags = ImGuiTabItemFlags_None; + + if (this->m_achievementToGoto != nullptr) { + if (this->m_achievementToGoto->getUnlocalizedCategory() == categoryName) { + flags |= ImGuiTabItemFlags_SetSelected; + } + } + + if (ImGui::BeginTabItem(LangEntry(categoryName), nullptr, flags)) { + auto drawList = ImGui::GetWindowDrawList(); + + const auto cursorPos = ImGui::GetCursorPos(); + const auto windowPos = ImGui::GetWindowPos() + ImVec2(0, cursorPos.y); + const auto windowSize = ImGui::GetWindowSize() - ImVec2(0, cursorPos.y);; + const float borderSize = 20.0_scaled; + + const auto windowPadding = ImGui::GetStyle().WindowPadding; + const auto innerWindowPos = windowPos + ImVec2(borderSize, borderSize); + const auto innerWindowSize = windowSize - ImVec2(borderSize * 2, borderSize * 2) - ImVec2(0, ImGui::GetTextLineHeightWithSpacing()); + drawList->PushClipRect(innerWindowPos, innerWindowPos + innerWindowSize, true); + + drawList->ChannelsSplit(4); + + drawList->ChannelsSetCurrent(0); + + drawBackground(drawList, innerWindowPos, innerWindowPos + innerWindowSize, this->m_offset); + auto maxPos = drawAchievementTree(drawList, nullptr, achievements, innerWindowPos + scaled({ 100, 100 }) + this->m_offset); + + drawList->ChannelsSetCurrent(3); + + drawOverlay(drawList, innerWindowPos, innerWindowPos + innerWindowSize, categoryName); + + drawList->ChannelsMerge(); + + if (ImGui::IsMouseHoveringRect(innerWindowPos, innerWindowPos + innerWindowSize)) { + auto dragDelta = ImGui::GetMouseDragDelta(ImGuiMouseButton_Left); + this->m_offset += dragDelta; + ImGui::ResetMouseDragDelta(ImGuiMouseButton_Left); + } + + this->m_offset = -ImClamp(-this->m_offset, { 0, 0 }, ImMax(maxPos - innerWindowPos - innerWindowSize, { 0, 0 })); + + drawList->PopClipRect(); + + ImGui::SetCursorScreenPos(innerWindowPos + ImVec2(0, innerWindowSize.y + windowPadding.y)); + ImGui::BeginGroup(); + { + if (ImGui::Checkbox("Show popup", &this->m_showPopup)) + ContentRegistry::Settings::write("hex.builtin.setting.interface", "hex.builtin.setting.interface.achievement_popup", i64(this->m_showPopup)); + } + ImGui::EndGroup(); + + ImGui::EndTabItem(); + } + } + + ImGui::EndTabBar(); + } + } + ImGui::End(); + + this->getWindowOpenState() = this->m_viewOpen; + + this->m_achievementToGoto = nullptr; + } + + void ViewAchievements::drawAlwaysVisible() { + if (this->m_achievementUnlockQueueTimer >= 0 && this->m_showPopup) { + this->m_achievementUnlockQueueTimer -= ImGui::GetIO().DeltaTime; + + if (this->m_currAchievement != nullptr) { + + const ImVec2 windowSize = scaled({ 200, 55 }); + ImGui::SetNextWindowPos(ImHexApi::System::getMainWindowPosition() + ImVec2 { ImHexApi::System::getMainWindowSize().x - windowSize.x - 100_scaled, 0 }); + ImGui::SetNextWindowSize(windowSize); + if (ImGui::Begin("##achievement_unlocked", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoInputs)) { + ImGui::TextFormattedColored(ImGui::GetCustomColorVec4(ImGuiCustomCol_ToolbarYellow), "{}", "hex.builtin.view.achievements.unlocked"_lang); + + ImGui::Image(this->m_currAchievement->getIcon(), scaled({ 20, 20 })); + ImGui::SameLine(); + ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical); + ImGui::SameLine(); + ImGui::TextFormattedWrapped("{}", LangEntry(this->m_currAchievement->getUnlocalizedName())); + + if (ImGui::IsWindowHovered() && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { + this->m_viewOpen = true; + this->getWindowOpenState() = this->m_viewOpen; + this->m_achievementToGoto = this->m_currAchievement; + } + } + ImGui::End(); + } + } else { + this->m_achievementUnlockQueueTimer = -1.0F; + this->m_currAchievement = nullptr; + if (!this->m_achievementUnlockQueue.empty()) { + this->m_currAchievement = this->m_achievementUnlockQueue.front(); + this->m_achievementUnlockQueue.pop_front(); + this->m_achievementUnlockQueueTimer = 2.5F; + } + } + } + +} \ No newline at end of file diff --git a/plugins/builtin/source/content/views/view_bookmarks.cpp b/plugins/builtin/source/content/views/view_bookmarks.cpp index eb12e6df1..3e6ce29f4 100644 --- a/plugins/builtin/source/content/views/view_bookmarks.cpp +++ b/plugins/builtin/source/content/views/view_bookmarks.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -289,8 +290,10 @@ namespace hex::plugin::builtin { auto newProvider = ImHexApi::Provider::createProvider("hex.builtin.provider.view", true); if (auto *viewProvider = dynamic_cast(newProvider); viewProvider != nullptr) { viewProvider->setProvider(region.getStartAddress(), region.getSize(), provider); - if (viewProvider->open()) + if (viewProvider->open()) { EventManager::post(viewProvider); + AchievementManager::unlockAchievement("hex.builtin.achievement.hex_editor", "hex.builtin.achievement.hex_editor.open_new_view.name"); + } } }); } diff --git a/plugins/builtin/source/content/views/view_data_inspector.cpp b/plugins/builtin/source/content/views/view_data_inspector.cpp index 4e173ea59..64fedf741 100644 --- a/plugins/builtin/source/content/views/view_data_inspector.cpp +++ b/plugins/builtin/source/content/views/view_data_inspector.cpp @@ -4,8 +4,7 @@ #include -#include - +#include #include #include @@ -144,6 +143,8 @@ namespace hex::plugin::builtin { editingFunction, false }); + + AchievementManager::unlockAchievement("hex.builtin.achievement.patterns", "hex.builtin.achievement.patterns.data_inspector.name"); } catch (const pl::core::err::EvaluatorError::Exception &error) { log::error("Failed to get value of pattern '{}': {}", pattern->getDisplayName(), error.what()); } diff --git a/plugins/builtin/source/content/views/view_data_processor.cpp b/plugins/builtin/source/content/views/view_data_processor.cpp index ad41549a2..e944aeb83 100644 --- a/plugins/builtin/source/content/views/view_data_processor.cpp +++ b/plugins/builtin/source/content/views/view_data_processor.cpp @@ -2,12 +2,11 @@ #include "content/popups/popup_notification.hpp" #include - -#include +#include +#include #include -#include - +#include #include #include @@ -166,6 +165,8 @@ namespace hex::plugin::builtin { editing = ImGui::IsItemActive(); if (ImGui::Button("hex.builtin.nodes.custom.custom.edit"_lang, ImVec2(200_scaled, ImGui::GetTextLineHeightWithSpacing()))) { + AchievementManager::unlockAchievement("hex.builtin.achievement.data_processor", "hex.builtin.achievement.data_processor.custom_node.name"); + this->m_dataProcessor->getWorkspaceStack().push_back(&this->m_workspace); this->m_requiresAttributeUpdate = true; @@ -595,6 +596,7 @@ namespace hex::plugin::builtin { ImNodes::SetNodeScreenSpacePos(node->getId(), this->m_rightClickedCoords); workspace.nodes.push_back(std::move(node)); ImHexApi::Provider::markDirty(); + AchievementManager::unlockAchievement("hex.builtin.achievement.data_processor", "hex.builtin.achievement.data_processor.place_node.name"); } ImGui::EndPopup(); @@ -818,6 +820,8 @@ namespace hex::plugin::builtin { fromAttr->addConnectedAttribute(newLink.getId(), toAttr); toAttr->addConnectedAttribute(newLink.getId(), fromAttr); + + AchievementManager::unlockAchievement("hex.builtin.achievement.data_processor", "hex.builtin.achievement.data_processor.create_connection.name"); } while (false); } } diff --git a/plugins/builtin/source/content/views/view_find.cpp b/plugins/builtin/source/content/views/view_find.cpp index 2edaea4e2..a3ed4162b 100644 --- a/plugins/builtin/source/content/views/view_find.cpp +++ b/plugins/builtin/source/content/views/view_find.cpp @@ -1,6 +1,8 @@ #include "content/views/view_find.hpp" #include +#include + #include #include @@ -439,6 +441,16 @@ namespace hex::plugin::builtin { void ViewFind::runSearch() { Region searchRegion = this->m_searchSettings.region; + if (this->m_searchSettings.mode == SearchSettings::Mode::Strings) + AchievementManager::unlockAchievement("hex.builtin.achievement.find", "hex.builtin.achievement.find.find_strings.name"); + else if (this->m_searchSettings.mode == SearchSettings::Mode::Sequence) + AchievementManager::unlockAchievement("hex.builtin.achievement.find", "hex.builtin.achievement.find.find_specific_string.name"); + else if (this->m_searchSettings.mode == SearchSettings::Mode::Value) { + if (this->m_searchSettings.value.inputMin == "250" && this->m_searchSettings.value.inputMax == "1000") + AchievementManager::unlockAchievement("hex.builtin.achievement.find", "hex.builtin.achievement.find.find_specific_string.name"); + } + + this->m_searchTask = TaskManager::createTask("hex.builtin.view.find.searching", searchRegion.getSize(), [this, settings = this->m_searchSettings, searchRegion](auto &task) { auto provider = ImHexApi::Provider::get(); diff --git a/plugins/builtin/source/content/views/view_hashes.cpp b/plugins/builtin/source/content/views/view_hashes.cpp index 8849b6b46..56a111545 100644 --- a/plugins/builtin/source/content/views/view_hashes.cpp +++ b/plugins/builtin/source/content/views/view_hashes.cpp @@ -3,6 +3,8 @@ #include "content/providers/memory_file_provider.hpp" #include +#include + #include #include @@ -176,8 +178,10 @@ namespace hex::plugin::builtin { ImGui::BeginDisabled(this->m_newHashName.empty() || this->m_selectedHash == nullptr); if (ImGui::IconButton(ICON_VS_ADD, ImGui::GetStyleColorVec4(ImGuiCol_Text))) { - if (this->m_selectedHash != nullptr) + if (this->m_selectedHash != nullptr) { this->m_hashFunctions->push_back(this->m_selectedHash->create(this->m_newHashName)); + AchievementManager::unlockAchievement("hex.builtin.achievement.misc", "hex.builtin.achievement.misc.create_hash.name"); + } } ImGui::EndDisabled(); diff --git a/plugins/builtin/source/content/views/view_hex_editor.cpp b/plugins/builtin/source/content/views/view_hex_editor.cpp index bf0952da1..0379b8e1a 100644 --- a/plugins/builtin/source/content/views/view_hex_editor.cpp +++ b/plugins/builtin/source/content/views/view_hex_editor.cpp @@ -2,10 +2,13 @@ #include #include -#include -#include -#include #include +#include + +#include +#include + +#include #include #include @@ -540,6 +543,8 @@ namespace hex::plugin::builtin { auto remainingSize = std::min(size - i, bytes.size()); provider->write(provider->getBaseAddress() + address + i, bytes.data(), remainingSize); } + + AchievementManager::unlockAchievement("hex.builtin.achievement.hex_editor", "hex.builtin.achievement.hex_editor.fill.name"); } private: diff --git a/plugins/builtin/source/content/views/view_information.cpp b/plugins/builtin/source/content/views/view_information.cpp index 259a4a41a..4f579ce5d 100644 --- a/plugins/builtin/source/content/views/view_information.cpp +++ b/plugins/builtin/source/content/views/view_information.cpp @@ -1,6 +1,7 @@ #include "content/views/view_information.hpp" #include +#include #include #include @@ -64,6 +65,8 @@ namespace hex::plugin::builtin { } void ViewInformation::analyze() { + AchievementManager::unlockAchievement("hex.builtin.achievement.misc", "hex.builtin.achievement.misc.analyze_file.name"); + this->m_analyzerTask = TaskManager::createTask("hex.builtin.view.information.analyzing", 0, [this](auto &task) { auto provider = ImHexApi::Provider::get(); diff --git a/plugins/builtin/source/content/views/view_pattern_editor.cpp b/plugins/builtin/source/content/views/view_pattern_editor.cpp index 373b7a386..2d1b4050b 100644 --- a/plugins/builtin/source/content/views/view_pattern_editor.cpp +++ b/plugins/builtin/source/content/views/view_pattern_editor.cpp @@ -1,6 +1,8 @@ #include "content/views/view_pattern_editor.hpp" #include +#include +#include #include #include @@ -11,7 +13,6 @@ #include #include -#include #include #include @@ -1086,6 +1087,7 @@ namespace hex::plugin::builtin { auto selection = ImHexApi::HexEditor::getSelection(); appendEditorText(hex::format("{0} {0}_at_0x{1:02X} @ 0x{1:02X};", type, selection->getStartAddress())); + AchievementManager::unlockAchievement("hex.builtin.achievement.patterns", "hex.builtin.achievement.patterns.place_menu.name"); } void ViewPatternEditor::appendArray(const std::string &type, size_t size) { @@ -1115,6 +1117,7 @@ namespace hex::plugin::builtin { PopupFileChooser::open(paths, std::vector{ { "Pattern File", "hexpat" } }, false, [this, provider](const std::fs::path &path) { this->loadPatternFile(path, provider); + AchievementManager::unlockAchievement("hex.builtin.achievement.patterns", "hex.builtin.achievement.patterns.load_existing.name"); }); }, ImHexApi::Provider::isValid); diff --git a/plugins/builtin/source/content/views/view_store.cpp b/plugins/builtin/source/content/views/view_store.cpp index 971126f15..e923f4e59 100644 --- a/plugins/builtin/source/content/views/view_store.cpp +++ b/plugins/builtin/source/content/views/view_store.cpp @@ -1,5 +1,6 @@ #include "content/views/view_store.hpp" #include +#include #include #include @@ -93,6 +94,7 @@ namespace hex::plugin::builtin { } else if (!entry.installed) { if (ImGui::Button("hex.builtin.view.store.download"_lang, buttonSize)) { entry.downloading = this->download(category.path, entry.fileName, entry.link, false); + AchievementManager::unlockAchievement("hex.builtin.achievement.misc", "hex.builtin.achievement.misc.download_from_store.name"); } } else { if (ImGui::Button("hex.builtin.view.store.remove"_lang, buttonSize)) { diff --git a/plugins/builtin/source/content/welcome_screen.cpp b/plugins/builtin/source/content/welcome_screen.cpp index a43bffe4c..e8ffda2a1 100644 --- a/plugins/builtin/source/content/welcome_screen.cpp +++ b/plugins/builtin/source/content/welcome_screen.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -284,6 +285,10 @@ namespace hex::plugin::builtin { { if (ImGui::DescriptionButton("hex.builtin.welcome.learn.latest.title"_lang, "hex.builtin.welcome.learn.latest.desc"_lang, ImVec2(ImGui::GetContentRegionAvail().x * 0.8F, 0))) hex::openWebpage("hex.builtin.welcome.learn.latest.link"_lang); + if (ImGui::DescriptionButton("hex.builtin.welcome.learn.imhex.title"_lang, "hex.builtin.welcome.learn.imhex.desc"_lang, ImVec2(ImGui::GetContentRegionAvail().x * 0.8F, 0))) { + AchievementManager::unlockAchievement("hex.builtin.achievement.starting_out", "hex.builtin.achievement.starting_out.docs.name"); + hex::openWebpage("hex.builtin.welcome.learn.imhex.link"_lang); + } if (ImGui::DescriptionButton("hex.builtin.welcome.learn.pattern.title"_lang, "hex.builtin.welcome.learn.pattern.desc"_lang, ImVec2(ImGui::GetContentRegionAvail().x * 0.8F, 0))) hex::openWebpage("hex.builtin.welcome.learn.pattern.link"_lang); if (ImGui::DescriptionButton("hex.builtin.welcome.learn.plugins.title"_lang, "hex.builtin.welcome.learn.plugins.desc"_lang, ImVec2(ImGui::GetContentRegionAvail().x * 0.8F, 0))) @@ -527,6 +532,10 @@ namespace hex::plugin::builtin { if (showTipOfTheDay) PopupTipOfTheDay::open(); } + + if (hasCrashed) { + AchievementManager::unlockAchievement("hex.builtin.achievement.starting_out", "hex.builtin.achievement.starting_out.crash.name"); + } } } diff --git a/plugins/builtin/source/plugin_builtin.cpp b/plugins/builtin/source/plugin_builtin.cpp index be1a5841d..97b12b899 100644 --- a/plugins/builtin/source/plugin_builtin.cpp +++ b/plugins/builtin/source/plugin_builtin.cpp @@ -38,6 +38,7 @@ namespace hex::plugin::builtin { void registerNetworkEndpoints(); void registerFileHandlers(); void registerProjectHandlers(); + void registerAchievements(); void addFooterItems(); void addTitleBarButtons(); @@ -96,6 +97,7 @@ IMHEX_PLUGIN_SETUP("Built-in", "WerWolv", "Default ImHex functionality") { registerFileHandlers(); registerProjectHandlers(); registerCommandForwarders(); + registerAchievements(); addFooterItems(); addTitleBarButtons(); diff --git a/plugins/builtin/source/ui/pattern_drawer.cpp b/plugins/builtin/source/ui/pattern_drawer.cpp index 25fee0f93..e7778d8bb 100644 --- a/plugins/builtin/source/ui/pattern_drawer.cpp +++ b/plugins/builtin/source/ui/pattern_drawer.cpp @@ -21,8 +21,10 @@ #include #include -#include +#include #include + +#include #include #include @@ -332,6 +334,7 @@ namespace hex::plugin::builtin::ui { if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { this->m_editingPattern = &pattern; this->m_editingPatternOffset = pattern.getOffset(); + AchievementManager::unlockAchievement("hex.builtin.achievement.patterns", "hex.builtin.achievement.patterns.modify_data.name"); } ImGui::SameLine(0, 0);