Explorar o código

VCMI Launcher/Mod manager. See forum post for details.

Ivan Savenko %!s(int64=12) %!d(string=hai) anos
pai
achega
7cbfdd509c

+ 6 - 2
config/schemas/settings.json

@@ -229,14 +229,18 @@
 			"type" : "object",
 			"default": {},
 			"additionalProperties" : false,
-			"required" : [ "repositoryURL" ],
+			"required" : [ "repositoryURL", "enableInstalledMods" ],
 			"properties" : {
 				"repositoryURL" : {
 					"type" : "array",
 					"default" : [ ],
 					"items" : {
 						"type" : "string"
-					}
+					},
+				},
+				"enableInstalledMods" : {
+					"type" : "boolean",
+					"default" : true
 				}
 			}
 		}

+ 58 - 0
launcher/CMakeLists.txt

@@ -0,0 +1,58 @@
+project(vcmilauncher)
+cmake_minimum_required(VERSION 2.8.7)
+
+include_directories(${CMAKE_HOME_DIRECTORY} ${CMAKE_CURRENT_SOURCE_DIR})
+include_directories(${Qt5Widgets_INCLUDE_DIRS} ${Qt5Network_INCLUDE_DIRS})
+
+set(launcher_modmanager_SRCS
+	modManager/cdownloadmanager.cpp
+	modManager/cmodlist.cpp
+	modManager/cmodlistmodel.cpp
+	modManager/cmodlistview.cpp
+	modManager/cmodmanager.cpp
+)
+
+set(launcher_settingsview_SRCS
+	settingsView/csettingsview.cpp
+)
+
+set(launcher_SRCS
+	${launcher_modmanager_SRCS}
+	${launcher_settingsview_SRCS}
+	main.cpp
+	mainwindow.cpp
+	launcherdirs.cpp
+)
+
+set(launcher_FORMS
+	modManager/cmodlistview.ui
+	settingsView/csettingsview.ui
+	mainwindow.ui
+)
+
+# Tell CMake to run moc when necessary:
+set(CMAKE_AUTOMOC ON)
+
+# As moc files are generated in the binary dir, tell CMake
+# to always look for includes there:
+set(CMAKE_INCLUDE_CURRENT_DIR ON)
+
+# We need add -DQT_WIDGETS_LIB when using QtWidgets in Qt 5.
+add_definitions(${Qt5Widgets_DEFINITIONS})
+add_definitions(${Qt5Network_DEFINITIONS})
+
+# Executables fail to build with Qt 5 in the default configuration
+# without -fPIE. We add that here.
+set(CMAKE_CXX_FLAGS "${Qt5Widgets_EXECUTABLE_COMPILE_FLAGS} ${CMAKE_CXX_FLAGS}")
+
+qt5_wrap_ui(launcher_UI_HEADERS ${launcher_FORMS})
+
+add_executable(vcmilauncher ${launcher_SRCS} ${launcher_UI_HEADERS})
+
+# The Qt5Widgets_LIBRARIES variable also includes QtGui and QtCore
+target_link_libraries(vcmilauncher vcmi ${Qt5Widgets_LIBRARIES} ${Qt5Network_LIBRARIES})
+
+if (NOT APPLE) # Already inside bundle
+    install(TARGETS vcmilauncher DESTINATION ${BIN_DIR})
+endif()
+

+ 1 - 0
launcher/StdInc.cpp

@@ -0,0 +1 @@
+#include "StdInc.h"

+ 11 - 0
launcher/StdInc.h

@@ -0,0 +1,11 @@
+#pragma once
+
+#include "../Global.h"
+
+#include <QtWidgets>
+#include <QStringList>
+#include <QSet>
+#include <QVector>
+#include <QList>
+#include <QString>
+#include <QFile>

BIN=BIN
launcher/icons/menu-game.png


BIN=BIN
launcher/icons/menu-mods.png


BIN=BIN
launcher/icons/menu-settings.png


BIN=BIN
launcher/icons/mod-delete.png


BIN=BIN
launcher/icons/mod-disabled.png


BIN=BIN
launcher/icons/mod-download.png


BIN=BIN
launcher/icons/mod-enabled.png


BIN=BIN
launcher/icons/mod-update.png


+ 26 - 0
launcher/launcherdirs.cpp

@@ -0,0 +1,26 @@
+#include "StdInc.h"
+#include "launcherdirs.h"
+
+#include "../lib/VCMIDirs.h"
+
+static CLauncherDirs launcherDirsGlobal;
+
+CLauncherDirs::CLauncherDirs()
+{
+	QDir().mkdir(downloadsPath());
+}
+
+CLauncherDirs & CLauncherDirs::get()
+{
+	return launcherDirsGlobal;
+}
+
+QString CLauncherDirs::downloadsPath()
+{
+	return QString::fromUtf8(VCMIDirs::get().userCachePath().c_str()) + "/downloads";
+}
+
+QString CLauncherDirs::modsPath()
+{
+	return QString::fromUtf8(VCMIDirs::get().userCachePath().c_str()) + "/Mods";
+}

+ 13 - 0
launcher/launcherdirs.h

@@ -0,0 +1,13 @@
+#pragma once
+
+/// similar to lib/VCMIDirs, controls where all launcher-related data will be stored
+class CLauncherDirs
+{
+public:
+	CLauncherDirs();
+
+	static CLauncherDirs & get();
+
+	QString downloadsPath();
+	QString modsPath();
+};

+ 11 - 0
launcher/main.cpp

@@ -0,0 +1,11 @@
+#include "mainwindow.h"
+#include <QApplication>
+
+int main(int argc, char *argv[])
+{
+	QApplication a(argc, argv);
+	MainWindow w;
+	w.show();
+	
+	return a.exec();
+}

+ 77 - 0
launcher/mainwindow.cpp

@@ -0,0 +1,77 @@
+#include "StdInc.h"
+#include "mainwindow.h"
+#include "ui_mainwindow.h"
+
+#include <QProcess>
+#include <QDir>
+
+#include "../lib/CConfigHandler.h"
+#include "../lib/VCMIDirs.h"
+#include "../lib/filesystem/Filesystem.h"
+#include "../lib/logging/CBasicLogConfigurator.h"
+
+void MainWindow::load()
+{
+	console = new CConsoleHandler;
+	CBasicLogConfigurator logConfig(VCMIDirs::get().userCachePath() + "/VCMI_Launcher_log.txt", console);
+	logConfig.configureDefault();
+
+	CResourceHandler::initialize();
+	CResourceHandler::loadMainFileSystem("config/filesystem.json");
+
+	for (auto & string : VCMIDirs::get().dataPaths())
+		QDir::addSearchPath("icons", QString::fromUtf8(string.c_str()) + "/launcher/icons");
+	QDir::addSearchPath("icons", QString::fromUtf8(VCMIDirs::get().userDataPath().c_str()) + "/launcher/icons");
+
+	settings.init();
+}
+
+MainWindow::MainWindow(QWidget *parent) :
+    QMainWindow(parent),
+    ui(new Ui::MainWindow)
+{
+	load(); // load FS before UI
+
+	ui->setupUi(this);
+	ui->tabListWidget->setCurrentIndex(0);
+
+	connect(ui->tabSelectList, SIGNAL(currentRowChanged(int)),
+	        ui->tabListWidget, SLOT(setCurrentIndex(int)));
+}
+
+MainWindow::~MainWindow()
+{
+	delete ui;
+}
+
+void MainWindow::on_startGameButon_clicked()
+{
+#if defined(Q_OS_WIN)
+	QString clientName = "VCMI_Client.exe";
+#else
+	// TODO: Right now launcher will only start vcmi from system-default locations
+	QString clientName = "vcmiclient";
+#endif
+	startExecutable(clientName);
+}
+
+void MainWindow::startExecutable(QString name)
+{
+	QProcess process;
+
+	// Start the executable
+	if (process.startDetached(name))
+	{
+		close(); // exit launcher
+	}
+	else
+	{
+		QMessageBox::critical(this,
+		                      "Error starting executable",
+		                      "Failed to start " + name + ": " + process.errorString(),
+		                      QMessageBox::Ok,
+		                      QMessageBox::Ok);
+		return;
+	}
+
+}

+ 25 - 0
launcher/mainwindow.h

@@ -0,0 +1,25 @@
+#pragma once
+#include <QMainWindow>
+
+namespace Ui {
+	class MainWindow;
+}
+
+class QTableWidgetItem;
+
+class MainWindow : public QMainWindow
+{
+	Q_OBJECT
+
+	void load();
+	void startExecutable(QString name);
+public:
+	explicit MainWindow(QWidget *parent = 0);
+	~MainWindow();
+
+private slots:
+	void on_startGameButon_clicked();
+
+private:
+	Ui::MainWindow *ui;
+};

+ 206 - 0
launcher/mainwindow.ui

