diff --git a/CMakeLists.txt b/CMakeLists.txt index ea5706fa..696adef3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,10 +11,10 @@ cmake_minimum_required (VERSION 2.8.9) project (boss) -set (BOSS_SRC "${CMAKE_SOURCE_DIR}/src/metadata.cpp") +set (BOSS_SRC "${CMAKE_SOURCE_DIR}/src/metadata.cpp" "${CMAKE_SOURCE_DIR}/src/game.cpp" "${CMAKE_SOURCE_DIR}/src/helpers.cpp") # Include source and library directories. -include_directories ("${BOSS_LIBS_DIR}/boost" "${BOSS_LIBS_DIR}/yaml-cpp/include" "${CMAKE_SOURCE_DIR}/src") +include_directories ("${BOSS_LIBS_DIR}/alphanum" "${BOSS_LIBS_DIR}/utf8" "${BOSS_LIBS_DIR}/boost" "${BOSS_LIBS_DIR}/yaml-cpp/include" "${CMAKE_SOURCE_DIR}/src") ############################## # Platform-Specific Settings @@ -37,7 +37,7 @@ ENDIF () # Settings when compiling and cross-compiling on Linux. IF (CMAKE_HOST_SYSTEM_NAME MATCHES "Linux") - set (BOSS_LIBS yaml-cpp) + set (BOSS_LIBS yaml-cpp boost_filesystem boost_system boost_regex) set (CMAKE_C_FLAGS "-m${BOSS_ARCH}") set (CMAKE_CXX_FLAGS "-m${BOSS_ARCH}") set (CMAKE_EXE_LINKER_FLAGS "-static-libstdc++ -static-libgcc") @@ -45,15 +45,18 @@ IF (CMAKE_HOST_SYSTEM_NAME MATCHES "Linux") set (CMAKE_MODULE_LINKER_FLAGS "-static-libstdc++ -static-libgcc") link_directories ("${BOSS_LIBS_DIR}/yaml-cpp/build/") + link_directories ("${BOSS_LIBS_DIR}/boost/stage-${BOSS_ARCH}/lib") IF (CMAKE_SYSTEM_NAME MATCHES "Windows") link_directories ("${BOSS_LIBS_DIR}/yaml-cpp/build/") + link_directories ("${BOSS_LIBS_DIR}/boost/stage-mingw-${BOSS_ARCH}/lib") ENDIF () ENDIF () # Settings when not cross-compiling. IF (CMAKE_SYSTEM_NAME MATCHES CMAKE_HOST_SYSTEM_NAME) link_directories ("${BOSS_LIBS_DIR}/yaml-cpp/build/") + link_directories ("${BOSS_LIBS_DIR}/boost/stage-${BOSS_ARCH}/lib") ENDIF () ############################## diff --git a/src/api.cpp b/src/api.cpp new file mode 100644 index 00000000..ff9a1262 --- /dev/null +++ b/src/api.cpp @@ -0,0 +1,148 @@ +/* BOSS + + A plugin load order optimiser for games that use the esp/esm plugin system. + + Copyright (C) 2012 WrinklyNinja + + This file is part of BOSS. + + BOSS 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. + + BOSS 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 BOSS. If not, see + . +*/ + +#include "api.h" + +////////////////////////////// +// 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. +BOSS_API uint32_t boss_get_error_message (uint8_t ** message) { + +} + + +////////////////////////////// +// Version Functions +////////////////////////////// + +// Returns whether this version of BOSS supports the API from the given +// BOSS version. Abstracts BOSS API stability policy away from clients. +BOSS_API bool boss_is_compatible (const uint32_t versionMajor, const uint32_t versionMinor, const uint32_t versionPatch) { + +} + +// Returns the version string for this version of BOSS. +// The string exists until this function is called again or until +// CleanUpAPI is called. +BOSS_API uint32_t boss_get_version (uint32_t * versionMajor, uint32_t * versionMinor, uint32_t * versionPatch) { + +} + + +//////////////////////////////////// +// 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 == NULL then the API will attempt to detect the data path of +// the specified game. +BOSS_API uint32_t boss_create_db (boss_db * db, const uint32_t clientGame, const uint8_t * gamePath) { + +} + +// Destroys the given DB, freeing any memory allocated as part of its use. +BOSS_API void boss_destroy_db (boss_db db) { + +} + +// Frees memory allocated to version and error strings. +BOSS_API void boss_cleanup () { + +} + + +/////////////////////////////////// +// 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. +BOSS_API uint32_t boss_load_lists (boss_db db, const uint8_t * masterlistPath, + const uint8_t * userlistPath) { + +} + +// 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. +BOSS_API uint32_t boss_eval_lists (boss_db db) { + +} + + +////////////////////////// +// 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. +BOSS_API uint32_t boss_get_tag_map (boss_db db, boss_tag ** tagMap, size_t * numTags) { + +} + +// Returns arrays of Bash Tag UIDs for Bash Tags suggested for addition and removal +// by BOSS'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 NULL. The userlistModified bool is true if the userlist contains Bash Tag +// suggestion message additions. +BOSS_API uint32_t boss_get_plugin_tags (boss_db db, const uint8_t * plugin, + uint32_t ** tagIds_added, + size_t * numTags_added, + uint32_t **tagIds_removed, + size_t *numTags_removed, + bool * userlistModified) { + +} + +// Returns the messages attached to the given plugin. Messages are valid until Load, +// DestroyBossDb or GetPluginMessages are next called. plugin is case-insensitive. +// If no messages are attached, *messages will be NULL and numMessages will equal 0. +BOSS_API uint32_t boss_get_plugin_messages (boss_db db, const uint8_t * plugin, + boss_message ** messages, + size_t * numMessages) { + +} + +// 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. +BOSS_API uint32_t boss_write_minimal_list (boss_db db, const uint8_t * outputFile, const bool overwrite) { + +} diff --git a/src/api.h b/src/api.h new file mode 100644 index 00000000..5694b230 --- /dev/null +++ b/src/api.h @@ -0,0 +1,240 @@ +/* BOSS + + A plugin load order optimiser for games that use the esp/esm plugin system. + + Copyright (C) 2012 WrinklyNinja + + This file is part of BOSS. + + BOSS 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. + + BOSS 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 BOSS. If not, see + . +*/ + +#ifndef __BOSS_API_H__ +#define __BOSS_API_H__ + +#include +#include + +#if defined(_MSC_VER) +//MSVC doesn't support C99, so do the stdbool.h definitions ourselves. +//START OF stdbool.h DEFINITIONS. +# ifndef __cplusplus +# define bool _Bool +# define true 1 +# define false 0 +# endif +# define __bool_true_false_are_defined 1 +//END OF stdbool.h DEFINITIONS. +#else +# include +#endif + +// set up dll import/export decorators +// when compiling the dll on windows, ensure BOSS_API_EXPORT is defined. clients +// that use this header do not need to define anything to import the symbols +// properly. +#if defined(_WIN32) || defined(_WIN64) +# ifdef BOSS_API_EXPORT +# define BOSS_API __declspec(dllexport) +# else +# define BOSS_API __declspec(dllimport) +# endif +#else +# define BOSS_API +#endif + +#ifdef __cplusplus +extern "C" +{ +#endif + + +//////////////////////// +// Types +//////////////////////// + +// All API strings are uint8_t* strings encoded in UTF-8. Strings returned +// by the API should not have their memory freed by the client: the API will +// clean up after itself. +// All API numbers and error codes are uint32_t integers. + +// Abstracts the definition of BOSS's internal state while still providing +// type safety across the API. +typedef struct _boss_db_int * boss_db; + +// boss_tag structure gives the Unique ID number (UID) for each Bash Tag and +// the corresponding Tag name string. +typedef struct { + uint32_t id; + const uint8_t * name; // don't use char for utf-8 since char can be signed +} boss_tag; + +// boss_message structure gives the type of message and it contents. +typedef struct { + uint32_t type; + const uint8_t * message; +} boss_message; + + +// The following are the possible codes that the API can return. +BOSS_API extern const uint32_t BOSS_API_OK; +BOSS_API extern const uint32_t BOSS_API_OK_NO_UPDATE_NECESSARY; +BOSS_API extern const uint32_t BOSS_API_WARN_BAD_FILENAME; +BOSS_API extern const uint32_t BOSS_API_WARN_LO_MISMATCH; +BOSS_API extern const uint32_t BOSS_API_ERROR_FILE_WRITE_FAIL; +BOSS_API extern const uint32_t BOSS_API_ERROR_FILE_DELETE_FAIL; +BOSS_API extern const uint32_t BOSS_API_ERROR_FILE_NOT_UTF8; +BOSS_API extern const uint32_t BOSS_API_ERROR_FILE_NOT_FOUND; +BOSS_API extern const uint32_t BOSS_API_ERROR_FILE_RENAME_FAIL; +BOSS_API extern const uint32_t BOSS_API_ERROR_TIMESTAMP_READ_FAIL; +BOSS_API extern const uint32_t BOSS_API_ERROR_TIMESTAMP_WRITE_FAIL; +BOSS_API extern const uint32_t BOSS_API_ERROR_PARSE_FAIL; +BOSS_API extern const uint32_t BOSS_API_ERROR_CONDITION_EVAL_FAIL; +BOSS_API extern const uint32_t BOSS_API_ERROR_REGEX_EVAL_FAIL; +BOSS_API extern const uint32_t BOSS_API_ERROR_NO_MEM; +BOSS_API extern const uint32_t BOSS_API_ERROR_INVALID_ARGS; +BOSS_API extern const uint32_t BOSS_API_ERROR_NETWORK_FAIL; +BOSS_API extern const uint32_t BOSS_API_ERROR_NO_INTERNET_CONNECTION; +BOSS_API extern const uint32_t BOSS_API_ERROR_NO_TAG_MAP; +BOSS_API extern const uint32_t BOSS_API_ERROR_PLUGINS_FULL; +BOSS_API extern const uint32_t BOSS_API_ERROR_GAME_NOT_FOUND; +BOSS_API extern const uint32_t BOSS_API_ERROR_PLUGIN_BEFORE_MASTER; +BOSS_API extern const uint32_t BOSS_API_RETURN_MAX; + +// The following are the games identifiers used by the API. +BOSS_API extern const uint32_t BOSS_API_GAME_OBLIVION; +BOSS_API extern const uint32_t BOSS_API_GAME_FALLOUT3; +BOSS_API extern const uint32_t BOSS_API_GAME_FALLOUTNV; +BOSS_API extern const uint32_t BOSS_API_GAME_NEHRIM; +BOSS_API extern const uint32_t BOSS_API_GAME_SKYRIM; +BOSS_API extern const uint32_t BOSS_API_GAME_MORROWIND; + +// BOSS message types. +BOSS_API extern const uint32_t BOSS_API_MESSAGE_SAY; +BOSS_API extern const uint32_t BOSS_API_MESSAGE_TAG; +BOSS_API extern const uint32_t BOSS_API_MESSAGE_REQUIREMENT; +BOSS_API extern const uint32_t BOSS_API_MESSAGE_INCOMPATIBILITY; +BOSS_API extern const uint32_t BOSS_API_MESSAGE_DIRTY; +BOSS_API extern const uint32_t BOSS_API_MESSAGE_WARN; +BOSS_API extern const uint32_t BOSS_API_MESSAGE_ERROR; + + + +////////////////////////////// +// 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. +BOSS_API uint32_t boss_get_error_message (uint8_t ** message); + + +////////////////////////////// +// Version Functions +////////////////////////////// + +// Returns whether this version of BOSS supports the API from the given +// BOSS version. Abstracts BOSS API stability policy away from clients. +BOSS_API bool boss_is_compatible (const uint32_t versionMajor, const uint32_t versionMinor, const uint32_t versionPatch); + +// Returns the version string for this version of BOSS. +// The string exists until this function is called again or until +// CleanUpAPI is called. +BOSS_API uint32_t boss_get_version (uint32_t * versionMajor, uint32_t * versionMinor, uint32_t * versionPatch); + + +//////////////////////////////////// +// 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 == NULL then the API will attempt to detect the data path of +// the specified game. +BOSS_API uint32_t boss_create_db (boss_db * db, const uint32_t clientGame, const uint8_t * gamePath); + +// Destroys the given DB, freeing any memory allocated as part of its use. +BOSS_API void boss_destroy_db (boss_db db); + +// Frees memory allocated to version and error strings. +BOSS_API void boss_cleanup (); + + +/////////////////////////////////// +// 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. +BOSS_API uint32_t boss_load_lists (boss_db db, const uint8_t * masterlistPath, + const uint8_t * userlistPath); + +// 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. +BOSS_API uint32_t boss_eval_lists (boss_db db); + + +////////////////////////// +// 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. +BOSS_API uint32_t boss_get_tag_map (boss_db db, boss_tag ** tagMap, size_t * numTags); + +// Returns arrays of Bash Tag UIDs for Bash Tags suggested for addition and removal +// by BOSS'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 NULL. The userlistModified bool is true if the userlist contains Bash Tag +// suggestion message additions. +BOSS_API uint32_t boss_get_plugin_tags (boss_db db, const uint8_t * plugin, + uint32_t ** tagIds_added, + size_t * numTags_added, + uint32_t **tagIds_removed, + size_t *numTags_removed, + bool * userlistModified); + +// Returns the messages attached to the given plugin. Messages are valid until Load, +// DestroyBossDb or GetPluginMessages are next called. plugin is case-insensitive. +// If no messages are attached, *messages will be NULL and numMessages will equal 0. +BOSS_API uint32_t boss_get_plugin_messages (boss_db db, const uint8_t * plugin, + boss_message ** messages, + size_t * numMessages); + +// 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. +BOSS_API uint32_t boss_write_minimal_list (boss_db db, const uint8_t * outputFile, const bool overwrite); + + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/game.cpp b/src/game.cpp new file mode 100644 index 00000000..3702f235 --- /dev/null +++ b/src/game.cpp @@ -0,0 +1,145 @@ +/* BOSS + + A plugin load order optimiser for games that use the esp/esm plugin system. + + Copyright (C) 2012 WrinklyNinja + + This file is part of BOSS. + + BOSS 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. + + BOSS 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 BOSS. If not, see + . +*/ + +#include "game.h" +#include "globals.h" +#include "helpers.h" + +#include + +#if _WIN32 || _WIN64 +# include +# include +#endif + +using namespace std; + +namespace fs = boost::filesystem; + +namespace boss { + + Game::Game() + : id(BOSS_GAME_AUTODETECT) {} + + Game::Game(const uint32_t gameCode, const string path, const bool noPathInit) + : id(gameCode) { + if (Id() == BOSS_GAME_TES4) { + name = "TES IV: Oblivion"; + + registryKey = "Software\\Bethesda Softworks\\Oblivion"; + registrySubKey = "Installed Path"; + + bossFolderName = "Oblivion"; + pluginsFolderName = "Data"; + } else if (Id() == BOSS_GAME_TES5) { + name = "TES V: Skyrim"; + + registryKey = "Software\\Bethesda Softworks\\Skyrim"; + registrySubKey = "Installed Path"; + + bossFolderName = "Skyrim"; + pluginsFolderName = "Data"; + } else if (Id() == BOSS_GAME_FO3) { + name = "Fallout 3"; + + registryKey = "Software\\Bethesda Softworks\\Fallout3"; + registrySubKey = "Installed Path"; + + bossFolderName = "Fallout 3"; + pluginsFolderName = "Data"; + } else if (Id() == BOSS_GAME_FONV) { + name = "Fallout: New Vegas"; + + registryKey = "Software\\Bethesda Softworks\\FalloutNV"; + registrySubKey = "Installed Path"; + + bossFolderName = "Fallout New Vegas"; + pluginsFolderName = "Data"; + } else + throw runtime_error("Invalid game ID supplied."); + + if (!noPathInit) { + if (path.empty()) { + //First look for local install, then look for Registry. + if (IsInstalledLocally()) + gamePath = ".."; + else if (RegKeyExists("HKEY_LOCAL_MACHINE", registryKey, registrySubKey)) + gamePath = fs::path(RegKeyStringValue("HKEY_LOCAL_MACHINE", registryKey, registrySubKey)); + else + throw runtime_error("Game path could not be detected."); + } else + gamePath = fs::path(path); + } + } + + bool Game::IsInstalled() const { + return (IsInstalledLocally() || RegKeyExists("HKEY_LOCAL_MACHINE", registryKey, registrySubKey)); + } + + bool Game::IsInstalledLocally() const { + return fs::exists(fs::path("..") / pluginsFolderName); + } + + uint32_t Game::Id() const { + return id; + } + + string Game::Name() const { + return name; + } + + fs::path Game::GamePath() const { + return gamePath; + } + + fs::path Game::DataPath() const { + return GamePath() / pluginsFolderName; + } + + void Game::CreateBOSSGameFolder() { + //Make sure that the BOSS game path exists. + try { + if (!fs::exists(bossFolderName)) + fs::create_directory(bossFolderName); + } catch (fs::filesystem_error e) { + throw runtime_error("Could not create BOSS folder for game."); + } + } + + //Can be used to get the location of the LOCALAPPDATA folder (and its Windows XP equivalent). + fs::path Game::GetLocalAppDataPath() { +#if _WIN32 || _WIN64 + HWND owner; + TCHAR path[MAX_PATH]; + + HRESULT res = SHGetFolderPath(owner, CSIDL_LOCAL_APPDATA, NULL, SHGFP_TYPE_CURRENT, path); + + if (res == S_OK) + return fs::path(path); + else + return fs::path(""); +#else + return fs::path(""); +#endif + } +} diff --git a/src/game.h b/src/game.h new file mode 100644 index 00000000..8709561e --- /dev/null +++ b/src/game.h @@ -0,0 +1,64 @@ +/* BOSS + + A plugin load order optimiser for games that use the esp/esm plugin system. + + Copyright (C) 2012 WrinklyNinja + + This file is part of BOSS. + + BOSS 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. + + BOSS 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 BOSS. If not, see + . +*/ + +#ifndef __BOSS_GAME__ +#define __BOSS_GAME__ + +#include + +namespace boss { + + class Game { + public: + Game(); //Sets game to BOSS_GAME_AUTODETECT, with all other vars being empty. + Game(const uint32_t gameCode, const std::string path = "", const bool noPathInit = false); //Empty path means constructor will detect its location. If noPathInit is true, then the game's BOSS subfolder will not be created. + + bool IsInstalled() const; + bool IsInstalledLocally() const; + + uint32_t Id() const; + std::string Name() const; //Returns the game's name, eg. "TES IV: Oblivion". + + boost::filesystem::path GamePath() const; + boost::filesystem::path DataPath() const; + + //Creates directory in BOSS folder for BOSS's game-specific files. + void CreateBOSSGameFolder(); + private: + uint32_t id; + std::string name; + + std::string registryKey; + std::string registrySubKey; + + std::string bossFolderName; + std::string pluginsFolderName; + + boost::filesystem::path gamePath; //Path to the game's folder. + + //Can be used to get the location of the LOCALAPPDATA folder (and its Windows XP equivalent). + boost::filesystem::path GetLocalAppDataPath(); + }; +} + +#endif diff --git a/src/globals.h b/src/globals.h new file mode 100644 index 00000000..e33a5ab0 --- /dev/null +++ b/src/globals.h @@ -0,0 +1,36 @@ +/* BOSS + + A plugin load order optimiser for games that use the esp/esm plugin system. + + Copyright (C) 2012 WrinklyNinja + + This file is part of BOSS. + + BOSS 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. + + BOSS 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 BOSS. If not, see + . +*/ +#ifndef __BOSS_GLOBALS__ +#define __BOSS_GLOBALS__ + +const int BOSS_GAME_AUTODETECT = 0; +const int BOSS_GAME_TES4 = 1; +const int BOSS_GAME_TES5 = 2; +const int BOSS_GAME_FO3 = 3; +const int BOSS_GAME_FONV = 4; + +const int BOSS_VERSION_MAJOR = 3; +const int BOSS_VERSION_MINOR = 0; +const int BOSS_VERSION_PATCH = 0; + +#endif diff --git a/src/helpers.cpp b/src/helpers.cpp new file mode 100644 index 00000000..9cd0b78f --- /dev/null +++ b/src/helpers.cpp @@ -0,0 +1,264 @@ +/* BOSS + + A "one-click" program for users that quickly optimises and avoids + detrimental conflicts in their TES IV: Oblivion, Nehrim - At Fate's Edge, + TES V: Skyrim, Fallout 3 and Fallout: New Vegas mod load orders. + + Copyright (C) 2009-2012 BOSS Development Team. + + This file is part of BOSS. + + BOSS 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. + + BOSS 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 BOSS. If not, see + . + + $Revision: 3184 $, $Date: 2011-08-26 20:52:13 +0100 (Fri, 26 Aug 2011) $ +*/ + +#include "helpers.h" + +#include +#include +#include +#include + +#include +#include "source/utf8.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#if _WIN32 || _WIN64 +# include "Windows.h" +# include "Shlobj.h" +#endif + +namespace boss { + using namespace std; + using namespace boost; + using boost::algorithm::replace_all; + using boost::algorithm::replace_first; + namespace karma = boost::spirit::karma; + + //Calculate the CRC of the given file for comparison purposes. + uint32_t GetCrc32(const fs::path& filename) { + uint32_t chksum = 0; + static const size_t buffer_size = 8192; + char buffer[buffer_size]; + ifstream ifile(filename.c_str(), ios::binary); + // LOG_TRACE("calculating CRC for: '%s'", filename.string().c_str()); + boost::crc_32_type result; + if (ifile) { + do { + ifile.read(buffer, buffer_size); + result.process_bytes(buffer, ifile.gcount()); + } while (ifile); + chksum = result.checksum(); + } else { + throw runtime_error("Unable to open \"" + filename.string() + "\" for CRC calculation."); + } + // LOG_DEBUG("CRC32('%s'): 0x%x", filename.string().c_str(), chksum); + return chksum; + } + + //Converts an integer to a string using BOOST's Spirit.Karma, which is apparently a lot faster than a stringstream conversion... + std::string IntToString(const uint32_t n) { + string out; + back_insert_iterator sink(out); + karma::generate(sink,karma::upper[karma::uint_],n); + return out; + } + + //Converts an integer to a hex string using BOOST's Spirit.Karma, which is apparently a lot faster than a stringstream conversion... + std::string IntToHexString(const uint32_t n) { + string out; + back_insert_iterator sink(out); + karma::generate(sink,karma::upper[karma::hex],n); + return out; + } + + //Converts a boolean to a string representation (true/false) + std::string BoolToString(const bool b) { + if (b) + return "true"; + else + return "false"; + } + + //Check if registry subkey exists. + bool RegKeyExists(const std::string& keyStr, const std::string& subkey, const std::string& value) { + return !RegKeyStringValue(keyStr, subkey, value).empty(); + } + + //Get registry subkey value string. + string RegKeyStringValue(const std::string& keyStr, const std::string& subkey, const std::string& value) { +#if _WIN32 || _WIN64 + HKEY hKey, key; + DWORD BufferSize = 4096; + wchar_t val[4096]; + + if (keyStr == "HKEY_CLASSES_ROOT") + key = HKEY_CLASSES_ROOT; + else if (keyStr == "HKEY_CURRENT_CONFIG") + key = HKEY_CURRENT_CONFIG; + else if (keyStr == "HKEY_CURRENT_USER") + key = HKEY_CURRENT_USER; + else if (keyStr == "HKEY_LOCAL_MACHINE") + key = HKEY_LOCAL_MACHINE; + else if (keyStr == "HKEY_USERS") + key = HKEY_USERS; + + LONG ret = RegOpenKeyEx(key, fs::path(subkey).wstring().c_str(), 0, KEY_READ|KEY_WOW64_32KEY, &hKey); + + if (ret == ERROR_SUCCESS) { + ret = RegQueryValueEx(hKey, fs::path(value).wstring().c_str(), NULL, NULL, (LPBYTE)&val, &BufferSize); + RegCloseKey(hKey); + + if (ret == ERROR_SUCCESS) + return fs::path(val).string(); //Easiest way to convert from wide to narrow character strings. + else + return ""; + } else + return ""; +#else + return ""; +#endif + } + + + ////////////////////////////// + // Version Class Functions + ////////////////////////////// + + Version::Version() {} + + Version::Version(const char * ver) + : verString(ver) {} + + Version::Version(const std::string ver) + : verString(ver) {} + + Version::Version(const fs::path file) { + // LOG_TRACE("extracting version from '%s'", file.string().c_str()); +#if _WIN32 || _WIN64 + DWORD dummy = 0; + DWORD size = GetFileVersionInfoSize(file.wstring().c_str(), &dummy); + + if (size > 0) { + LPBYTE point = new BYTE[size]; + UINT uLen; + VS_FIXEDFILEINFO *info; + string ver; + + GetFileVersionInfo(file.wstring().c_str(),0,size,point); + + VerQueryValue(point,L"\\",(LPVOID *)&info,&uLen); + + DWORD dwLeftMost = HIWORD(info->dwFileVersionMS); + DWORD dwSecondLeft = LOWORD(info->dwFileVersionMS); + DWORD dwSecondRight = HIWORD(info->dwFileVersionLS); + DWORD dwRightMost = LOWORD(info->dwFileVersionLS); + + delete [] point; + + verString = IntToString(dwLeftMost) + '.' + IntToString(dwSecondLeft) + '.' + IntToString(dwSecondRight) + '.' + IntToString(dwRightMost); + } +#else + // ensure filename has no quote characters in it to avoid command injection attacks + if (string::npos == file.string().find('"')) { + // LOG_WARN("filename has embedded quotes; skipping to avoid command injection: '%s'", file.string().c_str()); + // } else { + // command mostly borrowed from the gnome-exe-thumbnailer.sh script + // wrestool is part of the icoutils package + string cmd = "wrestool --extract --raw --type=version \"" + file.string() + "\" | tr '\\0, ' '\\t.\\0' | sed 's/\\t\\t/_/g' | tr -c -d '[:print:]' | sed -r 's/.*Version[^0-9]*([0-9]+(\\.[0-9]+)+).*/\\1/'"; + + FILE *fp = popen(cmd.c_str(), "r"); + + // read out the version string + static const uint32_t BUFSIZE = 32; + char buf[BUFSIZE]; + if (NULL != fgets(buf, BUFSIZE, fp)) { + /* LOG_DEBUG("failed to extract version from '%s'", file.string().c_str()); + } + else { + */ verString = string(buf); + // LOG_DEBUG("extracted version from '%s': %s", file.string().c_str(), retVal.c_str()); + } + pclose(fp); + } +#endif + } + + string Version::AsString() const { + return verString; + } + + bool Version::operator < (Version ver) { + //Version string could have a wide variety of formats. Use regex to choose specific comparison types. + + boost::regex reg1("(\\d+\\.?)+"); //a.b.c.d.e.f.... where the letters are all integers, and 'a' is the shortest possible match. + + //boost::regex reg2("(\\d+\\.?)+([a-zA-Z\\-]+(\\d+\\.?)*)+"); //Matches a mix of letters and numbers - from "0.99.xx", "1.35Alpha2", "0.9.9MB8b1", "10.52EV-D", "1.62EV" to "10.0EV-D1.62EV". + + if (boost::regex_match(verString, reg1) && boost::regex_match(ver.AsString(), reg1)) { + //First type: numbers separated by periods. If two versions have a different number of numbers, then the shorter should be padded + //with zeros. An arbitrary number of numbers should be supported. + istringstream parser1(verString); + istringstream parser2(ver.AsString()); + while (parser1.good() || parser2.good()) { + //Check if each stringstream is OK for i/o before doing anything with it. If not, replace its extracted value with a 0. + uint32_t n1, n2; + if (parser1.good()) { + parser1 >> n1; + parser1.get(); + } else + n1 = 0; + if (parser2.good()) { + parser2 >> n2; + parser2.get(); + } else + n2 = 0; + if (n1 < n2) + return true; + else if (n1 > n2) + return false; + } + return false; + } else { + //Wacky format. Use the Alphanum Algorithm. (what a name!) + return (doj::alphanum_comp(verString, ver.AsString()) < 0); + } + } + + bool Version::operator > (Version ver) { + return (*this != ver && !(*this < ver)); + } + + bool Version::operator >= (Version ver) { + return (*this == ver || *this > ver); + } + + bool Version::operator == (Version ver) { + return (verString == ver.AsString()); + } + + bool Version::operator != (Version ver) { + return !(*this == ver); + } +} diff --git a/src/helpers.h b/src/helpers.h new file mode 100644 index 00000000..297bec1b --- /dev/null +++ b/src/helpers.h @@ -0,0 +1,79 @@ +/* BOSS + + A "one-click" program for users that quickly optimises and avoids + detrimental conflicts in their TES IV: Oblivion, Nehrim - At Fate's Edge, + TES V: Skyrim, Fallout 3 and Fallout: New Vegas mod load orders. + + Copyright (C) 2009-2012 BOSS Development Team. + + This file is part of BOSS. + + BOSS 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. + + BOSS 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 BOSS. If not, see + . + + $Revision: 3163 $, $Date: 2011-08-21 22:03:18 +0100 (Sun, 21 Aug 2011) $ +*/ + +#ifndef __BOSS_HELPERS__ +#define __BOSS_HELPERS__ + +#include +#include + +namespace boss { + namespace fs = boost::filesystem; + + ////////////////////////////////////////////////////////////////////////// + // Helper functions + ////////////////////////////////////////////////////////////////////////// + + //Calculate the CRC of the given file for comparison purposes. + uint32_t GetCrc32(const fs::path& filename); + + //Converts an integer to a string using BOOST's Spirit.Karma. Faster than a stringstream conversion. + std::string IntToString(const uint32_t n); + + //Converts an integer to a hex string using BOOST's Spirit.Karma. Faster than a stringstream conversion. + std::string IntToHexString(const uint32_t n); + + //Converts a boolean to a string representation (true/false) + std::string BoolToString(const bool b); + + //Check if registry subkey exists. + bool RegKeyExists(const std::string& keyStr, const std::string& subkey, const std::string& value); + + //Get registry subkey value string. + std::string RegKeyStringValue(const std::string& keyStr, const std::string& subkey, const std::string& value); + + //Version class for more robust version comparisons. + class Version { + private: + std::string verString; + public: + Version(); + Version(const char * ver); + Version(const std::string ver); + Version(const fs::path file); + + std::string AsString() const; + + bool operator > (Version); + bool operator < (Version); + bool operator >= (Version); + bool operator == (Version); + bool operator != (Version); + }; +} + +#endif diff --git a/src/metadata.cpp b/src/metadata.cpp index 2f081326..88391497 100644 --- a/src/metadata.cpp +++ b/src/metadata.cpp @@ -29,84 +29,207 @@ namespace boss { ConditionalData::ConditionalData() {} - ConditionalData::ConditionalData(const string in) : condition(in) {} + ConditionalData::ConditionalData(const string& c) : condition(c) {} - bool ConditionalData::EvalCondition() const { + ConditionalData::ConditionalData(const std::string& c, const std::string& d) + : condition(c), data(d) {} - return true; + bool ConditionalData::IsConditional() const { + return !condition.empty(); + } + + std::string ConditionalData::Condition() const { + return condition; + } + + std::string ConditionalData::Data() const { + return data; + } + + void ConditionalData::Data(const std::string& d) { + data = d; + } + + Message::Message() {} + + Message::Message(const std::string& t, const std::string& cont) + : type(t), ConditionalData("", cont) {} + + Message::Message(const std::string& t, const std::string& cont, + const std::string& cond, const std::string& l) + : type(t), language(l), ConditionalData(cond, cont) {} + + std::string Message::Type() const { + return type; + } + + std::string Message::Language() const { + return language; + } + + std::string Message::Content() const { + return Data(); + } + + File::File() {} + File::File(const std::string& n) : ConditionalData("", n) {} + File::File(const std::string& n, const std::string& d, const std::string& c) + : display(d), ConditionalData(c, n) {} + + std::string File::Name() const { + return Data(); + } + + std::string File::DisplayName() const { + return display; } Tag::Tag() : addTag(true) {} - Tag::Tag(const string tag) { + Tag::Tag(const string& tag) { + string data; if (tag[0] == '-') { addTag = false; - name = tag.substr(1); + data = tag.substr(1); } else { addTag = true; - name = tag; + data = tag; } + Data(data); } - Tag::Tag(const string condition, const string tag) : ConditionalData(condition) { + Tag::Tag(const string& tag, const string& condition) : ConditionalData(condition) { + string data; if (tag[0] == '-') { addTag = false; - name = tag.substr(1); + data = tag.substr(1); } else { addTag = true; - name = tag; + data = tag; } + Data(data); } bool Tag::IsAddition() const { return addTag; } - string Tag::Data() const { + string Tag::PrefixedName() const { if (addTag) - return name; + return Name(); else - return "-" + name; + return "-" + Name(); } - void Plugin::EvalAllConditions() { + std::string Tag::Name() const { + return Data(); + } + + Plugin::Plugin() : enabled(true), priority(0) {} + Plugin::Plugin(const std::string n) : name(n), enabled(true), priority(0) {} + + std::string Plugin::Name() const { + return name; + } + + bool Plugin::Enabled() const { + return enabled; + } + + int Plugin::Priority() const { + return priority; + } + + std::list Plugin::LoadAfter() const { + return loadAfter; + } + + std::list Plugin::Reqs() const { + return requirements; + } + + std::set Plugin::Incs() const { + return incompatibilities; + } + + std::list Plugin::Messages() const { + return messages; + } + + std::list Plugin::Tags() const { + return tags; + } + + void Plugin::Enabled(const bool e) { + enabled = e; + } + + void Plugin::Priority(const int p) { + priority = p; + } + + void Plugin::LoadAfter(const std::list& l) { + loadAfter = l; + } + + void Plugin::Reqs(const std::list& r) { + requirements = r; + } + + void Plugin::Incs(const std::set& i) { + incompatibilities = i; + } + + void Plugin::Messages(const std::list& m) { + messages = m; + } + + void Plugin::Tags(const std::list& t) { + tags = t; + } + + void Plugin::EvalAllConditions(const boost::filesystem::path& gamePath) { for (list::iterator it = loadAfter.begin(); it != loadAfter.end();) { - if (!it->EvalCondition()) + if (!it->EvalCondition(gamePath)) it = loadAfter.erase(it); else ++it; } for (list::iterator it = requirements.begin(); it != requirements.end();) { - if (!it->EvalCondition()) + if (!it->EvalCondition(gamePath)) it = requirements.erase(it); else ++it; } for (set::iterator it = incompatibilities.begin(); it != incompatibilities.end();) { - if (!it->EvalCondition()) + if (!it->EvalCondition(gamePath)) incompatibilities.erase(it++); else ++it; } for (list::iterator it = messages.begin(); it != messages.end();) { - if (!it->EvalCondition()) + if (!it->EvalCondition(gamePath)) it = messages.erase(it); else ++it; } for (list::iterator it = tags.begin(); it != tags.end();) { - if (!it->EvalCondition()) + if (!it->EvalCondition(gamePath)) it = tags.erase(it++); else ++it; } } - bool Plugin::NameOnly() const { + bool Plugin::HasNameOnly() const { return priority == 0 && enabled == true && loadAfter.empty() && requirements.empty() && incompatibilities.empty() && messages.empty() && tags.empty(); } + + bool Plugin::IsRegexPlugin() const { + return name.substr(name.length()-5) == "\\.esp" || name.substr(name.length()-5) == "\\.esm"; + } } diff --git a/src/metadata.h b/src/metadata.h index e20394bf..06d82cf9 100644 --- a/src/metadata.h +++ b/src/metadata.h @@ -27,73 +27,120 @@ #include #include +#include + namespace boss { class ConditionalData { public: ConditionalData(); - ConditionalData(const std::string s); - std::string condition; + ConditionalData(const std::string& condition); + ConditionalData(const std::string& condition, const std::string& data); - bool EvalCondition() const; + bool IsConditional() const; + bool EvalCondition(const boost::filesystem::path& gamePath) const; + + std::string Condition() const; + std::string Data() const; + + void Data(const std::string& data); + private: + std::string condition; + std::string data; }; class Message : public ConditionalData { public: + Message(); + Message(const std::string& type, const std::string& content); + Message(const std::string& type, const std::string& content, + const std::string& condition, const std::string& language); + + std::string Type() const; + std::string Language() const; + std::string Content() const; + private: std::string type; std::string language; - std::string content; }; class File : public ConditionalData { public: - std::string name; + File(); + File(const std::string& name); + File(const std::string& name, const std::string& display, + const std::string& condition); + + std::string Name() const; + std::string DisplayName() const; + private: std::string display; }; class Tag : public ConditionalData { public: Tag(); - Tag(const std::string tag); - Tag(const std::string condition, const std::string tag); - - bool addTag; - std::string name; + Tag(const std::string& tag); + Tag(const std::string& tag, const std::string& condition); bool IsAddition() const; - std::string Data() const; //Name with '-' in front if suggested for removal. + std::string Name() const; + std::string PrefixedName() const; //Name with '-' in front if suggested for removal. + private: + bool addTag; }; struct file_comp { bool operator() (const File& lhs, const File& rhs) const { - return lhs.name < rhs.name; + return lhs.Name() < rhs.Name(); } }; struct tag_comp { bool operator() (const Tag& lhs, const Tag& rhs) const { - return lhs.name < rhs.name; + return lhs.Name() < rhs.Name(); } }; class Plugin { public: + Plugin(); + Plugin(const std::string name); + + std::string Name() const; + bool Enabled() const; + int Priority() const; + std::list LoadAfter() const; + std::list Reqs() const; + std::set Incs() const; + std::list Messages() const; + std::list Tags() const; + + void Enabled(const bool enabled); + void Priority(const int priority); + void LoadAfter(const std::list& after); + void Reqs(const std::list& reqs); + void Incs(const std::set& incs); + void Messages(const std::list& messages); + void Tags(const std::list& tags); + + void EvalAllConditions(const boost::filesystem::path& gamePath); + bool HasNameOnly() const; + bool IsRegexPlugin() const; + private: std::string name; bool enabled; //Default to true. - int priority; //Default to 0 : >0 his higher, <0 is lower priorities. + int priority; //Default to 0 : >0 is higher, <0 is lower priorities. std::list loadAfter; std::list requirements; std::set incompatibilities; std::list messages; std::list tags; - - void EvalAllConditions(); - bool NameOnly() const; }; struct plugin_comp { bool operator() (const Plugin& lhs, const Plugin& rhs) const { - return lhs.name < rhs.name; + return lhs.Name() < rhs.Name(); } }; } diff --git a/src/parsers.h b/src/parsers.h index bfa7c6aa..9e88ddb7 100644 --- a/src/parsers.h +++ b/src/parsers.h @@ -24,8 +24,21 @@ #ifndef __BOSS_PARSERS__ #define __BOSS_PARSERS__ +#ifndef BOOST_SPIRIT_UNICODE +#define BOOST_SPIRIT_UNICODE +#endif + #include "metadata.h" +#include + +#include +#include +#include +#include +#include +#include + namespace YAML { /////////////////////// @@ -36,10 +49,10 @@ namespace YAML { struct convert { static Node encode(const boss::Message& rhs) { Node node; - node["condition"] = rhs.condition; - node["type"] = rhs.type; - node["content"] = rhs.content; - node["lang"] = rhs.language; + node["condition"] = rhs.Condition(); + node["type"] = rhs.Type(); + node["content"] = rhs.Content(); + node["lang"] = rhs.Language(); return node; } @@ -47,14 +60,17 @@ namespace YAML { if(!node.IsMap()) return false; + std::string condition, type, content, language; if (node["condition"]) - rhs.condition = node["condition"].as(); + condition = node["condition"].as(); if (node["type"]) - rhs.type = node["type"].as(); + type = node["type"].as(); if (node["content"]) - rhs.content = node["content"].as(); + content = node["content"].as(); if (node["lang"]) - rhs.language = node["lang"].as(); + language = node["lang"].as(); + + rhs = boss::Message(type, content, condition, language); return true; } }; @@ -63,23 +79,24 @@ namespace YAML { struct convert { static Node encode(const boss::File& rhs) { Node node; - node["condition"] = rhs.condition; - node["name"] = rhs.name; - node["display"] = rhs.display; + node["condition"] = rhs.Condition(); + node["name"] = rhs.Name(); + node["display"] = rhs.DisplayName(); return node; } static bool decode(const Node& node, boss::File& rhs) { if(node.IsMap()) { + std::string condition, name, display; if (node["condition"]) - rhs.condition = node["condition"].as(); + condition = node["condition"].as(); if (node["name"]) - rhs.name = node["name"].as(); + name = node["name"].as(); if (node["display"]) - rhs.display = node["display"].as(); - } else { - rhs.name = node.as(); - } + display = node["display"].as(); + rhs = boss::File(name, display, condition); + } else + rhs = boss::File(node.as()); return true; } }; @@ -88,25 +105,22 @@ namespace YAML { struct convert { static Node encode(const boss::Tag& rhs) { Node node; - node["condition"] = rhs.condition; - if (!rhs.addTag) - node["name"] = "-" + rhs.name; - else - node["name"] = rhs.name; + node["condition"] = rhs.Condition(); + node["name"] = rhs.PrefixedName(); return node; } static bool decode(const Node& node, boss::Tag& rhs) { - std::string condition, tag; if(node.IsMap()) { + std::string condition, tag; if (node["condition"]) condition = node["condition"].as(); if (node["name"]) tag = node["name"].as(); + rhs = boss::Tag(tag, condition); } else if (node.IsScalar()) { - tag = node.as(); + rhs = boss::Tag(node.as()); } - rhs = boss::Tag(condition, tag); return true; } }; @@ -138,14 +152,14 @@ namespace YAML { struct convert { static Node encode(const boss::Plugin& rhs) { Node node; - node["name"] = rhs.name; - node["enabled"] = rhs.enabled; - node["priority"] = rhs.priority; - node["after"] = rhs.loadAfter; - node["req"] = rhs.requirements; - node["inc"] = rhs.incompatibilities; - node["msg"] = rhs.messages; - node["tag"] = rhs.tags; + node["name"] = rhs.Name(); + node["enabled"] = rhs.Enabled(); + node["priority"] = rhs.Priority(); + node["after"] = rhs.LoadAfter(); + node["req"] = rhs.Reqs(); + node["inc"] = rhs.Incs(); + node["msg"] = rhs.Messages(); + node["tag"] = rhs.Tags(); return node; } @@ -155,27 +169,23 @@ namespace YAML { return false; if (node["name"]) - rhs.name = node["name"].as(); + rhs = boss::Plugin(node["name"].as()); if (node["enabled"]) - rhs.enabled = node["enabled"].as(); - else - rhs.enabled = true; + rhs.Enabled(node["enabled"].as()); if (node["priority"]) - rhs.priority = node["priority"].as(); - else - rhs.priority = 0; + rhs.Priority(node["priority"].as()); if (node["after"]) - rhs.loadAfter = node["after"].as< std::list >(); + rhs.LoadAfter(node["after"].as< std::list >()); if (node["req"]) - rhs.requirements = node["req"].as< std::list >(); + rhs.Reqs(node["req"].as< std::list >()); if (node["inc"]) - rhs.incompatibilities = node["inc"].as< std::set >(); + rhs.Incs(node["inc"].as< std::set >()); if (node["msg"]) - rhs.messages = node["msg"].as< std::list >(); + rhs.Messages(node["msg"].as< std::list >()); if (node["tag"]) - rhs.tags = node["tag"].as< std::list >(); + rhs.Tags(node["tag"].as< std::list >()); return true; } }; @@ -195,72 +205,72 @@ namespace YAML { Emitter& operator << (Emitter& out, const boss::Message& rhs) { out << BeginMap - << Key << "type" << rhs.type - << Key << "content" << rhs.content; + << Key << "type" << rhs.Type() + << Key << "content" << rhs.Content(); - if (!rhs.language.empty()) - out << Key << "lang" << rhs.language; + if (!rhs.Language().empty()) + out << Key << "lang" << rhs.Language(); - if (!rhs.condition.empty()) - out << Key << "condition" << rhs.condition; + if (!rhs.Condition().empty()) + out << Key << "condition" << rhs.Condition(); out << EndMap; } Emitter& operator << (Emitter& out, const boss::File& rhs) { - if (rhs.condition.empty() && rhs.display.empty()) - out << rhs.name; + if (!rhs.IsConditional() && rhs.DisplayName().empty()) + out << rhs.Name(); else { out << BeginMap - << Key << "name" << rhs.name; + << Key << "name" << rhs.Name(); - if (!rhs.condition.empty()) - out << Key << "condition" << rhs.condition; + if (!rhs.Condition().empty()) + out << Key << "condition" << rhs.Condition(); - if (!rhs.display.empty()) - out << Key << "display" << rhs.display; + if (!rhs.DisplayName().empty()) + out << Key << "display" << rhs.DisplayName(); out << EndMap; } } Emitter& operator << (Emitter& out, const boss::Tag& rhs) { - if (rhs.condition.empty()) - out << rhs.Data(); + if (!rhs.IsConditional()) + out << rhs.PrefixedName(); else { out << BeginMap - << Key << "name" << rhs.Data() - << Key << "condition" << rhs.condition + << Key << "name" << rhs.PrefixedName() + << Key << "condition" << rhs.Condition() << EndMap; } } Emitter& operator << (Emitter& out, const boss::Plugin& rhs) { - if (!rhs.NameOnly()) { + if (!rhs.HasNameOnly()) { out << BeginMap - << Key << "name" << Value << rhs.name; + << Key << "name" << Value << rhs.Name(); - if (rhs.priority != 0) - out << Key << "priority" << Value << rhs.priority; + if (rhs.Priority() != 0) + out << Key << "priority" << Value << rhs.Priority(); - if (!rhs.enabled) - out << Key << "enabled" << Value << rhs.enabled; + if (!rhs.Enabled()) + out << Key << "enabled" << Value << rhs.Enabled(); - if (!rhs.loadAfter.empty()) - out << Key << "after" << Value << rhs.loadAfter; + if (!rhs.LoadAfter().empty()) + out << Key << "after" << Value << rhs.LoadAfter(); - if (!rhs.requirements.empty()) - out << Key << "req" << Value << rhs.requirements; + if (!rhs.Reqs().empty()) + out << Key << "req" << Value << rhs.Reqs(); - if (!rhs.incompatibilities.empty()) - out << Key << "inc" << Value << rhs.incompatibilities; + if (!rhs.Incs().empty()) + out << Key << "inc" << Value << rhs.Incs(); - if (!rhs.messages.empty()) - out << Key << "msg" << Value << rhs.messages; + if (!rhs.Messages().empty()) + out << Key << "msg" << Value << rhs.Messages(); - if (!rhs.tags.empty()) - out << Key << "tag" << Value << rhs.tags; + if (!rhs.Tags().empty()) + out << Key << "tag" << Value << rhs.Tags(); out << EndMap; } @@ -310,5 +320,65 @@ namespace boss { } */ + + namespace qi = boost::spirit::qi; + namespace unicode = boost::spirit::unicode; + + template + class condition_grammar : public qi::grammar { + public: + condition_grammar() : condition_grammar::base_type(expression, "condition grammar") { + + expression = + condition; + + condition = + ( qi::lit("if") >> type ) [qi::labels::_val = qi::labels::_1] + | ( qi::lit("ifnot") >> type ) [qi::labels::_val = !qi::labels::_1] + ; + + type = + ( "file(" > quotedStr > ')' ) [qi::labels::_val = true] + | ( "checksum(" > quotedStr > ',' > +unicode::xdigit > ')' ) [qi::labels::_val = true] + | ( "version(" > quotedStr > ',' > quotedStr > ',' > unicode::char_ > ')' ) [qi::labels::_val = true] + | ( "active(" > quotedStr > ')' ) [qi::labels::_val = true] + ; + + quotedStr = '"' > +(unicode::char_ - '"') > '"'; + + /* Need to add error handlers and actual type evaluation, then start on + compound conditional parsing. Need game path support. */ + } + + private: + qi::rule expression, condition, type; + qi::rule quotedStr; + + + + //Eval's regex and exact paths. + bool FileExists(bool& result, std::string file); + + + }; + + bool ConditionalData::EvalCondition(const boost::filesystem::path& gamePath) const { + if (condition.empty()) + return true; + condition_grammar grammar; + qi::space_type skipper; + std::string::const_iterator begin, end; + bool eval; + + begin = condition.begin(); + end = condition.end(); + + bool r = qi::phrase_parse(begin, end, grammar, skipper, eval); + + if (!r || begin != end) + throw std::runtime_error("Parsing of condition \"" + condition + "\" failed!"); + + return eval; + } } #endif diff --git a/src/plugindata.cpp b/src/plugindata.cpp new file mode 100644 index 00000000..19c87455 --- /dev/null +++ b/src/plugindata.cpp @@ -0,0 +1,212 @@ +/* BOSS + + A plugin load order optimiser for games that use the esp/esm plugin system. + + Copyright (C) 2012 WrinklyNinja + + This file is part of BOSS. + + BOSS 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. + + BOSS 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 BOSS. If not, see + . +*/ + +#include "plugindata.h" + +#include +#include +#include +#include +#include +#include + +namespace boss { + + PluginData::PluginData() : crc(0) {} + + PluginData::PluginData(std::string filename) : name(filename) { + //Load by scanning plugin file using libespm. + } + + PluginData::PluginData(std::string filename, uint32_t fileCRC) : name(filename), crc(fileCRC) { + //Load by scanning cache file. + std::string file = name[0] + '/' + name[1] + '/' + name + ".dc"; + //Lowercase the path. + std::for_each(file.begin(), file.end(), tolower); + + if (!boost::filesystem::exists(file)) + return; + + ifstream ifile(file.c_str(), ios_base::binary); + ifile.exceptions(ifstream::failbit | ifstream::badbit | ifstream::eofbit); + + //Get the number of entries. + uint32_t count; + ifile.seekg(4, ios_base::end); + ifile.read((char*)&count, 4); + + //Read index. + CacheIndexEntry * index; + try { + index = new CacheIndexEntry[count]; + } catch (std::bad_alloc& e) { + return; + } + ifile.seekg(4 + count * sizeof(CacheIndexEntry), ios_base::end); + ifile.read((char*)index, count * sizeof(CacheIndexEntry)); + + //Search index. + uint32_t i = 0; + for (i; i < count; i++) { + if (index[i].crc == crc) + break; + } + + if (i == count) + return; + + //Read plugin data size. + uint32_t dataSize; + ifile.seekg(index[i].offset, ios_base::beg); + ifile.read((char*)&dataSize, 4); + + //Read plugin data. + char * data; + try { + data = new char[dataSize]; + } catch (std::bad_alloc& e) { + return; + } + ifile.read(data, dataSize); + + //Now we need to read the list of masters. + uint16_t mastersLen = *(uint16_t*)data; + std::string master; + std::vector masters; + for (int i=2; i < mastersLen; i++) { + if (data[i] != NULL) + master += data[i]; + else { + masters.push_back(master); + master.clear(); + } + } + + //Now read the FormIDs, building up FormID objects from them and their + //associated masters. + for (int i=2+mastersLen; i < dataSize; i += 4) { + int masterID = data[i+3]; //FormIDs are encoded low to high byte. + FormID fid; + fid.plugin = masters[masterID]; + fid.objectIndex = *(uint32_t*)data & 0xFFFFFF; //Zero the masterID. + formIDs.insert(fid); + } + + ifile.close(); + } + + PluginData::Save() const { + //Save to a cache file. + std::string outPath = name[0] + '/' + name[1] + '/' + name + ".dc"; + //Lowercase the path. + std::for_each(outPath.begin(), outPath.end(), tolower); + + uint32_t count = 0; + CacheIndexEntry * index = NULL; + if (boost::filesystem::exists(file)) { + ifstream ifile(outPath.c_str(), ios_base::binary); + ifile.exceptions(ifstream::failbit | ifstream::badbit | ifstream::eofbit); + + //Get the number of entries. + ifile.seekg(4, ios_base::end); + ifile.read((char*)&count, 4); + + //Read index. + try { + index = new CacheIndexEntry[count]; + } catch (std::bad_alloc& e) { + return; + } + ifile.seekg(4 + count * sizeof(CacheIndexEntry), ios_base::end); + ifile.read((char*)&index, count * sizeof(CacheIndexEntry)); + + ifile.close(); + } + + /*Split the FormID object list into a FormID number list and a master list. + This doesn't retain the same ordering of masters as the original data, + but that doesn't matter. */ + std::map masters; + std::list fids; + std::string mastersList; + int id = 0; + for (std::list::iterator it = formIDs.begin(), endIt = formIDs.end(); it != endIt; ++it) { + std::map::iterator mit = masters.find(it->plugin); + if (mit != masters.end()) + fids.insert(it->objectIndex | (mit->second<<24)); + else { + id++; + masters.insert(pair(it->plugin, id); + fids.insert(it->objectIndex | (id<<24)); + mastersList += it->plugin + '\0'; + } + } + + ofstream ofile(file.c_str(), ios_base::binary); + ofile.exceptions(ifstream::failbit | ifstream::badbit | ifstream::eofbit); + + CacheIndexEntry entry; + entry.crc = crc; + + if (!boost::filesystem::exists(file)) { + uint8_t nameLength = name.length(); + ofile.write((char*)&nameLenth, 1); + ofile.write((char*)name.data(), nameLength); + entry.offset = tellg(); + } else { + ofile.seekg(4 + count * sizeof(CacheIndexEntry), ios_base::end); + entry.offset = ofile.tellg(); + } + + //Write out data. + uint16_t mLen = mastersList.size(); + ofile.write((char*)&mLen, 2); + ofile.write(mastersList.data(), mLen); + for (std::list::iterator it = formIDs.begin(), endIt = formIds.end(); it != endIt; ++it) { + ofile.write((char*)it, 4); + } + + //Write out updated index. + if (index != NULL) + ofile.write((char*)index, count * sizeof(CacheIndexEntry)); + ofile.write((char*)&entry, sizeof(CacheIndexEntry)); + count++; + ofile.write((char*)&count, 4); + + ofile.close(); + } + + PluginData::Empty() const { + return formIDs.empty(); + } + + int PluginData::Overlap(const PluginData& otherPlugin) const { + int count = 0; + for (std::list::iterator it1 = formIDs.begin(), endIt1 = formIDs.end(); it1 != endIt1; ++it1) { + for (std::list::iterator it2 = otherPlugin.formIDs.begin(), endIt2 = otherPlugin.formIDs.end(); it2 != endIt2; ++it2) { + if (*it1 == *it2) + count++; + } + } + } +} diff --git a/src/plugindata.h b/src/plugindata.h new file mode 100644 index 00000000..8e1096f9 --- /dev/null +++ b/src/plugindata.h @@ -0,0 +1,67 @@ +/* BOSS + + A plugin load order optimiser for games that use the esp/esm plugin system. + + Copyright (C) 2012 WrinklyNinja + + This file is part of BOSS. + + BOSS 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. + + BOSS 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 BOSS. If not, see + . +*/ + +#ifndef __BOSS_PLUGINDATA_H__ +#define __BOSS_PLUGINDATA_H__ + +#include +#include +#include + +//The functions below make no distinctions between different games. Once a game +//handler is implemented, they will be updated to use it. + +namespace boss { + + struct CacheIndexEntry { + uint32_t crc; + uint32_t offset; + } + + struct FormID { + std::string plugin; + uint32_t objectIndex; + } + + struct PluginData { + std::string name; + uint32_t crc; + std::list formIDs; // Only the edited FormIDs though. + + PluginData(); + PluginData(std::string pluginPath); + + /* Can throw std::bad_alloc or std::ios::failure */ + PluginData(std::string name, uint32_t crc); + + /* Can throw std::bad_alloc or std::ios::failure */ + void Save() const; + bool Empty() const; + + // Returns the record overlap of two PluginData objects. + int Overlap(const PluginData& otherPlugin) const; + } + +} + +#endif diff --git a/src/sorting.h b/src/sorting.h new file mode 100644 index 00000000..2f7d62f1 --- /dev/null +++ b/src/sorting.h @@ -0,0 +1,52 @@ +/* BOSS + + A plugin load order optimiser for games that use the esp/esm plugin system. + + Copyright (C) 2012 WrinklyNinja + + This file is part of BOSS. + + BOSS 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. + + BOSS 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 BOSS. If not, see + . +*/ + +#ifndef __BOSS_SORTING_H__ +#define __BOSS_SORTING_H__ + +#include "plugindata.h" + +namespace boss { + + // None of these comparisons care about plugin name or CRC. + inline bool operator==(const PluginData& lhs, const PluginData& rhs){ + return lhs.formIDs == rhs.formIDs; + } + + + inline bool operator!=(const PluginData& lhs, const PluginData& rhs){return !operator==(lhs,rhs);} + + + inline bool operator< (const PluginData& lhs, const PluginData& rhs){ + return lhs.formIDs.size() < rhs.formIDs.size(); + } + + + inline bool operator> (const PluginData& lhs, const PluginData& rhs){return operator< (rhs,lhs);} + inline bool operator<=(const PluginData& lhs, const PluginData& rhs){return !operator> (lhs,rhs);} + inline bool operator>=(const PluginData& lhs, const PluginData& rhs){return !operator< (lhs,rhs);} + + +} + +#endif diff --git a/src/tester.cpp b/src/tester.cpp index 91b1da4e..b2a77f14 100644 --- a/src/tester.cpp +++ b/src/tester.cpp @@ -12,13 +12,8 @@ using namespace std; void minimisePluginList(set& plugins) { set out; for (set::iterator it=plugins.begin(), endIt=plugins.end(); it != endIt; ++it) { - boss::Plugin p = *it; - p.priority = 0; - p.enabled = true; - p.loadAfter.clear(); - p.requirements.clear(); - p.incompatibilities.clear(); - p.messages.clear(); + boss::Plugin p(it->Name()); + p.Tags(it->Tags()); out.insert(p); } plugins.swap(out);