#include #include #include #include #include namespace hex { static AutoReset>>> s_achievements; const std::unordered_map>> &AchievementManager::getAchievements() { return *s_achievements; } static AutoReset>> s_nodeCategoryStorage; std::unordered_map>& getAchievementNodesMutable(bool rebuild) { if (!s_nodeCategoryStorage->empty() || !rebuild) return s_nodeCategoryStorage; s_nodeCategoryStorage->clear(); // Add all achievements to the node storage for (auto &[categoryName, achievements] : AchievementManager::getAchievements()) { auto &nodes = (*s_nodeCategoryStorage)[categoryName]; for (auto &[achievementName, achievement] : achievements) { nodes.emplace_back(achievement.get()); } } return s_nodeCategoryStorage; } const std::unordered_map>& AchievementManager::getAchievementNodes(bool rebuild) { return getAchievementNodesMutable(rebuild); } static AutoReset>> s_startNodes; const std::unordered_map>& AchievementManager::getAchievementStartNodes(bool rebuild) { if (!s_startNodes->empty() || !rebuild) return s_startNodes; auto &nodeCategoryStorage = getAchievementNodesMutable(rebuild); s_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::ranges::find_if(requirementAchievements, [&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::ranges::find_if(requirementAchievements, [&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()) { (*s_startNodes)[categoryName].emplace_back(&achievementNode); } for (const auto &parent : achievementNode.parents) { if (parent->achievement->getUnlocalizedCategory() != achievementNode.achievement->getUnlocalizedCategory()) (*s_startNodes)[categoryName].emplace_back(&achievementNode); } } } return s_startNodes; } void AchievementManager::unlockAchievement(const UnlocalizedString &unlocalizedCategory, const UnlocalizedString &unlocalizedName) { auto &categories = getAchievements(); auto categoryIter = categories.find(unlocalizedCategory); if (categoryIter == categories.end()) { return; } auto &[categoryName, achievements] = *categoryIter; const auto achievementIter = achievements.find(unlocalizedName); if (achievementIter == achievements.end()) { return; } const auto &nodes = getAchievementNodes(); if (!nodes.contains(categoryName)) return; for (const auto &node : nodes.at(categoryName)) { 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()) EventAchievementUnlocked::post(*achievement); return; } } void AchievementManager::clearTemporary() { auto &categories = *s_achievements; 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(); }); s_startNodes->clear(); s_nodeCategoryStorage->clear(); } std::pair AchievementManager::getProgress() { u32 unlocked = 0; u32 total = 0; for (auto &[categoryName, achievements] : getAchievements()) { for (auto &[achievementName, achievement] : achievements) { total += 1; if (achievement->isUnlocked()) { unlocked += 1; } } } return { unlocked, total }; } void AchievementManager::achievementAdded() { s_startNodes->clear(); s_nodeCategoryStorage->clear(); } Achievement &AchievementManager::addAchievementImpl(std::unique_ptr &&newAchievement) { const auto &category = newAchievement->getUnlocalizedCategory(); const auto &name = newAchievement->getUnlocalizedName(); auto [categoryIter, categoryInserted] = s_achievements->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; } constexpr static auto AchievementsFile = "achievements.json"; void AchievementManager::loadProgress() { for (const auto &directory : paths::Config.read()) { 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 { const auto &progress = json[categoryName][achievementName]; if (progress.is_null()) continue; achievement->setProgress(progress); } 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() { 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(); } } if (json.empty()) return; for (const auto &directory : paths::Config.write()) { auto path = directory / AchievementsFile; wolv::io::File file(path, wolv::io::File::Mode::Create); if (!file.isValid()) continue; file.writeString(json.dump(4)); break; } } }