@@ -0,0 +1,206 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>MainWindow</class>
+ <widget class="QMainWindow" name="MainWindow">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>800</width>
+    <height>480</height>
+   </rect>
+  </property>
+  <property name="sizePolicy">
+   <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+    <horstretch>0</horstretch>
+    <verstretch>0</verstretch>
+   </sizepolicy>
+  </property>
+  <property name="windowTitle">
+   <string>VCMI Launcher</string>
+  </property>
+  <property name="windowIcon">
+   <iconset>
+    <normaloff>icons:menu-game.png</normaloff>icons:menu-game.png</iconset>
+  </property>
+  <property name="iconSize">
+   <size>
+    <width>64</width>
+    <height>64</height>
+   </size>
+  </property>
+  <widget class="QWidget" name="centralWidget">
+   <layout class="QGridLayout" name="gridLayout">
+    <item row="0" column="0">
+     <widget class="QListWidget" name="tabSelectList">
+      <property name="sizePolicy">
+       <sizepolicy hsizetype="Minimum" vsizetype="Expanding">
+        <horstretch>0</horstretch>
+        <verstretch>0</verstretch>
+       </sizepolicy>
+      </property>
+      <property name="maximumSize">
+       <size>
+        <width>65</width>
+        <height>16777215</height>
+       </size>
+      </property>
+      <property name="verticalScrollBarPolicy">
+       <enum>Qt::ScrollBarAlwaysOff</enum>
+      </property>
+      <property name="horizontalScrollBarPolicy">
+       <enum>Qt::ScrollBarAlwaysOff</enum>
+      </property>
+      <property name="editTriggers">
+       <set>QAbstractItemView::NoEditTriggers</set>
+      </property>
+      <property name="showDropIndicator" stdset="0">
+       <bool>false</bool>
+      </property>
+      <property name="dragDropMode">
+       <enum>QAbstractItemView::NoDragDrop</enum>
+      </property>
+      <property name="selectionBehavior">
+       <enum>QAbstractItemView::SelectRows</enum>
+      </property>
+      <property name="iconSize">
+       <size>
+        <width>48</width>
+        <height>64</height>
+       </size>
+      </property>
+      <property name="movement">
+       <enum>QListView::Static</enum>
+      </property>
+      <property name="resizeMode">
+       <enum>QListView::Fixed</enum>
+      </property>
+      <property name="spacing">
+       <number>0</number>
+      </property>
+      <property name="gridSize">
+       <size>
+        <width>64</width>
+        <height>64</height>
+       </size>
+      </property>
+      <property name="viewMode">
+       <enum>QListView::IconMode</enum>
+      </property>
+      <property name="uniformItemSizes">
+       <bool>true</bool>
+      </property>
+      <property name="wordWrap">
+       <bool>true</bool>
+      </property>
+      <property name="selectionRectVisible">
+       <bool>false</bool>
+      </property>
+      <property name="currentRow">
+       <number>-1</number>
+      </property>
+      <item>
+       <property name="text">
+        <string>Mods</string>
+       </property>
+       <property name="icon">
+        <iconset>
+         <normaloff>icons:menu-mods.png</normaloff>icons:menu-mods.png</iconset>
+       </property>
+      </item>
+      <item>
+       <property name="text">
+        <string>Settings</string>
+       </property>
+       <property name="icon">
+        <iconset>
+         <normaloff>icons:menu-settings.png</normaloff>icons:menu-settings.png</iconset>
+       </property>
+      </item>
+     </widget>
+    </item>
+    <item row="1" column="0">
+     <widget class="QToolButton" name="startGameButon">
+      <property name="text">
+       <string>Play</string>
+      </property>
+      <property name="icon">
+       <iconset>
+        <normaloff>icons:menu-game.png</normaloff>icons:menu-game.png</iconset>
+      </property>
+      <property name="iconSize">
+       <size>
+        <width>60</width>
+        <height>60</height>
+       </size>
+      </property>
+      <property name="checkable">
+       <bool>false</bool>
+      </property>
+      <property name="checked">
+       <bool>false</bool>
+      </property>
+      <property name="toolButtonStyle">
+       <enum>Qt::ToolButtonIconOnly</enum>
+      </property>
+     </widget>
+    </item>
+    <item row="2" column="0">
+     <widget class="QLabel" name="startGameTitle">
+      <property name="font">
+       <font>
+        <weight>75</weight>
+        <bold>true</bold>
+       </font>
+      </property>
+      <property name="text">
+       <string>Start game</string>
+      </property>
+      <property name="alignment">
+       <set>Qt::AlignCenter</set>
+      </property>
+     </widget>
+    </item>
+    <item row="0" column="1" rowspan="3">
+     <widget class="QStackedWidget" name="tabListWidget">
+      <property name="enabled">
+       <bool>true</bool>
+      </property>
+      <property name="sizePolicy">
+       <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+        <horstretch>0</horstretch>
+        <verstretch>0</verstretch>
+       </sizepolicy>
+      </property>
+      <property name="currentIndex">
+       <number>1</number>
+      </property>
+      <widget class="CModListView" name="stackedWidgetPage2"/>
+      <widget class="CSettingsView" name="stackedWidgetPage3"/>
+     </widget>
+    </item>
+   </layout>
+  </widget>
+ </widget>
+ <layoutdefault spacing="6" margin="11"/>
+ <customwidgets>
+  <customwidget>
+   <class>CModListView</class>
+   <extends>QWidget</extends>
+   <header>modManager/cmodlistview.h</header>
+   <container>1</container>
+  </customwidget>
+  <customwidget>
+   <class>CSettingsView</class>
+   <extends>QWidget</extends>
+   <header>settingsView/csettingsview.h</header>
+   <container>1</container>
+  </customwidget>
+ </customwidgets>
+ <tabstops>
+  <tabstop>tabSelectList</tabstop>
+  <tabstop>startGameButon</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>

+ 114 - 0
launcher/modManager/cdownloadmanager.cpp

@@ -0,0 +1,114 @@
+#include "StdInc.h"
+#include "cdownloadmanager.h"
+
+#include "launcherdirs.h"
+
+CDownloadManager::CDownloadManager()
+{
+	connect(&manager, SIGNAL(finished(QNetworkReply*)),
+			SLOT(downloadFinished(QNetworkReply*)));
+}
+
+void CDownloadManager::downloadFile(const QUrl &url, const QString &file)
+{
+	QNetworkRequest request(url);
+	FileEntry entry;
+	entry.file.reset(new QFile(CLauncherDirs::get().downloadsPath() + '/' + file));
+	entry.bytesReceived = 0;
+	entry.totalSize = 0;
+
+	if (entry.file->open(QIODevice::WriteOnly | QIODevice::Truncate))
+	{
+		entry.status = FileEntry::IN_PROGRESS;
+		entry.reply = manager.get(request);
+
+		connect(entry.reply, SIGNAL(downloadProgress(qint64, qint64)),
+				SLOT(downloadProgressChanged(qint64, qint64)));
+	}
+	else
+	{
+		entry.status = FileEntry::FAILED;
+		entry.reply  = nullptr;
+		encounteredErrors += entry.file->errorString();
+	}
+
+	// even if failed - add it into list to report it in finished() call
+	currentDownloads.push_back(entry);
+}
+
+CDownloadManager::FileEntry & CDownloadManager::getEntry(QNetworkReply * reply)
+{
+	assert(reply);
+	for (auto & entry : currentDownloads)
+	{
+		if (entry.reply == reply)
+			return entry;
+	}
+	assert(0);
+	static FileEntry errorValue;
+	return errorValue;
+}
+
+void CDownloadManager::downloadFinished(QNetworkReply *reply)
+{
+	FileEntry & file = getEntry(reply);
+
+	if (file.reply->error())
+	{
+		encounteredErrors += file.reply->errorString();
+		file.file->remove();
+		file.status = FileEntry::FAILED;
+	}
+	else
+	{
+		file.file->write(file.reply->readAll());
+		file.file->close();
+		file.status = FileEntry::FINISHED;
+	}
+
+	file.reply->deleteLater();
+
+	bool downloadComplete = true;
+	for (auto & entry : currentDownloads)
+	{
+		if (entry.status == FileEntry::IN_PROGRESS)
+		{
+			downloadComplete = false;
+			break;
+		}
+	}
+
+	QStringList successful;
+	QStringList failed;
+
+	for (auto & entry : currentDownloads)
+	{
+		if (entry.status == FileEntry::FINISHED)
+			successful += entry.file->fileName();
+		else
+			failed += entry.file->fileName();
+	}
+
+	if (downloadComplete)
+		emit finished(successful, failed, encounteredErrors);
+}
+
+void CDownloadManager::downloadProgressChanged(qint64 bytesReceived, qint64 bytesTotal)
+{
+	auto reply = dynamic_cast<QNetworkReply *>(sender());
+	FileEntry & entry = getEntry(reply);
+
+	entry.file->write(entry.reply->readAll());
+	entry.bytesReceived = bytesReceived;
+	entry.totalSize = bytesTotal;
+
+	quint64 total = 0;
+	for (auto & entry : currentDownloads)
+		total += entry.totalSize > 0 ? entry.totalSize : 0;
+
+	quint64 received = 0;
+	for (auto & entry : currentDownloads)
+		received += entry.bytesReceived > 0 ? entry.bytesReceived : 0;
+
+	emit downloadProgress(received, total);
+}

+ 56 - 0
launcher/modManager/cdownloadmanager.h

@@ -0,0 +1,56 @@
+#pragma once
+
+#include <QSharedPointer>
+#include <QtNetwork/QNetworkReply>
+
+class QFile;
+
+class CDownloadManager: public QObject
+{
+	Q_OBJECT
+
+	struct FileEntry
+	{
+		enum Status
+		{
+			IN_PROGRESS,
+			FINISHED,
+			FAILED
+		};
+
+		QNetworkReply * reply;
+		QSharedPointer<QFile> file;
+		Status status;
+		qint64 bytesReceived;
+		qint64 totalSize;
+	};
+
+	QStringList encounteredErrors;
+
+	QNetworkAccessManager manager;
+
+	QList<FileEntry> currentDownloads;
+
+	FileEntry & getEntry(QNetworkReply * reply);
+public:
+	CDownloadManager();
+
+	// returns true if download with such URL is in progress/queued
+	// FIXME: not sure what's right place for "mod download in progress" check
+	bool downloadInProgress(const QUrl &url);
+
+	// returns network reply so caller can connect to required signals
+	void downloadFile(const QUrl &url, const QString &file);
+
+public slots:
+	void downloadFinished(QNetworkReply *reply);
+	void downloadProgressChanged(qint64 bytesReceived, qint64 bytesTotal);
+
+signals:
+	// for status bar updates. Merges all queued downloads into one
+	void downloadProgress(qint64 currentAmount, qint64 maxAmount);
+
+	// called when all files were downloaded and manager goes to idle state
+	// Lists contains files that were successfully downloaded / failed to download
+	void finished(QStringList savedFiles, QStringList failedFiles, QStringList errors);
+};

+ 204 - 0
launcher/modManager/cmodlist.cpp

