/* LOOT
A load order optimisation tool for Oblivion, Skyrim, Fallout 3 and
Fallout: New Vegas.
Copyright (C) 2013-2014 WrinklyNinja
This file is part of LOOT.
LOOT is free software: you can redistribute
it and/or modify it under the terms of the GNU General Public License
as published by the Free Software Foundation, either version 3 of
the License, or (at your option) any later version.
LOOT is distributed in the hope that it will
be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with LOOT. If not, see
.
*/
#include "api.h"
#include "../backend/game.h"
#include "../backend/metadata.h"
#include "../backend/parsers.h"
#include "../backend/generators.h"
#include "../backend/error.h"
#include "../backend/streams.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
const unsigned int loot_ok = loot::error::ok;
const unsigned int loot_error_liblo_error = loot::error::liblo_error;
const unsigned int loot_error_file_write_fail = loot::error::path_write_fail;
const unsigned int loot_error_parse_fail = loot::error::path_read_fail;
const unsigned int loot_error_condition_eval_fail = loot::error::condition_eval_fail;
const unsigned int loot_error_regex_eval_fail = loot::error::regex_eval_fail;
const unsigned int loot_error_no_mem = loot::error::no_mem;
const unsigned int loot_error_invalid_args = loot::error::invalid_args;
const unsigned int loot_error_no_tag_map = loot::error::no_tag_map;
const unsigned int loot_error_path_not_found = loot::error::path_not_found;
const unsigned int loot_error_no_game_detected = loot::error::no_game_detected;
const unsigned int loot_error_windows_error = loot::error::windows_error;
const unsigned int loot_error_sorting_error = loot::error::sorting_error;
const unsigned int loot_return_max = loot_error_sorting_error;
// The following are the games identifiers used by the API.
const unsigned int loot_game_tes4 = loot::Game::tes4;
const unsigned int loot_game_tes5 = loot::Game::tes5;
const unsigned int loot_game_fo3 = loot::Game::fo3;
const unsigned int loot_game_fonv = loot::Game::fonv;
// LOOT message types.
const unsigned int loot_message_say = loot::Message::say;
const unsigned int loot_message_warn = loot::Message::warn;
const unsigned int loot_message_error = loot::Message::error;
const unsigned int loot_message_tag = loot::Message::tag;
// LOOT message languages.
const unsigned int loot_lang_any = loot::Language::any;
const unsigned int loot_lang_english = loot::Language::english;
const unsigned int loot_lang_spanish = loot::Language::spanish;
const unsigned int loot_lang_russian = loot::Language::russian;
const unsigned int loot_lang_french = loot::Language::french;
const unsigned int loot_lang_chinese = loot::Language::chinese;
const unsigned int loot_lang_polish = loot::Language::polish;
const unsigned int loot_lang_brazilian_portuguese = loot::Language::brazilian_portuguese;
const unsigned int loot_lang_finnish = loot::Language::finnish;
const unsigned int loot_lang_german = loot::Language::german;
// LOOT cleanliness codes.
const unsigned int loot_needs_cleaning_no = 0;
const unsigned int loot_needs_cleaning_yes = 1;
const unsigned int loot_needs_cleaning_unknown = 2;
struct _loot_db_int {
_loot_db_int()
: extTagMap(nullptr),
extAddedTagIds(nullptr),
extRemovedTagIds(nullptr),
extMessageArray(nullptr),
extMessageArraySize(0) {
extMessage.type = loot_message_say;
extMessage.message = nullptr;
}
~_loot_db_int() {
delete [] extAddedTagIds;
delete [] extRemovedTagIds;
delete [] extMessage.message;
if (extTagMap != nullptr) {
for (size_t i=0; i < bashTagMap.size(); i++)
delete [] extTagMap[i]; //Gotta clear those allocated strings.
delete [] extTagMap;
}
if (extMessageArray != nullptr) {
for (size_t i=0; i < extMessageArraySize; i++)
delete [] extMessageArray[i].message; //Gotta clear those allocated strings.
delete [] extMessageArray;
}
}
loot::Game game;
std::list metadata, rawMetadata, userMetadata, rawUserMetadata;
std::unordered_map bashTagMap;
char ** extTagMap;
unsigned int * extAddedTagIds;
unsigned int * extRemovedTagIds;
loot_message extMessage;
loot_message * extMessageArray;
size_t extMessageArraySize;
};
char * extMessageStr = nullptr;
// std::string to null-terminated char string converter.
char * ToNewCString(std::string str) {
char * p = new char[str.length() + 1];
return strcpy(p, str.c_str());
}
unsigned int c_error(const loot::error& e) {
delete[] extMessageStr;
try {
extMessageStr = new char[strlen(e.what()) + 1];
strcpy(extMessageStr, e.what());
}
catch (std::bad_alloc& /*e*/) {
extMessageStr = nullptr;
}
return e.code();
}
unsigned int c_error(const unsigned int code, const std::string& what) {
return c_error(loot::error(code, what.c_str()));
}
//////////////////////////////
// Error Handling Functions
//////////////////////////////
// Outputs a string giving the details of the last time an error or
// warning return code was returned by a function. The string exists
// until this function is called again or until CleanUpAPI is called.
LOOT_API unsigned int loot_get_error_message (const char ** const message) {
if (message == nullptr)
return c_error(loot_error_invalid_args, "Null message pointer passed.");
*message = extMessageStr;
return loot_ok;
}
// Frees memory allocated to error string.
LOOT_API void loot_cleanup () {
delete [] extMessageStr;
extMessageStr = nullptr;
}
//////////////////////////////
// Version Functions
//////////////////////////////
// Returns whether this version of LOOT supports the API from the given
// LOOT version. Abstracts LOOT API stability policy away from clients.
LOOT_API bool loot_is_compatible (const unsigned int versionMajor, const unsigned int versionMinor, const unsigned int versionPatch) {
return versionMajor == loot::g_version_major && versionMinor == loot::g_version_minor;
}
// Returns the version string for this version of LOOT.
// The string exists until this function is called again or until
// CleanUpAPI is called.
LOOT_API unsigned int loot_get_version (unsigned int * const versionMajor, unsigned int * const versionMinor, unsigned int * const versionPatch) {
if (versionMajor == nullptr || versionMinor == nullptr || versionPatch == nullptr)
return c_error(loot_error_invalid_args, "Null pointer passed.");
*versionMajor = loot::g_version_major;
*versionMinor = loot::g_version_minor;
*versionPatch = loot::g_version_patch;
return loot_ok;
}
////////////////////////////////////
// Lifecycle Management Functions
////////////////////////////////////
// Explicitly manage database lifetime. Allows clients to free memory when
// they want/need to. clientGame sets the game the DB is for, and dataPath
// is the path to that game's Data folder, and is case-sensitive if the
// underlying filesystem is case-sensitive. This function also checks that
// plugins.txt and loadorder.txt (if they both exist) are in sync. If
// dataPath == nullptr then the API will attempt to detect the data path of
// the specified game.
LOOT_API unsigned int loot_create_db (loot_db * const db, const unsigned int clientGame, const char * const gamePath) {
if (db == nullptr || (clientGame != loot_game_tes4 && clientGame != loot_game_tes5 && clientGame != loot_game_fo3 && clientGame != loot_game_fonv))
return c_error(loot_error_invalid_args, "Null pointer passed.");
//Set the locale to get encoding conversions working correctly.
std::setlocale(LC_CTYPE, "");
std::locale global_loc = std::locale();
std::locale loc(global_loc, new boost::filesystem::detail::utf8_codecvt_facet());
boost::filesystem::path::imbue(loc);
//Disable logging or else stdout will get overrun.
boost::log::core::get()->set_logging_enabled(false);
std::string game_path = "";
if (gamePath != nullptr)
game_path = gamePath;
loot::Game game;
try {
game = loot::Game(clientGame).SetPath(game_path).Init(); //This also checks to see if the game is installed if game_path is empty and throws an exception if it is not detected. It also creates a folder in %LOCALAPPDATA% and reads the active plugins list, but that shouldn't be an issue.
} catch (loot::error& e) {
return c_error(e);
}
loot_db retVal;
try {
retVal = new _loot_db_int;
} catch (std::bad_alloc& e) {
return c_error(loot_error_no_mem, e.what());
}
retVal->game = game;
*db = retVal;
return loot_ok;
}
// Destroys the given DB, freeing any memory allocated as part of its use.
LOOT_API void loot_destroy_db (loot_db db) {
delete db;
}
///////////////////////////////////
// Database Loading Functions
///////////////////////////////////
// Loads the masterlist and userlist from the paths specified.
// Can be called multiple times. On error, the database is unchanged.
// Paths are case-sensitive if the underlying filesystem is case-sensitive.
// masterlistPath and userlistPath are files.
LOOT_API unsigned int loot_load_lists (loot_db db, const char * const masterlistPath,
const char * const userlistPath) {
if (db == nullptr || masterlistPath == nullptr)
return c_error(loot_error_invalid_args, "Null pointer passed.");
std::list temp;
std::list userTemp;
try {
if (boost::filesystem::exists(masterlistPath)) {
loot::ifstream in(masterlistPath);
YAML::Node tempNode = YAML::Load(in);
in.close();
temp = tempNode["plugins"].as< std::list >();
}
} catch (std::exception& e) {
return c_error(loot_error_parse_fail, e.what());
}
try {
if (userlistPath != nullptr) {
if (boost::filesystem::exists(userlistPath)) {
if (boost::algorithm::iends_with(userlistPath, ".yaml")) {
loot::ifstream in(userlistPath);
YAML::Node tempNode = YAML::Load(in);
in.close();
userTemp = tempNode["plugins"].as< std::list >();
}
}
}
} catch (YAML::Exception& e) {
return c_error(loot_error_parse_fail, e.what());
}
//Also free memory.
db->bashTagMap.clear();
delete [] db->extAddedTagIds;
delete [] db->extRemovedTagIds;
if (db->extTagMap != nullptr) {
for (size_t i=0; i < db->bashTagMap.size(); i++)
delete [] db->extTagMap[i]; //Gotta clear those allocated strings.
delete [] db->extTagMap;
}
if (db->extMessageArray != nullptr) {
for (size_t i=0; i < db->extMessageArraySize; i++)
delete [] db->extMessageArray[i].message; //Gotta clear those allocated strings.
delete [] db->extMessageArray;
}
db->extAddedTagIds = nullptr;
db->extRemovedTagIds = nullptr;
db->extTagMap = nullptr;
db->extMessageArray = nullptr;
db->rawMetadata = temp;
db->metadata = temp;
db->userMetadata = userTemp;
db->rawUserMetadata = userTemp;
return loot_ok;
}
// Evaluates all conditional lines and regex mods the loaded masterlist.
// This exists so that Load() doesn't need to be called whenever the mods
// installed are changed. Evaluation does not take place unless this function
// is called. Repeated calls re-evaluate the masterlist from scratch each time,
// ignoring the results of any previous evaluations. Paths are case-sensitive
// if the underlying filesystem is case-sensitive.
LOOT_API unsigned int loot_eval_lists (loot_db db, const unsigned int language) {
if (db == nullptr)
return c_error(loot_error_invalid_args, "Null pointer passed.");
std::list temp = db->rawMetadata;
try {
db->game.RefreshActivePluginsList();
for (auto it=temp.begin(); it != temp.end();) {
it->EvalAllConditions(db->game, language);
if (it->IsRegexPlugin()) {
boost::regex regex;
try {
regex = boost::regex(it->Name(), boost::regex::perl|boost::regex::icase);
} catch (boost::regex_error& e) {
return c_error(loot_error_regex_eval_fail, e.what());
}
for (boost::filesystem::directory_iterator itr(db->game.DataPath()); itr != boost::filesystem::directory_iterator(); ++itr) {
const std::string filename = itr->path().filename().string();
if (boost::regex_match(filename, regex)) {
loot::Plugin p = *it;
p.Name(filename);
temp.push_back(p);
}
}
it = temp.erase(it);
} else {
++it;
}
}
} catch (loot::error& e) {
return c_error(e);
}
db->metadata = temp;
temp = db->rawUserMetadata;
try {
for (auto it=temp.begin(); it != temp.end();) {
it->EvalAllConditions(db->game, language);
if (it->IsRegexPlugin()) {
boost::regex regex;
try {
regex = boost::regex(it->Name(), boost::regex::perl|boost::regex::icase);
} catch (boost::regex_error& e) {
return c_error(loot_error_regex_eval_fail, e.what());
}
for (boost::filesystem::directory_iterator itr(db->game.DataPath()); itr != boost::filesystem::directory_iterator(); ++itr) {
const std::string filename = itr->path().filename().string();
if (boost::regex_match(filename, regex)) {
loot::Plugin p = *it;
p.Name(filename);
temp.push_back(p);
}
}
it = temp.erase(it);
} else {
++it;
}
}
} catch (loot::error& e) {
return c_error(e);
}
db->userMetadata = temp;
return loot_ok;
}
//////////////////////////
// DB Access Functions
//////////////////////////
// Returns an array of the Bash Tags encounterred when loading the masterlist
// and userlist, and the number of tags in the returned array. The array and
// its contents are static and should not be freed by the client.
LOOT_API unsigned int loot_get_tag_map (loot_db db, char *** const tagMap, size_t * const numTags) {
if (db == nullptr || tagMap == nullptr || numTags == nullptr)
return c_error(loot_error_invalid_args, "Null pointer passed.");
//Clear existing array allocation.
if (db->extTagMap != nullptr) {
for (size_t i=0, max=db->bashTagMap.size(); i < max; ++i) {
delete [] db->extTagMap[i];
}
delete [] db->extTagMap;
db->extTagMap = nullptr;
}
//Initialise output.
*tagMap = nullptr;
*numTags = 0;
std::unordered_set allTags;
for (const auto &plugin: db->metadata) {
std::set tags(plugin.Tags());
for (const auto &tag: tags) {
allTags.insert(tag.Name());
}
}
for (const auto &plugin : db->userMetadata) {
std::set tags(plugin.Tags());
for (const auto &tag : tags) {
allTags.insert(tag.Name());
}
}
if (allTags.empty())
return loot_ok;
try {
db->extTagMap = new char*[allTags.size()];
} catch (std::bad_alloc& e) {
return c_error(loot_error_no_mem, e.what());
}
unsigned int UID = 0;
try {
for (const auto &tag: allTags) {
db->bashTagMap.emplace(tag, UID);
//Also allocate memory.
db->extTagMap[UID] = ToNewCString(tag);
UID++;
}
} catch (std::bad_alloc& e) {
return c_error(loot_error_no_mem, e.what());
}
*tagMap = db->extTagMap;
*numTags = allTags.size();
return loot_ok;
}
// Returns arrays of Bash Tag UIDs for Bash Tags suggested for addition and removal
// by LOOT's masterlist and userlist, and the number of tags in each array.
// The returned arrays are valid until the db is destroyed or until the Load
// function is called. The arrays should not be freed by the client. modName is
// case-insensitive. If no Tags are found for an array, the array pointer (*tagIds)
// will be nullptr. The userlistModified bool is true if the userlist contains Bash Tag
// suggestion message additions.
LOOT_API unsigned int loot_get_plugin_tags (loot_db db, const char * const plugin,
unsigned int ** const tagIds_added,
size_t * const numTags_added,
unsigned int ** const tagIds_removed,
size_t * const numTags_removed,
bool * const userlistModified) {
if (db == nullptr || plugin == nullptr || tagIds_added == nullptr || numTags_added == nullptr || tagIds_removed == nullptr || numTags_removed == nullptr || userlistModified == nullptr)
return c_error(loot_error_invalid_args, "Null pointer passed.");
//Clear existing array allocations.
delete [] db->extAddedTagIds;
delete [] db->extRemovedTagIds;
db->extAddedTagIds = nullptr;
db->extRemovedTagIds = nullptr;
//Initialise output.
*tagIds_added = nullptr;
*tagIds_removed = nullptr;
*userlistModified = false;
*numTags_added = 0;
*numTags_removed = 0;
std::unordered_set tagsAdded, tagsRemoved;
std::list::iterator pluginIt = std::find(db->metadata.begin(), db->metadata.end(), loot::Plugin(plugin));
if (pluginIt != db->metadata.end()) {
std::set tags(pluginIt->Tags());
for (const auto &tag: tags) {
if (tag.IsAddition())
tagsAdded.insert(tag.Name());
else
tagsRemoved.insert(tag.Name());
}
}
pluginIt = std::find(db->userMetadata.begin(), db->userMetadata.end(), loot::Plugin(plugin));
if (pluginIt != db->userMetadata.end()) {
*userlistModified = true;
std::set tags(pluginIt->Tags());
for (const auto &tag : tags) {
if (tag.IsAddition())
tagsAdded.insert(tag.Name());
else
tagsRemoved.insert(tag.Name());
}
}
if ((!tagsAdded.empty() || !tagsRemoved.empty()) && db->bashTagMap.empty()) {
return c_error(loot_error_no_tag_map, "No Bash Tag map has been previously generated.");
}
std::vector tagsAddedIDs, tagsRemovedIDs;
for (const auto &tagNames: tagsAdded) {
const auto mapIter(db->bashTagMap.find(tagNames));
if (mapIter != db->bashTagMap.end())
tagsAddedIDs.push_back(mapIter->second);
}
for (const auto &tagNames : tagsRemoved) {
const auto mapIter(db->bashTagMap.find(tagNames));
if (mapIter != db->bashTagMap.end())
tagsAddedIDs.push_back(mapIter->second);
}
//Allocate memory.
size_t numAdded = tagsAddedIDs.size();
size_t numRemoved = tagsRemovedIDs.size();
try {
if (numAdded != 0) {
db->extAddedTagIds = new uint32_t[numAdded];
for (size_t i=0; i < numAdded; i++)
db->extAddedTagIds[i] = tagsAddedIDs[i];
}
if (numRemoved != 0) {
db->extRemovedTagIds = new uint32_t[numRemoved];
for (size_t i=0; i < numRemoved; i++)
db->extRemovedTagIds[i] = tagsRemovedIDs[i];
}
} catch (std::bad_alloc& e) {
return c_error(loot_error_no_mem, e.what());
}
//Set outputs.
*tagIds_added = db->extAddedTagIds;
*tagIds_removed = db->extRemovedTagIds;
*numTags_added = numAdded;
*numTags_removed = numRemoved;
return loot_ok;
}
// Returns the messages attached to the given plugin. Messages are valid until Load,
// loot_destroy_db or loot_get_plugin_messages are next called. plugin is case-insensitive.
// If no messages are attached, *messages will be nullptr and numMessages will equal 0.
LOOT_API unsigned int loot_get_plugin_messages (loot_db db, const char * const plugin,
loot_message ** const messages,
size_t * const numMessages) {
if (db == nullptr || plugin == nullptr || messages == nullptr || numMessages == nullptr)
return c_error(loot_error_invalid_args, "Null pointer passed.");
//Clear existing array allocation.
if (db->extMessageArray != nullptr) {
for (size_t i=0; i < db->extMessageArraySize; ++i) {
delete [] db->extMessageArray[i].message;
}
delete [] db->extMessageArray;
db->extMessageArray = nullptr;
}
//Initialise output.
*messages = nullptr;
*numMessages = 0;
std::list pluginMessages;
std::list::iterator pluginIt = std::find(db->metadata.begin(), db->metadata.end(), loot::Plugin(plugin));
if (pluginIt != db->metadata.end()) {
pluginMessages = pluginIt->Messages();
}
pluginIt = std::find(db->userMetadata.begin(), db->userMetadata.end(), loot::Plugin(plugin));
if (pluginIt != db->userMetadata.end()) {
std::list temp = pluginIt->Messages();
pluginMessages.insert(pluginMessages.end(), temp.begin(), temp.end());
}
db->extMessageArraySize = pluginMessages.size();
try {
db->extMessageArray = new loot_message[db->extMessageArraySize];
int i = 0;
for (const auto &message: pluginMessages) {
db->extMessageArray[i].type = message.Type();
db->extMessageArray[i].message = ToNewCString(message.ChooseContent(loot::Language::any).Str());
}
} catch (std::bad_alloc& e) {
return c_error(loot_error_no_mem, e.what());
}
*messages = db->extMessageArray;
*numMessages = db->extMessageArraySize;
return loot_ok;
}
LOOT_API unsigned int loot_get_dirty_info(loot_db db, const char * const plugin, unsigned int * const needsCleaning) {
if (db == nullptr || plugin == nullptr || needsCleaning == nullptr)
return c_error(loot_error_invalid_args, "Null pointer passed.");
*needsCleaning = loot_needs_cleaning_unknown;
//Get all dirty info.
std::set dirtyInfo;
std::list::iterator pluginIt = std::find(db->metadata.begin(), db->metadata.end(), loot::Plugin(plugin));
if (pluginIt != db->metadata.end()) {
dirtyInfo = pluginIt->DirtyInfo();
}
pluginIt = std::find(db->userMetadata.begin(), db->userMetadata.end(), loot::Plugin(plugin));
if (pluginIt != db->userMetadata.end()) {
std::set temp = pluginIt->DirtyInfo();
dirtyInfo.insert(temp.begin(), temp.end());
}
if (!dirtyInfo.empty()) {
*needsCleaning = loot_needs_cleaning_yes;
}
return loot_ok;
}
// Writes a minimal masterlist that only contains mods that have Bash Tag suggestions,
// and/or dirty messages, plus the Tag suggestions and/or messages themselves and their
// conditions, in order to create the Wrye Bash taglist. outputFile is the path to use
// for output. If outputFile already exists, it will only be overwritten if overwrite is true.
LOOT_API unsigned int loot_write_minimal_list (loot_db db, const char * const outputFile, const bool overwrite) {
if (db == nullptr || outputFile == nullptr)
return c_error(loot_error_invalid_args, "Null pointer passed.");
if (boost::filesystem::exists(outputFile) && !overwrite)
return c_error(loot_error_invalid_args, "Output file exists but overwrite is not set to true.");
std::list temp = db->metadata;
for (auto &plugin: temp) {
loot::Plugin p(plugin.Name());
p.Tags(plugin.Tags());
p.DirtyInfo(plugin.DirtyInfo());
plugin = p;
}
YAML::Emitter yout;
yout.SetIndent(2);
yout << YAML::BeginMap
<< YAML::Key << "plugins" << YAML::Value << temp
<< YAML::EndMap;
boost::filesystem::path p(outputFile);
loot::ofstream out(p);
if (out.fail())
return c_error(loot_error_invalid_args, "Couldn't open output file");
out << yout.c_str();
out.close();
return loot_ok;
}