Skip to content

Commit

Permalink
Make plugin loading and sorting more granular
Browse files Browse the repository at this point in the history
- Don't clear the cache in LoadPlugins()
- Don't load plugins in SortPlugins(), and make it take a vector of strings, not paths.
- Add a ClearLoadedPlugins() method to clear the loaded plugins cache.
- Remove IdentifyMainMasterFile()

Instead of calling IdentifyMainMasterFile(), callers can use LoadPlugins() to initially load all plugin headers only, then omit the main master file when calling LoadPlugins() to fully load plugins.
  • Loading branch information
Ortham committed Feb 2, 2025
1 parent d8a3604 commit c838161
Show file tree
Hide file tree
Showing 12 changed files with 158 additions and 95 deletions.
16 changes: 13 additions & 3 deletions docs/api/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ Added
paths to ensure that they are resolved correctly.
- Ghosted plugins are not supported for OpenMW.

- :cpp:any:`loot::GameInterface::ClearLoadedPlugins()`

Fixed
-----

Expand All @@ -38,9 +40,13 @@ Fixed
Changed
-------

- :cpp:any:`loot::GameInterface::IdentifyMainMasterFile()` now takes a
``const std::filesystem::path&`` instead of a
``const std::string&``.
- :cpp:any:`loot::GameInterface::LoadPlugins()` no longer clears the data of
previously-loaded plugins, though if any of the given paths have filenames
that match previously-loaded plugins, the previously-loaded data will be
still be replaced.
- :cpp:any:`loot::GameInterface::SortPlugins()` now takes a vector of filenames
instead of a vector of strings, and no longer loads the given plugins. It
instead expects the plugins to have already been loaded.
- The application of plugin groups as part of the sorting process has been
overhauled. As well as fixing several known bugs, the new approach avoids
causing cyclic interaction errors, handles groups more consistently and is
Expand All @@ -64,6 +70,10 @@ Changed
Removed
-------

