7e0f60615b
### Problem description Loading a file on the command line or using the context menu in windows was triggering an achievement before the achievements file was loaded. This resulted on the achievements.json file to be overwritten with all the achievements reset except for the one to open files. ### Implementation description This PR fixes the problem by introducing a boolean that is used to check if the file has been loaded. --------- Co-authored-by: Nik <werwolv98@gmail.com>
269 lines
9.7 KiB
C++
269 lines
9.7 KiB
C++
#include <hex/api/achievement_manager.hpp>
|
|
#include <hex/api/event_manager.hpp>
|
|
|
|
#include <hex/helpers/auto_reset.hpp>
|
|
#include <hex/helpers/default_paths.hpp>
|
|
|
|
#include <nlohmann/json.hpp>
|
|
|
|
namespace hex {
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
|
|
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<std::string, std::list<AchievementManager::AchievementNode>>& AchievementManager::getAchievementNodes(bool rebuild) {
|
|
return getAchievementNodesMutable(rebuild);
|
|
}
|
|
|
|
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) {
|
|
|
|
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<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 };
|
|
}
|
|
|
|
void AchievementManager::achievementAdded() {
|
|
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;
|
|
}
|
|
|
|
|
|
constexpr static auto AchievementsFile = "achievements.json";
|
|
bool AchievementManager::s_initialized = false;
|
|
|
|
void AchievementManager::loadProgress() {
|
|
if (s_initialized)
|
|
return;
|
|
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());
|
|
}
|
|
}
|
|
}
|
|
|
|
s_initialized = true;
|
|
} catch (const std::exception &e) {
|
|
log::error("Failed to load achievements: {}", e.what());
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
void AchievementManager::storeProgress() {
|
|
if (!s_initialized)
|
|
loadProgress();
|
|
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;
|
|
}
|
|
}
|
|
|
|
} |