@@ -0,0 +1,204 @@
+#include "StdInc.h"
+#include "cmodlist.h"
+
+bool CModEntry::compareVersions(QString lesser, QString greater)
+{
+	static const int maxSections = 3; // versions consist from up to 3 sections, major.minor.patch
+
+	QStringList lesserList = lesser.split(".");
+	QStringList greaterList = greater.split(".");
+
+	assert(lesserList.size() <= maxSections);
+	assert(greaterList.size() <= maxSections);
+
+	for (int i=0; i< maxSections; i++)
+	{
+		if (greaterList.size() <= i) // 1.1.1 > 1.1
+			return false;
+
+		if (lesserList.size() <= i) // 1.1 < 1.1.1
+			return true;
+
+		if (lesserList[i].toInt() != greaterList[i].toInt())
+			return lesserList[i].toInt() < greaterList[i].toInt(); // 1.1 < 1.2
+	}
+	return false;
+}
+
+CModEntry::CModEntry(QJsonObject repository, QJsonObject localData, QJsonValue modSettings, QString modname):
+    repository(repository),
+    localData(localData),
+    modSettings(modSettings),
+    modname(modname)
+{
+}
+
+bool CModEntry::isEnabled() const
+{
+	if (!isInstalled())
+		return false;
+
+	return modSettings.toBool(false);
+}
+
+bool CModEntry::isDisabled() const
+{
+	if (!isInstalled())
+		return false;
+	return !isEnabled();
+}
+
+bool CModEntry::isAvailable() const
+{
+	if (isInstalled())
+		return false;
+	return !repository.isEmpty();
+}
+
+bool CModEntry::isUpdateable() const
+{
+	if (!isInstalled())
+		return false;
+
+	QString installedVer = localData["installedVersion"].toString();
+	QString availableVer = repository["latestVersion"].toString();
+
+	if (compareVersions(installedVer, availableVer))
+		return true;
+	return false;
+}
+
+bool CModEntry::isInstalled() const
+{
+	return !localData.isEmpty();
+}
+
+int CModEntry::getModStatus() const
+{
+	return
+	(isEnabled()   ? ModStatus::ENABLED    : 0) |
+	(isInstalled() ? ModStatus::INSTALLED  : 0) |
+	(isUpdateable()? ModStatus::UPDATEABLE : 0);
+}
+
+QString CModEntry::getName() const
+{
+	return modname;
+}
+
+QVariant CModEntry::getValue(QString value) const
+{
+	if (repository.contains(value))
+		return repository[value].toVariant();
+
+	if (localData.contains(value))
+		return localData[value].toVariant();
+
+	return QVariant();
+}
+
+QJsonObject CModList::copyField(QJsonObject data, QString from, QString to)
+{
+	QJsonObject renamed;
+
+	for (auto it = data.begin(); it != data.end(); it++)
+	{
+		QJsonObject object = it.value().toObject();
+
+		object.insert(to, object.value(from));
+		renamed.insert(it.key(), QJsonValue(object));
+	}
+	return renamed;
+}
+
+void CModList::addRepository(QJsonObject data)
+{
+	repositores.push_back(copyField(data, "version", "latestVersion"));
+}
+
+void CModList::setLocalModList(QJsonObject data)
+{
+	localModList = copyField(data, "version", "installedVersion");
+}
+
+void CModList::setModSettings(QJsonObject data)
+{
+	modSettings = data;
+}
+
+CModEntry CModList::getMod(QString modname) const
+{
+	assert(hasMod(modname));
+
+	QJsonObject repo;
+	QJsonObject local = localModList[modname].toObject();
+	QJsonValue settings = modSettings[modname];
+
+	for (auto entry : repositores)
+	{
+		if (entry.contains(modname))
+		{
+			if (repo.empty())
+				repo = entry[modname].toObject();
+			else
+			{
+				if (CModEntry::compareVersions(repo["version"].toString(),
+				                               entry[modname].toObject()["version"].toString()))
+					repo = entry[modname].toObject();
+			}
+		}
+	}
+
+	return CModEntry(repo, local, settings, modname);
+}
+
+bool CModList::hasMod(QString modname) const
+{
+	if (localModList.contains(modname))
+		return true;
+
+	for (auto entry : repositores)
+		if (entry.contains(modname))
+			return true;
+
+	return false;
+}
+
+QStringList CModList::getRequirements(QString modname)
+{
+	QStringList ret;
+
+	if (hasMod(modname))
+	{
+		auto mod = getMod(modname);
+
+		for (auto entry : mod.getValue("depends").toStringList())
+			ret += getRequirements(entry);
+	}
+	ret += modname;
+
+	return ret;
+}
+
+QVector<QString> CModList::getModList() const
+{
+	QSet<QString> knownMods;
+	QVector<QString> modList;
+	for (auto repo : repositores)
+	{
+		for (auto it = repo.begin(); it != repo.end(); it++)
+		{
+			knownMods.insert(it.key());
+		}
+	}
+	for (auto it = localModList.begin(); it != localModList.end(); it++)
+	{
+		knownMods.insert(it.key());
+	}
+
+	for (auto entry : knownMods)
+	{
+		modList.push_back(entry);
+	}
+	return modList;
+}

+ 77 - 0
launcher/modManager/cmodlist.h

@@ -0,0 +1,77 @@
+#pragma once
+
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QVariant>
+
+namespace ModStatus
+{
+	enum EModStatus
+	{
+		MASK_NONE = 0,
+		ENABLED = 1,
+		INSTALLED = 2,
+		UPDATEABLE = 4,
+		MASK_ALL = 255
+	};
+}
+
+class CModEntry
+{
+	// repository contains newest version only (if multiple are available)
+	QJsonObject repository;
+	QJsonObject localData;
+	QJsonValue modSettings;
+
+	QString modname;
+public:
+	CModEntry(QJsonObject repository, QJsonObject localData, QJsonValue modSettings, QString modname);
+
+	// installed and enabled
+	bool isEnabled() const;
+	// installed but disabled
+	bool isDisabled() const;
+	// available in any of repositories but not installed
+	bool isAvailable() const;
+	// installed and greater version exists in repository
+	bool isUpdateable() const;
+	// installed
+	bool isInstalled() const;
+
+	// see ModStatus enum
+	int getModStatus() const;
+
+	QString getName() const;
+
+	// get value of some field in mod structure. Returns empty optional if value is not present
+	QVariant getValue(QString value) const;
+
+	// returns true if less < greater comparing versions section by section
+	static bool compareVersions(QString lesser, QString greater);
+};
+
+class CModList
+{
+	QVector<QJsonObject> repositores;
+	QJsonObject localModList;
+	QJsonObject modSettings;
+
+	QJsonObject copyField(QJsonObject data, QString from, QString to);
+public:
+	virtual void addRepository(QJsonObject data);
+	virtual void setLocalModList(QJsonObject data);
+	virtual void setModSettings(QJsonObject data);
+
+	// returns mod by name. Note: mod MUST exist
+	CModEntry getMod(QString modname) const;
+
+	// returns list of all mods necessary to run selected one, including mod itself
+	// order is: first mods in list don't have any dependencies, last mod is modname
+	// note: may include mods not present in list
+	QStringList getRequirements(QString modname);
+
+	bool hasMod(QString modname) const;
+
+	// returns list of all available mods
+	QVector<QString> getModList() const;
+};

+ 193 - 0
launcher/modManager/cmodlistmodel.cpp

@@ -0,0 +1,193 @@
+#include "StdInc.h"
+#include "cmodlistmodel.h"
+
+#include <QIcon>
+
+namespace ModFields
+{
+	static const QString names [ModFields::COUNT] =
+	{
+		"",
+		"",
+		"modType",
+		"name",
+		"version",
+		"size",
+		"author"
+	};
+
+	static const QString header [ModFields::COUNT] =
+	{
+		"", // status icon
+		"", // status icon
+		"Type",
+		"Name",
+		"Version",
+		"Size (KB)",
+		"Author"
+	};
+}
+
+namespace ModStatus
+{
+	static const QString iconDelete   = "icons:mod-delete.png";
+	static const QString iconDisabled = "icons:mod-disabled.png";
+	static const QString iconDownload = "icons:mod-download.png";
+	static const QString iconEnabled  = "icons:mod-enabled.png";
+	static const QString iconUpdate   = "icons:mod-update.png";
+}
+
+CModListModel::CModListModel(QObject *parent) :
+    QAbstractTableModel(parent)
+{
+}
+
+QString CModListModel::modIndexToName(int index) const
+{
+	return indexToName[index];
+}
+
+QVariant CModListModel::data(const QModelIndex &index, int role) const
+{
+	if (index.isValid())
+	{
+		auto mod = getMod(modIndexToName(index.row()));
+
+		if (index.column() == ModFields::STATUS_ENABLED)
+		{
+			if (role == Qt::DecorationRole)
+			{
+				if (mod.isEnabled())
+					return QIcon(ModStatus::iconEnabled);
+
+				if (mod.isDisabled())
+					return QIcon(ModStatus::iconDisabled);
+
+				return QVariant();
+			}
+		}
+		if (index.column() == ModFields::STATUS_UPDATE)
+		{
+			if (role == Qt::DecorationRole)
+			{
+				if (mod.isUpdateable())
+					return QIcon(ModStatus::iconUpdate);
+
+				if (!mod.isInstalled())
+					return QIcon(ModStatus::iconDownload);
+
+				return QVariant();
+			}
+		}
+
+		if (role == Qt::DisplayRole)
+		{
+			return mod.getValue(ModFields::names[index.column()]);
+		}
+	}
+	return QVariant();
+}
+
+int CModListModel::rowCount(const QModelIndex &) const
+{
+	return indexToName.size();
+}
+
+int CModListModel::columnCount(const QModelIndex &) const
+{
+	return ModFields::COUNT;
+}
+
+Qt::ItemFlags CModListModel::flags(const QModelIndex &) const
+{
+	return Qt::ItemIsSelectable | Qt::ItemIsEnabled;
+}
+
+QVariant CModListModel::headerData(int section, Qt::Orientation orientation, int role) const
+{
+	if (role == Qt::DisplayRole && orientation == Qt::Horizontal)
+		return ModFields::header[section];
+	return QVariant();
+}
+
+void CModListModel::addRepository(QJsonObject data)
+{
+	beginResetModel();
+	CModList::addRepository(data);
+	endResetModel();
+}
+
+void CModListModel::setLocalModList(QJsonObject data)
+{
+	beginResetModel();
+	CModList::setLocalModList(data);
+	endResetModel();
+}
+
+void CModListModel::setModSettings(QJsonObject data)
+{
+	beginResetModel();
+	CModList::setModSettings(data);
+	endResetModel();
+}
+
+void CModListModel::endResetModel()
+{
+	indexToName = getModList();
+	QAbstractItemModel::endResetModel();
+}
+
+void CModFilterModel::setTypeFilter(int filteredType, int filterMask)
+{
+	this->filterMask = filterMask;
+	this->filteredType = filteredType;
+	invalidateFilter();
+}
+
+bool CModFilterModel::filterMatches(int modIndex) const
+{
+	CModEntry mod = base->getMod(base->modIndexToName(modIndex));
+
+	return (mod.getModStatus() & filterMask) == filteredType;
+}
+
+bool CModFilterModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
+{
+	if (filterMatches(source_row))
+		return QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent);
+	return false;
+}
+
+bool CModFilterModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
+{
+	assert(left.column() == right.column());
+
+	CModEntry mod = base->getMod(base->modIndexToName(left.row()));
+
+	switch (left.column())
+	{
+		case ModFields::STATUS_ENABLED:
+		{
+			return (mod.getModStatus() & (ModStatus::ENABLED | ModStatus::INSTALLED))
+			     < (mod.getModStatus() & (ModStatus::ENABLED | ModStatus::INSTALLED));
+		}
+		case ModFields::STATUS_UPDATE:
+		{
+			return (mod.getModStatus() & (ModStatus::UPDATEABLE | ModStatus::INSTALLED))
+			     < (mod.getModStatus() & (ModStatus::UPDATEABLE | ModStatus::INSTALLED));
+		}
+		default:
+		{
+			return QSortFilterProxyModel::lessThan(left, right);
+		}
+	}
+}
+
+CModFilterModel::CModFilterModel(CModListModel * model, QObject * parent):
+    QSortFilterProxyModel(parent),
+    base(model),
+    filteredType(ModStatus::MASK_NONE),
+    filterMask(ModStatus::MASK_NONE)
+{
+	setSourceModel(model);
+}

+ 68 - 0
launcher/modManager/cmodlistmodel.h

@@ -0,0 +1,68 @@
+#pragma once
+
+#include "cmodlist.h"
+
+#include <QAbstractTableModel>
+#include <QSortFilterProxyModel>
+
+namespace ModFields
+{
+	enum EModFields
+	{
+		STATUS_ENABLED,
+		STATUS_UPDATE,
+		TYPE,
+		NAME,
+		VERSION,
+		SIZE,
+		AUTHOR,
+		COUNT
+	};
+}
+
+class CModListModel : public QAbstractTableModel, public CModList
+{
+	Q_OBJECT
+
+	QVector<QString> indexToName;
+
+	void endResetModel();
+public:
+	/// CModListContainer overrides
+	void addRepository(QJsonObject data);
+	void setLocalModList(QJsonObject data);
+	void setModSettings(QJsonObject data);
+
+	QString modIndexToName(int index) const;
+
+	explicit CModListModel(QObject *parent = 0);
+	
+	QVariant data(const QModelIndex &index, int role) const;
+	QVariant headerData(int section, Qt::Orientation orientation, int role) const;
+
+	int rowCount(const QModelIndex &parent) const;
+	int columnCount(const QModelIndex &parent) const;
+
+	Qt::ItemFlags flags(const QModelIndex &index) const;
+signals:
+	
+public slots:
+	
+};
+
+class CModFilterModel : public QSortFilterProxyModel
+{
+	CModListModel * base;
+	int filteredType;
+	int filterMask;
+
+	bool filterMatches(int modIndex) const;
+
+	bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const;
+
+	bool lessThan(const QModelIndex &left, const QModelIndex &right) const;
+public:
+	void setTypeFilter(int filteredType, int filterMask);
+
+	CModFilterModel(CModListModel * model, QObject *parent = 0);
+};

