Partially implemented plugin data generation.

* Created class for easy JSON <-> YAML object conversion.
* Deleted test UI data, as it is mostly obsolete.
* LOOT now passes some relevant data to the UI. Masterlist & userlist
data are not yet passed, and so display of that data is not yet
implemented.
This commit is contained in:
WrinklyNinja
2014-07-15 23:24:38 +01:00
parent 8482a8625f
commit ecd12e0104
6 changed files with 358 additions and 1139 deletions
+171 -158
View File
@@ -22,6 +22,7 @@
<http://www.gnu.org/licenses/>.
*/
'use strict';
var loot = {};
function isStorageSupported() {
try {
return ('localStorage' in window && window['localStorage'] !== null && window['localStorage'] !== undefined);
@@ -683,15 +684,18 @@ function initUI() {
settingsLangSelect.value = loot.settings.language;
debugVerbositySelect.value = loot.settings.debugVerbosity;
}
var loot;
function initJavascriptVars() {
function initGlobalVars() {
// Create and send a new query.
var request_id = window.cefQuery({
request: 'initJavascriptVars',
request: 'initGlobalVars',
persistent: false,
onSuccess: function(response) {
try {
loot = JSON.parse(response);
var temp = JSON.parse(response);
loot.version = temp.version;
loot.settings = temp.settings;
loot.gameTypes = temp.gameTypes;
loot.languages = temp.languages;
} catch (e) {
console.log(e);
console.log('Response: ' + response);
@@ -703,164 +707,173 @@ function initJavascriptVars() {
}
});
}
function processURLParams() {
/* Get the data path from the URL and load it. */
/*var pos = document.URL.indexOf("?data=");*/
var pos = 0;
if (pos != -1) {
/*var datapath = 'file:///' + document.URL.substring(pos+6);*/
var datapath = 'testdata';
require([datapath], function(){
var totalMessageNo = 0;
var warnMessageNo = 0;
var errorMessageNo = 0;
var activePluginNo = 0;
var dirtyPluginNo = 0;
/* Fill report with data. */
document.getElementById('masterlistRevision').textContent = data.masterlist.revision.substr(0, 9);
document.getElementById('masterlistDate').textContent = data.masterlist.date;
var generalMessagesList = document.getElementById('generalMessages').getElementsByTagName('ul')[0];
for (var i = 0; i < data.globalMessages.length; ++i) {
var li = document.createElement('li');
li.className = data.globalMessages[i].type;
/* innerHTML is open to abuse, but for hyperlinking it's too useful. */
li.innerHTML = data.globalMessages[i].content;
generalMessagesList.appendChild(li);
function updateInterfaceWithGameInfo(response) {
if (li.className == 'warn') {
try {
loot.game = JSON.parse(response);
} catch (e) {
console.log(e);
console.log('Response: ' + response);
}
var totalMessageNo = 0;
var warnMessageNo = 0;
var errorMessageNo = 0;
var activePluginNo = 0;
var dirtyPluginNo = 0;
/* Fill report with data. */
document.getElementById('masterlistRevision').textContent = loot.game.masterlist.revision;
document.getElementById('masterlistDate').textContent = loot.game.masterlist.date;
var generalMessagesList = document.getElementById('generalMessages').getElementsByTagName('ul')[0];
for (var i = 0; i < loot.game.globalMessages.length; ++i) {
var li = document.createElement('li');
li.className = loot.game.globalMessages[i].type;
/* innerHTML is open to abuse, but for hyperlinking it's too useful. */
li.innerHTML = loot.game.globalMessages[i].content[0].str;
generalMessagesList.appendChild(li);
if (li.className == 'warn') {
warnMessageNo++;
} else if (li.className == 'error') {
errorMessageNo++;
}
}
totalMessageNo = loot.game.globalMessages.length;
var pluginsList = document.getElementById('main');
var pluginsNav = document.getElementById('pluginsNav');
for (var i = 0; i < loot.game.plugins.length; ++i) {
var content, clone;
/* First add link to navbar. */
content = document.getElementById('pluginNav').content;
clone = document.importNode(content, true);
pluginsNav.appendChild(clone);
clone = pluginsNav.lastElementChild;
clone.getElementsByClassName('name')[0].textContent = loot.game.plugins[i].name;
clone.getElementsByTagName('a')[0].href = '#' + loot.game.plugins[i].name.replace(/\s+/g, '');
//clone.getElementsByClassName('priority')[0].textContent = 'Priority: 500, Global: ✓'; // Or '✗'.
if (loot.game.plugins[i].isDummy) {
clone.getElementsByClassName('dummyPlugin')[0].className += ' fa fa-eye-slash';
}
if (loot.game.plugins[i].loadsBSA) {
clone.getElementsByClassName('loadsBSA')[0].className += ' fa fa-paperclip';
}
/*// This won't actually be handled anything like this in the real data implementation.
if (data.plugins[i].hasUserEdits) {
clone.getElementsByClassName('hasUserEdits')[0].className += ' fa fa-user';
}*/
/* Now add plugin 'card'. */
content = document.getElementById('pluginSection').content;
clone = document.importNode(content, true);
pluginsList.appendChild(clone);
clone = pluginsList.lastElementChild;
clone.setAttribute('data-active', loot.game.plugins[i].isActive);
clone.id = loot.game.plugins[i].name.replace(/\s+/g, '');
if (loot.game.plugins[i].isActive) {
++activePluginNo;
}
/*if (loot.game.plugins[i].isDirty) {
++dirtyPluginNo;
}*/
clone.getElementsByTagName('h1')[0].textContent = loot.game.plugins[i].name;
if (loot.game.plugins[i].crc != 0) {
clone.getElementsByClassName('crc')[0].textContent = 'CRC: ' + loot.game.plugins[i].crc;
} else {
clone.getElementsByClassName('crc')[0].textContent = '';
}
if (loot.game.plugins[i].isDummy) {
showElement(clone.getElementsByClassName('dummyPlugin')[0]);
}
if (loot.game.plugins[i].loadsBSA) {
showElement(clone.getElementsByClassName('loadsBSA')[0]);
}
/*// This won't actually be handled anything like this in the real data implementation.
if (data.plugins[i].hasUserEdits) {
showElement(clone.getElementsByClassName('hasUserEdits')[0]);
}*/
if (loot.game.plugins[i].version) {
clone.getElementsByClassName('version')[0].textContent = 'Version: ' + loot.game.plugins[i].version;
} else {
hideElement(clone.getElementsByClassName('version')[0]);
}
/*if (data.plugins[i].tagsAdd && data.plugins[i].tagsAdd.length != 0) {
clone.getElementsByClassName('tag add')[0].textContent = data.plugins[i].tagsAdd.join(', ');
} else {*/
hideElement(clone.getElementsByClassName('tag add')[0]);
//}
/*if (data.plugins[i].tagRemove && data.plugins[i].tagRemove.length != 0) {
clone.getElementsByClassName('tag remove')[0].textContent = data.plugins[i].tagsRemove.join(', ');
} else {*/
hideElement(clone.getElementsByClassName('tag remove')[0]);
//}
clone.getElementsByClassName('editMetadata')[0].setAttribute('data-target', clone.id);
clone.getElementsByClassName('copyMetadata')[0].setAttribute('data-target', clone.id);
clone.getElementsByClassName('clearMetadata')[0].setAttribute('data-target', clone.id);
/*if (data.plugins[i].messages && data.plugins[i].messages.length != 0) {
for (var j = 0; j < data.plugins[i].messages.length; ++j) {
var messageLi = document.createElement('li');
messageLi.className = data.plugins[i].messages[j].type;
// innerHTML is open to abuse, but for hyperlinking it's too useful.
messageLi.innerHTML = data.plugins[i].messages[j].content;
clone.getElementsByTagName('ul')[0].appendChild(messageLi);
if (messageLi.className == 'warn') {
warnMessageNo++;
} else if (li.className == 'error') {
} else if (messageLi.className == 'error') {
errorMessageNo++;
}
totalMessageNo++;
}
totalMessageNo = data.globalMessages.length;
var pluginsList = document.getElementById('main');
var pluginsNav = document.getElementById('pluginsNav');
for (var i = 0; i < data.plugins.length; ++i) {
var content, clone;
/* First add link to navbar. */
content = document.getElementById('pluginNav').content;
clone = document.importNode(content, true);
pluginsNav.appendChild(clone);
clone = pluginsNav.lastElementChild;
clone.getElementsByClassName('name')[0].textContent = data.plugins[i].name;
clone.getElementsByTagName('a')[0].href = '#' + data.plugins[i].name.replace(/\s+/g, '');
clone.getElementsByClassName('priority')[0].textContent = 'Priority: 500, Global: ✓'; // Or '✗'.
if (data.plugins[i].isDummy) {
clone.getElementsByClassName('dummyPlugin')[0].className += ' fa fa-eye-slash';
}
if (data.plugins[i].loadsBSA) {
clone.getElementsByClassName('loadsBSA')[0].className += ' fa fa-paperclip';
}
if (data.plugins[i].hasUserEdits) {
/* This won't actually be handled anything like this in the real data implementation. */
clone.getElementsByClassName('hasUserEdits')[0].className += ' fa fa-user';
}
/* Now add plugin 'card'. */
content = document.getElementById('pluginSection').content;
clone = document.importNode(content, true);
pluginsList.appendChild(clone);
clone = pluginsList.lastElementChild;
clone.setAttribute('data-active', data.plugins[i].isActive);
clone.id = data.plugins[i].name.replace(/\s+/g, '');
if (data.plugins[i].isActive) {
++activePluginNo;
}
if (data.plugins[i].isDirty) {
++dirtyPluginNo;
}
clone.getElementsByTagName('h1')[0].textContent = data.plugins[i].name;
clone.getElementsByClassName('crc')[0].textContent = 'CRC: ' + data.plugins[i].crc;
if (data.plugins[i].isDummy) {
showElement(clone.getElementsByClassName('dummyPlugin')[0]);
}
if (data.plugins[i].loadsBSA) {
showElement(clone.getElementsByClassName('loadsBSA')[0]);
}
if (data.plugins[i].hasUserEdits) {
/* This won't actually be handled anything like this in the real data implementation. */
showElement(clone.getElementsByClassName('hasUserEdits')[0]);
}
if (data.plugins[i].version) {
clone.getElementsByClassName('version')[0].textContent = 'Version: ' + data.plugins[i].version;
} else {
hideElement(clone.getElementsByClassName('version')[0]);
}
if (data.plugins[i].tagsAdd && data.plugins[i].tagsAdd.length != 0) {
clone.getElementsByClassName('tag add')[0].textContent = data.plugins[i].tagsAdd.join(', ');
} else {
hideElement(clone.getElementsByClassName('tag add')[0]);
}
if (data.plugins[i].tagRemove && data.plugins[i].tagRemove.length != 0) {
clone.getElementsByClassName('tag remove')[0].textContent = data.plugins[i].tagsRemove.join(', ');
} else {
hideElement(clone.getElementsByClassName('tag remove')[0]);
}
clone.getElementsByClassName('editMetadata')[0].setAttribute('data-target', clone.id);
clone.getElementsByClassName('copyMetadata')[0].setAttribute('data-target', clone.id);
clone.getElementsByClassName('clearMetadata')[0].setAttribute('data-target', clone.id);
if (data.plugins[i].messages && data.plugins[i].messages.length != 0) {
for (var j = 0; j < data.plugins[i].messages.length; ++j) {
var messageLi = document.createElement('li');
messageLi.className = data.plugins[i].messages[j].type;
/* innerHTML is open to abuse, but for hyperlinking it's too useful. */
messageLi.innerHTML = data.plugins[i].messages[j].content;
clone.getElementsByTagName('ul')[0].appendChild(messageLi);
if (messageLi.className == 'warn') {
warnMessageNo++;
} else if (messageLi.className == 'error') {
errorMessageNo++;
}
totalMessageNo++;
}
} else {
clone.getElementsByTagName('ul')[0].className += ' hidden';
}
}
document.getElementById('filterTotalMessageNo').textContent = totalMessageNo;
document.getElementById('totalMessageNo').textContent = totalMessageNo;
document.getElementById('totalWarningNo').textContent = warnMessageNo;
document.getElementById('totalErrorNo').textContent = errorMessageNo;
document.getElementById('filterTotalPluginNo').textContent = data.plugins.length;
document.getElementById('totalPluginNo').textContent = data.plugins.length;
document.getElementById('activePluginNo').textContent = activePluginNo;
document.getElementById('dirtyPluginNo').textContent = dirtyPluginNo;
/* Now apply translated UI strings. */
for (var id in data.l10n) {
var elem = document.getElementById(id);
if (elem) {
elem.textContent = data.l10n[id];
}
}
/* Now initialise the rest of the report. */
initJavascriptVars();
setupEventHandlers();
if (isStorageSupported()) {
loadSettings();
}
showElement(document.getElementsByTagName('section')[0]);
});
} else {*/
clone.getElementsByTagName('ul')[0].className += ' hidden';
//}
}
document.getElementById('filterTotalMessageNo').textContent = totalMessageNo;
document.getElementById('totalMessageNo').textContent = totalMessageNo;
document.getElementById('totalWarningNo').textContent = warnMessageNo;
document.getElementById('totalErrorNo').textContent = errorMessageNo;
document.getElementById('filterTotalPluginNo').textContent = loot.game.plugins.length;
document.getElementById('totalPluginNo').textContent = loot.game.plugins.length;
document.getElementById('activePluginNo').textContent = activePluginNo;
document.getElementById('dirtyPluginNo').textContent = dirtyPluginNo;
// Now set up event handlers, as they depend on the plugin cards having been created.
setupEventHandlers();
}
function initGameVars() {
// Create and send a new query.
var request_id = window.cefQuery({
request: 'initGameVars',
persistent: false,
onSuccess: updateInterfaceWithGameInfo,
onFailure: function(error_code, error_message) {
showMessageBox('error', "Error", "Error code: " + error_code + "; " + error_message);
}
});
}
initGlobalVars();
initGameVars();
if (isStorageSupported()) {
loadSettings();
}
processURLParams();
File diff suppressed because it is too large Load Diff
+65
View File
@@ -0,0 +1,65 @@
/* LOOT
A load order optimisation tool for Oblivion, Skyrim, Fallout 3 and
Fallout: New Vegas.
Copyright (C) 2014 WrinklyNinja
This file is part of LOOT.
LOOT is free software: you can redistribute
it and/or modify it under the terms of the GNU General Public License
as published by the Free Software Foundation, either version 3 of
the License, or (at your option) any later version.
LOOT is distributed in the hope that it will
be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with LOOT. If not, see
<http://www.gnu.org/licenses/>.
*/
#ifndef __LOOT_JSON__
#define __LOOT_JSON__
#include <yaml-cpp/yaml.h>
namespace loot {
// Handy class for turning YAML objects into JSON and vice-versa.
class JSON {
public:
inline static YAML::Node parse(std::string& json) {
return YAML::Load(json);
}
inline static std::string stringify(YAML::Node& yaml) {
YAML::Emitter out;
out.SetOutputCharset(YAML::EscapeNonAscii);
out.SetStringFormat(YAML::DoubleQuoted);
out.SetBoolFormat(YAML::TrueFalseBool);
out.SetSeqFormat(YAML::Flow);
out.SetMapFormat(YAML::Flow);
out << yaml;
// yaml-cpp produces `!<!>` artifacts in its output, so remove them.
std::string json = out.c_str();
boost::replace_all(json, "!<!>", "");
// There's also a bit of weirdness where there are some \x escapes and some \u escapes.
// They should all be \u, so transform them.
boost::replace_all(json, "\\x", "\\u00");
return json;
}
};
}
#endif
+8 -4
View File
@@ -132,6 +132,8 @@ namespace loot {
// LootState member functions
//---------------------------
LootState::LootState() : _currentGame(0) {}
void LootState::Init(const std::string& cmdLineGame) {
string initError;
@@ -226,23 +228,25 @@ namespace loot {
BOOST_LOG_TRIVIAL(debug) << "Selecting game.";
size_t gameIndex(0);
try {
gameIndex = SelectGame(_settings, _games, cmdLineGame);
_currentGame = SelectGame(_settings, _games, cmdLineGame);
}
catch (exception &) {
BOOST_LOG_TRIVIAL(error) << "None of the supported games were detected.";
}
BOOST_LOG_TRIVIAL(debug) << "Game selected is " << _games[gameIndex].Name();
BOOST_LOG_TRIVIAL(debug) << "Game selected is " << _games[_currentGame].Name();
//Now that game is selected, initialise it.
BOOST_LOG_TRIVIAL(debug) << "Initialising game-specific settings.";
try {
_games[gameIndex].Init();
_games[_currentGame].Init();
}
catch (std::exception& e) {
BOOST_LOG_TRIVIAL(error) << "Game-specific settings could not be initialised. " << e.what();
}
}
Game& LootState::CurrentGame() {
return _games[_currentGame];
}
}
+3
View File
@@ -72,11 +72,14 @@ namespace loot {
class LootState {
public:
LootState();
void Init(const std::string& cmdLineGame);
Game& CurrentGame();
YAML::Node _settings;
std::vector<loot::Game> _games;
size_t _currentGame;
};
extern LootState g_app_state;
+111 -19
View File
@@ -26,11 +26,17 @@
#include "resource.h"
#include "app.h"
#include "../backend/json.h"
#include "../backend/parsers.h"
#include <include/cef_app.h>
#include <include/cef_runnable.h>
#include <include/cef_task.h>
#include <boost/log/trivial.hpp>
#include <boost/filesystem.hpp>
#include <boost/format.hpp>
#include <boost/locale.hpp>
#include <sstream>
#include <string>
@@ -38,6 +44,11 @@
using namespace std;
using boost::format;
namespace fs = boost::filesystem;
namespace loc = boost::locale;
namespace loot {
namespace {
@@ -75,7 +86,7 @@ namespace loot {
callback->Failure((int)ret, "Shell execute failed.");
return true;
}
else if (request == "initJavascriptVars") {
else if (request == "initGlobalVars") {
// Convert the _settings YAML object to a JSON string, and also add members for the LOOT version, game types and languages.
if (g_app_state._games.empty()) {
@@ -123,28 +134,109 @@ namespace loot {
temp["languages"] = Language::Names();
// Now output as JSON.
// Now output as JSON
//-------------------
YAML::Emitter tempout;
tempout.SetOutputCharset(YAML::EscapeNonAscii);
tempout.SetStringFormat(YAML::DoubleQuoted);
tempout.SetBoolFormat(YAML::TrueFalseBool);
tempout.SetSeqFormat(YAML::Flow);
tempout.SetMapFormat(YAML::Flow);
tempout << temp;
// yaml-cpp produces `!<!>` artifacts in its output, so remove them.
std::string json = tempout.c_str();
boost::replace_all(json, "!<!>", "");
// There's also a bit of weirdness where there are some \x escapes and some \u escapes.
// They should all be \u, so transform them.
boost::replace_all(json, "\\x", "\\u00");
callback->Success(json);
callback->Success(JSON::stringify(temp));
return true;
}
else if (request == "initGameVars") {
// Get masterlist revision info and parse if it exists. Also get plugin headers info and parse userlist if it exists.
if (g_app_state._games.empty()) {
BOOST_LOG_TRIVIAL(warning) << "Application state not yet initialised, initialising now...";
g_app_state.Init("");
BOOST_LOG_TRIVIAL(info) << "Application state initialised.";
}
g_app_state.CurrentGame().LoadPlugins(true);
//Sort plugins into their load order.
list<loot::Plugin> installed;
list<string> loadOrder;
g_app_state.CurrentGame().GetLoadOrder(loadOrder);
for (const auto &pluginName : loadOrder) {
const auto pos = g_app_state.CurrentGame().plugins.find(pluginName);
if (pos != g_app_state.CurrentGame().plugins.end())
installed.push_back(pos->second);
}
//Parse masterlist, don't update it.
if (fs::exists(g_app_state.CurrentGame().MasterlistPath())) {
BOOST_LOG_TRIVIAL(debug) << "Parsing masterlist.";
g_app_state.CurrentGame().masterlist.MetadataList::Load(g_app_state.CurrentGame().MasterlistPath());
}
//Parse userlist.
if (fs::exists(g_app_state.CurrentGame().UserlistPath())) {
BOOST_LOG_TRIVIAL(debug) << "Parsing userlist.";
g_app_state.CurrentGame().userlist.Load(g_app_state.CurrentGame().UserlistPath());
}
// Now convert to a single object that can be turned into a JSON string
//---------------------------------------------------------------------
// The data structure is to be set as 'loot.game'.
YAML::Node gameNode;
// ID the game using its folder value.
gameNode["folder"] = g_app_state.CurrentGame().FolderName();
// Store the masterlist revision and date.
gameNode["masterlist"]["revision"] = g_app_state.CurrentGame().masterlist.GetRevision(g_app_state.CurrentGame().MasterlistPath());
gameNode["masterlist"]["date"] = g_app_state.CurrentGame().masterlist.GetDate(g_app_state.CurrentGame().MasterlistPath());
// Now store plugin data.
for (const auto& plugin : g_app_state.CurrentGame().plugins) {
// Test data has 'hasUserEdits', and 'tagsAdd', 'tagsRemove' keys, but
// the first will be handled by userlist lookups, and the other two are probably
// going to get moved around, haven't decided how best to handle the split between masterlist, userlist and plugin-sourced metadata.
YAML::Node pluginNode;
pluginNode["name"] = plugin.second.Name();
pluginNode["isActive"] = g_app_state.CurrentGame().IsActive(plugin.first);
pluginNode["isDummy"] = (plugin.second.FormIDs().size() == 0);
pluginNode["loadsBSA"] = plugin.second.LoadsBSA(g_app_state.CurrentGame());
pluginNode["crc"] = IntToHexString(plugin.second.Crc());
pluginNode["version"] = plugin.second.Version();
gameNode["plugins"].push_back(pluginNode);
}
//Set language.
unsigned int language;
if (g_app_state._settings["Language"])
language = Language(g_app_state._settings["Language"].as<string>()).Code();
else
language = loot::Language::any;
BOOST_LOG_TRIVIAL(info) << "Using message language: " << Language(language).Name();
//Evaluate any conditions in the global messages.
BOOST_LOG_TRIVIAL(debug) << "Evaluating global message conditions.";
try {
list<loot::Message>::iterator it = g_app_state.CurrentGame().masterlist.messages.begin();
while (it != g_app_state.CurrentGame().masterlist.messages.end()) {
if (!it->EvalCondition(g_app_state.CurrentGame(), language))
it = g_app_state.CurrentGame().masterlist.messages.erase(it);
else
++it;
}
}
catch (std::exception& e) {
BOOST_LOG_TRIVIAL(error) << "A global message contains a condition that could not be evaluated. Details: " << e.what();
g_app_state.CurrentGame().masterlist.messages.push_back(loot::Message(loot::Message::error, (format(loc::translate("A global message contains a condition that could not be evaluated. Details: %1%")) % e.what()).str()));
}
// Now store global messages from masterlist.
gameNode["globalMessages"] = g_app_state.CurrentGame().masterlist.messages;
callback->Success(JSON::stringify(gameNode));
return true;
}
return false;
}