2023-08-06 21:33:15 +02:00
|
|
|
#include <hex/api/achievement_manager.hpp>
|
2023-11-30 11:23:12 +01:00
|
|
|
#include <hex/api/event_manager.hpp>
|
2023-08-06 21:33:15 +02:00
|
|
|
|
2024-01-30 11:21:34 +01:00
|
|
|
#include <hex/helpers/auto_reset.hpp>
|
|
|
|
|
2023-08-06 21:33:15 +02:00
|
|
|
#include <nlohmann/json.hpp>
|
|
|
|
|
|
|
|
namespace hex {
|
|
|
|
|
2024-02-10 23:31:05 +01:00
|
|
|
static AutoReset<std::unordered_map<std::string, std::unordered_map<std::string, std::unique_ptr<Achievement>>>> s_achievements;
|
|
|
|
const std::unordered_map<std::string, std::unordered_map<std::string, std::unique_ptr<Achievement>>> &AchievementManager::getAchievements() {
|
|
|
|
return *s_achievements;
|
2023-08-06 21:33:15 +02:00
|
|
|
}
|
|
|
|
|
2024-02-10 23:31:05 +01:00
|
|
|
static AutoReset<std::unordered_map<std::string, std::list<AchievementManager::AchievementNode>>> s_nodeCategoryStorage;
|
|
|
|
std::unordered_map<std::string, std::list<AchievementManager::AchievementNode>>& getAchievementNodesMutable(bool rebuild) {
|
|
|
|
if (!s_nodeCategoryStorage->empty() || !rebuild)
|
|
|
|
return s_nodeCategoryStorage;
|
2023-08-06 21:33:15 +02:00
|
|
|
|
2024-02-10 23:31:05 +01:00
|
|
|
s_nodeCategoryStorage->clear();
|
2023-08-06 21:33:15 +02:00
|
|
|
|
|
|
|
// Add all achievements to the node storage
|
2024-02-10 23:31:05 +01:00
|
|
|
for (auto &[categoryName, achievements] : AchievementManager::getAchievements()) {
|
|
|
|
auto &nodes = (*s_nodeCategoryStorage)[categoryName];
|
2023-08-06 21:33:15 +02:00
|
|
|
|
|
|
|
for (auto &[achievementName, achievement] : achievements) {
|
|
|
|
nodes.emplace_back(achievement.get());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-10 23:31:05 +01:00
|
|
|
return s_nodeCategoryStorage;
|
|
|
|
}
|
|
|
|
|
|
|
|
const std::unordered_map<std::string, std::list<AchievementManager::AchievementNode>>& AchievementManager::getAchievementNodes(bool rebuild) {
|
|
|
|
return getAchievementNodesMutable(rebuild);
|
2023-08-06 21:33:15 +02:00
|
|
|
}
|
|
|
|
|
2024-02-10 23:31:05 +01:00
|
|
|
static AutoReset<std::unordered_map<std::string, std::vector<AchievementManager::AchievementNode*>>> s_startNodes;
|
|
|
|
const std::unordered_map<std::string, std::vector<AchievementManager::AchievementNode*>>& AchievementManager::getAchievementStartNodes(bool rebuild) {
|
2023-08-06 21:33:15 +02:00
|
|
|
|
2024-02-10 23:31:05 +01:00
|
|
|
if (!s_startNodes->empty() || !rebuild)
|
|
|
|
return s_startNodes;
|
2023-08-06 21:33:15 +02:00
|
|
|
|
2024-02-10 23:31:05 +01:00
|
|
|
auto &nodeCategoryStorage = getAchievementNodesMutable(rebuild);
|
2023-08-06 21:33:15 +02:00
|
|
|
|
2024-02-10 23:31:05 +01:00
|
|
|
s_startNodes->clear();
|
2023-08-06 21:33:15 +02:00
|
|
|
|
|
|
|
// 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) {
|
2024-01-30 11:21:34 +01:00
|
|
|
auto iter = std::ranges::find_if(requirementAchievements, [&requirement](auto &node) {
|
2023-08-06 21:33:15 +02:00
|
|
|
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) {
|
2024-01-30 11:21:34 +01:00
|
|
|
auto iter = std::ranges::find_if(requirementAchievements, [&requirement](auto &node) {
|
2023-08-06 21:33:15 +02:00
|
|
|
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()) {
|
2024-02-10 23:31:05 +01:00
|
|
|
(*s_startNodes)[categoryName].emplace_back(&achievementNode);
|
2023-08-06 21:33:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
for (const auto &parent : achievementNode.parents) {
|
|
|
|
if (parent->achievement->getUnlocalizedCategory() != achievementNode.achievement->getUnlocalizedCategory())
|
2024-02-10 23:31:05 +01:00
|
|
|
(*s_startNodes)[categoryName].emplace_back(&achievementNode);
|
2023-08-06 21:33:15 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-10 23:31:05 +01:00
|
|
|
return s_startNodes;
|
2023-08-06 21:33:15 +02:00
|
|
|
}
|
|
|
|
|
2023-12-19 12:22:28 +01:00
|
|
|
void AchievementManager::unlockAchievement(const UnlocalizedString &unlocalizedCategory, const UnlocalizedString &unlocalizedName) {
|
2023-08-06 21:33:15 +02:00
|
|
|
auto &categories = getAchievements();
|
|
|
|
|
|
|
|
auto categoryIter = categories.find(unlocalizedCategory);
|
|
|
|
if (categoryIter == categories.end()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
auto &[categoryName, achievements] = *categoryIter;
|
|
|
|
|
2024-01-30 11:21:34 +01:00
|
|
|
const auto achievementIter = achievements.find(unlocalizedName);
|
2023-08-06 21:33:15 +02:00
|
|
|
if (achievementIter == achievements.end()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-02-10 23:31:05 +01:00
|
|
|
const auto &nodes = getAchievementNodes();
|
|
|
|
if (!nodes.contains(categoryName))
|
|
|
|
return;
|
|
|
|
|
|
|
|
for (const auto &node : nodes.at(categoryName)) {
|
2023-08-06 21:33:15 +02:00
|
|
|
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())
|
2023-12-08 10:29:44 +01:00
|
|
|
EventAchievementUnlocked::post(*achievement);
|
2023-10-20 13:34:45 +02:00
|
|
|
|
|
|
|
return;
|
2023-08-06 21:33:15 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void AchievementManager::clearTemporary() {
|
2024-02-10 23:31:05 +01:00
|
|
|
auto &categories = *s_achievements;
|
2023-08-06 21:33:15 +02:00
|
|
|
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();
|
|
|
|
});
|
|
|
|
|
2024-02-10 23:31:05 +01:00
|
|
|
s_startNodes->clear();
|
|
|
|
s_nodeCategoryStorage->clear();
|
2023-08-06 21:33:15 +02:00
|
|
|
}
|
|
|
|
|
2023-11-16 13:23:28 +01:00
|
|
|
std::pair<u32, u32> 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 };
|
|
|
|
}
|
|
|
|
|
2023-08-06 21:33:15 +02:00
|
|
|
void AchievementManager::achievementAdded() {
|
2024-02-10 23:31:05 +01:00
|
|
|
s_startNodes->clear();
|
|
|
|
s_nodeCategoryStorage->clear();
|
|
|
|
}
|
|
|
|
|
|
|
|
Achievement &AchievementManager::addAchievementImpl(std::unique_ptr<Achievement> &&newAchievement) {
|
|
|
|
const auto &category = newAchievement->getUnlocalizedCategory();
|
|
|
|
const auto &name = newAchievement->getUnlocalizedName();
|
|
|
|
|
|
|
|
auto [categoryIter, categoryInserted] = s_achievements->insert({ category, std::unordered_map<std::string, std::unique_ptr<Achievement>>{} });
|
|
|
|
auto &[categoryKey, achievements] = *categoryIter;
|
|
|
|
|
|
|
|
auto [achievementIter, achievementInserted] = achievements.insert({ name, std::move(newAchievement) });
|
|
|
|
auto &[achievementKey, achievement] = *achievementIter;
|
|
|
|
|
|
|
|
achievementAdded();
|
|
|
|
|
|
|
|
return *achievement;
|
2023-08-06 21:33:15 +02:00
|
|
|
}
|
|
|
|
|
2024-02-10 23:31:05 +01:00
|
|
|
|
2023-08-06 21:33:15 +02:00
|
|
|
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 {
|
2023-12-31 13:53:28 +01:00
|
|
|
const auto &progress = json[categoryName][achievementName];
|
|
|
|
if (progress.is_null())
|
|
|
|
continue;
|
|
|
|
|
|
|
|
achievement->setProgress(progress);
|
2023-08-06 21:33:15 +02:00
|
|
|
} 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() {
|
2024-02-20 00:10:05 +01:00
|
|
|
nlohmann::json json;
|
|
|
|
for (const auto &[categoryName, achievements] : getAchievements()) {
|
|
|
|
json[categoryName] = nlohmann::json::object();
|
2023-08-06 21:33:15 +02:00
|
|
|
|
2024-02-20 00:10:05 +01:00
|
|
|
for (const auto &[achievementName, achievement] : achievements) {
|
|
|
|
json[categoryName][achievementName] = achievement->getProgress();
|
2023-08-06 21:33:15 +02:00
|
|
|
}
|
2024-02-20 00:10:05 +01:00
|
|
|
}
|
2023-08-06 21:33:15 +02:00
|
|
|
|
2024-02-20 00:10:05 +01:00
|
|
|
if (json.empty())
|
|
|
|
return;
|
2023-08-06 21:33:15 +02:00
|
|
|
|
2024-02-20 00:10:05 +01:00
|
|
|
for (const auto &directory : fs::getDefaultPaths(fs::ImHexPath::Config)) {
|
|
|
|
auto path = directory / AchievementsFile;
|
2023-08-06 21:33:15 +02:00
|
|
|
|
2024-02-20 00:10:05 +01:00
|
|
|
wolv::io::File file(path, wolv::io::File::Mode::Create);
|
|
|
|
if (!file.isValid())
|
|
|
|
continue;
|
2023-08-06 21:33:15 +02:00
|
|
|
|
2024-02-20 00:10:05 +01:00
|
|
|
file.writeString(json.dump(4));
|
2023-08-06 21:33:15 +02:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|