- ``loot::GameInterface::IdentifyMainMasterFile()``: callers should instead
call :cpp:any:`loot::GameInterface::LoadPlugins()` with the main master file
to load only its headers, and omit the main master file when calling
:cpp:any:`loot::GameInterface::LoadPlugins()` to fully load plugins.
- Prebuilt Linux release binaries are no longer provided, as the binaries that
were previously provided were not very portable beyond the Linux distribution
versions that they were built on.
Expand Down
50 changes: 27 additions & 23 deletions include/loot/game_interface.h
Original file line number Diff line number Diff line change
Expand Up @@ -103,37 +103,53 @@ class GameInterface {

/**
* @brief Parses plugins and loads their data.
* @details Any previously-loaded plugin data is discarded when this function
* is called.
* @details If a given plugin filename (or one that is case-insensitively
* equal) has already been loaded, its previously-loaded data
* data is discarded, invalidating any existing shared pointers to
* that plugin's PluginInterface object.
*
* If the game is Morrowind, OpenMW or Starfield, it's only valid to
* fully load a plugin if its masters are already loaded or included
* in the same input vector.
* @param pluginPaths
* The plugin paths to load. Relative paths are resolved relative to
* the game's plugins directory, while absolute paths are used as
* given. Each plugin filename must be unique within the vector.
* @param loadHeadersOnly
* If true, only the plugins' headers are loaded. If false, all records
* in the plugins are parsed, apart from the main master file if it has
* been identified by a previous call to ``IdentifyMainMasterFile()``.
* in the plugins are parsed.
*/
virtual void LoadPlugins(
const std::vector<std::filesystem::path>& pluginPaths,
bool loadHeadersOnly) = 0;

/**
* @brief Clears the plugins loaded by previous calls to `LoadPlugins()`.
* @details This invalidates any PluginInterface pointers retrieved using
* `GetPlugin()` or `GetLoadedPlugins()`.
*/
virtual void ClearLoadedPlugins() = 0;

/**
* @brief Get data for a loaded plugin.
* @param pluginName
* The filename of the plugin to get data for.
* @returns A shared pointer to a const PluginInterface implementation. The
* pointer is null if the given plugin has not been loaded.
* pointer is null if the given plugin has not been loaded. The
* pointer remains valid until the `ClearLoadedPlugins()` function
* is called, this GameInterface is destroyed, or until a plugin with
* a case-insensitively equal filename is loaded.
*/
virtual const PluginInterface* GetPlugin(
const std::string& pluginName) const = 0;

/**
* @brief Get a set of const references to all loaded plugins' PluginInterface
* objects.
* @returns A set of const PluginInterface references. The references remain
* valid until the ``LoadPlugins()`` or ``SortPlugins()`` functions
* are next called or this GameInterface is destroyed.
* @returns A set of shared pointers to const PluginInterface. The pointers
* remain valid until the `ClearLoadedPlugins()` function is called,
* this GameInterface is destroyed, or until a plugin with a
* case-insensitively equal filename is loaded.
*/
virtual std::vector<const PluginInterface*> GetLoadedPlugins() const = 0;

Expand All @@ -143,16 +159,6 @@ class GameInterface {
* @{
*/

/**
* @brief Identify the game's main master file.
* @details When sorting, LOOT always only loads the headers of the game's
* main master file as a performance optimisation.
*
* A relative path is resolved relative to the game's plugins
* directory, while an absolute path is used as given.
*/
virtual void IdentifyMainMasterFile(const std::filesystem::path& masterFile) = 0;

/**
* @brief Calculates a new load order for the game's installed plugins
* (including inactive plugins) and outputs the sorted order.
Expand All @@ -161,15 +167,13 @@ class GameInterface {
* applied to the load order used by the game. This function does
* not load or evaluate the masterlist or userlist.
* @param pluginPaths
* The plugin paths to sort, in their current load order. Relative
* paths are resolved relative to the game's plugins directory, while
* absolute paths are used as given. Each plugin filename must be
* unique within the vector.
* The plugins to sort, in their current load order. All given plugins
* must have been loaded using `LoadPlugins()`.
* @returns A vector of the given plugin filenames in their sorted load
* order.
*/
virtual std::vector<std::string> SortPlugins(
const std::vector<std::filesystem::path>& pluginPaths) = 0;
const std::vector<std::string>& pluginFilenames) = 0;

/**
* @}
Expand Down
2 changes: 1 addition & 1 deletion include/loot/metadata/plugin_metadata.h
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ class PluginMetadata {

/**
* Check if the plugin name is a regular expression.
* @return True if the plugin name contains any of the characters ``:\*?|``,
* @return True if the plugin name contains any of the characters `:\*?|`,
* false otherwise.
*/
LOOT_API bool IsRegexPlugin() const;
Expand Down
29 changes: 7 additions & 22 deletions src/api/game/game.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -255,9 +255,6 @@ void Game::LoadPlugins(const std::vector<std::filesystem::path>& pluginPaths,
"\" is not a valid plugin");
}

// Clear the existing plugin and archive caches.
cache_.ClearCachedPlugins();

// Search for and cache archives.
CacheArchives();

Expand All @@ -275,12 +272,8 @@ void Game::LoadPlugins(const std::vector<std::filesystem::path>& pluginPaths,
const auto resolvedPluginPath =
ResolvePluginPath(GetType(), DataPath(), pluginPath);

const bool loadHeader =
loadHeadersOnly ||
loot::equivalent(resolvedPluginPath, masterFilePath_);

cache_.AddPlugin(
Plugin(GetType(), cache_, resolvedPluginPath, loadHeader));
Plugin(GetType(), cache_, resolvedPluginPath, loadHeadersOnly));
} catch (const std::exception& e) {
if (logger) {
logger->error(
Expand All @@ -304,6 +297,10 @@ void Game::LoadPlugins(const std::vector<std::filesystem::path>& pluginPaths,
conditionEvaluator_->RefreshLoadedPluginsState(GetLoadedPlugins());
}

void Game::ClearLoadedPlugins() {
cache_.ClearCachedPlugins();
}

const PluginInterface* Game::GetPlugin(const std::string& pluginName) const {
return cache_.GetPlugin(pluginName);
}
Expand All @@ -317,21 +314,9 @@ std::vector<const PluginInterface*> Game::GetLoadedPlugins() const {
return interfacePointers;
}

void Game::IdentifyMainMasterFile(const std::filesystem::path& masterFile) {
masterFilePath_ = ResolvePluginPath(GetType(), DataPath(), masterFile);
}

std::vector<std::string> Game::SortPlugins(
const std::vector<std::filesystem::path>& pluginPaths) {
LoadPlugins(pluginPaths, false);

std::vector<std::string> loadOrder;
for (const auto& pluginPath : pluginPaths) {
loadOrder.push_back(pluginPath.filename().u8string());
}

// Sort plugins into their load order.
return loot::SortPlugins(*this, loadOrder);
const std::vector<std::string>& pluginFilenames) {
return loot::SortPlugins(*this, pluginFilenames);
}

void Game::LoadCurrentLoadOrderState() {
Expand Down
8 changes: 3 additions & 5 deletions src/api/game/game.h
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,15 @@ class Game final : public GameInterface {
void LoadPlugins(const std::vector<std::filesystem::path>& pluginPaths,
bool loadHeadersOnly) override;

void ClearLoadedPlugins() override;

const PluginInterface* GetPlugin(
const std::string& pluginName) const override;

std::vector<const PluginInterface*> GetLoadedPlugins() const override;

void IdentifyMainMasterFile(const std::filesystem::path& masterFile) override;

std::vector<std::string> SortPlugins(
const std::vector<std::filesystem::path>& pluginPaths) override;
const std::vector<std::string>& pluginFilenames) override;

void LoadCurrentLoadOrderState() override;

Expand All @@ -103,8 +103,6 @@ class Game final : public GameInterface {
std::shared_ptr<ConditionEvaluator> conditionEvaluator_;
ApiDatabase database_;

std::filesystem::path masterFilePath_;

std::vector<std::filesystem::path> additionalDataPaths_;
};
}
Expand Down
10 changes: 6 additions & 4 deletions src/api/game/game_cache.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,14 @@ void GameCache::AddPlugin(Plugin&& plugin) {
lock_guard<mutex> lock(mutex_);

auto normalizedName = NormalizeFilename(plugin.GetName());
auto pluginPointer = std::make_shared<Plugin>(std::move(plugin));

const auto it = plugins_.find(normalizedName);
if (it != end(plugins_))
plugins_.erase(it);

plugins_.emplace(normalizedName, std::make_shared<Plugin>(std::move(plugin)));
if (it != end(plugins_)) {
it->second = pluginPointer;
} else {
plugins_.emplace(normalizedName, pluginPointer);
}
}

std::set<std::filesystem::path> GameCache::GetArchivePaths() const {
Expand Down
47 changes: 23 additions & 24 deletions src/api/sorting/plugin_sort.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,33 +34,23 @@
namespace loot {
std::vector<PluginSortingData> GetPluginsSortingData(
const DatabaseInterface& db,
const std::vector<const PluginInterface*> loadedPluginInterfaces,
const std::vector<std::string>& loadOrder) {
const std::vector<const Plugin*>& loadOrder) {
std::vector<PluginSortingData> pluginsSortingData;
pluginsSortingData.reserve(loadedPluginInterfaces.size());
pluginsSortingData.reserve(loadOrder.size());

std::vector<ComparableFilename> comparableLoadOrder;
for (const auto& pluginName : loadOrder) {
comparableLoadOrder.push_back(ToComparableFilename(pluginName));
for (const auto& plugin : loadOrder) {
comparableLoadOrder.push_back(ToComparableFilename(plugin->GetName()));
}

for (const auto& pluginInterface : loadedPluginInterfaces) {
if (!pluginInterface) {
continue;
}

const auto plugin = dynamic_cast<const Plugin* const>(pluginInterface);

if (!plugin) {
throw std::logic_error(
"Tried to case a PluginInterface pointer to a Plugin pointer.");
}
for (const auto& plugin : loadOrder) {
const auto pluginFilename = plugin->GetName();

const auto masterlistMetadata =
db.GetPluginMetadata(plugin->GetName(), false, true)
.value_or(PluginMetadata(plugin->GetName()));
const auto userMetadata = db.GetPluginUserMetadata(plugin->GetName(), true)
.value_or(PluginMetadata(plugin->GetName()));
db.GetPluginMetadata(pluginFilename, false, true)
.value_or(PluginMetadata(pluginFilename));
const auto userMetadata = db.GetPluginUserMetadata(pluginFilename, true)
.value_or(PluginMetadata(pluginFilename));

const auto pluginSortingData = PluginSortingData(
plugin, masterlistMetadata, userMetadata, comparableLoadOrder);
Expand Down Expand Up @@ -313,8 +303,7 @@ std::vector<std::string> SortPlugins(
return {};
}

// Sort the plugins according to into their existing load order, or
// lexicographical ordering for pairs of plugins without load order positions.
// Sort the plugins according to the lexicographical order of their names.
// This ensures a consistent iteration order for vertices given the same input
// data. The vertex iteration order can affect what edges get added and so
// the final sorting result, so consistency is important.
Expand Down Expand Up @@ -387,8 +376,18 @@ std::vector<std::string> SortPlugins(
std::vector<std::string> SortPlugins(
Game& game,
const std::vector<std::string>& loadOrder) {
auto pluginsSortingData = GetPluginsSortingData(
game.GetDatabase(), game.GetLoadedPlugins(), loadOrder);
std::vector<const Plugin*> plugins;
for (const auto& pluginFilename : loadOrder) {
const auto plugin = game.GetCache().GetPlugin(pluginFilename);
if (plugin == nullptr) {
throw std::invalid_argument("The plugin \"" + pluginFilename +
"\" has not been loaded.");
}

plugins.push_back(plugin);
}

auto pluginsSortingData = GetPluginsSortingData(game.GetDatabase(), plugins);

const auto logger = getLogger();
if (logger) {
Expand Down
23 changes: 20 additions & 3 deletions src/tests/api/interface/game_interface_test.h
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ TEST_P(GameInterfaceTest, isValidPluginShouldReturnFalseForAnEmptyFile) {

TEST_P(
GameInterfaceTest,
loadPluginsWithHeadersOnlyTrueShouldLoadTheHeadersOfAllInstalledPlugins) {
loadPluginsWithHeadersOnlyTrueShouldLoadTheHeadersOfAllGivenPlugins) {
handle_->LoadPlugins(pluginsToLoad, true);
if (GetParam() == GameType::starfield) {
EXPECT_EQ(6, handle_->GetLoadedPlugins().size());
Expand Down Expand Up @@ -184,7 +184,17 @@ TEST_P(GameInterfaceTest, loadPluginsWithANonAsciiPluginShouldLoadIt) {
EXPECT_EQ(blankEsmCrc, plugin->GetCRC().value());
}

TEST_P(GameInterfaceTest, getPluginThatIsNotCachedShouldReturnAnEmptyOptional) {
TEST_P(GameInterfaceTest, clearLoadedPluginsShouldClearThePluginsCache) {
handle_->LoadPlugins({std::filesystem::u8path(blankEsm)}, true);
const auto pointer = handle_->GetPlugin(blankEsm);
ASSERT_NE(nullptr, pointer);

handle_->ClearLoadedPlugins();

EXPECT_EQ(nullptr, handle_->GetPlugin(blankEsm));
}

TEST_P(GameInterfaceTest, getPluginThatIsNotCachedShouldReturnANullPointer) {
EXPECT_FALSE(handle_->GetPlugin(blankEsm));
}

Expand Down Expand Up @@ -232,7 +242,14 @@ TEST_P(GameInterfaceTest, sortPluginsShouldSucceedIfPassedValidArguments) {
}

handle_->LoadCurrentLoadOrderState();
std::vector<std::string> actualOrder = handle_->SortPlugins(pluginsToLoad);
handle_->LoadPlugins(pluginsToLoad, false);

std::vector<std::string> pluginsToSort;
for (const auto& plugin : pluginsToLoad) {
pluginsToSort.push_back(plugin.filename().u8string());
}

std::vector<std::string> actualOrder = handle_->SortPlugins(pluginsToSort);

EXPECT_EQ(expectedOrder, actualOrder);
}
Expand Down
Loading

0 comments on commit c838161

Please sign in to comment.