2013-11-01 14:59:25 +01:00
/*
2014-05-04 14:50:01 +02:00
* Copyright (C) 2013 Sebastian Herbord. All rights reserved.
*
* This file is part of the basic diagnosis plugin for Mod Organizer
*
* This plugin 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.
*
* This plugin 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 this plugin. If not, see <http://www.gnu.org/licenses/>.
*/
2013-11-01 14:59:25 +01:00
2013-04-13 19:23:18 +02:00
#include "diagnosebasic.h"
2015-06-20 15:37:32 +01:00
#include "filenamestring.h"
2015-11-22 08:48:48 +00:00
#include "iplugingame.h"
2013-04-13 19:23:18 +02:00
#include <report.h>
#include <utility.h>
2015-01-22 19:44:38 +01:00
#include <imodlist.h>
#include <ipluginlist.h>
2015-11-22 08:48:48 +00:00
2013-04-13 19:23:18 +02:00
#include <QtPlugin>
#include <QFile>
#include <QDir>
2015-06-20 15:37:32 +01:00
#include <QDirIterator>
2014-05-31 13:43:48 +02:00
#include <QDebug>
2013-04-13 19:23:18 +02:00
#include <QCoreApplication>
2013-11-18 20:17:14 +01:00
#include <QMessageBox>
#include <QDateTime>
2015-11-22 08:48:48 +00:00
2013-07-14 14:59:01 +02:00
#include <regex>
2013-11-06 18:35:27 +01:00
#include <functional>
2014-05-04 14:50:01 +02:00
#include <vector>
#include <algorithm>
#pragma warning( push, 2 )
#include <boost/assign.hpp>
#include <boost/graph/graph_traits.hpp>
#include <boost/graph/adjacency_list.hpp>
#include <boost/graph/topological_sort.hpp>
#include <boost/graph/connected_components.hpp>
#pragma warning( pop )
2013-04-13 19:23:18 +02:00
using namespace MOBase ;
DiagnoseBasic :: DiagnoseBasic ()
2014-11-28 11:19:20 +01:00
: m_MOInfo ( nullptr )
2013-04-13 19:23:18 +02:00
{
}
bool DiagnoseBasic :: init ( IOrganizer * moInfo )
{
m_MOInfo = moInfo ;
2013-11-06 18:35:27 +01:00
m_MOInfo -> modList () -> onModStateChanged ([ & ] ( const QString & modName , IModList :: ModStates ) {
2014-05-04 14:50:01 +02:00
if ( modName == "Overwrite" ) invalidate ();
});
2014-06-08 15:10:08 +02:00
m_MOInfo -> modList () -> onModMoved ([ & ] ( const QString & , int , int ) {
// invalidates only the assetOrder check but there is currently no way to recheck individual
// checks
invalidate ();
});
2014-06-16 21:49:57 +02:00
m_MOInfo -> pluginList () -> onPluginMoved ([ & ] ( const QString & , int , int ) {
invalidate ();
});
2015-04-08 18:53:43 +02:00
m_MOInfo -> pluginList () -> onRefreshed ([ & ] () { invalidate (); });
2015-05-11 21:41:00 +02:00
m_MOInfo -> pluginList () -> onPluginStateChanged ([ & ] ( const QString & , IPluginList :: PluginStates ) {
2015-04-08 18:53:43 +02:00
invalidate ();
});
2013-11-06 18:35:27 +01:00
2013-04-13 19:23:18 +02:00
return true ;
}
QString DiagnoseBasic :: name () const
{
2014-09-08 20:37:23 +02:00
return "Basic diagnosis plugin" ;
2013-04-13 19:23:18 +02:00
}
QString DiagnoseBasic :: author () const
{
return "Tannin" ;
}
QString DiagnoseBasic :: description () const
{
2013-05-10 10:57:35 +02:00
return tr ( "Checks for problems unrelated to other plugins" );
2013-04-13 19:23:18 +02:00
}
VersionInfo DiagnoseBasic :: version () const
{
2014-09-10 20:32:25 +02:00
return VersionInfo ( 1 , 1 , 2 , VersionInfo :: RELEASE_FINAL );
2013-04-13 19:23:18 +02:00
}
bool DiagnoseBasic :: isActive () const
{
return true ;
}
QList < PluginSetting > DiagnoseBasic :: settings () const
{
2014-09-08 20:37:23 +02:00
return QList < PluginSetting > ()
<< PluginSetting ( "check_errorlog" , tr ( "Warn when an error occured last time an application was run" ), true )
<< PluginSetting ( "check_overwrite" , tr ( "Warn when there are files in the overwrite directory" ), true )
<< PluginSetting ( "check_font" , tr ( "Warn when the font configuration refers to files that aren't installed" ), true )
<< PluginSetting ( "check_conflict" , tr ( "Warn when mods are installed that conflict with MO functionality" ), true )
2015-06-24 20:46:31 +01:00
<< PluginSetting ( "check_missingmasters" , tr ( "Warn when there are esps with missing masters" ), true )
<< PluginSetting ( "ow_ignore_empty" , tr ( "Ignore empty directories when checking overwrite directory" ), false )
<< PluginSetting ( "ow_ignore_log" , tr ( "Ignore .log files and empty directories when checking overwrite directory" ), false )
;
2013-04-13 19:23:18 +02:00
}
bool DiagnoseBasic :: errorReported () const
{
2017-01-17 16:34:45 -06:00
//QDir dir(qApp->property("dataPath").toString() + "/logs");
QDir dir ( m_MOInfo -> basePath () + "/logs" );
2013-04-13 19:23:18 +02:00
QFileInfoList files = dir . entryInfoList ( QStringList ( "ModOrganizer_??_??_??_??_??.log" ),
QDir :: Files , QDir :: Name | QDir :: Reversed );
if ( files . count () > 0 ) {
QString logFile = files . at ( 0 ). absoluteFilePath ();
QFile file ( logFile );
if ( file . open ( QIODevice :: ReadOnly | QIODevice :: Text )) {
char buffer [ 1024 ];
int line = 0 ;
qint64 lineLengths [ NUM_CONTEXT_ROWS ];
for ( int i = 0 ; i < NUM_CONTEXT_ROWS ; ++ i ) {
lineLengths [ i ] = 0 ;
}
while ( ! file . atEnd ()) {
lineLengths [ line % NUM_CONTEXT_ROWS ] = file . readLine ( buffer , 1024 ) + 1 ;
if ( strncmp ( buffer , "ERROR" , 5 ) == 0 ) {
qint64 sumChars = 0 ;
for ( int i = 0 ; i < NUM_CONTEXT_ROWS ; ++ i ) {
sumChars += lineLengths [ i ];
}
file . seek ( file . pos () - sumChars );
m_ErrorMessage = "" ;
for ( int i = 0 ; i < 2 * NUM_CONTEXT_ROWS ; ++ i ) {
file . readLine ( buffer , 1024 );
QString lineString = QString :: fromUtf8 ( buffer );
if ( lineString . startsWith ( "ERROR" )) {
m_ErrorMessage += "<b>" + lineString + "</b>" ;
} else {
m_ErrorMessage += lineString ;
}
}
return true ;
}
// prevent this function from taking forever
if ( line ++ >= 50000 ) {
break ;
}
}
}
}
return false ;
}
2015-06-24 20:46:31 +01:00
bool DiagnoseBasic :: checkEmpty ( QString const & path ) const
2015-06-20 15:37:32 +01:00
{
QDir dir ( path );
dir . setFilter ( QDir :: Files | QDir :: Hidden | QDir :: System );
2015-07-07 20:53:38 +02:00
//Search files first
2015-06-20 15:37:32 +01:00
for ( QString const & f : dir . entryList ()) {
FileNameString file ( f );
2015-06-24 20:46:31 +01:00
if ( ! m_MOInfo -> pluginSetting ( name (), "ow_ignore_log" ). toBool () ||
! file . endsWith ( ".log" )) {
2015-06-20 15:37:32 +01:00
return false ;
}
}
//Then directories
dir . setFilter ( QDir :: AllDirs | QDir :: NoDotAndDotDot | QDir :: NoSymLinks );
2015-07-07 20:53:38 +02:00
for ( QFileInfo const & subdir : dir . entryInfoList ()) {
if ( ! checkEmpty ( subdir . absoluteFilePath ())) {
2015-06-20 15:37:32 +01:00
return false ;
}
}
return true ;
}
2013-05-22 20:43:08 +02:00
bool DiagnoseBasic :: overwriteFiles () const
{
2017-01-17 16:34:45 -06:00
//QString dirname(qApp->property("dataPath").toString() + "/overwrite");
QString dirname ( m_MOInfo -> overwritePath ());
2015-06-24 20:46:31 +01:00
if ( m_MOInfo -> pluginSetting ( name (), "ow_ignore_empty" ). toBool () ||
m_MOInfo -> pluginSetting ( name (), "ow_ignore_log" ). toBool ()) {
2015-06-20 15:37:32 +01:00
return ! checkEmpty ( dirname );
}
QDir dir ( dirname );
2013-05-22 20:43:08 +02:00
return dir . count () != 2 ; // account for . and ..
}
2013-09-30 18:33:45 +02:00
bool DiagnoseBasic :: nitpickInstalled () const
{
QString path = m_MOInfo -> resolvePath ( "skse/plugins/nitpick.dll" );
2014-05-04 14:50:01 +02:00
2013-09-30 18:33:45 +02:00
return ! path . isEmpty ();
}
2013-11-01 14:59:25 +01:00
2013-11-06 18:35:27 +01:00
/// unused code to remove duplicates from a vector
2013-11-01 14:59:25 +01:00
template < typename T >
void makeUnique ( std :: vector < T > & vector )
{
std :: set < T > done ;
auto read = vector . begin ();
auto write = vector . begin ();
2014-05-04 14:50:01 +02:00
2013-11-01 14:59:25 +01:00
for (; read != vector . end (); ++ read ) {
if ( done . insert ( * read ). second ) {
* write = * read ;
++ write ;
}
}
vector . erase ( write , vector . end ());
}
2014-09-10 20:32:25 +02:00
bool DiagnoseBasic :: missingMasters () const
{
std :: set < QString > enabledPlugins ;
QStringList esps = m_MOInfo -> findFiles ( "" ,
[] ( const QString & fileName ) -> bool { return fileName . endsWith ( ".esp" , Qt :: CaseInsensitive )
|| fileName . endsWith ( ".esm" , Qt :: CaseInsensitive ); });
// gather enabled masters first
2015-12-01 21:05:39 +01:00
for ( const QString & esp : esps ) {
2014-09-10 20:32:25 +02:00
QString baseName = QFileInfo ( esp ). fileName ();
if ( m_MOInfo -> pluginList () -> state ( baseName ) == IPluginList :: STATE_ACTIVE ) {
2014-09-10 20:33:23 +02:00
enabledPlugins . insert ( baseName . toLower ());
2014-09-10 20:32:25 +02:00
}
}
m_MissingMasters . clear ();
// for each required master in each esp, test if it's in the list of enabled masters.
2015-12-01 21:05:39 +01:00
for ( const QString & esp : esps ) {
2014-09-10 20:32:25 +02:00
QString baseName = QFileInfo ( esp ). fileName ();
if ( m_MOInfo -> pluginList () -> state ( baseName ) == IPluginList :: STATE_ACTIVE ) {
2015-12-01 21:05:39 +01:00
for ( const QString master : m_MOInfo -> pluginList () -> masters ( baseName )) {
2014-09-10 20:33:23 +02:00
if ( enabledPlugins . find ( master . toLower ()) == enabledPlugins . end ()) {
2014-09-10 20:32:25 +02:00
m_MissingMasters . insert ( master );
}
}
}
}
2014-09-24 19:51:51 +02:00
return ! m_MissingMasters . empty ();
2014-09-10 20:32:25 +02:00
}
2013-07-14 14:59:01 +02:00
bool DiagnoseBasic :: invalidFontConfig () const
{
2016-12-22 06:40:33 +02:00
if (( m_MOInfo -> managedGame () -> gameName () != "Skyrim" ) && ( m_MOInfo -> managedGame () -> gameName () != "SkyrimSE" )) {
2013-07-14 14:59:01 +02:00
// this check is only for skyrim
return false ;
}
// files from skyrim_interface.bsa
static std :: vector < QString > defaultFonts = boost :: assign :: list_of ( "interface \\ fonts_console.swf" )
2014-05-04 14:50:01 +02:00
( "interface \\ fonts_en.swf" );
2013-07-14 14:59:01 +02:00
QString configPath = m_MOInfo -> resolvePath ( "interface/fontconfig.txt" );
if ( configPath . isEmpty ()) {
return false ;
}
QFile config ( configPath );
if ( ! config . open ( QIODevice :: ReadOnly | QIODevice :: Text )) {
qDebug ( "failed to open %s" , qPrintable ( configPath ));
return false ;
}
std :: tr1 :: regex exp ( "^fontlib \" ([^ \" ]*) \" $" );
while ( ! config . atEnd ()) {
QByteArray row = config . readLine ();
std :: tr1 :: cmatch match ;
if ( std :: tr1 :: regex_search ( row . constData (), match , exp )) {
std :: string temp = match [ 1 ];
QString path ( temp . c_str ());
bool isDefault = false ;
2015-12-01 21:05:39 +01:00
for ( const QString & def : defaultFonts ) {
2013-07-14 14:59:01 +02:00
if ( QString :: compare ( def , path , Qt :: CaseInsensitive ) == 0 ) {
isDefault = true ;
break ;
}
}
if ( ! isDefault && m_MOInfo -> resolvePath ( path ). isEmpty ()) {
return true ;
}
}
}
return false ;
}
2013-04-13 19:23:18 +02:00
std :: vector < unsigned int > DiagnoseBasic :: activeProblems () const
{
std :: vector < unsigned int > result ;
2014-09-08 20:37:23 +02:00
if ( m_MOInfo -> pluginSetting ( name (), "check_errorlog" ). toBool () && errorReported ()) {
2013-04-13 19:23:18 +02:00
result . push_back ( PROBLEM_ERRORLOG );
}
2014-09-08 20:37:23 +02:00
if ( m_MOInfo -> pluginSetting ( name (), "check_overwrite" ). toBool () && overwriteFiles ()) {
2013-05-22 20:43:08 +02:00
result . push_back ( PROBLEM_OVERWRITE );
}
2014-09-08 20:37:23 +02:00
if ( m_MOInfo -> pluginSetting ( name (), "check_font" ). toBool () && invalidFontConfig ()) {
2013-07-14 14:59:01 +02:00
result . push_back ( PROBLEM_INVALIDFONT );
}
2014-09-08 20:37:23 +02:00
if ( m_MOInfo -> pluginSetting ( name (), "check_conflict" ). toBool () && nitpickInstalled ()) {
2013-09-30 18:33:45 +02:00
result . push_back ( PROBLEM_NITPICKINSTALLED );
}
2014-09-10 20:32:25 +02:00
if ( m_MOInfo -> pluginSetting ( name (), "check_missingmasters" ). toBool () && missingMasters ()) {
result . push_back ( PROBLEM_MISSINGMASTERS );
}
2014-02-17 21:15:20 +01:00
if ( QFile :: exists ( m_MOInfo -> profilePath () + "/profile_tweaks.ini" )) {
result . push_back ( PROBLEM_PROFILETWEAKS );
}
2013-04-13 19:23:18 +02:00
return result ;
}
2014-09-10 20:32:25 +02:00
QString DiagnoseBasic :: shortDescription ( unsigned int key ) const
{
switch ( key ) {
case PROBLEM_ERRORLOG :
return tr ( "There was an error reported recently" );
case PROBLEM_OVERWRITE :
return tr ( "There are files in your overwrite mod" );
case PROBLEM_INVALIDFONT :
return tr ( "Your font configuration may be broken" );
case PROBLEM_NITPICKINSTALLED :
return tr ( "Nitpick installed" );
case PROBLEM_PROFILETWEAKS :
return tr ( "Ini Tweaks overwritten" );
case PROBLEM_MISSINGMASTERS :
return tr ( "Missing Masters" );
default :
throw MyException ( tr ( "invalid problem key %1" ). arg ( key ));
}
}
2013-04-13 19:23:18 +02:00
QString DiagnoseBasic :: fullDescription ( unsigned int key ) const
{
switch ( key ) {
case PROBLEM_ERRORLOG :
return "<code>" + m_ErrorMessage . replace ( " \n " , "<br>" ) + "</code>" ;
2013-05-22 20:43:08 +02:00
case PROBLEM_OVERWRITE :
2013-09-21 19:25:33 +02:00
return tr ( "Files in the <font color= \" red \" ><i>Overwrite</i></font> mod are are usually files created by an external tool (i.e. Wrye Bash, Automatic Variants, ...).<br>"
2014-06-02 19:15:23 +02:00
"It is advisable you empty the Overwrite directory by moving those files to an existing mod. You can do this by double-clicking the <font color= \" red \" ><i>Overwrite</i></font> mod and use drag&drop to move the files to a mod.<br>"
2013-09-21 19:25:33 +02:00
"Alternatively, right-click on <font color= \" red \" ><i>Overwrite</i></font> and create a new regular mod from the files there.<br>"
"<br>"
"Why is this necessary? Generated files may depend on the other mods active in a profile and may thus be incompatible with a different profile (i.e. bashed patches from Wrye Bash). "
"On the other hand the file may be necessary in all profiles (i.e. dlc esms after cleaning with TESVEdit)<br>"
"This can NOT be automated you HAVE to read up on the tools you use and make an educated decision." );
2013-07-14 14:59:01 +02:00
case PROBLEM_INVALIDFONT :
return tr ( "Your current configuration seems to reference a font that is not installed. You may see only boxes instead of letters.<br>"
"The font configuration is in Data \\ interface \\ fontconfig.txt. Most likely you have a broken installation of a font replacer mod." );
2013-09-30 18:33:45 +02:00
case PROBLEM_NITPICKINSTALLED :
return tr ( "You have the nitpick skse plugin installed. This plugin is not needed with Mod Organizer because MO already offers the same functionality. "
"Worse: The two solutions may conflict so it's strongly suggested you remove this plugin." );
2014-02-17 21:15:20 +01:00
case PROBLEM_PROFILETWEAKS : {
QString fileContent = readFileText ( m_MOInfo -> profilePath () + "/profile_tweaks.ini" );
return tr ( "Settings provided in ini tweaks have been overwritten in-game or in an applications.<br>"
"These overwrites are stored in a separate file (<i>profile_tweaks.ini</i> within the profile directory) < br > "
"to keep ini-tweaks in their original state but you should really get rid of this file as there is<br>"
"no tool support in MO to work on it. <br>"
"Advice: Copy settings you want to keep to an appropriate ini tweak, then delete <i>profile_tweaks.ini</i>.<br>"
"Hitting the <i>Fix</i> button will delete that file" )
2014-05-04 14:50:01 +02:00
+ "<hr><i>profile_tweaks.ini:</i><pre>" + fileContent + "</pre>" ;
2014-02-17 21:15:20 +01:00
} break ;
2014-09-10 20:32:25 +02:00
case PROBLEM_MISSINGMASTERS : {
return tr ( "The masters for some plugins (esp/esm) are not enabled . < br > "
"The game will crash unless you install and enable the following plugins: " )
+ "<ul><li>" + SetJoin ( m_MissingMasters , "</li><li>" ) + "</li></ul>" ;
} break ;
2013-04-13 19:23:18 +02:00
default :
throw MyException ( tr ( "invalid problem key %1" ). arg ( key ));
}
}
2013-11-01 14:59:25 +01:00
bool DiagnoseBasic :: hasGuidedFix ( unsigned int key ) const
2013-04-13 19:23:18 +02:00
{
2015-12-01 21:05:39 +01:00
return ( key == PROBLEM_PROFILETWEAKS );
2013-04-13 19:23:18 +02:00
}
void DiagnoseBasic :: startGuidedFix ( unsigned int key ) const
{
2013-11-01 14:59:25 +01:00
switch ( key ) {
2014-02-17 21:15:20 +01:00
case PROBLEM_PROFILETWEAKS : {
shellDeleteQuiet ( m_MOInfo -> profilePath () + "/profile_tweaks.ini" );
} break ;
2014-05-25 15:39:45 +02:00
default :
throw MyException ( tr ( "invalid problem key %1" ). arg ( key ));
2013-11-01 14:59:25 +01:00
}
2013-04-13 19:23:18 +02:00
}