+ 518 - 0
launcher/modManager/cmodlistview.cpp

@@ -0,0 +1,518 @@
+#include "StdInc.h"
+#include "cmodlistview.h"
+#include "ui_cmodlistview.h"
+
+#include <QJsonArray>
+#include <QCryptographicHash>
+
+#include "cmodlistmodel.h"
+#include "cmodmanager.h"
+#include "cdownloadmanager.h"
+#include "launcherdirs.h"
+
+#include "../lib/CConfigHandler.h"
+
+void CModListView::setupModModel()
+{
+	modModel = new CModListModel();
+	manager = new CModManager(modModel);
+}
+
+void CModListView::setupFilterModel()
+{
+	filterModel = new CModFilterModel(modModel);
+
+	filterModel->setFilterKeyColumn(-1); // filter across all columns
+	filterModel->setSortCaseSensitivity(Qt::CaseInsensitive); // to make it more user-friendly
+}
+
+void CModListView::setupModsView()
+{
+	ui->allModsView->setModel(filterModel);
+	// input data is not sorted - sort it before display
+	ui->allModsView->sortByColumn(ModFields::TYPE, Qt::AscendingOrder);
+	ui->allModsView->setColumnWidth(ModFields::STATUS_ENABLED, 30);
+	ui->allModsView->setColumnWidth(ModFields::STATUS_UPDATE, 30);
+	ui->allModsView->setColumnWidth(ModFields::NAME, 120);
+	ui->allModsView->setColumnWidth(ModFields::SIZE, 60);
+	ui->allModsView->setColumnWidth(ModFields::VERSION, 60);
+
+	connect( ui->allModsView->selectionModel(), SIGNAL( currentRowChanged( const QModelIndex &, const QModelIndex & )),
+	         this, SLOT( modSelected( const QModelIndex &, const QModelIndex & )));
+
+	connect( filterModel, SIGNAL( modelReset()),
+	         this, SLOT( modelReset()));
+}
+
+CModListView::CModListView(QWidget *parent) :
+	QWidget(parent),
+	ui(new Ui::CModListView)
+{
+	ui->setupUi(this);
+
+	setupModModel();
+	setupFilterModel();
+	setupModsView();
+
+	ui->progressWidget->setVisible(false);
+	dlManager = nullptr;
+
+	// hide mod description on start. looks better this way
+	hideModInfo();
+
+	for (auto entry : settings["launcher"]["repositoryURL"].Vector())
+	{
+		QString str = QString::fromUtf8(entry.String().c_str());
+
+		// URL must be encoded to something else to get rid of symbols illegal in file names
+		auto hashed = QCryptographicHash::hash(str.toUtf8(), QCryptographicHash::Md5);
+		auto hashedStr = QString::fromUtf8(hashed.toHex());
+
+		downloadFile(hashedStr + ".json", str, "repository index");
+	}
+}
+
+CModListView::~CModListView()
+{
+	delete ui;
+}
+
+void CModListView::showModInfo()
+{
+	ui->modInfoWidget->show();
+	ui->hideModInfoButton->setArrowType(Qt::RightArrow);
+}
+
+void CModListView::hideModInfo()
+{
+	ui->modInfoWidget->hide();
+	ui->hideModInfoButton->setArrowType(Qt::LeftArrow);
+}
+
+static QString replaceIfNotEmpty(QVariant value, QString pattern)
+{
+	if (value.canConvert<QStringList>())
+		return pattern.arg(value.toStringList().join(", "));
+
+	if (value.canConvert<QString>())
+		return pattern.arg(value.toString());
+
+	// all valid types of data should have been filtered by code above
+	assert(!value.isValid());
+
+	return "";
+}
+
+static QVariant sizeToString(QVariant value)
+{
+	if (value.canConvert<QString>())
+	{
+		static QString symbols = "kMGTPE";
+		auto number = value.toUInt();
+		size_t i=0;
+
+		while (number >= 1000)
+		{
+			number /= 1000;
+			i++;
+		}
+		return QVariant(QString("%1 %2B").arg(number).arg(symbols.at(i)));
+	}
+	return value;
+}
+
+static QString replaceIfNotEmpty(QStringList value, QString pattern)
+{
+	if (!value.empty())
+		return pattern.arg(value.join(", "));
+	return "";
+}
+
+QString CModListView::genModInfoText(CModEntry &mod)
+{
+	QString prefix = "<p><span style=\" font-weight:600;\">%1: </span>"; // shared prefix
+	QString lineTemplate = prefix + "%2</p>";
+	QString urlTemplate  = prefix + "<a href=\"%2\"><span style=\" text-decoration: underline; color:#0000ff;\">%2</span></a></p>";
+	QString textTemplate = prefix + "</p><p align=\"justify\">%2</p>";
+	QString noteTemplate = "<p align=\"justify\">%1: %2</p>";
+
+	QString result;
+
+	result += "<html><body>";
+	result += replaceIfNotEmpty(mod.getValue("name"), lineTemplate.arg("Mod name"));
+	result += replaceIfNotEmpty(mod.getValue("installedVersion"), lineTemplate.arg("Installed version"));
+	result += replaceIfNotEmpty(mod.getValue("latestVersion"), lineTemplate.arg("Latest version"));
+	result += replaceIfNotEmpty(sizeToString(mod.getValue("size")), lineTemplate.arg("Download size"));
+	result += replaceIfNotEmpty(mod.getValue("author"), lineTemplate.arg("Authors"));
+	result += replaceIfNotEmpty(mod.getValue("contact"), urlTemplate.arg("Home"));
+	result += replaceIfNotEmpty(mod.getValue("depends"), lineTemplate.arg("Required mods"));
+	result += replaceIfNotEmpty(mod.getValue("conflicts"), lineTemplate.arg("Conflicting mods"));
+	result += replaceIfNotEmpty(mod.getValue("description"), textTemplate.arg("Description"));
+
+	result += "<p></p>"; // to get some empty space
+
+	QString unknownDeps  = "This mod can not be installed or enabled because following dependencies are not present";
+	QString blockingMods = "This mod can not be enabled because following mods are incompatible with this mod";
+	QString hasActiveDependentMods = "This mod can not be disabled because it is required to run following mods";
+	QString hasDependentMods = "This mod can not be uninstalled or updated because it is required to run following mods";
+
+	QString notes;
+
+	notes += replaceIfNotEmpty(findInvalidDependencies(mod.getName()), noteTemplate.arg(unknownDeps));
+	notes += replaceIfNotEmpty(findBlockingMods(mod.getName()), noteTemplate.arg(blockingMods));
+	if (mod.isEnabled())
+		notes += replaceIfNotEmpty(findDependentMods(mod.getName(), true), noteTemplate.arg(hasActiveDependentMods));
+	if (mod.isInstalled())
+		notes += replaceIfNotEmpty(findDependentMods(mod.getName(), false), noteTemplate.arg(hasDependentMods));
+
+	if (notes.size())
+		result += textTemplate.arg("Notes").arg(notes);
+
+	result += "</body></html>";
+	return result;
+}
+
+void CModListView::enableModInfo()
+{
+	ui->hideModInfoButton->setEnabled(true);
+}
+
+void CModListView::disableModInfo()
+{
+	hideModInfo();
+	ui->hideModInfoButton->setEnabled(false);
+}
+
+void CModListView::selectMod(int index)
+{
+	if (index < 0)
+	{
+		disableModInfo();
+	}
+	else
+	{
+		enableModInfo();
+
+		auto mod = modModel->getMod(modModel->modIndexToName(index));
+
+		ui->textBrowser->setHtml(genModInfoText(mod));
+
+		bool hasInvalidDeps = !findInvalidDependencies(modModel->modIndexToName(index)).empty();
+		bool hasBlockingMods = !findBlockingMods(modModel->modIndexToName(index)).empty();
+		bool hasDependentMods = !findDependentMods(modModel->modIndexToName(index), true).empty();
+
+		ui->disableButton->setVisible(mod.isEnabled());
+		ui->enableButton->setVisible(mod.isDisabled());
+		ui->installButton->setVisible(mod.isAvailable());
+		ui->uninstallButton->setVisible(mod.isInstalled());
+		ui->updateButton->setVisible(mod.isUpdateable());
+
+		// Block buttons if action is not allowed at this time
+		// TODO: automate handling of some of these cases instead of forcing player
+		// to resolve all conflicts manually.
+		ui->disableButton->setEnabled(!hasDependentMods);
+		ui->enableButton->setEnabled(!hasBlockingMods && !hasInvalidDeps);
+		ui->installButton->setEnabled(!hasInvalidDeps);
+		ui->uninstallButton->setEnabled(!hasDependentMods);
+		ui->updateButton->setEnabled(!hasInvalidDeps && !hasDependentMods);
+	}
+}
+
+void CModListView::keyPressEvent(QKeyEvent * event)
+{
+	if (event->key() == Qt::Key_Escape && ui->modInfoWidget->isVisible() )
+	{
+		ui->modInfoWidget->hide();
+	}
+	else
+	{
+		return QWidget::keyPressEvent(event);
+	}
+}
+
+void CModListView::modSelected(const QModelIndex & current, const QModelIndex & )
+{
+	selectMod(filterModel->mapToSource(current).row());
+}
+
+void CModListView::on_hideModInfoButton_clicked()
+{
+	if (ui->modInfoWidget->isVisible())
+		hideModInfo();
+	else
+		showModInfo();
+}
+
+void CModListView::on_allModsView_doubleClicked(const QModelIndex &index)
+{
+	showModInfo();
+	selectMod(filterModel->mapToSource(index).row());
+}
+
+void CModListView::on_lineEdit_textChanged(const QString &arg1)
+{
+	QRegExp regExp(arg1, Qt::CaseInsensitive, QRegExp::Wildcard);
+	filterModel->setFilterRegExp(regExp);
+}
+
+void CModListView::on_comboBox_currentIndexChanged(int index)
+{
+	switch (index)
+	{
+	break; case 0: filterModel->setTypeFilter(ModStatus::MASK_NONE, ModStatus::MASK_NONE);
+	break; case 1: filterModel->setTypeFilter(ModStatus::MASK_NONE, ModStatus::INSTALLED);
+	break; case 2: filterModel->setTypeFilter(ModStatus::INSTALLED, ModStatus::INSTALLED);
+	break; case 3: filterModel->setTypeFilter(ModStatus::UPDATEABLE, ModStatus::UPDATEABLE);
+	break; case 4: filterModel->setTypeFilter(ModStatus::ENABLED | ModStatus::INSTALLED, ModStatus::ENABLED | ModStatus::INSTALLED);
+	break; case 5: filterModel->setTypeFilter(ModStatus::INSTALLED, ModStatus::ENABLED | ModStatus::INSTALLED);
+	}
+}
+
+QStringList CModListView::findInvalidDependencies(QString mod)
+{
+	QStringList ret;
+	for (QString requrement : modModel->getRequirements(mod))
+	{
+		if (!modModel->hasMod(requrement))
+			ret += requrement;
+	}
+	return ret;
+}
+
+QStringList CModListView::findBlockingMods(QString mod)
+{
+	QStringList ret;
+	auto required = modModel->getRequirements(mod);
+
+	for (QString name : modModel->getModList())
+	{
+		auto mod = modModel->getMod(name);
+
+		if (mod.isEnabled())
+		{
+			// one of enabled mods have requirement (or this mod) marked as conflict
+			for (auto conflict : mod.getValue("conflicts").toStringList())
+				if (required.contains(conflict))
+					ret.push_back(name);
+		}
+	}
+
+	return ret;
+}
+
+QStringList CModListView::findDependentMods(QString mod, bool excludeDisabled)
+{
+	QStringList ret;
+	for (QString modName : modModel->getModList())
+	{
+		auto current = modModel->getMod(modName);
+
+		if (current.getValue("depends").toStringList().contains(mod) &&
+		    !(current.isDisabled() && excludeDisabled))
+			ret += modName;
+	}
+	return ret;
+}
+
+void CModListView::on_enableButton_clicked()
+{
+	QString modName = modModel->modIndexToName(filterModel->mapToSource(ui->allModsView->currentIndex()).row());
+
+	assert(findBlockingMods(modName).empty());
+	assert(findInvalidDependencies(modName).empty());
+
+	for (auto & name : modModel->getRequirements(modName))
+		if (modModel->getMod(name).isDisabled())
+			manager->enableMod(name);
+}
+
+void CModListView::on_disableButton_clicked()
+{
+	QString modName = modModel->modIndexToName(filterModel->mapToSource(ui->allModsView->currentIndex()).row());
+
+	for (auto & name : modModel->getRequirements(modName))
+		if (modModel->hasMod(name) &&
+		    modModel->getMod(name).isEnabled())
+			manager->disableMod(name);
+}
+
+void CModListView::on_updateButton_clicked()
+{
+	QString modName = modModel->modIndexToName(filterModel->mapToSource(ui->allModsView->currentIndex()).row());
+
+	assert(findInvalidDependencies(modName).empty());
+
+	for (auto & name : modModel->getRequirements(modName))
+	{
+		auto mod = modModel->getMod(name);
+		// update required mod, install missing (can be new dependency)
+		if (mod.isUpdateable() || !mod.isInstalled())
+			downloadFile(name + ".zip", mod.getValue("download").toString(), "mods");
+	}
+}
+
+void CModListView::on_uninstallButton_clicked()
+{
+	QString modName = modModel->modIndexToName(filterModel->mapToSource(ui->allModsView->currentIndex()).row());
+	// NOTE: perhaps add "manually installed" flag and uninstall those dependencies that don't have it?
+
+	if (modModel->hasMod(modName) &&
+	    modModel->getMod(modName).isInstalled())
+	{
+		manager->disableMod(modName);
+		manager->uninstallMod(modName);
+	}
+}
+
+void CModListView::on_installButton_clicked()
+{
+	QString modName = modModel->modIndexToName(filterModel->mapToSource(ui->allModsView->currentIndex()).row());
+
+	assert(findInvalidDependencies(modName).empty());
+
+	for (auto & name : modModel->getRequirements(modName))
+	{
+		auto mod = modModel->getMod(name);
+		if (!mod.isInstalled())
+			downloadFile(name + ".zip", mod.getValue("download").toString(), "mods");
+	}
+}
+
+void CModListView::downloadFile(QString file, QString url, QString description)
+{
+	if (!dlManager)
+	{
+		dlManager = new CDownloadManager();
+		ui->progressWidget->setVisible(true);
+		connect(dlManager, SIGNAL(downloadProgress(qint64,qint64)),
+				this, SLOT(downloadProgress(qint64,qint64)));
+
+		connect(dlManager, SIGNAL(finished(QStringList,QStringList,QStringList)),
+				this, SLOT(downloadFinished(QStringList,QStringList,QStringList)));
+
+
+		QString progressBarFormat = "Downloading %s%. %p% (%v KB out of %m KB) finished";
+
+		progressBarFormat.replace("%s%", description);
+		ui->progressBar->setFormat(progressBarFormat);
+	}
+
+	dlManager->downloadFile(QUrl(url), file);
+}
+
+void CModListView::downloadProgress(qint64 current, qint64 max)
+{
+	// display progress, in kilobytes
+	ui->progressBar->setValue(current/1024);
+	ui->progressBar->setMaximum(max/1024);
+}
+
+void CModListView::downloadFinished(QStringList savedFiles, QStringList failedFiles, QStringList errors)
+{
+	QString title = "Download failed";
+	QString firstLine = "Unable to download all files.\n\nEncountered errors:\n\n";
+	QString lastLine = "\n\nInstall successfully downloaded?";
+
+	// if all files were d/loaded there should be no errors. And on failure there must be an error
+	assert(failedFiles.empty() == errors.empty());
+
+	if (savedFiles.empty())
+	{
+		// no successfully downloaded mods
+		QMessageBox::warning(this, title, firstLine + errors.join("\n"), QMessageBox::Ok, QMessageBox::Ok );
+	}
+	else if (!failedFiles.empty())
+	{
+		// some mods were not downloaded
+		int result = QMessageBox::warning (this, title, firstLine + errors.join("\n") + lastLine,
+		                                   QMessageBox::Yes | QMessageBox::No, QMessageBox::No );
+
+		if (result == QMessageBox::Yes)
+			installFiles(savedFiles);
+	}
+	else
+	{
+		// everything OK
+		installFiles(savedFiles);
+	}
+
+	// remove progress bar after some delay so user can see that download was complete and not interrupted.
+	QTimer::singleShot(1000, this,  SLOT(hideProgressBar()));
+
+	dlManager->deleteLater();
+	dlManager = nullptr;
+}
+
+void CModListView::hideProgressBar()
+{
+	if (dlManager == nullptr) // it was not recreated meanwhile
+	{
+		ui->progressWidget->setVisible(false);
+		ui->progressBar->setMaximum(0);
+		ui->progressBar->setValue(0);
+	}
+}
+
+void CModListView::installFiles(QStringList files)
+{
+	QStringList mods;
+
+	// TODO: some better way to separate zip's with mods and downloaded repository files
+	for (QString filename : files)
+	{
+		if (filename.contains(".zip"))
+			mods.push_back(filename);
+		if (filename.contains(".json"))
+			manager->loadRepository(filename);
+	}
+	if (!mods.empty())
+		installMods(mods);
+}
+
+void CModListView::installMods(QStringList archives)
+{
+	//TODO: check return status of all calls to manager!!!
+
+	QStringList modNames;
+
+	for (QString archive : archives)
+	{
+		// get basename out of full file name
+		//                remove path                  remove extension
+		QString modName = archive.section('/', -1, -1).section('.', 0, 0);
+
+		modNames.push_back(modName);
+	}
+
+	// disable mod(s), to properly recalculate dependencies, if changed
+	for (QString mod : boost::adaptors::reverse(modNames))
+		manager->disableMod(mod);
+
+	// uninstall old version of mod, if installed
+	for (QString mod : boost::adaptors::reverse(modNames))
+		manager->uninstallMod(mod);
+
+	for (int i=0; i<modNames.size(); i++)
+		manager->installMod(modNames[i], archives[i]);
+
+	if (settings["launcher"]["enableInstalledMods"].Bool())
+	{
+		for (QString mod : modNames)
+			manager->enableMod(mod);
+	}
+
+	for (QString archive : archives)
+		QFile::remove(archive);
+}
+
+void CModListView::on_pushButton_clicked()
+{
+	delete dlManager;
+	dlManager = nullptr;
+	hideProgressBar();
+}
+
+void CModListView::modelReset()
+{
+	selectMod(filterModel->mapToSource(ui->allModsView->currentIndex()).row());
+}

