diff --git a/CMakeLists.txt b/CMakeLists.txt index 696adef3..a456298c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,7 +11,7 @@ cmake_minimum_required (VERSION 2.8.9) project (boss) -set (BOSS_SRC "${CMAKE_SOURCE_DIR}/src/metadata.cpp" "${CMAKE_SOURCE_DIR}/src/game.cpp" "${CMAKE_SOURCE_DIR}/src/helpers.cpp") +set (BOSS_SRC "${CMAKE_SOURCE_DIR}/src/metadata.cpp" "${CMAKE_SOURCE_DIR}/src/game.cpp" "${CMAKE_SOURCE_DIR}/src/helpers.cpp" "${CMAKE_SOURCE_DIR}/src/plugin/ModFormat.cpp" "${CMAKE_SOURCE_DIR}/src/plugin/VersionRegex.cpp") # Include source and library directories. include_directories ("${BOSS_LIBS_DIR}/alphanum" "${BOSS_LIBS_DIR}/utf8" "${BOSS_LIBS_DIR}/boost" "${BOSS_LIBS_DIR}/yaml-cpp/include" "${CMAKE_SOURCE_DIR}/src") diff --git a/README b/README index 13a294c9..2c5cd8b4 100644 --- a/README +++ b/README @@ -175,9 +175,8 @@ possible, for example: * Boost: for filesystem interaction, various helpful functions. As a reflection of this, LOOT's API will provide functions only for -accessing LOOT-specific functionality and data, eg. auto-sorting, -masterlist / userlist reading, masterlist download and user -edits submission. +accessing LOOT-specific functionality and data, eg. masterlist / userlist reading, +masterlist download and user edits submission. The support for arbitrary data sources will be implemented by having the source LOOT uses specified in a settings file, which will be editable via the GUI. @@ -529,7 +528,6 @@ bUseOnlineCache Boolean Cache = Database bGetMetadataUpdates Boolean Metadata = Masterlist bPromptMetadataSubmit Boolean bAlwaysSubmitMetadata Boolean Only has effect if the above is false. -bDoStartupUpdateCheck Boolean Check for program updates. sOnlineCacheURL String URL to the base of the database repository. sMetadataURL String diff --git a/examples/masterlist.yaml b/examples/masterlist.yaml new file mode 100644 index 00000000..8288a3e7 --- /dev/null +++ b/examples/masterlist.yaml @@ -0,0 +1,313 @@ +# This is the Oblivion masterlist up to the end of the ESMs group, converted by +# hand to MF3 (ie. YAML-based format for BOSSv3). Mods without messages attached +# have been removed (since they wouldn't be included unless extra data was +# required for BOSS to sort them. + +globals: + + - condition: IF VERSION("../BOSS/BOSS.exe", "2.1.1.0", <) + type: warn + content: An update for BOSS (v2.1.1) has been released. Download it at "http://code.google.com/p/better-oblivion-sorting-software/downloads/list" + + - condition: IFNOT VERSION("../BOSS/BOSS.exe", "2.1.1.0", <) + type: say + content: You are using the latest version of BOSS. + +plugins: + + - name: Oblivion_1.1.esm + msg: + - &modESM + type: say + content: A modding esm - do not use in game, deactivate. + + - name: Oblivion_SI.esm + msg: + - *modESM + + - name: Oblivion_GOTY non-SI.esm + msg: + - *modESM + + - name: Hammerfell.esm + msg: + - condition: if checksum("Hammerfell.esm", 7D22F9DF) + type: warn + content: 'Contains dirty edits; 0 ITM, 4 UDR records. Needs TES4Edit cleaning: "http://cs.elderscrolls.com/constwiki/index.php/TES4Edit_Cleaning_Guide"' + + - name: GTAesgaard_2.esm + msg: + - &warnWorldspaces + type: warn + content: This needs to load directly after Oblivion.esm to avoid landscape issues caused by a failure by the mod author to use TES4Gecko's "Move Worldspaces" function. + + - name: DibellasWatch.esm + msg: + - *warnWorldspaces + + - name: Belas.esm + msg: + - type: warn + content: Part of Chronicles of Reeb. Do not use. Has serious landscape issues due to editing the Tamriel worldspace. Uninstall and delete. + + - name: Morrowind_ob.esm + msg: + - *warnWorldspaces + + - name: Silgrad_Tower.esm + msg: + - condition: if file("Silgrad_Tower_T.esm") + type: warn + content: Use either Silgrad_Tower.esm or Silgrad_Tower_T.esm but not both together. + + - name: Silgrad_Tower_T.esm + msg: + - condition: if file("Silgrad_Tower.esm") + type: warn + content: Use either Silgrad_Tower.esm or Silgrad_Tower_T.esm but not both together. + + - name: Beautiful People 2ch-Ed.esm + tag: + - Eyes + - Graphics + - Hair + - R.Relations + + - name: xeoex.esm + tag: + - Body-F + - Body-Size-F + - Body-M + - Body-Size-M + - Eyes + + - name: EnhancedWeatherSIOnly.esm + msg: + - type: warn + content: Obsolete. Remove this and install Enhanced Weather. + + - name: Francesco's Leveled Creatures-Items Mod.esm + tag: + - Actors.ACBS + - Actors.AIData + - Actors.AIPackages + - Actors.CombatStype + - Actors.DeathItem + - Actors.Stats + - Delev + - Invent + - NpcFaces + - Relev + - Scripts + - Stats + msg: + - condition: IF CHECKSUM("Francesco's Leveled Creatures-Items Mod.esm", 17F6FD1A) + type: warn + content: 'Contains dirty edits; 6 ITM, 0 UDR records. Needs TES4Edit cleaning: "http://cs.elderscrolls.com/constwiki/index.php/TES4Edit_Cleaning_Guide"' + + - name: Francesco's Optional New Items Add-On.esm + tag: + - Delev + - Relev + msg: + - condition: IF CHECKSUM("Francesco's Optional New Items Add-On.esm", DFD4FB97) + type: warn + content: 'Contains dirty edits; 2 ITM, 0 UDR records. Needs TES4Edit cleaning: "http://cs.elderscrolls.com/constwiki/index.php/TES4Edit_Cleaning_Guide"' + + - name: Francesco's Optional New Creatures Add-On.esm + tag: + - Actors.AIPackages + - Delev + - Factions + - Invent + - Relations + - Relev + - Scripts + msg: + - condition: &fcomCond IF FILE("FCOM_Convergence.esm") + type: error + content: Delete. Not compatible with FCOM. Already integrated into FCOM. + + - name: Crafting Skill Engine.esm + msg: + - condition: IF FILE("Retros Fletching and Imbueing 1.7.1_rus.esp") + type: error + content: Delete. Included in Retros Fletching and Imbueing 1.7.1_rus.esp + + - name: Tamrielic Ingredients.esm + msg: + - type: say + content: Do not use Tamrielic Ingredients and COBL at the same time. + + - name: Cobl Main.esm + msg: + - type: say + content: Do not use Tamrielic Ingredients and COBL at the same time. + + - name: Item interchange.esm + msg: + - type: say + content: Outdated. All functions are now handled by Cobl.esm. Update to the latest versions of 'Item Interchange' and Cobl. + + - name: Qarls_Harvest.esm + tag: + - Scripts + + - name: DEJ Harvest - Flora.esm + msg: + - type: say + content: Obsolete. Update to 'Harvest [Flora]' v3.0.1+. + + - name: Tamriel's Glittering Geology.esm + msg: + - condition: &oooCond IF FILE("Oscuro's_Oblivion_Overhaul.esm") + type: error + content: Not compatible with OOO. Already included in OOO. + + - name: Oscuro's_Oblivion_Overhaul.esm + tag: + - Actors.Spells + - Graphics + - Invent + - Relations + - Scripts + - Stats + - condition: IF FILE("Mart's Monster Mod for OOO.esm") || IF VAR(FCOM) + name: -Relations + msg: + - type: say + content: Do not clean. "Dirty" edits are intentional and required for the mod to function. + + - name: Mart's Monster Mod.esm + tag: + - Actors.AIData + - Actors.AIPackages + - Actors.Spells + - Actors.Stats + - Delev + - Factions + - Graphics + - Invent + - Names + - Relations + - Relev + - Scripts + - Stats + + - name: Mart's Monster Mod for OOO.esm + msg: + - condition: *fcomCond + type: error + content: Delete. Not compatible with FCOM. + tag: + - Delev + - Factions + - Relations + - Relev + +# The message attached to the Waalx plugin below is too vague for the specialised +# incompatibility list in MF3, and should probably be removed. + - name: Waalx Animals & Creatures.esm + msg: + - type: say + content: Incompatible with other overhaul mods. + + - name: FCOM_Convergence.esm + tag: + - Delev + - Factions + - Relations + - Relev + + - name: Armamentarium.esm + tag: + - Relev + + - name: ArmamentariumFran.esm + tag: + - Relev + + - name: ArmamentariumEnchanted.esm + msg: + - type: say + content: Outdated, included in Armamentarium Complete. + + - name: bookplacing.esm + msg: + - type: warn + content: Check you are using v2+. If not, Update. v1 has a severe bug with the Mystic Emporium disappearing. + + - name: No Lights Flicker.esm + msg: + - condition: &bpTweakMsg IF FILE("Bashed Patch.*\.esp") + type: say + content: Use Wrye Bash Bashed Patch tweak instead. + +# If a plugin name was given for Reznod's Mannequins, the incompatibility list +# syntax could be used. + - name: TLS Modded Mannequins.esm + msg: + - type: say + content: Incompatible with Reznod's Mannequins. + + - name: DKImperials.esm + tag: + - Hair + - Eyes + + - name: Minotaur Race.esm + req: + - name: &OBSE ../obse_loader.exe + condition: if version("../obse_loader.exe", "0.0.18.0", >=) + display: OBSE v18+ + msg: + - type: say + content: Requires 'Race Repository' + - condition: IF CHECKSUM("Minotaur Race.esm", 477340C8) + type: warn + content: 'Contains dirty edits; 7 ITM, 0 UDR records. Needs TES4Edit cleaning: "http://cs.elderscrolls.com/constwiki/index.php/TES4Edit_Cleaning_Guide"' + + - name: x117race.esm + tag: + - Hair + + - name: Chocolate Elves.esm + tag: + - NpcFaces + - Hair + - Eyes + - R.Relations + + - name: Independent High Elf Body (Replace)(v1.1).esm + tag: + - NpcFaces + - Hair + - Eyes + - R.Relations + + - name: p2p'sAshDraconian.esm + tag: + - NpcFaces + - Hair + - Eyes + - R.Relations + + - name: SPTDiverseGuards-Resources.esm + msg: + - condition: *oooCond + type: say + content: Probably not compatible with OOO. Untested. + + - name: John's Leveled List Overhaul.esm + tag: + - Graphics + +# I've just made up the OBME DLL name and version here for an example. + - name: Diverse Magic Effects Resource.esm + req: + - name: *OBSE + condition: if version("../obse_loader.exe", "0.0.19.0", >=) + display: OBSE v19+ + - name: OBSE/Plugins/obme.dll + condition: if version("../obse_loader.exe", "0.1.0.0", >=) + display: OBME v1+ diff --git a/examples/settings.yaml b/examples/settings.yaml new file mode 100644 index 00000000..88da0e7a --- /dev/null +++ b/examples/settings.yaml @@ -0,0 +1,20 @@ +# BOSS Settings File +# This file is written in YAML . +# If you don't know YAML, just think of a normal ini file, but replace the +# equals signs with colons followed by a space. No tabs allowed. + +bEnableNetworking: true +bUseOnlineCache: true +bGetMetadataUpdates: true +bPromptMetadataSubmit: true +bAlwaysSubmitMetadata: true + +sOnlineCacheURL: "http://better-oblivion-sorting-software.googlecode.com/svn/data/boss-nehrim/masterlist.txt" +sMetadataURL: "http://better-oblivion-sorting-software.googlecode.com/svn/data/boss-nehrim/masterlist.txt" +sVersionCheckerURL: "http://better-oblivion-sorting-software.googlecode.com/svn/data/boss-nehrim/masterlist.txt" + +sGame: auto # auto, oblivion, skyrim, fallout3, falloutnv +sLastGame: auto # auto, oblivion, skyrim, fallout3, falloutnv +iDebugVerbosity: 0 # 0, 1, 2, 3. Logging takes place if > 0. +bDoTrialRun: false +sLanguage: eng # An ISO 639-3 language string. diff --git a/src/game.cpp b/src/game.cpp index 3702f235..299257ad 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -27,6 +27,8 @@ #include +#include + #if _WIN32 || _WIN64 # include # include @@ -116,6 +118,15 @@ namespace boss { return GamePath() / pluginsFolderName; } + bool Game::IsActive(const std::string& plugin) const { + if (activePlugins.empty()) { + //Use libloadorder to fetch the active plugins list. Fill the set + //with the lowercased filenames. + } + + return activePlugins.find(boost::to_lower_copy(plugin)) != activePlugins.end(); + } + void Game::CreateBOSSGameFolder() { //Make sure that the BOSS game path exists. try { diff --git a/src/game.h b/src/game.h index 8709561e..ac9e9419 100644 --- a/src/game.h +++ b/src/game.h @@ -24,7 +24,12 @@ #ifndef __BOSS_GAME__ #define __BOSS_GAME__ +#include +#include + #include +#include +#include namespace boss { @@ -44,6 +49,12 @@ namespace boss { //Creates directory in BOSS folder for BOSS's game-specific files. void CreateBOSSGameFolder(); + + bool IsActive(const std::string& plugin) const; + + //Caches for condition results, active plugins and CRCs. + boost::unordered_map conditionCache; //Holds lowercased strings. + boost::unordered_map crcCache; //Holds lowercased strings. private: uint32_t id; std::string name; @@ -55,6 +66,7 @@ namespace boss { std::string pluginsFolderName; boost::filesystem::path gamePath; //Path to the game's folder. + boost::unordered_set activePlugins; //Holds lowercased strings. //Can be used to get the location of the LOCALAPPDATA folder (and its Windows XP equivalent). boost::filesystem::path GetLocalAppDataPath(); diff --git a/src/helpers.cpp b/src/helpers.cpp index 9cd0b78f..2b88efb5 100644 --- a/src/helpers.cpp +++ b/src/helpers.cpp @@ -26,6 +26,7 @@ */ #include "helpers.h" +#include "plugin/ModFormat.h" #include #include @@ -78,15 +79,15 @@ namespace boss { } //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) { + std::string IntToString(const int n) { string out; back_insert_iterator sink(out); - karma::generate(sink,karma::upper[karma::uint_],n); + karma::generate(sink,karma::upper[karma::int_],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) { + std::string IntToHexString(const int n) { string out; back_insert_iterator sink(out); karma::generate(sink,karma::upper[karma::hex],n); @@ -156,53 +157,58 @@ namespace boss { Version::Version(const fs::path file) { // LOG_TRACE("extracting version from '%s'", file.string().c_str()); + if (file.extension().string() == ".esm" || file.extension().string() == ".esp") { + //Use libespm interface. + verString = ReadHeader(file).Version; + } else { #if _WIN32 || _WIN64 - DWORD dummy = 0; - DWORD size = GetFileVersionInfoSize(file.wstring().c_str(), &dummy); + 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; + if (size > 0) { + LPBYTE point = new BYTE[size]; + UINT uLen; + VS_FIXEDFILEINFO *info; + string ver; - GetFileVersionInfo(file.wstring().c_str(),0,size,point); + GetFileVersionInfo(file.wstring().c_str(),0,size,point); - VerQueryValue(point,L"\\",(LPVOID *)&info,&uLen); + 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); + DWORD dwLeftMost = HIWORD(info->dwFileVersionMS); + DWORD dwSecondLeft = LOWORD(info->dwFileVersionMS); + DWORD dwSecondRight = HIWORD(info->dwFileVersionLS); + DWORD dwRightMost = LOWORD(info->dwFileVersionLS); - delete [] point; + delete [] point; - verString = IntToString(dwLeftMost) + '.' + IntToString(dwSecondLeft) + '.' + IntToString(dwSecondRight) + '.' + IntToString(dwRightMost); - } + 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/'"; + // 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"); + 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()); + // 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); } - 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 { @@ -254,6 +260,10 @@ namespace boss { return (*this == ver || *this > ver); } + bool Version::operator <= (Version ver) { + return (*this == ver || *this < ver); + } + bool Version::operator == (Version ver) { return (verString == ver.AsString()); } diff --git a/src/helpers.h b/src/helpers.h index 297bec1b..fa10083d 100644 --- a/src/helpers.h +++ b/src/helpers.h @@ -28,6 +28,7 @@ #ifndef __BOSS_HELPERS__ #define __BOSS_HELPERS__ +#include #include #include @@ -42,10 +43,10 @@ namespace boss { 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); + std::string IntToString(const int 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); + std::string IntToHexString(const int n); //Converts a boolean to a string representation (true/false) std::string BoolToString(const bool b); @@ -71,6 +72,7 @@ namespace boss { bool operator > (Version); bool operator < (Version); bool operator >= (Version); + bool operator <= (Version); bool operator == (Version); bool operator != (Version); }; diff --git a/src/metadata.cpp b/src/metadata.cpp index 88391497..711fe1d7 100644 --- a/src/metadata.cpp +++ b/src/metadata.cpp @@ -22,6 +22,7 @@ */ #include "metadata.h" +#include using namespace std; @@ -188,37 +189,37 @@ namespace boss { tags = t; } - void Plugin::EvalAllConditions(const boost::filesystem::path& gamePath) { + void Plugin::EvalAllConditions(boss::Game& game) { for (list::iterator it = loadAfter.begin(); it != loadAfter.end();) { - if (!it->EvalCondition(gamePath)) + if (!it->EvalCondition(game)) it = loadAfter.erase(it); else ++it; } for (list::iterator it = requirements.begin(); it != requirements.end();) { - if (!it->EvalCondition(gamePath)) + if (!it->EvalCondition(game)) it = requirements.erase(it); else ++it; } for (set::iterator it = incompatibilities.begin(); it != incompatibilities.end();) { - if (!it->EvalCondition(gamePath)) + if (!it->EvalCondition(game)) incompatibilities.erase(it++); else ++it; } for (list::iterator it = messages.begin(); it != messages.end();) { - if (!it->EvalCondition(gamePath)) + if (!it->EvalCondition(game)) it = messages.erase(it); else ++it; } for (list::iterator it = tags.begin(); it != tags.end();) { - if (!it->EvalCondition(gamePath)) + if (!it->EvalCondition(game)) it = tags.erase(it++); else ++it; @@ -230,6 +231,6 @@ namespace boss { } bool Plugin::IsRegexPlugin() const { - return name.substr(name.length()-5) == "\\.esp" || name.substr(name.length()-5) == "\\.esm"; + return boost::iends_with(name, "\\.esm") || boost::iends_with(name, "\\.esp"); } } diff --git a/src/metadata.h b/src/metadata.h index 06d82cf9..8a17549e 100644 --- a/src/metadata.h +++ b/src/metadata.h @@ -23,6 +23,8 @@ #ifndef __BOSS_METADATA__ #define __BOSS_METADATA__ +#include "game.h" + #include #include #include @@ -38,7 +40,7 @@ namespace boss { ConditionalData(const std::string& condition, const std::string& data); bool IsConditional() const; - bool EvalCondition(const boost::filesystem::path& gamePath) const; + bool EvalCondition(boss::Game& game) const; std::string Condition() const; std::string Data() const; @@ -124,7 +126,7 @@ namespace boss { void Messages(const std::list& messages); void Tags(const std::list& tags); - void EvalAllConditions(const boost::filesystem::path& gamePath); + void EvalAllConditions(boss::Game& game); bool HasNameOnly() const; bool IsRegexPlugin() const; private: diff --git a/src/parsers.h b/src/parsers.h index 9e88ddb7..70aa555a 100644 --- a/src/parsers.h +++ b/src/parsers.h @@ -29,10 +29,14 @@ #endif #include "metadata.h" +#include "helpers.h" #include +#include +#include #include +#include #include #include #include @@ -279,50 +283,16 @@ namespace YAML { namespace boss { - /* Conditional parser + /////////////////////////////// + // Condition parser/evaluator + /////////////////////////////// - Expression is of the form: - - condition = keyword type - - expression = -'(' condition (operator condition)* -')' operator - - comp_exp = expression (operator expression)* - -'(' keyword type (operator -'(' keyword type -')' )* - - keyword = if | ifnot - operator = and | or - type = file(...) | checksum(...) | version(...) | active(...) - - 'and' takes precedence over 'or', and brackets can also be used to indicate - overriding precedences. - - Split string up into sub-strings according to the bracketing if present, then - according to operators, until each sub-string only contains one condition. - - { - expression = - term - >> *(string("or") >> term) - ; - - term = - factor - >> *(string("and") >> factor) - - factor = - condition - | '(' >> expression >> ')' - | - - condition = keyword >> type - - } - - */ + /* Still need to add support for bracketing and evaluation of the compound + conditions. */ namespace qi = boost::spirit::qi; namespace unicode = boost::spirit::unicode; + namespace phoenix = boost::phoenix; template class condition_grammar : public qi::grammar { @@ -330,7 +300,14 @@ namespace boss { condition_grammar() : condition_grammar::base_type(expression, "condition grammar") { expression = - condition; + andStatement + >> *(qi::lit("or") >> andStatement) + ; + + andStatement = + condition + >> *(qi::lit("and") >> condition) + ; condition = ( qi::lit("if") >> type ) [qi::labels::_val = qi::labels::_1] @@ -338,38 +315,144 @@ namespace boss { ; 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] + ( "file(" > quotedStr > ')' ) [phoenix::bind(&condition_grammar::CheckFile, this, qi::labels::_val, qi::labels::_1)] + | ( "checksum(" > quotedStr > ',' > qi::hex > ')' ) [phoenix::bind(&condition_grammar::CheckSum, this, qi::labels::_val, qi::labels::_1, qi::labels::_2)] + | ( "version(" > quotedStr > ',' > quotedStr > ',' > unicode::char_ > ')' ) [phoenix::bind(&condition_grammar::CheckVersion, this, qi::labels::_val, qi::labels::_1, qi::labels::_2, qi::labels::_3)] + | ( "active(" > quotedStr > ')' ) [phoenix::bind(&condition_grammar::CheckActive, this, qi::labels::_val, qi::labels::_1)] ; quotedStr = '"' > +(unicode::char_ - '"') > '"'; - /* Need to add error handlers and actual type evaluation, then start on - compound conditional parsing. Need game path support. */ + expression.name("expression"); + condition.name("condition"); + type.name("condition type"); + quotedStr.name("quoted string"); + + qi::on_error(expression, phoenix::bind(&condition_grammar::SyntaxError, this, qi::labels::_1, qi::labels::_2, qi::labels::_3, qi::labels::_4)); + qi::on_error(condition, phoenix::bind(&condition_grammar::SyntaxError, this, qi::labels::_1, qi::labels::_2, qi::labels::_3, qi::labels::_4)); + qi::on_error(type, phoenix::bind(&condition_grammar::SyntaxError, this, qi::labels::_1, qi::labels::_2, qi::labels::_3, qi::labels::_4)); + qi::on_error(quotedStr, phoenix::bind(&condition_grammar::SyntaxError, this, qi::labels::_1, qi::labels::_2, qi::labels::_3, qi::labels::_4)); + } + + void SetGame(boss::Game& g) { + game = &g; } private: qi::rule expression, condition, type; qi::rule quotedStr; + boss::Game * game; + + //Eval's regex and exact paths. Check for files and ghosted plugins. + void CheckFile(bool& result, const std::string& file) { + if (boost::contains(file, "\\.")) { //Regex. Only supports filenames right now. + + result = false; + boost::regex regex; + try { + regex = boost::regex(file, boost::regex::extended|boost::regex::icase); + } catch (boost::regex_error e) { + throw std::runtime_error("The regex string \"" + file + "\" is invalid."); + // LOG_ERROR("\"%s\" is not a valid regular expression. Item skipped.", reg.c_str()); + } + for (fs::directory_iterator itr(game->DataPath()); itr != fs::directory_iterator(); ++itr) { + if (fs::is_regular_file(itr->status())) { + if (boost::regex_match(itr->path().filename().string(), regex)) { + result = true; + break; + } + } + } - //Eval's regex and exact paths. - bool FileExists(bool& result, std::string file); + } else if (IsPlugin(file)) + result = boost::filesystem::exists(game->DataPath() / file) || boost::filesystem::exists(game->DataPath() / (file + ".ghost")); + else + result = boost::filesystem::exists(game->DataPath() / file); + } + void CheckSum(bool& result, const std::string& file, const uint32_t checksum) { + uint32_t crc; + boost::unordered_map::iterator it = game->crcCache.find(boost::to_lower_copy(file)); + if (it != game->crcCache.end()) + crc = it->second; + else { + if (boost::filesystem::exists(game->DataPath() / file)) + crc = GetCrc32(game->DataPath() / file); + else if (IsPlugin(file) && boost::filesystem::exists(game->DataPath() / (file + ".ghost"))) + crc = GetCrc32(game->DataPath() / (file + ".ghost")); + else { + result = false; + return; + } + + game->crcCache.emplace(boost::to_lower_copy(file), crc); + } + + result = checksum == crc; + } + + void CheckVersion(bool& result, const std::string& file, const std::string& version, const char comparator) { + + CheckFile(result, file); + if (!result) { + if (comparator == '<') + result = true; + return; + } + + Version givenVersion = Version(version); + Version trueVersion = Version(game->DataPath() / file); + + switch (comparator) { + case '>': + if (trueVersion <= givenVersion) + result = false; + break; + case '<': + if (trueVersion >= givenVersion) + result = false; + break; + case '=': + if (trueVersion != givenVersion) + result = false; + break; + } + } + + void CheckActive(bool& result, const std::string& file) { + result = game->IsActive(file); + } + + bool IsPlugin(const std::string& file) { + return boost::iends_with(file, ".esm") || boost::iends_with(file, ".esp"); + } + + void SyntaxError(Iterator const& /*first*/, Iterator const& last, Iterator const& errorpos, boost::spirit::info const& what) { + + std::string context(errorpos, min(errorpos +50, last)); + boost::trim(context); + + throw std::runtime_error("Error parsing condition at \"" + context + "\", expected \"" + what.tag + "\""); + } }; - bool ConditionalData::EvalCondition(const boost::filesystem::path& gamePath) const { + bool ConditionalData::EvalCondition(boss::Game& game) const { if (condition.empty()) return true; + + boost::unordered_map::const_iterator it = game.conditionCache.find(boost::to_lower_copy(condition)); + if (it != game.conditionCache.end()) + return it->second; + condition_grammar grammar; qi::space_type skipper; std::string::const_iterator begin, end; bool eval; + grammar.SetGame(game); begin = condition.begin(); end = condition.end(); @@ -378,6 +461,8 @@ namespace boss { if (!r || begin != end) throw std::runtime_error("Parsing of condition \"" + condition + "\" failed!"); + game.conditionCache.emplace(boost::to_lower_copy(condition), eval); + return eval; } } diff --git a/src/plugin/ModFormat.cpp b/src/plugin/ModFormat.cpp new file mode 100644 index 00000000..fadbdc66 --- /dev/null +++ b/src/plugin/ModFormat.cpp @@ -0,0 +1,241 @@ +/* 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: 2488 $, $Date: 2011-03-27 14:31:33 +0100 (Sun, 27 Mar 2011) $ +*/ + + +#include "Types.h" +#include "ModFormat.h" +#include "VersionRegex.h" + +#include +#include +#include +#include + +namespace boss { + using namespace std; + + using boost::algorithm::trim_copy; + using boost::regex; + + // + // string ParseVersion(string&): + // - Tries to extract the version string value from the given text, + // using the above defined regexes to do the dirty work. + // + string ParseVersion(const string& text){ + + string::const_iterator begin, end; + + begin = text.begin(); + end = text.end(); + + for(int i = 0; regex* re = version_checks[i]; i++) { + + smatch what; + while (regex_search(begin, end, what, *re)) { + + if (what.empty()){ + continue; + } + + ssub_match match = what[1]; + + if (!match.matched) { + continue; + } + + return trim_copy(string(match.first, match.second)); + + } + } + + return string(); + } + + // + // string ReadString(pointer&, maxsize): + // - Reads a consecutive array of charactes up to maxsize length and + // returns them as a new string. + // + string ReadString(char*& bufptr, ushort size){ + string data; + + data.reserve(size + 1); + while (char c = *bufptr++) { + data.append(1, c); + } + + return data; + } + + // + // T Peek(pointer&): + // - Peeks into the received buffer and returns the value pointed + // converting it to the type T. + // + template + T Peek(char* buffer) { + return *reinterpret_cast(buffer); + } + + // + // T Read(pointer&): + // - Tries to extract a value of the specified type T from the + // received buffer, incrementing the pointer to point past the readen + // value. + // + template + inline T Read(char*& buffer) { + T value = Peek(buffer); + buffer += sizeof(T); + return value; + } + + //- + // ModHeader ReadHeader(string): + // - Parses the mod file contents and extract the header information + // returning the most important data using a ModHeader struct. + // --> see: + // http://www.uesp.net/wiki/Tes4Mod:Mod_File_Format, + // + // and in particular this link: + // http://www.uesp.net/wiki/Tes4Mod:Mod_File_Format/TES4 + // + + bool IsPluginMaster(boost::filesystem::path filename) { + char buffer[MAXLENGTH]; + char* bufptr = buffer; + ModHeader modHeader; + + if (filename.empty()) + return false; + + ifstream file(filename.native().c_str(), ios_base::binary | ios_base::in); + + if (file.bad()) + //throw boss_error(BOSS_ERROR_FILE_READ_FAIL, filename.string()); + return false; + + // Reads the first MAXLENGTH bytes into the buffer + file.read(&buffer[0], sizeof(buffer)); + + // Check for the 'magic' marker at start + if (Read(bufptr) != Record::TES4){ + return false; + } + + // Next field is the total header size + /*uint headerSize =*/ Read(bufptr); + + // Next comes the header record Flags + uint flags = Read(bufptr); + + // LSb of this record's flags is used to indicate if the + // mod is a master or a plugin + return ((flags & 0x1) != 0); + } + + ModHeader ReadHeader(boost::filesystem::path filename) { + char buffer[MAXLENGTH]; + char* bufptr = buffer; + ModHeader modHeader; + ifstream file(filename.native().c_str(), ios_base::binary | ios_base::in); + + modHeader.Name = filename.string(); + + // Reads the first MAXLENGTH bytes into the buffer + file.read(&buffer[0], sizeof(buffer)); + + // Check for the 'magic' marker at start + if (Read(bufptr) != Record::TES4){ + return modHeader; + } + + // Next field is the total header size + /*uint headerSize =*/ Read(bufptr); + + // Next comes the header record Flags + uint flags = Read(bufptr); + + // LSb of this record's flags is used to indicate if the + // mod is a master or a plugin + modHeader.IsMaster = (flags & 0x1) != 0; + + // Next comes the FormID... + /*uint formId =*/ Read(bufptr); // skip formID + + // ...and extra flags + /*uint flags2 =*/ Read(bufptr); // skip flags2 + + // Here the Header record starts, check for its signature 'HEDR' + if (Read(bufptr) != Record::HEDR){ + return modHeader; + } + + // HEDR record has fields: DataSize, Version (0.8 o 1.0), Record Count + // and Next Object Id + /*ushort dataSize =*/ Read(bufptr); + /*float version =*/ Read(bufptr); + /*int numRecords =*/ Read(bufptr); + /*uint nextObjId =*/ Read(bufptr); + + // Then comes the sub-records + uint signature = Read(bufptr); + + // skip optional records + bool loop = true; + while (loop){ + switch (signature) + { + case Record::OFST: + case Record::DELE: + bufptr += Read(bufptr); // skip + signature = Read(bufptr); + break; + + // extract author name, if present + case Record::CNAM: + modHeader.Author = ReadString(bufptr, Read(bufptr)); + signature = Read(bufptr); + break; + + // extract description and version, if present + case Record::SNAM: + modHeader.Description = ReadString(bufptr, Read(bufptr)); + modHeader.Version = ParseVersion(modHeader.Description); + signature = Read(bufptr); + break; + + default: + loop = false; + } + } + + // We should have all the required information. + return modHeader; + } +} diff --git a/src/plugin/ModFormat.h b/src/plugin/ModFormat.h new file mode 100644 index 00000000..54fcf5c4 --- /dev/null +++ b/src/plugin/ModFormat.h @@ -0,0 +1,64 @@ +/* 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: 2488 $, $Date: 2011-03-27 14:31:33 +0100 (Sun, 27 Mar 2011) $ +*/ + +#ifndef __SUPPORT_MODFORMAT__HPP__ +#define __SUPPORT_MODFORMAT__HPP__ + +#include "Types.h" +#include +#include + + +namespace boss { + + using namespace std; + + // Structure for grouping the information gathered from each mod's header. + struct ModHeader { + string Name; + string Description; + string Author; + string Version; + bool IsMaster; + }; + + + struct Record { + static const ulong TES4 = '4SET'; + static const ulong HEDR = 'RDEH'; + static const ulong OFST = 'TSFO'; + static const ulong DELE = 'ELED'; + static const ulong CNAM = 'MANC'; + static const ulong SNAM = 'MANS'; + }; + + ModHeader ReadHeader(boost::filesystem::path filename); + + bool IsPluginMaster(boost::filesystem::path filename); //Shorter version of the above, to only get master flag. +} + +#endif diff --git a/src/plugin/Types.h b/src/plugin/Types.h new file mode 100644 index 00000000..7062cce9 --- /dev/null +++ b/src/plugin/Types.h @@ -0,0 +1,51 @@ +/* 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: 1679 $, $Date: 2010-10-17 20:41:49 +0100 (Sun, 17 Oct 2010) $ +*/ + +#ifndef __SUPPORT_TYPES__HPP__ +#define __SUPPORT_TYPES__HPP__ + + +namespace boss { + + ////////////////////////////////////////////////////////////////////////// + // Common type definitions + ////////////////////////////////////////////////////////////////////////// + + typedef unsigned long ulong; + typedef unsigned int uint; + typedef unsigned short ushort; + + + ////////////////////////////////////////////////////////////////////////// + // Constants + ////////////////////////////////////////////////////////////////////////// + + // An arbitrary large number. Controls the size of some buffers used to read data from files. + const ulong MAXLENGTH = 4096UL; +} + +#endif diff --git a/src/plugin/VersionRegex.cpp b/src/plugin/VersionRegex.cpp new file mode 100644 index 00000000..c5842431 --- /dev/null +++ b/src/plugin/VersionRegex.cpp @@ -0,0 +1,92 @@ +/* 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: 1681 $, $Date: 2010-10-17 21:01:25 +0100 (Sun, 17 Oct 2010) $ +*/ + + +#include +#include + + +namespace boss { + + using namespace std; + using namespace boost; + + /// REGEX expression definition + /// Each expression is composed of three parts: + /// 1. The marker string "version", "ver", "rev", "v" or "r" + /// 2. The version string itself. + + const char* regex1 = + "^(?:\\bversion\\b[ ]*(?:[:.\\-]?)|\\brevision\\b(?:[:.\\-]?))[ ]*" + "((?:alpha|beta|test|debug)?\\s*[-0-9a-zA-Z._+]+\\s*(?:alpha|beta|test|debug)?\\s*(?:[0-9]*))$" + ; + + const char* regex2 = + "(?:\\bversion\\b(?:[ :]?)|\\brevision\\b(?:[:.\\-]?))[ ]*" + "([0-9][-0-9a-zA-Z._]+\\+?)" + ; + + const char* regex3 = + "(?:\\bver(?:[:.]?)|\\brev(?:[:.]?))\\s*" + "([0-9][-0-9a-zA-Z._]*\\+?)" + ; + + // Matches "Updated: " for the Bashed patch + const char* regex4 = + "(?:Updated:)\\s*" + "([-0-9aAmMpP/ :]+)$" + ; + + // Matches isolated versions as last resort + const char* regex5 = + "(?:(?:\\bv|\\br)(?:\\s?)(?:[-.:])?(?:\\s*))" + "((?:(?:\\balpha\\b)?|(?:\\bbeta\\b)?)\\s*[0-9][-0-9a-zA-Z._]*\\+?)" + ; + + // Matches isolated versions as last resort + const char* regex6 = + "((?:(?:\\balpha\\b)?|(?:\\bbeta\\b)?)\\s*\\b[0-9][-0-9a-zA-Z._]*\\+?)$" + ; + + const char* regex7 = + "(^\\bmark\\b\\s*\\b[IVX0-9][-0-9a-zA-Z._+]*\\s*(?:alpha|beta|test|debug)?\\s*(?:[0-9]*)?)$" + ; + + /// Array used to try each of the expressions defined above using + /// an iteration for each of them. + regex* version_checks[] = { + new regex(regex1, regex::icase), + new regex(regex2, regex::icase), + new regex(regex3, regex::icase), + new regex(regex4, regex::icase), + new regex(regex5, regex::icase), //This incorrectly identifies "OBSE v19" where 19 is any integer. + new regex(regex6, regex::icase), //This is responsible for metallicow's false positive. + new regex(regex7, regex::icase), + 0 + }; + +} \ No newline at end of file diff --git a/src/plugin/VersionRegex.h b/src/plugin/VersionRegex.h new file mode 100644 index 00000000..c4a2a661 --- /dev/null +++ b/src/plugin/VersionRegex.h @@ -0,0 +1,45 @@ +/* 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: 1681 $, $Date: 2010-10-17 21:01:25 +0100 (Sun, 17 Oct 2010) $ +*/ + +#ifndef __SUPPORT_VERSIONREGEX__HPP__ +#define __SUPPORT_VERSIONREGEX__HPP__ + + +#include + + +namespace boss { + + using namespace boost; + + /// Array used to try each of the expressions defined above using + /// an iteration for each of them. + extern regex* version_checks[]; + +} + +#endif