1
0
mirror of synced 2025-02-02 20:37:18 +01:00
ImHex/lib/libimhex/source/api/achievement_manager.cpp
Wolf b646ece14b
impr: Refactor and restructure Event Manager (#2082)
### Problem description
This PR addresses issue #2013 that described a cluttered Event Manager.

This is a DX issue and should not impact the users whatsoever.

### Implementation description
The changes revolve around three main points:

1. the Event Manager (`event_manager.hpp`) was split into four
categories: GUI, Interaction, Lifecycle, and Provider, and two types:
Events, and Requests. This results in the following files:
    - `events_gui.hpp`
    - `events_interaction.hpp`
    - `events_lifecycle.hpp`
    - `events_provider.hpp`
    - `requests_gui.hpp`
    - `requests_interaction.hpp`
    - `requests_lifecycle.hpp`
    - `requests_provider.hpp`

2. Every event and request now has its own piece of documentation, with
a `@brief`, accompanied by a longer comment if needed, and gets its
`@param`s described.

3. The old `event_manager.hpp` import was removed and replaced by the
correct imports wherever needed, as to reduce spread of those files only
to where they are truly useful.

### Additional things
The commits have been split into (chrono-)logical steps:
- `feat`: split the Event Manager, and replace the imports
- `refactor`, `chore`: make various small changes to match the required
structure
- `docs`: add documentation for events and requests

Hopefully, this will help to review the PR.
*Note: Beware of very long rebuild times in between the commits, use
them sparingly! The Actions will ensure this PR builds anyways*

Closes #2013

---------

Signed-off-by: BioTheWolff <47079795+BioTheWolff@users.noreply.github.com>
Co-authored-by: Nik <werwolv98@gmail.com>
2025-01-25 15:32:07 +00:00

289 lines
10 KiB
C++

#include <hex/api/achievement_manager.hpp>
#include <hex/api/events/events_interaction.hpp>
#include <hex/helpers/auto_reset.hpp>
#include <hex/helpers/default_paths.hpp>
#include <nlohmann/json.hpp>
#if defined(OS_WEB)
#include <emscripten.h>
#endif
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 {
#if defined(OS_WEB)
auto data = (char *) MAIN_THREAD_EM_ASM_INT({
let data = localStorage.getItem("achievements");
return data ? stringToNewUTF8(data) : null;
});
#else
auto data = file.readString();
#endif
auto json = nlohmann::json::parse(data);
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;
#if defined(OS_WEB)
auto data = json.dump();
MAIN_THREAD_EM_ASM({
localStorage.setItem("achievements", UTF8ToString($0));
}, data.c_str());
#else
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;
}
#endif
}
}