+ 84 - 0
launcher/modManager/cmodlistview.h

@@ -0,0 +1,84 @@
+#pragma once
+
+namespace Ui {
+	class CModListView;
+}
+
+class CModManager;
+class CModListModel;
+class CModFilterModel;
+class CDownloadManager;
+class QTableWidgetItem;
+
+class CModEntry;
+
+class CModListView : public QWidget
+{
+	Q_OBJECT
+
+	CModManager * manager;
+	CModListModel * modModel;
+	CModFilterModel * filterModel;
+	CDownloadManager * dlManager;
+
+	void keyPressEvent(QKeyEvent * event);
+
+	void setupModModel();
+	void setupFilterModel();
+	void setupModsView();
+
+	// find mods unknown to mod list (not present in repo and not installed)
+	QStringList findInvalidDependencies(QString mod);
+	// find mods that block enabling of this mod: conflicting with this mod or one of required mods
+	QStringList findBlockingMods(QString mod);
+	// find mods that depend on this one
+	QStringList findDependentMods(QString mod, bool excludeDisabled);
+
+	void downloadFile(QString file, QString url, QString description);
+
+	void installMods(QStringList archives);
+	void installFiles(QStringList mods);
+
+	QString genModInfoText(CModEntry & mod);
+public:
+	explicit CModListView(QWidget *parent = 0);
+	~CModListView();
+	
+	void showModInfo();
+	void hideModInfo();
+
+	void enableModInfo();
+	void disableModInfo();
+
+	void selectMod(int index);
+
+private slots:
+	void modSelected(const QModelIndex & current, const QModelIndex & previous);
+	void downloadProgress(qint64 current, qint64 max);
+	void downloadFinished(QStringList savedFiles, QStringList failedFiles, QStringList errors);
+	void modelReset ();
+	void hideProgressBar();
+
+	void on_hideModInfoButton_clicked();
+
+	void on_allModsView_doubleClicked(const QModelIndex &index);
+
+	void on_lineEdit_textChanged(const QString &arg1);
+
+	void on_comboBox_currentIndexChanged(int index);
+
+	void on_enableButton_clicked();
+
+	void on_disableButton_clicked();
+
+	void on_updateButton_clicked();
+
+	void on_uninstallButton_clicked();
+
+	void on_installButton_clicked();
+
+	void on_pushButton_clicked();
+
+private:
+	Ui::CModListView *ui;
+};

+ 453 - 0
launcher/modManager/cmodlistview.ui

