From a261ca9de637a0860ab44319c62c0b3e470c9823 Mon Sep 17 00:00:00 2001 From: WrinklyNinja Date: Mon, 13 May 2013 00:44:20 +0100 Subject: [PATCH] Report generator is now complete. Fixed some bugs with report CSS/JS. --- README.md | 38 ++--------- examples/script.js | 21 ++++--- examples/style.css | 8 +-- src/generators.h | 154 +++++++++++++++++++++++++++++++++++---------- src/gui/main.cpp | 10 ++- src/metadata.cpp | 46 +++++++++++--- src/metadata.h | 10 ++- 7 files changed, 195 insertions(+), 92 deletions(-) diff --git a/README.md b/README.md index 095313ad..8daf8080 100644 --- a/README.md +++ b/README.md @@ -60,40 +60,13 @@ BOSSv3 won't have a command line interface, to simplify things. It also allows B - [ ] Make validity checks non-fatal. - [ ] Add a massive "RUN THE GAME LAUNCHER IF YOUR GAME IS NOT DETECTED" message somewhere. - [ ] Generalise Total Conversion support, so that any TC for any of the supported 'base' games can be used with BOSS. -- [ ] Write XHTML report generator. +- [x] Write XHTML report generator. - [ ] Write report CSS. - [ ] Write report Javascript. - - -## BOSS Report - -Once BOSS has applied a load order, it will display a report, similar to the BOSS Log, that covers the following: - -* The number of messages, broken down into the total, the number of warnings and the number of errors. -* The masterlist version used. -* Any parser or validity checker errors encountered. -* If there were any changes since BOSS was last run. -* The new load order. -* The messages attached to plugins, version numbers read, CRCs calculated and whether or not each plugin is active. - -The current BOSS Log has a number of useful filters. Depending on the UI used for the report, some or all of these filters may be useful for implementation: - -* Hide versions -* Hide whether a plugin is active or not. -* Hide CRCs -* Hide "say" messages. -* Hide Bash Tag suggestions. -* Hide all messages. -* Hide "do not clean" messages. -* Hide inactive plugins. -* Hide messageless plugins. - -The report data will be stored in a YAML file, which could use an extended form of the metadata file syntax, with the following additions: - -* Plugins get 'version' (string) and 'crc' (int) nodes added to them. -* A new top-level node that holds a 'masterlist version' (string), a 'masterlist updated' (boolean) and a 'report changed' (boolean) node. - -There are a whole slew of options when it comes to how the report UI will be implemented. Using a native interface isn't a realistic option because it's so difficult to get anything that isn't very limited implemented, and the report is mostly static so wouldn't take advantage of native controls anyway. A HTML/CSS/JS approach is therefore preferable. The neatest way to do this would be to write the HTML as XHTML using PugiXML, then include external CSS & JS that gets distributed with BOSS. +- [ ] Double-check ghosted file support. +- [ ] Add a setting that lets users choose whether to use the viewer window or their own browser when viewing BOSS's report. +- [ ] Somehow implement filter settings memory for the BOSS report. +- [ ] Implement checking of details part of report against previous report. ## Optimisation @@ -133,6 +106,7 @@ BOSS uses the following libraries: * Boost * Libespm * Libloadorder +* PugiXML * wxWidgets * yaml-cpp diff --git a/examples/script.js b/examples/script.js index a6dc7175..58d5d057 100755 --- a/examples/script.js +++ b/examples/script.js @@ -25,17 +25,13 @@ if (!document.getElementsByClassName) { function showElement(element) { if (element != null) { if (element.className.indexOf('hidden') != -1) { - element.className = element.className.replace(' hidden', ''); - } else if (element.className.indexOf('visible') == -1) { - element.className += ' visible'; + element.className = element.className.replace('hidden', ''); } } } function hideElement(element) { if (element != null) { - if (element.className.indexOf('visible') != -1) { - element.className = element.className.replace('visible', ''); - } else if (element.className.indexOf('hidden') == -1) { + if (element.className.indexOf('hidden') == -1) { element.className += ' hidden'; } } @@ -58,7 +54,7 @@ function toggleDisplayCSS(evt) { } } else { for (var i = 0, z = e.length; i < z; i++) { - e[i].className = e[i].className.replace(' hidden', ''); + e[i].className = e[i].className.replace('hidden', ''); } } } @@ -68,15 +64,20 @@ function toggleFilters(evt) { evt.target.className += ' current'; } else { hideElement(filters); - evt.target.className = evt.target.className.replace(' current', ''); + evt.target.className = evt.target.className.replace('current', ''); } } function showSection(evt) { - hideElement(document.querySelector('#summary,#plugins')); + elemArr = document.getElementById('nav').querySelectorAll('.button[data-section]'); + var i = elemArr.length - 1; + while (i > -1) { + hideElement(document.getElementById(elemArr[i].getAttribute('data-section'))); + i--; + } showElement(document.getElementById(evt.target.getAttribute('data-section'))); var elem = document.querySelector('#nav div.current[data-section]'); if (elem != null) { - elem.className = elem.className.replace(' current', ''); + elem.className = elem.className.replace('current', ''); } if (evt.target.className.indexOf('current') == -1) { evt.target.className += ' current'; diff --git a/examples/style.css b/examples/style.css index 6117a1b1..4f2d50d9 100755 --- a/examples/style.css +++ b/examples/style.css @@ -88,12 +88,12 @@ table tr:last-child td:last-child { position:fixed; right:0; top:32px; - height:0; -} -#filters.visible { - height:auto; + display:block; box-shadow:0 0 3px 1px rgba(0,0,0,0.5); } +#filters.hidden { + display:none; +} #filters label { display:block; padding:0.2em 0.5em; diff --git a/src/generators.h b/src/generators.h index 6101e5dc..e9d03f7e 100644 --- a/src/generators.h +++ b/src/generators.h @@ -34,7 +34,14 @@ namespace boss { - void WriteMessage(pugi::xml_node& listItem, std::string content) { + void WriteMessage(pugi::xml_node& listItem, std::string type, std::string content) { + + if (type == "say") + content = "Note: " + content; + else if (type == "warn") + content = "Warning: " + content; + else + content = "Error: " + content; size_t pos1f = content.find("\"file:"); size_t pos1h = content.find("\"http"); @@ -136,6 +143,28 @@ namespace boss { div.text().set("Filters"); } + void AppendMessages(pugi::xml_node& parent, const std::list& messages, int& warnNo, int& errorNo) { + if (!messages.empty()) { + pugi::xml_node list = parent.append_child(); + list.set_name("ul"); + + for (std::list::const_iterator it=messages.begin(), endit=messages.end(); it != endit; ++it) { + + pugi::xml_node li = list.append_child(); + li.set_name("li"); + li.append_attribute("class").set_value(it->Type().c_str()); + + //Turn any urls into hyperlinks. + WriteMessage(li, it->Type(), it->Content()); + + if (boost::iequals(it->Type(), "warn")) + ++warnNo; + else if (boost::iequals(it->Type(), "error")) + ++errorNo; + } + } + } + void AppendSummary(pugi::xml_node& main, bool hasChanged, const std::string& masterlistVersion, @@ -143,8 +172,7 @@ namespace boss { int messageNo, int warnNo, int errorNo, - const std::list& messages, - const std::string& language) { + const std::list& messages) { pugi::xml_node summary = main.append_child(); summary.set_name("div"); @@ -187,6 +215,16 @@ namespace boss { else cell.text().set("Disabled"); + if (hasChanged) { + pugi::xml_node note = summary.append_child(); + note.set_name("div"); + note.append_attribute("id").set_value("noChanges"); + note.text().set("No change in details since last run."); + } + + AppendMessages(summary, messages, warnNo, errorNo); + messageNo += messages.size(); + row = table.append_child(); row.set_name("tr"); cell = row.append_child(); @@ -213,43 +251,84 @@ namespace boss { cell = row.append_child(); cell.set_name("td"); cell.text().set(IntToString(errorNo).c_str()); + } - if (hasChanged) { - pugi::xml_node note = summary.append_child(); - note.set_name("div"); - note.append_attribute("id").set_value("noChanges"); - note.text().set("No changes since last run."); - } + void AppendDetails(pugi::xml_node& main, const std::list& plugins, int& messageNo, int& warnNo, int& errorNo) { - if (!messages.empty()) { - pugi::xml_node list = summary.append_child(); - list.set_name("ul"); + pugi::xml_node details = main.append_child(); + details.set_name("div"); + details.append_attribute("id").set_value("plugins"); + details.append_attribute("class").set_value("hidden"); - for (std::list::const_iterator it=messages.begin(), endit=messages.end(); it != endit; ++it) { - //Need to weed out different language messages. - if (it->Language().empty() || boost::iequals(it->Language(), language)) { + if (!plugins.empty()) { - pugi::xml_node li = list.append_child(); - li.set_name("li"); - li.append_attribute("class").set_value(it->Type().c_str()); + details = details.append_child(); + details.set_name("ul"); - //Turn any urls into hyperlinks. - WriteMessage(li, it->Content()); - + for (std::list::const_iterator it=plugins.begin(), endit=plugins.end(); it != endit; ++it) { + pugi::xml_node plugin = details.append_child(); + plugin.set_name("li"); + + pugi::xml_node node = plugin.append_child(); + node.set_name("span"); + node.append_attribute("class").set_value("mod"); + node.text().set(it->Name().c_str()); + + if (!it->Version().empty()) { + node = plugin.append_child(); + node.set_name("span"); + node.append_attribute("class").set_value("version"); + node.text().set(("Version: " + it->Version()).c_str()); } + + if (it->Crc() > 0) { + node = plugin.append_child(); + node.set_name("span"); + node.append_attribute("class").set_value("crc"); + node.text().set(("CRC: " + IntToHexString(it->Crc())).c_str()); + } + + if (it->IsActive()) { + node = plugin.append_child(); + node.set_name("span"); + node.append_attribute("class").set_value("active"); + node.text().set("Active"); + } + + std::list messages = it->Messages(); + + std::set tags = it->Tags(); + + if (!tags.empty()) { + std::string add, remove, content; + for (std::set::const_iterator jt=tags.begin(), endjt=tags.end(); jt != endjt; ++jt) { + if (jt->IsAddition()) + add += ", " + jt->Name(); + else + remove += ", " + jt->Name(); + } + if (!add.empty()) + content += "Bash Tags suggested for addition are " + add.substr(2) + ". "; + if (!remove.empty()) + content += "Bash Tags suggested for removal are " + remove.substr(2) + ". "; + messages.push_back(Message("say", content)); + } + + AppendMessages(plugin, messages, warnNo, errorNo); + messageNo += messages.size(); } } + } void AppendMain(pugi::xml_node& body, bool hasChanged, const std::string& masterlistVersion, bool masterlistUpdateEnabled, - int messageNo, - int warnNo, - int errorNo, const std::list& messages, - const std::string& language) { + const std::list& plugins, + int& pluginMessageNo + ) { pugi::xml_node main = body.append_child(); main.set_name("div"); @@ -263,7 +342,11 @@ namespace boss { div.set_name("div"); div.text().set("The BOSS Report requires Javascript to be enabled in order to function."); - AppendSummary(main, hasChanged, masterlistVersion, masterlistUpdateEnabled, messageNo, warnNo, errorNo, messages, language); + int messageNo=0, warnNo=0, errorNo=0; + AppendDetails(main, plugins, messageNo, warnNo, errorNo); + pluginMessageNo = messageNo; + + AppendSummary(main, hasChanged, masterlistVersion, masterlistUpdateEnabled, messageNo, warnNo, errorNo, messages); } void AppendFilters(pugi::xml_node& body, int messageNo, int pluginNo) { @@ -271,6 +354,7 @@ namespace boss { pugi::xml_node filters = body.append_child(); filters.set_name("div"); filters.append_attribute("id").set_value("filters"); + filters.append_attribute("class").set_value("hidden"); pugi::xml_node label, input; @@ -384,7 +468,12 @@ namespace boss { } - void GenerateReport(const std::string& file) { + void GenerateReport(const std::string& file, + const std::list& messages, + const std::list& plugins, + const std::string& masterlistVersion, + const bool masterlistUpdateEnabled, + const bool hasChanged) { pugi::xml_document doc; @@ -395,18 +484,15 @@ namespace boss { AppendNav(body); - std::list messages; - messages.push_back(Message("say", "This is a test message with a link to \"http://www.google.com Google\" in it.")); - - AppendMain(body, true, "4308 (2013-06-06)", true, 500, 4, 2, messages, "eng"); + int messageNo=0; + AppendMain(body, hasChanged, masterlistVersion, masterlistUpdateEnabled, messages, plugins, messageNo); - AppendFilters(body, 5, 5); + AppendFilters(body, messageNo, plugins.size()); AppendScripts(body); - if (!doc.save_file(file.c_str(), "\t", pugi::format_default | pugi::format_no_declaration)) - throw std::runtime_error("Could not write XML file."); + throw boss::error(ERROR_PATH_WRITE_FAIL, "Could not write BOSS report."); } diff --git a/src/gui/main.cpp b/src/gui/main.cpp index 77e9576f..07e9eb4b 100644 --- a/src/gui/main.cpp +++ b/src/gui/main.cpp @@ -340,6 +340,7 @@ void Launcher::OnSortPlugins(wxCommandEvent& event) { string filename = it->path().filename().string(); out << "Reading plugin: " << filename << endl; boss::Plugin plugin(_game, filename); + //boss::Plugin plugin(filename); plugins.push_back(plugin); progDia->Pulse(); @@ -436,7 +437,7 @@ void Launcher::OnSortPlugins(wxCommandEvent& event) { for (list::iterator it=plugins.begin(), endIt=plugins.end(); it != endIt; ++it) { try { - it->EvalAllConditions(_game); + it->EvalAllConditions(_game, _settings["Language"].as()); } catch (boss::error& e) { //LOG_ERROR("Error: %s", e.what()); wxMessageBox( @@ -515,7 +516,12 @@ void Launcher::OnSortPlugins(wxCommandEvent& event) { out << "Writing results file..." << endl; - GenerateReport(_game.ReportPath().string()); + GenerateReport(_game.ReportPath().string(), + messages, + plugins, + "4030 (2020-13-13)", + _settings["Update Masterlist"].as(), + true); progDia->Pulse(); diff --git a/src/metadata.cpp b/src/metadata.cpp index bc01f8c2..e602739c 100644 --- a/src/metadata.cpp +++ b/src/metadata.cpp @@ -154,6 +154,13 @@ namespace boss { return (boost::iequals(Type(), rhs.Type()) && boost::iequals(Content(), rhs.Content())); } + bool Message::EvalCondition(boss::Game& game, const std::string& lang) const { + if (language.empty() || boost::iequals(language, lang)) + return ConditionalData::EvalCondition(game); + else + return false; + } + std::string Message::Type() const { return type; } @@ -236,12 +243,12 @@ namespace boss { return Data(); } - Plugin::Plugin() : enabled(true), priority(0), isMaster(false) {} - Plugin::Plugin(const std::string& n) : name(n), enabled(true), priority(0), isMaster(false) {} + Plugin::Plugin() : enabled(true), priority(0), isMaster(false), isActive(false), crc(0) {} + Plugin::Plugin(const std::string& n) : name(n), enabled(true), priority(0), isMaster(false), isActive(false), crc(0) {} - Plugin::Plugin(const boss::Game& game, const std::string& n) + Plugin::Plugin(boss::Game& game, const std::string& n) : name(n), enabled(true), priority(0) { - /* + // Get data from file contents using libespm. Assumes libespm has already been initialised. boost::filesystem::path filepath = game.DataPath() / n; espm::File file(filepath.string(), game.espm_settings, false, false); @@ -281,7 +288,9 @@ namespace boss { size_t pos1 = text.find("{{BASH:"); if (pos1 == string::npos) - continue; + break; + + pos1 += 7; size_t pos2 = text.find("}}", pos1); if (pos2 == string::npos) @@ -298,7 +307,20 @@ namespace boss { break; } - }*/ + } + + //Calculate the plugin's CRC, and add it to the hashset. + boost::unordered_map::iterator it = game.crcCache.find(boost::to_lower_copy(name)); + + if (it != game.crcCache.end()) + crc = it->second; + else { + crc = GetCrc32(game.DataPath() / name); + game.crcCache.emplace(boost::to_lower_copy(name), crc); + } + + //Check if plugin is active. + isActive = game.IsActive(name); } void Plugin::Merge(const Plugin& plugin, bool ifDisabled) { @@ -437,7 +459,7 @@ namespace boss { tags = t; } - void Plugin::EvalAllConditions(boss::Game& game) { + void Plugin::EvalAllConditions(boss::Game& game, const std::string& language) { for (set::iterator it = loadAfter.begin(); it != loadAfter.end();) { if (!it->EvalCondition(game)) loadAfter.erase(it++); @@ -460,7 +482,7 @@ namespace boss { } for (list::iterator it = messages.begin(); it != messages.end();) { - if (!it->EvalCondition(game)) + if (!it->EvalCondition(game, language)) it = messages.erase(it); else ++it; @@ -559,6 +581,14 @@ namespace boss { return version; } + uint32_t Plugin::Crc() const { + return crc; + } + + bool Plugin::IsActive() const { + return isActive; + } + bool Plugin::MustLoadAfter(const Plugin& plugin) const { for (vector::const_iterator it=masters.begin(), endIt=masters.end(); it != endIt; ++it) { if (boost::iequals(*it, plugin.Name())) diff --git a/src/metadata.h b/src/metadata.h index de6db84c..5bf03e5b 100644 --- a/src/metadata.h +++ b/src/metadata.h @@ -85,6 +85,8 @@ namespace boss { bool operator < (const Message& rhs) const; bool operator == (const Message& rhs) const; + bool EvalCondition(boss::Game& game, const std::string& language) const; + std::string Type() const; std::string Language() const; std::string Content() const; @@ -129,7 +131,7 @@ namespace boss { public: Plugin(); Plugin(const std::string& name); - Plugin(const boss::Game& game, const std::string& name); + Plugin(boss::Game& game, const std::string& name); void Merge(const Plugin& plugin, bool ifdDisabled = false); Plugin DiffMetadata(const Plugin& plugin) const; @@ -147,6 +149,8 @@ namespace boss { std::vector Masters() const; bool IsMaster() const; //Checks master bit flag. std::string Version() const; + uint32_t Crc() const; + bool IsActive() const; void Name(const std::string& name); void Enabled(const bool enabled); @@ -157,7 +161,7 @@ namespace boss { void Messages(const std::list& messages); void Tags(const std::set& tags); - void EvalAllConditions(boss::Game& game); + void EvalAllConditions(boss::Game& game, const std::string& language); bool HasNameOnly() const; bool IsRegexPlugin() const; @@ -191,6 +195,8 @@ namespace boss { std::set formIDs; std::string version; //Obtained from description field. bool isMaster; + bool isActive; + uint32_t crc; }; bool operator == (const File& lhs, const Plugin& rhs);