summaryrefslogtreecommitdiff
path: root/src/game_features.cpp
diff options
context:
space:
mode:
authorMikaƫl Capelle <capelle.mikael@gmail.com>2024-06-09 12:18:17 +0200
committerGitHub <noreply@github.com>2024-06-09 12:18:17 +0200
commit88c386d74d7d94ec16d4d84ed674eb51e31647ac (patch)
treeadf0b2537f12d11d8df77f68a84926d15b063554 /src/game_features.cpp
parentc43535f5bcf1043c3b9c00c77967267cf1acf60f (diff)
Refactoring of game features for better management. (#2043)
Diffstat (limited to 'src/game_features.cpp')
-rw-r--r--src/game_features.cpp340
1 files changed, 340 insertions, 0 deletions
diff --git a/src/game_features.cpp b/src/game_features.cpp
new file mode 100644
index 00000000..bc083bc8
--- /dev/null
+++ b/src/game_features.cpp
@@ -0,0 +1,340 @@
+#include "game_features.h"
+
+#include <algorithm>
+
+#include <QTimer>
+
+#include "iplugingame.h"
+#include "log.h"
+
+#include "bsainvalidation.h"
+#include "dataarchives.h"
+#include "gameplugins.h"
+#include "localsavegames.h"
+#include "organizercore.h"
+#include "plugincontainer.h"
+#include "savegameinfo.h"
+#include "scriptextender.h"
+#include "unmanagedmods.h"
+
+using namespace MOBase;
+
+// avoid updating features more than once every 100ms
+constexpr std::chrono::milliseconds UPDATE_QUEUE_TIMER{100};
+
+// specific type index for checker and content that behaves differently
+const std::type_index ModDataCheckerIndex{typeid(ModDataChecker)};
+const std::type_index ModDataContentIndex{typeid(ModDataContent)};
+
+class GameFeatures::CombinedModDataChecker : public ModDataChecker
+{
+ std::vector<std::shared_ptr<ModDataChecker>> m_modDataCheckers;
+ mutable std::shared_ptr<ModDataChecker> m_fixer = nullptr;
+
+public:
+ void setCheckers(std::vector<std::shared_ptr<ModDataChecker>> checkers)
+ {
+ m_modDataCheckers = std::move(checkers);
+ m_fixer = nullptr;
+ }
+
+ bool isValid() const { return !m_modDataCheckers.empty(); }
+
+ CheckReturn
+ dataLooksValid(std::shared_ptr<const MOBase::IFileTree> fileTree) const override
+ {
+ m_fixer = nullptr;
+
+ for (auto& modDataChecker : m_modDataCheckers) {
+ auto check = modDataChecker->dataLooksValid(fileTree);
+
+ switch (check) {
+ case CheckReturn::FIXABLE:
+ m_fixer = modDataChecker;
+ [[fallthrough]];
+ case CheckReturn::VALID:
+ return CheckReturn::VALID;
+ case CheckReturn::INVALID:
+ break;
+ }
+ }
+ return CheckReturn::INVALID;
+ }
+
+ std::shared_ptr<MOBase::IFileTree>
+ fix(std::shared_ptr<MOBase::IFileTree> fileTree) const override
+ {
+ if (m_fixer) {
+ return m_fixer->fix(fileTree);
+ }
+
+ return nullptr;
+ }
+};
+class GameFeatures::CombinedModDataContent : public ModDataContent
+{
+ // store the ModDataContent and the offset to add to the content
+ std::vector<std::pair<std::shared_ptr<ModDataContent>, int>> m_modDataContents;
+ std::vector<Content> m_allContents;
+
+public:
+ bool isValid() const { return !m_modDataContents.empty(); }
+
+ void setContents(std::vector<std::shared_ptr<ModDataContent>> modDataContents)
+ {
+ m_modDataContents.clear();
+ m_modDataContents.reserve(modDataContents.size());
+
+ m_allContents.clear();
+
+ // update all contents and offsets
+ std::size_t offset = 0;
+ for (auto& modDataContent : modDataContents) {
+
+ m_modDataContents.emplace_back(modDataContent, static_cast<int>(offset));
+
+ // add to the list of contents
+ auto contents = modDataContent->getAllContents();
+ m_allContents.insert(m_allContents.end(),
+ std::make_move_iterator(contents.begin()),
+ std::make_move_iterator(contents.end()));
+
+ // increase offset for next mod data content
+
+ offset += contents.size();
+ }
+ }
+
+ std::vector<Content> getAllContents() const override { return m_allContents; }
+
+ std::vector<int>
+ getContentsFor(std::shared_ptr<const MOBase::IFileTree> fileTree) const
+ {
+ std::vector<int> contentsFor;
+ for (auto& modDataContent : m_modDataContents) {
+ auto contentsForFrom = modDataContent.first->getContentsFor(fileTree);
+ std::transform(contentsForFrom.begin(), contentsForFrom.end(),
+ std::back_inserter(contentsFor),
+ [offset = modDataContent.second](auto content) {
+ return content + offset;
+ });
+ }
+
+ return contentsFor;
+ }
+};
+
+GameFeatures::GameFeatures(OrganizerCore* core, PluginContainer* plugins)
+ : m_pluginContainer(*plugins),
+ m_modDataChecker(std::make_unique<CombinedModDataChecker>()),
+ m_modDataContent(std::make_unique<CombinedModDataContent>())
+{
+ // can be nullptr since the plugin container can be initialized with a Core (e.g.,
+ // on first MO2 start)
+ if (core) {
+ connect(core, &OrganizerCore::managedGameChanged, [this] {
+ updateCurrentFeatures();
+ });
+ }
+
+ auto updateFeatures = [this] {
+ QTimer::singleShot(UPDATE_QUEUE_TIMER, [this] {
+ updateCurrentFeatures();
+ });
+ };
+
+ connect(plugins, &PluginContainer::pluginEnabled, updateFeatures);
+ connect(plugins, &PluginContainer::pluginDisabled, updateFeatures);
+ connect(plugins, &PluginContainer::pluginRegistered, updateFeatures);
+ connect(plugins, &PluginContainer::pluginUnregistered,
+ [this, updateFeatures](MOBase::IPlugin* plugin) {
+ // remove features from the current plugin
+ for (auto& [_, features] : m_allFeatures) {
+ features.erase(std::remove_if(features.begin(), features.end(),
+ [plugin](const auto& feature) {
+ return feature.plugin() == plugin;
+ }),
+ features.end());
+ }
+
+ // update current features
+ updateFeatures();
+ });
+}
+
+GameFeatures::~GameFeatures() {}
+
+GameFeatures::CombinedModDataChecker& GameFeatures::modDataChecker() const
+{
+ return dynamic_cast<CombinedModDataChecker&>(*m_modDataChecker);
+}
+GameFeatures::CombinedModDataContent& GameFeatures::modDataContent() const
+{
+ return dynamic_cast<CombinedModDataContent&>(*m_modDataContent);
+}
+
+void GameFeatures::updateCurrentFeatures(std::type_index const& index)
+{
+ auto& features = m_allFeatures[index];
+
+ m_currentFeatures[index].clear();
+
+ // this can occur when starting MO2, just wait for the next update
+ if (!m_pluginContainer.managedGame()) {
+ return;
+ }
+
+ for (const auto& dataFeature : features) {
+
+ // registering plugin is disabled
+ if (!m_pluginContainer.isEnabled(dataFeature.plugin())) {
+ continue;
+ }
+
+ // games does not match
+ if (!dataFeature.games().isEmpty() &&
+ !dataFeature.games().contains(m_pluginContainer.managedGame()->gameName())) {
+ continue;
+ }
+
+ m_currentFeatures[index].push_back(dataFeature.feature());
+ }
+
+ // update mod data checker
+ if (index == ModDataCheckerIndex) {
+ auto& currentCheckers = m_currentFeatures[ModDataCheckerIndex];
+ std::vector<std::shared_ptr<ModDataChecker>> checkers;
+ checkers.reserve(currentCheckers.size());
+ std::transform(currentCheckers.begin(), currentCheckers.end(),
+ std::back_inserter(checkers), [](auto const& checker) {
+ return std::dynamic_pointer_cast<ModDataChecker>(checker);
+ });
+ modDataChecker().setCheckers(std::move(checkers));
+ emit modDataCheckerUpdated(gameFeature<ModDataChecker>().get());
+ }
+
+ // update mod data content
+ if (index == ModDataContentIndex) {
+ auto& currentContents = m_currentFeatures[ModDataContentIndex];
+ std::vector<std::shared_ptr<ModDataContent>> contents;
+ contents.reserve(currentContents.size());
+ std::transform(currentContents.begin(), currentContents.end(),
+ std::back_inserter(contents), [](auto const& checker) {
+ return std::dynamic_pointer_cast<ModDataContent>(checker);
+ });
+ modDataContent().setContents(std::move(contents));
+ emit modDataContentUpdated(gameFeature<ModDataContent>().get());
+ }
+}
+
+void GameFeatures::updateCurrentFeatures()
+{
+ // TODO: this completely refilters everything currently, which should be ok since
+ // this should only be triggered by function that are uncommon (enabling/disabling
+ // plugin or changing the game), it should be possible to filter more finely by
+ // maintaining more information (e.g., only removing features from current when a
+ // plugin is disabled or unregistered)
+ //
+
+ for (auto& [info, _] : m_allFeatures) {
+ updateCurrentFeatures(info);
+ }
+}
+
+bool GameFeatures::registerGameFeature(MOBase::IPlugin* plugin,
+ QStringList const& games,
+ std::shared_ptr<MOBase::GameFeature> feature,
+ int priority)
+{
+ auto& features = m_allFeatures[feature->typeInfo()];
+
+ if (std::find_if(features.begin(), features.end(), [&feature](const auto& data) {
+ return data.feature() == feature;
+ }) != features.end()) {
+ log::error("cannot register feature multiple time");
+ return false;
+ }
+
+ std::decay_t<decltype(features)>::iterator it;
+ if (dynamic_cast<IPluginGame*>(plugin)) {
+ it = features.end();
+ } else {
+ it = std::lower_bound(features.begin(), features.end(), priority,
+ [](const auto& feature, int priority) {
+ return feature.priority() > priority;
+ });
+ }
+
+ features.emplace(it, feature, plugin, games, std::numeric_limits<int>::min());
+
+ // TODO: only update if relevant
+ updateCurrentFeatures(feature->typeInfo());
+
+ return true;
+}
+
+// unregister game features
+//
+bool GameFeatures::unregisterGameFeature(std::shared_ptr<MOBase::GameFeature> feature)
+{
+ bool removed = false;
+ for (auto& [_, features] : m_allFeatures) {
+ auto it =
+ std::find_if(features.begin(), features.end(), [&feature](const auto& data) {
+ return data.feature() == feature;
+ });
+
+ // the feature can only exist for one kind of features and cannot be duplicated
+ if (it != features.end()) {
+ features.erase(it);
+ removed = true;
+ break;
+ }
+ }
+
+ if (removed) {
+ updateCurrentFeatures();
+ }
+
+ return removed;
+}
+
+int GameFeatures::unregisterGameFeatures(MOBase::IPlugin* plugin,
+ std::type_info const& info)
+{
+ auto& features = m_allFeatures[info];
+
+ const auto initialSize = features.size();
+
+ features.erase(std::remove_if(features.begin(), features.end(),
+ [plugin](const auto& feature) {
+ return feature.plugin() == plugin;
+ }),
+ features.end());
+
+ const int removed = features.size() - initialSize;
+
+ if (removed) {
+ updateCurrentFeatures();
+ }
+
+ return removed;
+}
+
+std::shared_ptr<GameFeature> GameFeatures::gameFeature(std::type_info const& info) const
+{
+ if (info == ModDataCheckerIndex) {
+ return modDataChecker().isValid() ? m_modDataChecker : nullptr;
+ }
+
+ if (info == ModDataContentIndex) {
+ return modDataContent().isValid() ? m_modDataContent : nullptr;
+ }
+
+ auto it = m_currentFeatures.find(info);
+ if (it == m_currentFeatures.end() || it->second.empty()) {
+ return nullptr;
+ }
+
+ return it->second.front();
+}