@@ -0,0 +1,453 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>CModListView</class>
+ <widget class="QWidget" name="CModListView">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>596</width>
+    <height>342</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Form</string>
+  </property>
+  <layout class="QGridLayout" name="gridLayout_3">
+   <property name="leftMargin">
+    <number>0</number>
+   </property>
+   <property name="topMargin">
+    <number>0</number>
+   </property>
+   <property name="rightMargin">
+    <number>0</number>
+   </property>
+   <property name="bottomMargin">
+    <number>0</number>
+   </property>
+   <item row="0" column="0">
+    <layout class="QGridLayout" name="gridLayout_2">
+     <property name="spacing">
+      <number>6</number>
+     </property>
+     <item row="0" column="0">
+      <widget class="QLineEdit" name="lineEdit">
+       <property name="sizePolicy">
+        <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+         <horstretch>0</horstretch>
+         <verstretch>0</verstretch>
+        </sizepolicy>
+       </property>
+       <property name="minimumSize">
+        <size>
+         <width>0</width>
+         <height>0</height>
+        </size>
+       </property>
+       <property name="maximumSize">
+        <size>
+         <width>16777215</width>
+         <height>16777215</height>
+        </size>
+       </property>
+       <property name="text">
+        <string/>
+       </property>
+       <property name="placeholderText">
+        <string>Filter</string>
+       </property>
+      </widget>
+     </item>
+     <item row="0" column="1">
+      <widget class="QComboBox" name="comboBox">
+       <property name="sizePolicy">
+        <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+         <horstretch>0</horstretch>
+         <verstretch>0</verstretch>
+        </sizepolicy>
+       </property>
+       <property name="maximumSize">
+        <size>
+         <width>16777215</width>
+         <height>16777215</height>
+        </size>
+       </property>
+       <property name="currentIndex">
+        <number>0</number>
+       </property>
+       <item>
+        <property name="text">
+         <string>All mods</string>
+        </property>
+       </item>
+       <item>
+        <property name="text">
+         <string>Downloadable</string>
+        </property>
+       </item>
+       <item>
+        <property name="text">
+         <string>Installed</string>
+        </property>
+       </item>
+       <item>
+        <property name="text">
+         <string>Updatable</string>
+        </property>
+       </item>
+       <item>
+        <property name="text">
+         <string>Active</string>
+        </property>
+       </item>
+       <item>
+        <property name="text">
+         <string>Inactive</string>
+        </property>
+       </item>
+      </widget>
+     </item>
+     <item row="1" column="0" colspan="2">
+      <widget class="QTableView" name="allModsView">
+       <property name="selectionMode">
+        <enum>QAbstractItemView::SingleSelection</enum>
+       </property>
+       <property name="selectionBehavior">
+        <enum>QAbstractItemView::SelectRows</enum>
+       </property>
+       <property name="iconSize">
+        <size>
+         <width>32</width>
+         <height>20</height>
+        </size>
+       </property>
+       <property name="verticalScrollMode">
+        <enum>QAbstractItemView::ScrollPerItem</enum>
+       </property>
+       <property name="horizontalScrollMode">
+        <enum>QAbstractItemView::ScrollPerPixel</enum>
+       </property>
+       <property name="sortingEnabled">
+        <bool>true</bool>
+       </property>
+       <attribute name="horizontalHeaderStretchLastSection">
+        <bool>true</bool>
+       </attribute>
+       <attribute name="verticalHeaderVisible">
+        <bool>false</bool>
+       </attribute>
+      </widget>
+     </item>
+    </layout>
+   </item>
+   <item row="0" column="1">
+    <widget class="QToolButton" name="hideModInfoButton">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Fixed" vsizetype="Minimum">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+     <property name="maximumSize">
+      <size>
+       <width>16</width>
+       <height>100</height>
+      </size>
+     </property>
+     <property name="text">
+      <string/>
+     </property>
+     <property name="autoRaise">
+      <bool>true</bool>
+     </property>
+     <property name="arrowType">
+      <enum>Qt::RightArrow</enum>
+     </property>
+    </widget>
+   </item>
+   <item row="0" column="2">
+    <widget class="QWidget" name="modInfoWidget" native="true">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+     <property name="minimumSize">
+      <size>
+       <width>0</width>
+       <height>0</height>
+      </size>
+     </property>
+     <property name="maximumSize">
+      <size>
+       <width>16777215</width>
+       <height>16777215</height>
+      </size>
+     </property>
+     <property name="layoutDirection">
+      <enum>Qt::LeftToRight</enum>
+     </property>
+     <property name="autoFillBackground">
+      <bool>false</bool>
+     </property>
+     <layout class="QGridLayout" name="gridLayout">
+      <property name="leftMargin">
+       <number>0</number>
+      </property>
+      <property name="topMargin">
+       <number>0</number>
+      </property>
+      <property name="rightMargin">
+       <number>0</number>
+      </property>
+      <property name="bottomMargin">
+       <number>0</number>
+      </property>
+      <item row="0" column="0" colspan="6">
+       <widget class="QTextBrowser" name="textBrowser">
+        <property name="sizePolicy">
+         <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+          <horstretch>0</horstretch>
+          <verstretch>0</verstretch>
+         </sizepolicy>
+        </property>
+        <property name="readOnly">
+         <bool>true</bool>
+        </property>
+        <property name="html">
+         <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
+&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
+p, li { white-space: pre-wrap; }
+&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Ubuntu'; font-size:11pt; font-weight:400; font-style:normal;&quot;&gt;
+&lt;p style=&quot;-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+        </property>
+        <property name="openExternalLinks">
+         <bool>true</bool>
+        </property>
+        <property name="openLinks">
+         <bool>true</bool>
+        </property>
+       </widget>
+      </item>
+      <item row="1" column="0">
+       <spacer name="modButtonSpacer">
+        <property name="orientation">
+         <enum>Qt::Horizontal</enum>
+        </property>
+        <property name="sizeType">
+         <enum>QSizePolicy::MinimumExpanding</enum>
+        </property>
+        <property name="sizeHint" stdset="0">
+         <size>
+          <width>0</width>
+          <height>20</height>
+         </size>
+        </property>
+       </spacer>
+      </item>
+      <item row="1" column="1">
+       <widget class="QPushButton" name="enableButton">
+        <property name="sizePolicy">
+         <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+          <horstretch>0</horstretch>
+          <verstretch>0</verstretch>
+         </sizepolicy>
+        </property>
+        <property name="minimumSize">
+         <size>
+          <width>51</width>
+          <height>0</height>
+         </size>
+        </property>
+        <property name="maximumSize">
+         <size>
+          <width>100</width>
+          <height>16777215</height>
+         </size>
+        </property>
+        <property name="text">
+         <string>Enable</string>
+        </property>
+       </widget>
+      </item>
+      <item row="1" column="2">
+       <widget class="QPushButton" name="disableButton">
+        <property name="sizePolicy">
+         <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+          <horstretch>0</horstretch>
+          <verstretch>0</verstretch>
+         </sizepolicy>
+        </property>
+        <property name="minimumSize">
+         <size>
+          <width>51</width>
+          <height>0</height>
+         </size>
+        </property>
+        <property name="maximumSize">
+         <size>
+          <width>100</width>
+          <height>16777215</height>
+         </size>
+        </property>
+        <property name="text">
+         <string>Disable</string>
+        </property>
+       </widget>
+      </item>
+      <item row="1" column="3">
+       <widget class="QPushButton" name="updateButton">
+        <property name="sizePolicy">
+         <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+          <horstretch>0</horstretch>
+          <verstretch>0</verstretch>
+         </sizepolicy>
+        </property>
+        <property name="minimumSize">
+         <size>
+          <width>51</width>
+          <height>0</height>
+         </size>
+        </property>
+        <property name="maximumSize">
+         <size>
+          <width>100</width>
+          <height>16777215</height>
+         </size>
+        </property>
+        <property name="text">
+         <string>Update</string>
+        </property>
+       </widget>
+      </item>
+      <item row="1" column="4">
+       <widget class="QPushButton" name="uninstallButton">
+        <property name="sizePolicy">
+         <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+          <horstretch>0</horstretch>
+          <verstretch>0</verstretch>
+         </sizepolicy>
+        </property>
+        <property name="minimumSize">
+         <size>
+          <width>51</width>
+          <height>0</height>
+         </size>
+        </property>
+        <property name="maximumSize">
+         <size>
+          <width>100</width>
+          <height>16777215</height>
+         </size>
+        </property>
+        <property name="text">
+         <string>Uninstall</string>
+        </property>
+       </widget>
+      </item>
+      <item row="1" column="5">
+       <widget class="QPushButton" name="installButton">
+        <property name="sizePolicy">
+         <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+          <horstretch>0</horstretch>
+          <verstretch>0</verstretch>
+         </sizepolicy>
+        </property>
+        <property name="minimumSize">
+         <size>
+          <width>51</width>
+          <height>0</height>
+         </size>
+        </property>
+        <property name="maximumSize">
+         <size>
+          <width>100</width>
+          <height>16777215</height>
+         </size>
+        </property>
+        <property name="text">
+         <string>Install</string>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item row="1" column="0" colspan="3">
+    <widget class="QWidget" name="progressWidget" native="true">
+     <property name="enabled">
+      <bool>true</bool>
+     </property>
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+     <property name="minimumSize">
+      <size>
+       <width>0</width>
+       <height>0</height>
+      </size>
+     </property>
+     <layout class="QHBoxLayout" name="horizontalLayout">
+      <item>
+       <widget class="QProgressBar" name="progressBar">
+        <property name="sizePolicy">
+         <sizepolicy hsizetype="Expanding" vsizetype="Maximum">
+          <horstretch>0</horstretch>
+          <verstretch>0</verstretch>
+         </sizepolicy>
+        </property>
+        <property name="maximum">
+         <number>0</number>
+        </property>
+        <property name="value">
+         <number>0</number>
+        </property>
+        <property name="textVisible">
+         <bool>true</bool>
+        </property>
+        <property name="invertedAppearance">
+         <bool>false</bool>
+        </property>
+        <property name="format">
+         <string> %p% (%v KB out of %m KB)</string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QPushButton" name="pushButton">
+        <property name="sizePolicy">
+         <sizepolicy hsizetype="Minimum" vsizetype="Minimum">
+          <horstretch>0</horstretch>
+          <verstretch>0</verstretch>
+         </sizepolicy>
+        </property>
+        <property name="text">
+         <string>Abort</string>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <tabstops>
+  <tabstop>lineEdit</tabstop>
+  <tabstop>comboBox</tabstop>
+  <tabstop>allModsView</tabstop>
+  <tabstop>textBrowser</tabstop>
+  <tabstop>hideModInfoButton</tabstop>
+  <tabstop>enableButton</tabstop>
+  <tabstop>disableButton</tabstop>
+  <tabstop>updateButton</tabstop>
+  <tabstop>uninstallButton</tabstop>
+  <tabstop>installButton</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>

+ 256 - 0
launcher/modManager/cmodmanager.cpp

