diff --git a/CMakeLists.txt b/CMakeLists.txt index f0d67354..47c938ec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,10 +11,12 @@ 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" "${CMAKE_SOURCE_DIR}/src/plugin/ModFormat.cpp" "${CMAKE_SOURCE_DIR}/src/plugin/VersionRegex.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/VersionRegex.cpp") + +set (BOSS_SRC ${BOSS_SRC} "${PROJECT_LIBS_DIR}/libespm/src/commonSupport.cpp" "${PROJECT_LIBS_DIR}/libespm/src/fileFormat.cpp") # Include source and library directories. -include_directories ("${PROJECT_LIBS_DIR}/alphanum" "${PROJECT_LIBS_DIR}/utf8" "${PROJECT_LIBS_DIR}/boost" "${PROJECT_LIBS_DIR}/yaml-cpp/include" "${CMAKE_SOURCE_DIR}/src" "${PROJECT_LIBS_DIR}/libloadorder/src") +include_directories ("${PROJECT_LIBS_DIR}/alphanum" "${PROJECT_LIBS_DIR}/utf8" "${PROJECT_LIBS_DIR}/boost" "${PROJECT_LIBS_DIR}/yaml-cpp/include" "${CMAKE_SOURCE_DIR}/src" "${PROJECT_LIBS_DIR}/libloadorder/src" "${PROJECT_LIBS_DIR}/BOSS/trunk" "${PROJECT_LIBS_DIR}/libespm" "${PROJECT_LIBS_DIR}/zlib") ############################## @@ -39,6 +41,7 @@ ENDIF () link_directories ("${PROJECT_LIBS_DIR}/yaml-cpp/build") link_directories ("${PROJECT_LIBS_DIR}/libloadorder/build") +link_directories ("${PROJECT_LIBS_DIR}/zlib" ) # Settings when compiling and cross-compiling on Linux. IF (CMAKE_HOST_SYSTEM_NAME MATCHES "Linux") @@ -53,12 +56,12 @@ IF (CMAKE_HOST_SYSTEM_NAME MATCHES "Linux") set (BOSS_LIBS version) ENDIF () - set (BOSS_LIBS ${BOSS_LIBS} loadorder yaml-cpp boost_filesystem boost_system boost_regex) + set (BOSS_LIBS ${BOSS_LIBS} zlibstatic loadorder yaml-cpp boost_locale boost_filesystem boost_system boost_regex) ENDIF () # Settings when compiling on Windows. IF (CMAKE_HOST_SYSTEM_NAME MATCHES "Windows") - set (BOSS_LIBS loadorder yaml-cpp libboost_filesystem-vc110-mt-1_52 libboost_system-vc110-mt-1_52) + set (BOSS_LIBS zlibstatic loadorder yaml-cpp boost_locale-vc110-mt-1_52 libboost_filesystem-vc110-mt-1_52 libboost_system-vc110-mt-1_52) set (CMAKE_CXX_FLAGS "/EHsc") ENDIF () @@ -67,8 +70,8 @@ ENDIF () ############################## # Build API. -add_library (boss ${PROJECT_LINK} "${CMAKE_SOURCE_DIR}/src/api.cpp" ${BOSS_SRC}) -target_link_libraries (boss ${BOSS_LIBS}) +#add_library (boss ${PROJECT_LINK} "${CMAKE_SOURCE_DIR}/src/api.cpp" ${BOSS_SRC}) +#target_link_libraries (boss ${BOSS_LIBS}) # Build tester. add_executable (tester "${CMAKE_SOURCE_DIR}/src/tester.cpp" ${BOSS_SRC}) diff --git a/File Format b/File Format deleted file mode 100644 index 8e02ba17..00000000 --- a/File Format +++ /dev/null @@ -1,248 +0,0 @@ -LOOT Metadata File Format -========================== - -The LOOT metadata file format is a YAML file which contains a specific set of -data in a specific structure. It is used by the masterlist, the userlist and the -lastrun file. - -General properties are: - 1. Keys and keywords (such as message types, conditional types) are lowercase. - 2. Whitespace is important: a two-space indentation should be used between - each level. - 3. All YAML properties. - -In YAML, the message object is: ---- -!message: - condition: OPTIONAL !condition - type: REQUIRED !!string - content: REQUIRED !!string - lang: OPTIONAL !!string -... -Allowed values for the 'type' node are "say", "warn" or "error". The semantics -of the types are: - say Generic message type, carries no semantics. - warn A warning message, detailing a non-critical installation issue. - error An error message, detailing a critical installation issue. - -The 'content' node string will treat all file:// , http:// and https:// links as -hyperlinks. A link can be given a label by enclosing it and the label in double -quotes, eg. "http://www.example.com example link". -The 'lang' node takes an ISO 639-3 language string, of which there are a set defined for the languages LOOT supports (as yet, just 'eng'). - - -The file object: ---- -!file: - condition: OPTIONAL !condition - name: REQUIRED !!string - display: OPTIONAL !!string -... -The 'display' node allows the name of the parent mod or program to be used instead -of the filename in messages. - -The tag object: ---- -!tag: - condition: OPTIONAL !condition - name: !!string -... -This object can be used to represent a conditional tag. 'name' is the tag name, -eg. Graphics. Tags that are prepended with a '-' (eg. '-Graphics') are suggested -for removal, otherwise tags are suggested for addition. - - -The condition string: ---- -!condition: REQUIRED !!string -... -Syntax of the string is the same as is currently for BOSS, except the following: - 1. The VAR, REGEX and LANG condition types have been removed. - 2. The FILE condition type now evaluates regex path strings. - 3. The '&&' and '||' operators have been replaced with 'and' and 'or' respectively. - 4. Condition keywords, types and operators are all lowercase. - 5. Brackets may be used to logically group conditions in compound conditions. - 6. Evaluation of compound conditionals is done according to standard operator - precedence rules. - - -The plugin object: ---- -!plugin: - name: REQUIRED !!string - priority: OPTIONAL !undecided - enabled: OPTIONAL !bool - after: OPTIONAL [!file] - req: OPTIONAL [!file] - inc: OPTIONAL [!file] - msg: OPTIONAL [!message] - tag: OPTIONAL [!!string or !tag] -... -The 'name' node can be either an exact filename or a regular expression. The -difference will be determined by looking for the "\.esp" or "\.esm" substrings, -which are only valid for a regular expression. -The type of the 'priority' node has not been decided yet, as it's not yet clear -how much fidelity is required for it. -If any of the 'req' node objects are not present, an error message will be -displayed. If any of the 'incs' node objects are present, an error message will -be displayed. The 'req' node is intended for use with implicit dependencies. -The 'req' node objects must be listed in load order. The 'req' and 'inc' -nodes may contain a mix of plugin and non-plugin objects. -If the 'enabled' node is not specified, its value is assumed true. It is -provided for use in the userlist. -The 'after' node is a list of plugins that the object plugin must load after, intended for use specifying plugins which the object plugin must load after, but which are not dependencies. It may also be used in the userlist to specify custom positions. -The 'tag' node holds an array of Bash Tags to be suggested. - - -Simple Example --------------- - ---- -- name: MyMod.esp - req: - - name: AnotherMod.esp - - name: AnotherModPatch.esp - msg: - - type: say - content: "This is my mod." - - type: say - content: "This is a second message." -... - - -Complex Example ---------------- - ---- -# Note that classes are not specified, YAML will treat them as assoc. arrays, -# LOOT will assume they are file objects. -filevars: - - &GAME name: "TESV.exe" - - &MASTER {name: "Oblivion.esm", ver: "1.2.416"} - - &SE name: "obse_loader.exe" - - &TEST {name: "Test.esp", ver: 0x0537AB3C} - - &SE20 <<: [*SE, ver: "0.0.20.1"] - -msgvars: - - &OBSOLETE {type: SAY, content: "Obsolete. Remove and upgrade to the latest version."} - -globals: - - {type: SAY, content: "You're using LOOT!"} - -- name: Oblivion.esm - msg: - - {condition: exp: "IF FILE("TESV.exe")", type: WARN, content: "False alarm."} - - <<: {*OBSOLETE, content: Quotes aren't necessary, unless the message contains special chars. } -... - - -LOOT Plugin Data Cache Format -============================= - -The formatting of this cache doesn't need to be human-readable, but must -contain the following information for each plugin: - - * Filename (for roughly identifying the file) - * CRC (for precisely identifying the exact file) - * Masters in the order they appear in the plugin's master list. - * FormIDs of the records it edits (not added records). - -Rather than have a single cache file, it is more efficient past the short -term to use a different file for each plugin filename's data, even after -taking into account the additional networking cost of using many small -files. See the section after the divider below for the analysis. - -The cache file extension will be ".dc", an acronym for "data cache". Each -cache file will share the filename of the plugin that it holds the data of, -including the plugin's file extension, eg. "Unofficial Oblivion Patch.esp.dc". - -In each cache file, the data will be divided into entries for each of the -different versions of that file, which are handled using their CRCs. These -entries make up the bulk of the file, and are followed by an index of CRCs -and offsets to their corresponding data at the end of the file. - - Name Type Details - --------------------------------------------------------------------------- - Version uint8_t Version number for the file's - format (in case it changes). - Name Length uint8_t The plugin filename's length. - Filename char[Name Length] Not null terminated. - Data Entries data entry[Entry Count] - Index index entry[Entry Count] - Entry Count uint32_t - -Each data entry has the following structure: - - Name Type Details - --------------------------------------------------------------------------- - Size uint32_t Total size of data entry, not - including this value. - Masters Length uint16_t Includes null characters. - Masters char[Masters Length] Null-terminated filenames in order - of how they appeared in the plugin. - FormIDs uint32_t[len] len = (Size - Masters Length) / 4 - -Each index entry has the following structure: - - Name Type Details - --------------------------------------------------------------------------- - File CRC uint32_t - Data offset uint32_t From beginning of file. - -All numbers are encoded low to high byte. All strings are encoded in UTF-8. - -The reason for this structure is to optimise the processes that will be -performed upon cache files: searching, reading data and appending new data. -The index allows quick searching of the file as it is composed of a given -number of entries with defined size. It is located at the end of the file -rather than the more usual beginning so that appending new data does not -need to rewrite the whole file to also update the index: only the index and -filename data will ever be rewritten. - -The plugin filename is contained within the cache file so that in case the -cache file's filename ever gets changed, it is still possible to work out -which plugins' data it holds. - -Each cache file will be able to store up to ~10^7 entries, though the -probability of there being a CRC collision reaches 1% at ~10^4 entries, due -to the birthday problem. Still, it's unlikely that there will ever be -several thousand versions of a file with the same filename in use, so it's -probably not worth worrying about. - - -Single vs. Multi-File ---------------------- - -A single-file cache would use the above structure, but with an additional -uint64_t in each index entry to store a filename hash, and without the filename -at the start of the file. - -Let's say the cache file holds info for 100,000 plugins (25,000 mods, each -with two plugins, and having two versions released - probably still a -conservative estimate). Assuming each plugin changes 10 records, and has 2 -masters with filenames 10 characters long, the size is ~11.4 MB. - -Using a multi-file cache and the same assumptions being made as above, -the size of each file will be 390 bytes. - -However, because we're dealing with lots of small files, HTTP request/response -time has to be accounted for. Google says that the best average for the -upload/download speed ratio of most Internet connections is 1:4, and that -headers are roughly 300 bytes long without cookies. Each file's effective size -is therefore 1890 bytes. - -For a load order of 250 plugins, the total effective size is ~461 kB. This is -~25x smaller than the equivalent single-file cache that would need to be -transferred, but it doesn't take into account the HTTP connection time, which -is significant for small files. I don't know how I can take that into account, -but if I've understood things correctly, I don't need to, as a persistent -connection could be used, which would require only one HTTP connection to be -made. HTTP pipelining could also be used to improve performance. A multi-file -cache could also be cached, with new files only being downloaded if they had -changed or weren't already on the user's system, rather than the whole cache -having to be re-downloaded even if the only change was the addition of a plugin -the user doesn't have. This would only save 95 kB over the assumed load order at -maximum though. - -Given all that, it's probably better to use a multi-file cache over a single -file cache. diff --git a/README b/README index 51b4b3ba..2cdd04f7 100644 --- a/README +++ b/README @@ -150,13 +150,12 @@ A general workflow for a user running LOOT to sort would be: LOOT must also provide a means to manage past user choices, to undo its changes (only back one run though), and to edit its settings. -LOOT should ask for permission to send plugin contents data on first run, and -to send plugin metadata whenever there are metadata changes to send, unless the -user selects a "do not ask me again" option. These settings should be -configurable at any time. +LOOT should ask for permission to send plugin metadata whenever there are +metadata changes to send, unless the user selects a "do not ask me again" +option. This setting should be configurable at any time. Documentation will be split between three readmes: one general readme, one API -readme and one rule syntax readme. The version history can be split into a +readme and one syntax readme. The version history can be split into a fourth readme when it grows sufficiently long. @@ -183,33 +182,17 @@ LOOT uses specified in a settings file, which will be editable via the GUI. Because auto-sorting requires LOOT to know what is inside the plugins it sorts, it must scan the plugins, and determine which of its records are new and which -are edited (new records aren't used for determining load order). However, this -requires significant computation, so LOOT will use a database that holds which -records each plugin changes. The database will be hosted online, and will be -used in the following manner: +are edited (new records aren't used for determining load order). - 1. LOOT checks to see if a database update is available, and downloads the - update if so. This may be done by diff or by downloading the whole file if - a diff is not feasible. - 2. LOOT scans the installed plugins, calculating the CRC of each plugin. - 3. For each plugin, the database is scanned for an entry with a matching file - name and CRC. If an entry is found, the entry's data is used and scanning - of the plugin is skipped. - 4. Any plugins which do not already have database entries are scanned. - 5. The correct load order of the plugins is calculated using data from scanned - plugins, the database, the masterlist and the userlist. - 6. The data from the scanned plugins is appended to the database file, which - is then uploaded to its hosted location. - -In addition to having this database file as a sort of permanent and -self-updating cache, optimisations can also be made to skip load order +Optimisations can be made to skip load order calculation. For example, if the only changes from the last load order set by LOOT is that a plugin or plugins have been removed, then the last load order may be used. It might also be possible to only perform partial calculation -when plugins are added, though this is not certain. +when plugins are added, though this is not certain. It might not also be worth +the effort, since preliminary testing shows that scanning is considerably +faster than first anticipated. -Note: For clarity, the database that holds the results of plugin scanning will -be referred to as the "cache". The list of metadata that is maintained by the +Note: For clarity, the he list of metadata that is maintained by the LOOT team will be referred to as the "masterlist", and the list of metadata that is individual to each user will be referred to as the "userlist". @@ -261,59 +244,16 @@ Further details can be found in the 'docs/BOSS Metadata File Syntax.html' file. Networking ========== -LOOT has four target networking functionalities: +LOOT has two target networking functionalities: - * Remote / local cache sync. * Masterlist updating. * User feedback submission. -None of these functionalities are critical to usage of LOOT. +Neither of these functionalities are critical to usage of LOOT. -The masterlist and cache repositories will be Subversion repositories, as this -allows users to most easily download only the files they require, and only the -diffs of those files. - -As LOOT needs commit as well as checkout access to the cache repository, for -users without their own credentials, it must access the repository using a -robot account. Google Code does not allow the use of robot accounts, but -Sourceforge does. However, the actions performed by the robot account are the responsibility of the account creator. This brings up the issue of account -abuse. This will be covered in the 'Security' section below. - -Conflicts between different users' commits will probably be an issue. -Because the database files are binary, not text, and because they use an index -with data offsets, they can't be merged automatically. The database file -workflow is: - 1. User svn updates their working copy of a file. - 2. User performs scanning actions. - 3. User edits their working copy of the file. - 4. User commits their edit to the repository. - -If another user changes the same file between steps 1 and 4, the commit will -fail. If this happens, LOOT should svn revert followed by svn update, then -re-apply the user's edits to the updated file, checking to make sure that the -updated file doesn't already include the same data. A second commit should then -be attempted. If this commit also fails, it's best just to discard the edits. -While that means the user won't benefit from having a local cache, it becomes -too complicated to store the edits on disk and re-apply them for a potentially -uncapped number of failures. - -The database repository may store tens if not hundreds of thousands of files, -so they need to be organised well to ease browsing. Grouping the files into -the game they are for, then subgrouping them into folders based on the first -character of their filename, then subgrouping again based on the second -character would probably do. For example: - -repo/ - Oblivion/ - o/ - b/ - oblivion.esm.bpdc - -Files and the 'letter folders' will be in lowercase. - -"repo" does not have to be the repository root, but LOOT's settings should -use the URL pointing to that location. The repository will be required to use -the same game folder names as LOOT's install structure (see the section below). +The masterlist repository will use Subversion, as this +allows users to most easily download only the file they require, and only the +diffs of that file. The masterlists would be stored using a folder structure that mirrors LOOT's install structure, for example: @@ -345,86 +285,6 @@ update is easily performed through a global masterlist message and so does not require an additional transport mechanism. -Security -======== - -The abuse of the database repository credentials to be distributed with LOOT is -a very real concern. Steps must be taken to prevent or minimise the associated -risk. Unfortunately, I have no experience with this sort of thing, so I could -be making some incorrect assumptions below. - -One possible way to make obtaining the SSH key for use outside of LOOT -non-trivial is to compile it into the executable. The key could be stored in a -header file, eg. "key.h", of which a template with an empty or dummy key is -committed to the source code repository, and then this header could be included -in the file(s) that require it. So long as the actual key is not distributed -beyond the team member who builds releases, this would then require people to -either reverse-engineer the executable or monitor its allocated memory to obtain -the key. - -However, this doesn't prevent the swapping out of files that LOOT uploads. It -may be possible for an attacker to replace the file that LOOT generates or edits -for upload with another containing rubbish data or malicious code. Attackers -could also "cut and paste" the section of the executable that deals with the -uploading into another executable that allows arbitrary uploads. I don't -actually know if the latter is possible, but it sounds like it could be. - -In that case, may we lose any upload safeguards, but the attacker could not -disable the download safeguards present in other users' copies of the LOOT -executable unless their systems were already compromised. We have to then assume -that the database is not necessarily safe. Given an unsafe database, we can do -the following: - - * Ensure that the robot only has repository commit/checkout permissions and - no admin abilities. Team members should be given repository access to - allow oversight, with admins having admin abilities. - * Check for the validity of database files. A database file should only ever - be read or generated by LOOT, so invalid database files should never - exist except on a compromised system. Any invalid files found could - cause a message to be displayed and a notification sent to the team using - the feedback mechanism. The invalid file would then be deleted. The parser - should fail immediately upon encountering invalid content. - * Don't execute database file content. The format doesn't have any - executable data, so this is pretty obvious in this case. - * Revoke access to the repository for the compromised key. If we only have - one valid robot key at any one time, this is pretty easy. The downside is - that it means that nobody can access the repository with the key, - effectively disabling the online cache. The procedure in case this was - done would be to then figure out how the compromise happened, then try to - stop it happening again, before releasing an update to LOOT with a new - key. - * Only download a database file if a plugin's data isn't found in the local - cache. This means that clients will only download database files for - plugins which are installed and which don't already have cached data. So - if A.esp and B.esp have database file updates available, but the specific - version of A.esp that the user has installed already has its data cached - locally, then only B.esp.lmdb will be updated. - * Users could disable online database access in their copies of LOOT. If - a user thinks that they have downloaded rubbish data, they can delete - their local cache and LOOT will regenerate it the next time it runs. - -The validity and non-execute points would not prevent an attacker from -distributing rubbish data, but it would only take one user noticing and -informing the team for this to be caught and dealt with. - -Since the database repository would be separate from the masterlist repository, -and the robot wouldn't have access to the latter, users can easily be informed -of any compromise via masterlist messages. - -An attacker may also upload files that may never be downloaded by users, but -which are still malicious or illegal in nature. As such, we can't rely on -users vetting the files they download as an indicator for a compromise. One -possibility is for the database to be monitored by having an email notification -system in place that would notify team members of any file uploads and their -content. That would allow us to detect database compromise without any user -involvement, though it does require team activity. - -All the above points are suggested on the assumption that we can't have a -dedicated machine to filter out malicious use. Such a machine would be likely -flawed anyway, so the above points are still worth considering even if we had -a dedicated machine. - - Install Structure ================= diff --git a/examples/settings.yaml b/examples/settings.yaml index 88da0e7a..5184cb53 100644 --- a/examples/settings.yaml +++ b/examples/settings.yaml @@ -17,4 +17,4 @@ 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. +sLanguage: eng # An ISO 639-3 language string. diff --git a/src/error.h b/src/error.h index 68a18117..ea740284 100644 --- a/src/error.h +++ b/src/error.h @@ -25,6 +25,7 @@ #define __BOSS_ERROR__ #include +#include namespace boss { @@ -41,7 +42,7 @@ namespace boss { class error : public std::exception { public: - error(const int code_arg, const std::string& what_arg) : _code(code_arg), _what(what_arg) {} + error(const unsigned int code_arg, const std::string& what_arg) : _code(code_arg), _what(what_arg) {} ~error() throw() {}; unsigned int code() const { return _code; } diff --git a/src/game.cpp b/src/game.cpp index b63086a3..dc6fcc7d 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -164,21 +164,4 @@ namespace boss { throw error(ERROR_PATH_WRITE_FAIL, "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 index 7218fa96..2b0dbde8 100644 --- a/src/game.h +++ b/src/game.h @@ -68,9 +68,6 @@ namespace boss { 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/globals.h b/src/globals.h index c574624a..1110805b 100644 --- a/src/globals.h +++ b/src/globals.h @@ -34,6 +34,10 @@ namespace boss { const unsigned int VERSION_MAJOR = 3; const unsigned int VERSION_MINOR = 0; const unsigned int VERSION_PATCH = 0; + + // Temporary hack. + const char * const libespm_options_path = "../../libespm/opts/input"; + const char * const libespm_game = "Skyrim"; } diff --git a/src/helpers.cpp b/src/helpers.cpp index 49156628..a782e496 100644 --- a/src/helpers.cpp +++ b/src/helpers.cpp @@ -26,7 +26,7 @@ */ #include "helpers.h" -#include "plugin/ModFormat.h" +#include "VersionRegex.h" #include "error.h" #include @@ -46,6 +46,10 @@ #include #include +// Libespm files. +#include +#include + #if _WIN32 || _WIN64 # ifndef UNICODE # define UNICODE @@ -163,10 +167,37 @@ namespace boss { : verString(ver) {} 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; + //Use libespm interface. Assumes that it has previously been initialised. + ifstream input(file.string().c_str(), ios::binary); + espm::file FileStruct; + espm::readFile(input, FileStruct); + input.close(); + + for(size_t i=0,max=FileStruct.fields.size(); i < max; ++i){ + if (FileStruct.fields[i].name == "SNAM") { + string text = FileStruct.fields[i].data; + + 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; + + verString = trim_copy(string(match.first, match.second)); + } + } + } + } + } else { #if _WIN32 || _WIN64 DWORD dummy = 0; @@ -193,10 +224,8 @@ namespace boss { } #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 + if (string::npos != file.string().find('"')) { + // 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/'"; @@ -206,11 +235,7 @@ namespace boss { 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()); + verString = string(buf); } pclose(fp); } diff --git a/src/legacy.py b/src/legacy.py deleted file mode 100755 index e9e6f176..00000000 --- a/src/legacy.py +++ /dev/null @@ -1,212 +0,0 @@ -#! /usr/bin/env python3 -# 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 -# . - -# This is a script that converts a MF 2.3 masterlist to a MF 3 masterlist. It's -# a bit hacky, but the YAML parser accepts it. Limitations are: -# - Doesn't strip plugins that no longer belong in the masterlist, ie. without -# messages or positioning comments. -# - Requirements and incompatibilities do not use their new data structures, -# and are just converted to 'say' messages. -# - No extraneous comments/messages are removed. -# - VAR conditions are not replaced with their conditional statements. SET lines -# are stripped, so the old masterlist must be used to check what they are. -# - Each message or list of tags comes under a different 'msg' or 'tag' object. -# - Tag message text, aside from the tags themselves, is lost. -# - Conditional plugin positions are all made unconditional, so there will be -# duplicate entries. - -class Plugin: - - -def escapeYAMLStr(s): - special = ['-', '?', ':', ',', '[', ']', '{', '}', '&', '*', '!', '|', '>', '\'', '"', '%', '@', '`'] - if ('\'' in s): - return '"' + s.replace('\\', '\\\\').replace('"', '\\"').strip() + '"' - elif (1 in [c in s for c in special]): - return '\'' + s.strip() + '\'' - else: - return s; - -def mContent(line): - content = line[line.find(':')+1:] - return escapeYAMLStr(content) - -def convertCondition(condition): - #IF no longer exists. - #IFNOT is now 'not'. - #The VAR condition no longer exists. - #The '=' comparator is now '=='. - #The LANG condition no longer exists. - #'&&' is now 'and'. - #'||' is now 'or'. - - #Really, these will probably have to be done manually. - - parts = condition.split('"') # Every odd index is quoted and shouldn't be lowercased. - - for i in range(len(parts)): - if (i % 2 == 0): - parts[i] = parts[i].lower() - - condition = '"'.join(parts) - - condition.replace(' && ', ' and ').replace(' || ', ' or ').replace(' =)', ' ==)') - - return escapeYAMLStr(condition) - -inFile = open('../../BOSS/data/boss-oblivion/masterlist.txt', 'r') -outFile = open('../build/masterlist.yaml', 'w') - -outFile.write('---\n'); - -isInComment = False -lastLine = '' -lastLineType = 0 -for line in inFile: - - line = line.replace('\n', '') - - if (len(line.strip()) == 0): - continue - - # Skip group lines. - if ("BEGINGROUP:" in line or "ENDGROUP:" in line): - continue - - # Skip var lines. - if ('SET:' in line): - continue - - # Write comments. - if (line[0:2] == '//'): - outFile.write('#' + line[2:] + '\n') - continue - - if ('/*' in line): - isInComment = True - outFile.write('#' + line + '\n') - - if ('*/' in line): - isInComment = False - outFile.write('#' + line + '\n') - - if ('*/' in line or '/*' in line): - continue - - if (isInComment): - outFile.write('#' + line + '\n') - continue - - # Write plugin lines. - if (':' not in line): - outFile.write(' - name: ' + escapeYAMLStr(line) + '\n') - else: - pos = line.find(':') - if ('MOD:' in line[:pos+1]): - outFile.write(' - name: ' + escapeYAMLStr(line[pos+1:]) + '\n') - elif ('REGEX:' in line[:pos+1]): - outFile.write(' - name: ' + escapeYAMLStr(line[pos+1:]) + '\n') - - # Extract conditional from line. - if ('IF' in line and ':' in line): - key = line[:line.find(':')] - key = key.replace('GLOBAL', '') - condition = key[:key.rfind(' ')].strip() - elif ('ELSE' in line and ':' in line): - if (condition[0] != 'n'): - condition = 'not ' + condition - condition = condition.replace('not ', '`~||~`') - condition = condition.replace('d f', 'd not f').replace('d c', 'd not c', ', 'and - else: - condition = '' - condition = convertCondition(condition); - - # Write tag lines. - if ('TAG:' in line): - if ('{{BASH' in line): - tags = line[line.find('{{BASH:') + 7:line.find('}}') ] - tags = tags.split(',') - outFile.write(' tag:\n') - for tag in tags: - if (condition): - outFile.write(' - condition: ' + condition + '\n') - outFile.write(' name: ' + tag.strip(' ') + '\n') - else: - outFile.write(' - ' + tag.strip(' ') + '\n') - if ('[' in line): - tags = line[line.find('[')+1:line.find(']')] - tags = tags.split(',') - outFile.write(' tag:\n') - for tag in tags: - if (condition): - outFile.write(' - condition: ' + condition + '\n') - outFile.write(' name: -' + tag.strip(' ') + '\n') - else: - outFile.write(' - -' + tag.strip(' ') + '\n') - - # Write 'say' messages. - if ('SAY:' in line or 'INC:' in line or 'REQ:' in line): - if ('GLOBAL ' not in line): - outFile.write(' msg:\n') - indent = ' ' - else: - indent = ' ' - prefix = '' - if ('INC:' in line): - line = 'Incompatible with ' + line.split(':')[1] - elif ('REQ:' in line): - line = 'Requires ' + line.split(':')[1] - else: - line = line.split(':')[1] - outFile.write(indent + '- type: say\n') - if (condition): - outFile.write(indent + ' condition: ' + condition + '\n') - outFile.write(indent + ' content: ' + escapeYAMLStr(line) + '\n') - - # Write 'warn' messages. - if ('DIRTY:' in line or 'WARN:' in line): - if ('GLOBAL ' not in line): - outFile.write(' msg:\n') - indent = ' ' - else: - indent = ' ' - outFile.write(indent + '- type: warn\n') - if (condition): - outFile.write(indent + ' condition: ' + condition + '\n') - outFile.write(indent + ' content: ' + mContent(line) + '\n') - - # Write 'error' messages. - if ('ERROR:' in line): - if ('GLOBAL ' not in line): - outFile.write(' msg:\n') - indent = ' ' - else: - indent = ' ' - outFile.write(indent + '- type: error\n') - if (condition): - outFile.write(indent + ' condition: ' + condition + '\n') - outFile.write(indent + ' content: ' + mContent(line) + '\n') - - -inFile.close(); -outFile.close(); diff --git a/src/metadata.cpp b/src/metadata.cpp index 47e3f304..ee70b207 100644 --- a/src/metadata.cpp +++ b/src/metadata.cpp @@ -22,7 +22,11 @@ */ #include "metadata.h" +#include "helpers.h" #include +#include + +#include using namespace std; @@ -126,8 +130,31 @@ namespace boss { return Data(); } - Plugin::Plugin() : enabled(true), priority(0) {} - Plugin::Plugin(const std::string n) : name(n), enabled(true), priority(0) {} + Plugin::Plugin() : enabled(true), priority(0), crc(0), isMaster(false) {} + Plugin::Plugin(const std::string& n) : name(n), enabled(true), priority(0), crc(0), isMaster(false) {} + + Plugin::Plugin(const std::string& n, const std::string& path) + : name(n), enabled(true), priority(0) { + + // Get data from file contents using libespm. Assumes libespm has already been initialised. + boost::filesystem::path filepath = boost::filesystem::path(path) / n; + ifstream input(filepath.string().c_str(), ios::binary); + espm::file File; + espm::readFile(input, File); + input.close(); + + crc = GetCrc32(filepath); + isMaster = espm::isMaster(File); + vector rawMasters = espm::getMasters(File); + for (size_t i=0,max=rawMasters.size(); i < max; ++i) { + masters.push_back(rawMasters[i]); + } + + vector records = espm::getRecords(File); + for (vector::const_iterator it = records.begin(),endIt = records.end(); it != endIt; ++it){ + formIDs.insert(*reinterpret_cast(it->record.recID)); + } + } std::string Plugin::Name() const { return name; @@ -241,4 +268,33 @@ namespace boss { bool Plugin::operator == (Plugin rhs) { return name == rhs.Name(); } + + std::set Plugin::FormIDs() const { + return formIDs; + } + + std::set Plugin::OverrideFormIDs() const { + int modIndex = masters.size() << 24; + + set fidSubset; + + for (set::const_iterator it = formIDs.begin(), endIt=formIDs.end(); it != endIt; ++it) { + if (*it < modIndex) + fidSubset.insert(*it); + } + + return fidSubset; + } + + std::vector Plugin::Masters() const { + return masters; + } + + uint32_t Plugin::Crc() const { + return crc; + } + + bool Plugin::IsMaster() const { + return isMaster; + } } diff --git a/src/metadata.h b/src/metadata.h index 43d1eb3b..ee5325af 100644 --- a/src/metadata.h +++ b/src/metadata.h @@ -25,7 +25,9 @@ #include "game.h" +#include #include +#include #include #include @@ -107,7 +109,8 @@ namespace boss { class Plugin { public: Plugin(); - Plugin(const std::string name); + Plugin(const std::string& name); + Plugin(const std::string& name, const std::string& path); std::string Name() const; bool Enabled() const; @@ -132,6 +135,12 @@ namespace boss { bool IsRegexPlugin() const; bool operator == (Plugin rhs); + + std::set FormIDs() const; + std::set OverrideFormIDs() const; + std::vector Masters() const; + uint32_t Crc() const; + bool IsMaster() const; private: std::string name; bool enabled; //Default to true. @@ -141,11 +150,38 @@ namespace boss { std::set incompatibilities; std::list messages; std::list tags; + + std::vector masters; + std::set formIDs; + uint32_t crc; + bool isMaster; }; + + inline bool isMasterOf(const Plugin& servant, const Plugin& master) { + std::vector masters = servant.Masters(); + for (int i = 0; i < masters.size(); ++i) { + if (masters[i] == master.Name()) + return true; + } + return false; + } struct plugin_comp { bool operator() (const Plugin& lhs, const Plugin& rhs) const { - return lhs.Name() < rhs.Name(); + if (lhs.IsMaster() && !rhs.IsMaster()) + return true; + else if (!lhs.IsMaster() && rhs.IsMaster()) + return false; + else if (isMasterOf(rhs, lhs)) + return true; + else if (isMasterOf(lhs, rhs)) // Should probably also check for cyclic masters. + return false; + else if (lhs.OverrideFormIDs().size() > rhs.OverrideFormIDs().size()) + return true; + else if (lhs.OverrideFormIDs().size() < rhs.OverrideFormIDs().size()) + return false; + else + return lhs.Name() < rhs.Name(); } }; diff --git a/src/parsers.h b/src/parsers.h index de1d3bc2..6d1903d7 100644 --- a/src/parsers.h +++ b/src/parsers.h @@ -213,14 +213,14 @@ namespace YAML { Emitter& operator << (Emitter& out, const boss::Message& rhs) { out << BeginMap - << Key << "type" << rhs.Type() - << Key << "content" << rhs.Content(); + << Key << "type" << Value << rhs.Type() + << Key << "content" << Value << rhs.Content(); if (!rhs.Language().empty()) - out << Key << "lang" << rhs.Language(); + out << Key << "lang" << Value << rhs.Language(); if (!rhs.Condition().empty()) - out << Key << "condition" << rhs.Condition(); + out << Key << "condition" << Value << rhs.Condition(); out << EndMap; } @@ -230,13 +230,13 @@ namespace YAML { out << rhs.Name(); else { out << BeginMap - << Key << "name" << rhs.Name(); + << Key << "name" << Value << rhs.Name(); if (rhs.IsConditional()) - out << Key << "condition" << rhs.Condition(); + out << Key << "condition" << Value << rhs.Condition(); if (!rhs.DisplayName().empty()) - out << Key << "display" << rhs.DisplayName(); + out << Key << "display" << Value << rhs.DisplayName(); out << EndMap; } @@ -247,8 +247,8 @@ namespace YAML { out << rhs.PrefixedName(); else { out << BeginMap - << Key << "name" << rhs.PrefixedName() - << Key << "condition" << rhs.Condition() + << Key << "name" << Value << rhs.PrefixedName() + << Key << "condition" << Value << rhs.Condition() << EndMap; } } diff --git a/src/plugin/ModFormat.cpp b/src/plugin/ModFormat.cpp deleted file mode 100644 index cad802ec..00000000 --- a/src/plugin/ModFormat.cpp +++ /dev/null @@ -1,240 +0,0 @@ -/* 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.string().c_str(), ios_base::binary | ios_base::in); - - if (file.bad()) - 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.string().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 deleted file mode 100644 index 54fcf5c4..00000000 --- a/src/plugin/ModFormat.h +++ /dev/null @@ -1,64 +0,0 @@ -/* 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 deleted file mode 100644 index 7062cce9..00000000 --- a/src/plugin/Types.h +++ /dev/null @@ -1,51 +0,0 @@ -/* 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 deleted file mode 100644 index c5842431..00000000 --- a/src/plugin/VersionRegex.cpp +++ /dev/null @@ -1,92 +0,0 @@ -/* 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 deleted file mode 100644 index c4a2a661..00000000 --- a/src/plugin/VersionRegex.h +++ /dev/null @@ -1,45 +0,0 @@ -/* 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 diff --git a/src/plugindata.cpp b/src/plugindata.cpp deleted file mode 100644 index 19c87455..00000000 --- a/src/plugindata.cpp +++ /dev/null @@ -1,212 +0,0 @@ -/* 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 deleted file mode 100644 index 8e1096f9..00000000 --- a/src/plugindata.h +++ /dev/null @@ -1,67 +0,0 @@ -/* 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 deleted file mode 100644 index 2f7d62f1..00000000 --- a/src/sorting.h +++ /dev/null @@ -1,52 +0,0 @@ -/* 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 8abeede4..4ef57981 100644 --- a/src/tester.cpp +++ b/src/tester.cpp @@ -8,6 +8,15 @@ #include #include +#include +#include + +#include +#include + +#include + +namespace fs = boost::filesystem; using namespace std; @@ -22,9 +31,9 @@ void minimisePluginList(set& plugins) { plugins.swap(out); } -int main() { +int main(int argc, char *argv[]) { - cout << "Testing masterlist parser." << endl; + /* cout << "Testing masterlist parser." << endl; YAML::Node test = YAML::LoadFile("masterlist-example.yaml"); @@ -68,6 +77,52 @@ int main() { out2 << yout2.c_str(); out2.close(); - + */ + cout << "Testing plugin reading." << endl; + + time_t start, end; + start = time(NULL); + + // Set up libesm. + common::options::setGame(boss::libespm_game); + ifstream input(boss::libespm_options_path); + common::readOptions(input); + input.close(); + + // Get a list of the plugins. + list plugins; + for (fs::directory_iterator it(argv[1]); it != fs::directory_iterator(); ++it) { + if (fs::is_regular_file(it->status()) && (it->path().extension().string() == ".esp" || it->path().extension().string() == ".esm")) { + + if (it->path().filename().string() == "Skyrim.esm") + continue; // Libespm crashes with v1.9 Skyrim.esm. + + cout << "Found plugin: " << it->path().string() << endl; + boss::Plugin plugin(it->path().filename().string(),it->path().parent_path().string()); + plugins.push_back(plugin); + } + } + + cout << "Finished looking for plugins." << endl; + + plugins.sort(boss::plugin_comp()); + + for (list::iterator it=plugins.begin(), endIt = plugins.end(); it != endIt; ++it) { + cout << it->Name() << endl + << '\t' << "Is Master: " << it->IsMaster() << endl + << '\t' << "Masters:" << endl; + + vector masters = it->Masters(); + for(int i = 0; i < masters.size(); ++i) + cout << '\t' << '\t' << i << ": " << masters[i] << endl; + + cout << '\t' << "Number of Records: " << it->FormIDs().size() << endl; + + cout << '\t' << "Number of Override Records: " << it->OverrideFormIDs().size() << endl; + } + + end = time(NULL); + cout << "Time taken to sort plugins: " << (end - start) << " seconds." << endl; + return 0; }