@@ -0,0 +1,256 @@
+#include "StdInc.h"
+#include "cmodmanager.h"
+
+#include "../lib/VCMIDirs.h"
+#include "../lib/filesystem/Filesystem.h"
+#include "../lib/filesystem/CZipLoader.h"
+
+#include "../launcherdirs.h"
+
+static QJsonObject JsonFromFile(QString filename)
+{
+	QFile file(filename);
+	file.open(QFile::ReadOnly);
+
+	return QJsonDocument::fromJson(file.readAll()).object();
+}
+
+static void JsonToFile(QString filename, QJsonObject object)
+{
+	QFile file(filename);
+	file.open(QFile::WriteOnly);
+	file.write(QJsonDocument(object).toJson());
+}
+
+static QString detectModArchive(QString path, QString modName)
+{
+	auto files = ZipArchive::listFiles(path.toUtf8().data());
+
+	QString modDirName;
+
+	for (auto file : files)
+	{
+		QString filename = QString::fromUtf8(file.c_str());
+		if (filename.toLower().startsWith(modName))
+		{
+			// archive must contain mod.json file
+			if (filename.toLower() == modName + "/mod.json")
+				modDirName = filename.section('/', 0, 0);
+		}
+		else // all files must be in <modname> directory
+			return "";
+	}
+	return modDirName;
+}
+
+CModManager::CModManager(CModList * modList):
+    modList(modList)
+{
+	loadMods();
+	loadModSettings();
+}
+
+QString CModManager::settingsPath()
+{
+	return QString::fromUtf8(VCMIDirs::get().userConfigPath().c_str()) + "/modSettings.json";
+}
+
+void CModManager::loadModSettings()
+{
+	modSettings = JsonFromFile(settingsPath());
+	modList->setModSettings(modSettings["activeMods"].toObject());
+}
+
+void CModManager::loadRepository(QString file)
+{
+	modList->addRepository(JsonFromFile(file));
+}
+
+void CModManager::loadMods()
+{
+	auto installedMods = CResourceHandler::getAvailableMods();
+
+	for (auto modname : installedMods)
+	{
+		ResourceID resID("Mods/" + modname + "/mod.json");
+
+		if (CResourceHandler::get()->existsResource(resID))
+		{
+			auto data = CResourceHandler::get()->load(resID)->readAll();
+			auto array = QByteArray(reinterpret_cast<char *>(data.first.get()), data.second);
+
+			auto mod = QJsonDocument::fromJson(array);
+			assert (mod.isObject()); // TODO: use JsonNode from vcmi code here - QJsonNode parser is just too pedantic
+
+			localMods.insert(QString::fromUtf8(modname.c_str()).toLower(), QJsonValue(mod.object()));
+		}
+	}
+	modList->setLocalModList(localMods);
+}
+
+bool CModManager::installMod(QString modname, QString archivePath)
+{
+	return canInstallMod(modname) && doInstallMod(modname, archivePath);
+}
+
+bool CModManager::uninstallMod(QString modname)
+{
+	return canUninstallMod(modname) && doUninstallMod(modname);
+}
+
+bool CModManager::enableMod(QString modname)
+{
+	return canEnableMod(modname) && doEnableMod(modname, true);
+}
+
+bool CModManager::disableMod(QString modname)
+{
+	return canDisableMod(modname) && doEnableMod(modname, false);
+}
+
+bool CModManager::canInstallMod(QString modname)
+{
+	auto mod = modList->getMod(modname);
+
+	if (mod.isInstalled())
+		return false;
+
+	if (!mod.isAvailable())
+		return false;
+	return true;
+}
+
+bool CModManager::canUninstallMod(QString modname)
+{
+	auto mod = modList->getMod(modname);
+
+	if (!mod.isInstalled())
+		return false;
+
+	if (mod.isEnabled())
+		return false;
+	return true;
+}
+
+bool CModManager::canEnableMod(QString modname)
+{
+	auto mod = modList->getMod(modname);
+
+	if (mod.isEnabled())
+		return false;
+
+	if (!mod.isInstalled())
+		return false;
+
+	for (auto modEntry : mod.getValue("depends").toStringList())
+	{
+		if (!modList->hasMod(modEntry)) // required mod is not available
+			return false;
+		if (!modList->getMod(modEntry).isEnabled())
+			return false;
+	}
+
+	for (QString name : modList->getModList())
+	{
+		auto mod = modList->getMod(name);
+
+		if (mod.isEnabled() && mod.getValue("conflicts").toStringList().contains(modname))
+			return false; // "reverse conflict" - enabled mod has this one as conflict
+	}
+
+	for (auto modEntry : mod.getValue("conflicts").toStringList())
+	{
+		if (modList->hasMod(modEntry) &&
+		    modList->getMod(modEntry).isEnabled()) // conflicting mod installed and enabled
+			return false;
+	}
+	return true;
+}
+
+bool CModManager::canDisableMod(QString modname)
+{
+	auto mod = modList->getMod(modname);
+
+	if (mod.isDisabled())
+		return false;
+
+	if (!mod.isInstalled())
+		return false;
+
+	for (QString modEntry : modList->getModList())
+	{
+		auto current = modList->getMod(modEntry);
+
+		if (current.getValue("depends").toStringList().contains(modname) &&
+		    !current.isDisabled())
+			return false; // this mod must be disabled first
+	}
+	return true;
+}
+
+bool CModManager::doEnableMod(QString mod, bool on)
+{
+	QJsonValue value(on);
+	QJsonObject list = modSettings["activeMods"].toObject();
+
+	list.insert(mod, value);
+	modSettings.insert("activeMods", list);
+
+	modList->setModSettings(modSettings["activeMods"].toObject());
+
+	JsonToFile(settingsPath(), modSettings);
+
+	return true;
+}
+
+bool CModManager::doInstallMod(QString modname, QString archivePath)
+{
+	QString destDir = CLauncherDirs::get().modsPath() + "/";
+
+	if (!QFile(archivePath).exists())
+		return false; // archive with mod data exists
+
+	if (QDir(destDir + modname).exists()) // FIXME: recheck wog/vcmi data behavior - they have bits of data in our trunk
+		return false; // no mod with such name installed
+
+	if (localMods.contains(modname))
+		return false; // no installed data known
+
+	QString modDirName = detectModArchive(archivePath, modname);
+	if (!modDirName.size())
+		return false; // archive content looks like mod FS
+
+	if (!ZipArchive::extract(archivePath.toUtf8().data(), destDir.toUtf8().data()))
+	{
+		QDir(destDir + modDirName).removeRecursively();
+		return false; // extraction failed
+	}
+
+	QJsonObject json = JsonFromFile(destDir + modDirName + "/mod.json");
+
+	localMods.insert(modname, json);
+	modList->setLocalModList(localMods);
+
+	return true;
+}
+
+bool CModManager::doUninstallMod(QString modname)
+{
+	ResourceID resID(std::string("Mods/") + modname.toUtf8().data(), EResType::DIRECTORY);
+	// Get location of the mod, in case-insensitive way
+	QString modDir = QString::fromUtf8(CResourceHandler::get()->getResourceName(resID)->c_str());
+
+	if (!QDir(modDir).exists())
+		return false;
+
+	if (!localMods.contains(modname))
+		return false;
+
+	if (!QDir(modDir).removeRecursively())
+		return false;
+
+	localMods.remove(modname);
+	modList->setLocalModList(localMods);
+
+	return true;
+}

+ 38 - 0
launcher/modManager/cmodmanager.h

@@ -0,0 +1,38 @@
+#pragma once
+
+#include "cmodlist.h"
+
+class CModManager
+{
+	CModList * modList;
+
+	QString settingsPath();
+
+	// check-free version of public method
+	bool doEnableMod(QString mod, bool on);
+	bool doInstallMod(QString mod, QString archivePath);
+	bool doUninstallMod(QString mod);
+
+	QJsonObject modSettings;
+	QJsonObject localMods;
+
+public:
+	CModManager(CModList * modList);
+
+	void loadRepository(QString filename);
+	void loadModSettings();
+	void loadMods();
+
+	/// mod management functions. Return true if operation was successful
+
+	/// installs mod from zip archive located at archivePath
+	bool installMod(QString mod, QString archivePath);
+	bool uninstallMod(QString mod);
+	bool enableMod(QString mod);
+	bool disableMod(QString mod);
+
+	bool canInstallMod(QString mod);
+	bool canUninstallMod(QString mod);
+	bool canEnableMod(QString mod);
+	bool canDisableMod(QString mod);
+};

+ 112 - 0
launcher/settingsView/csettingsview.cpp

@@ -0,0 +1,112 @@
+#include "StdInc.h"
+#include "csettingsview.h"
+#include "ui_csettingsview.h"
+
+#include "../lib/CConfigHandler.h"
+#include "../lib/VCMIDirs.h"
+
+void CSettingsView::loadSettings()
+{
+	int resX = settings["video"]["screenRes"]["width"].Float();
+	int resY = settings["video"]["screenRes"]["height"].Float();
+
+	int resIndex = ui->comboBoxResolution->findText(QString("%1x%2").arg(resX).arg(resY));
+
+	ui->comboBoxResolution->setCurrentIndex(resIndex);
+	ui->comboBoxFullScreen->setCurrentIndex(settings["video"]["fullscreen"].Bool());
+
+	int neutralAIIndex = ui->comboBoxNeutralAI->findText(QString::fromUtf8(settings["server"]["neutralAI"].String().c_str()));
+	int playerAIIndex = ui->comboBoxPlayerAI->findText(QString::fromUtf8(settings["server"]["playerAI"].String().c_str()));
+
+	ui->comboBoxNeutralAI->setCurrentIndex(neutralAIIndex);
+	ui->comboBoxPlayerAI->setCurrentIndex(playerAIIndex);
+
+	ui->spinBoxNetworkPort->setValue(settings["server"]["port"].Float());
+
+	ui->comboBoxEnableMods->setCurrentIndex(settings["launcher"]["enableInstalledMods"].Bool());
+
+	// all calls to plainText will trigger textChanged() signal overwriting config. Create backup before editing widget
+	JsonNode urls = settings["launcher"]["repositoryURL"];
+
+	ui->plainTextEditRepos->clear();
+	for (auto entry : urls.Vector())
+		ui->plainTextEditRepos->appendPlainText(QString::fromUtf8(entry.String().c_str()));
+
+	ui->lineEditUserDataDir->setText(QString::fromUtf8(VCMIDirs::get().userDataPath().c_str()));
+	QStringList dataDirs;
+	for (auto string : VCMIDirs::get().dataPaths())
+		dataDirs += QString::fromUtf8(string.c_str());
+	ui->lineEditGameDir->setText(dataDirs.join(':'));
+}
+
+CSettingsView::CSettingsView(QWidget *parent) :
+    QWidget(parent),
+    ui(new Ui::CSettingsView)
+{
+	ui->setupUi(this);
+
+	loadSettings();
+}
+
+CSettingsView::~CSettingsView()
+{
+	delete ui;
+}
+
+void CSettingsView::on_comboBoxResolution_currentIndexChanged(const QString &arg1)
+{
+	QStringList list = arg1.split("x");
+
+	Settings node = settings.write["video"]["screenRes"];
+	node["width"].Float() = list[0].toInt();
+	node["height"].Float() = list[1].toInt();
+}
+
+void CSettingsView::on_comboBoxFullScreen_currentIndexChanged(int index)
+{
+	Settings node = settings.write["video"]["fullscreen"];
+	node->Bool() = index;
+}
+
+void CSettingsView::on_comboBoxPlayerAI_currentIndexChanged(const QString &arg1)
+{
+	Settings node = settings.write["server"]["playerAI"];
+	node->String() = arg1.toUtf8().data();
+}
+
+void CSettingsView::on_comboBoxNeutralAI_currentIndexChanged(const QString &arg1)
+{
+	Settings node = settings.write["server"]["neutralAI"];
+	node->String() = arg1.toUtf8().data();
+}
+
+void CSettingsView::on_comboBoxEnableMods_currentIndexChanged(int index)
+{
+	Settings node = settings.write["launcher"]["enableInstalledMods"];
+	node->Bool() = index;
+}
+
+void CSettingsView::on_spinBoxNetworkPort_valueChanged(int arg1)
+{
+	Settings node = settings.write["server"]["port"];
+	node->Float() = arg1;
+}
+
+void CSettingsView::on_plainTextEditRepos_textChanged()
+{
+	Settings node = settings.write["launcher"]["repositoryURL"];
+
+	QStringList list = ui->plainTextEditRepos->toPlainText().split('\n');
+
+	node->Vector().clear();
+	for (QString line : list)
+	{
+		if (line.trimmed().size() > 0)
+		{
+			JsonNode entry;
+			entry.String() = line.trimmed().toUtf8().data();
+			node->Vector().push_back(entry);
+		}
+	}
+
+}

+ 34 - 0
launcher/settingsView/csettingsview.h

@@ -0,0 +1,34 @@
+#pragma once
+
+namespace Ui {
+	class CSettingsView;
+}
+
+class CSettingsView : public QWidget
+{
+	Q_OBJECT
+	
+public:
+	explicit CSettingsView(QWidget *parent = 0);
+	~CSettingsView();
+
+	void loadSettings();
+	
+private slots:
+	void on_comboBoxResolution_currentIndexChanged(const QString &arg1);
+
+	void on_comboBoxFullScreen_currentIndexChanged(int index);
+
+	void on_comboBoxPlayerAI_currentIndexChanged(const QString &arg1);
+
+	void on_comboBoxNeutralAI_currentIndexChanged(const QString &arg1);
+
+	void on_comboBoxEnableMods_currentIndexChanged(int index);
+
+	void on_spinBoxNetworkPort_valueChanged(int arg1);
+
+	void on_plainTextEditRepos_textChanged();
+
+private:
+	Ui::CSettingsView *ui;
+};

+ 367 - 0
launcher/settingsView/csettingsview.ui

@@ -0,0 +1,367 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>CSettingsView</class>
+ <widget class="QWidget" name="CSettingsView">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>700</width>
+    <height>303</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Form</string>
+  </property>
+  <layout class="QGridLayout" name="gridLayout">
+   <item row="1" column="4">
+    <widget class="QLineEdit" name="lineEditGameDir">
+     <property name="enabled">
+      <bool>false</bool>
+     </property>
+     <property name="minimumSize">
+      <size>
+       <width>150</width>
+       <height>0</height>
+      </size>
+     </property>
+     <property name="text">
+      <string>/usr/share/vcmi</string>
+     </property>
+     <property name="readOnly">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+   <item row="7" column="0">
+    <spacer name="spacerRepos">
+     <property name="orientation">
+      <enum>Qt::Vertical</enum>
+     </property>
+     <property name="sizeType">
+      <enum>QSizePolicy::Fixed</enum>
+     </property>
+     <property name="sizeHint" stdset="0">
+      <size>
+       <width>20</width>
+       <height>8</height>
+      </size>
+     </property>
+    </spacer>
+   </item>
+   <item row="1" column="0">
+    <widget class="QLabel" name="labelResolution">
+     <property name="text">
+      <string>Resolution</string>
+     </property>
+    </widget>
+   </item>
+   <item row="3" column="0">
+    <spacer name="spacerSections">
+     <property name="orientation">
+      <enum>Qt::Vertical</enum>
+     </property>
+     <property name="sizeType">
+      <enum>QSizePolicy::Fixed</enum>
+     </property>
+     <property name="sizeHint" stdset="0">
+      <size>
+       <width>56</width>
+       <height>8</height>
+      </size>
+     </property>
+    </spacer>
+   </item>
+   <item row="2" column="0">
+    <widget class="QLabel" name="labelFullScreen">
+     <property name="text">
+      <string>Fullscreen</string>
+     </property>
+    </widget>
+   </item>
+   <item row="5" column="4">
+    <widget class="QSpinBox" name="spinBoxNetworkPort">
+     <property name="minimum">
+      <number>1024</number>
+     </property>
+     <property name="maximum">
+      <number>65535</number>
+     </property>
+     <property name="value">
+      <number>3030</number>
+     </property>
+    </widget>
+   </item>
+   <item row="5" column="1">
+    <widget class="QComboBox" name="comboBoxPlayerAI">
+     <item>
+      <property name="text">
+       <string>VCAI</string>
+      </property>
+     </item>
+    </widget>
+   </item>
+   <item row="5" column="3">
+    <widget class="QLabel" name="labelNetworkPort">
+     <property name="text">
+      <string>Network port</string>
+     </property>
+    </widget>
+   </item>
+   <item row="1" column="1">
+    <widget class="QComboBox" name="comboBoxResolution">
+     <item>
+      <property name="text">
+       <string>800x600</string>
+      </property>
+     </item>
+     <item>
+      <property name="text">
+       <string>1024x600</string>
+      </property>
+     </item>
+     <item>
+      <property name="text">
+       <string>1024x768</string>
+      </property>
+     </item>
+     <item>
+      <property name="text">
+       <string>1280x800</string>
+      </property>
+     </item>
+     <item>
+      <property name="text">
+       <string>1280x960</string>
+      </property>
+     </item>
+     <item>
+      <property name="text">
+       <string>1280x1024</string>
+      </property>
+     </item>
+     <item>
+      <property name="text">
+       <string>1366x768</string>
+      </property>
+     </item>
+     <item>
+      <property name="text">
+       <string>1440x900</string>
+      </property>
+     </item>
+     <item>
+      <property name="text">
+       <string>1600x1200</string>
+      </property>
+     </item>
+     <item>
+      <property name="text">
+       <string>1680x1050</string>
+      </property>
+     </item>
+     <item>
+      <property name="text">
+       <string>1920x1080</string>
+      </property>
+     </item>
+    </widget>
+   </item>
+   <item row="2" column="3">
+    <widget class="QLabel" name="labelUserDataDir">
+     <property name="text">
+      <string>User data directory</string>
+     </property>
+    </widget>
+   </item>
+   <item row="6" column="4">
+    <widget class="QComboBox" name="comboBoxEnableMods">
+     <property name="currentIndex">
+      <number>1</number>
+     </property>
+     <item>
+      <property name="text">
+       <string>Off</string>
+      </property>
+     </item>
+     <item>
+      <property name="text">
+       <string>On</string>
+      </property>
+     </item>
+    </widget>
+   </item>
+   <item row="8" column="0" colspan="2">
+    <widget class="QLabel" name="labelRepositories">
+     <property name="font">
+      <font>
+       <weight>75</weight>
+       <bold>true</bold>
+      </font>
+     </property>
+     <property name="text">
+      <string>Repositories</string>
+     </property>
+    </widget>
+   </item>
+   <item row="10" column="0" colspan="5">
+    <widget class="QPlainTextEdit" name="plainTextEditRepos">
+     <property name="lineWrapMode">
+      <enum>QPlainTextEdit::NoWrap</enum>
+     </property>
+     <property name="plainText">
+      <string>http://downloads.vcmi.eu/Mods/repository.json</string>
+     </property>
+    </widget>
+   </item>
+   <item row="5" column="0">
+    <widget class="QLabel" name="labelPlayerAI">
+     <property name="text">
+      <string>Player AI</string>
+     </property>
+    </widget>
+   </item>
+   <item row="2" column="2">
+    <spacer name="spacerColumns">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+     <property name="sizeType">
+      <enum>QSizePolicy::Fixed</enum>
+     </property>
+     <property name="sizeHint" stdset="0">
+      <size>
+       <width>8</width>
+       <height>20</height>
+      </size>
+     </property>
+    </spacer>
+   </item>
+   <item row="2" column="1">
+    <widget class="QComboBox" name="comboBoxFullScreen">
+     <property name="currentIndex">
+      <number>0</number>
+     </property>
+     <item>
+      <property name="text">
+       <string>Off</string>
+      </property>
+     </item>
+     <item>
+      <property name="text">
+       <string>On</string>
+      </property>
+     </item>
+    </widget>
+   </item>
+   <item row="6" column="3">
+    <widget class="QLabel" name="labelEnableMods">
+     <property name="text">
+      <string>Enable mods on install</string>
+     </property>
+    </widget>
+   </item>
+   <item row="2" column="4">
+    <widget class="QLineEdit" name="lineEditUserDataDir">
+     <property name="enabled">
+      <bool>false</bool>
+     </property>
+     <property name="minimumSize">
+      <size>
+       <width>150</width>
+       <height>0</height>
+      </size>
+     </property>
+     <property name="text">
+      <string>/home/user/.vcmi</string>
+     </property>
+     <property name="readOnly">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+   <item row="6" column="0">
+    <widget class="QLabel" name="labelNeutralAI">
+     <property name="text">
+      <string>Neutral AI</string>
+     </property>
+    </widget>
+   </item>
+   <item row="6" column="1">
+    <widget class="QComboBox" name="comboBoxNeutralAI">
+     <item>
+      <property name="text">
+       <string>StupidAI</string>
+      </property>
+     </item>
+     <item>
+      <property name="text">
+       <string>BattleAI</string>
+      </property>
+     </item>
+    </widget>
+   </item>
+   <item row="1" column="3">
+    <widget class="QLabel" name="labelGameDir">
+     <property name="text">
+      <string>Game directory</string>
+     </property>
+    </widget>
+   </item>
+   <item row="4" column="0" colspan="2">
+    <widget class="QLabel" name="labelAISettings">
+     <property name="font">
+      <font>
+       <weight>75</weight>
+       <bold>true</bold>
+      </font>
+     </property>
+     <property name="text">
+      <string>AI Settings</string>
+     </property>
+    </widget>
+   </item>
+   <item row="0" column="0" colspan="2">
+    <widget class="QLabel" name="labelVideo">
+     <property name="font">
+      <font>
+       <weight>75</weight>
+       <bold>true</bold>
+      </font>
+     </property>
+     <property name="text">
+      <string>Video</string>
+     </property>
+    </widget>
+   </item>
+   <item row="0" column="3" colspan="2">
+    <widget class="QLabel" name="labelDataDirs">
+     <property name="font">
+      <font>
+       <weight>75</weight>
+       <bold>true</bold>
+      </font>
+     </property>
+     <property name="text">
+      <string>Data Directories (unchangeable)</string>
+     </property>
+    </widget>
+   </item>
+   <item row="4" column="3" colspan="2">
+    <widget class="QLabel" name="labelGeneral">
+     <property name="font">
+      <font>
+       <weight>75</weight>
+       <bold>true</bold>
+      </font>
+     </property>
+     <property name="text">
+      <string>General</string>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>