diff --git a/config/schemas/settings.json b/config/schemas/settings.json
index 412ec9263..fc41d1b4a 100644
--- a/config/schemas/settings.json
+++ b/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
}
}
}
diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt
new file mode 100644
index 000000000..b7fd754eb
--- /dev/null
+++ b/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()
+
diff --git a/launcher/StdInc.cpp b/launcher/StdInc.cpp
new file mode 100644
index 000000000..b64b59be5
--- /dev/null
+++ b/launcher/StdInc.cpp
@@ -0,0 +1 @@
+#include "StdInc.h"
diff --git a/launcher/StdInc.h b/launcher/StdInc.h
new file mode 100644
index 000000000..751c21f85
--- /dev/null
+++ b/launcher/StdInc.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "../Global.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
\ No newline at end of file
diff --git a/launcher/icons/menu-game.png b/launcher/icons/menu-game.png
new file mode 100644
index 000000000..5f632e2b7
Binary files /dev/null and b/launcher/icons/menu-game.png differ
diff --git a/launcher/icons/menu-mods.png b/launcher/icons/menu-mods.png
new file mode 100644
index 000000000..92e5deaad
Binary files /dev/null and b/launcher/icons/menu-mods.png differ
diff --git a/launcher/icons/menu-settings.png b/launcher/icons/menu-settings.png
new file mode 100644
index 000000000..4cce5d91e
Binary files /dev/null and b/launcher/icons/menu-settings.png differ
diff --git a/launcher/icons/mod-delete.png b/launcher/icons/mod-delete.png
new file mode 100644
index 000000000..fa0c04d95
Binary files /dev/null and b/launcher/icons/mod-delete.png differ
diff --git a/launcher/icons/mod-disabled.png b/launcher/icons/mod-disabled.png
new file mode 100644
index 000000000..14b11cdf4
Binary files /dev/null and b/launcher/icons/mod-disabled.png differ
diff --git a/launcher/icons/mod-download.png b/launcher/icons/mod-download.png
new file mode 100644
index 000000000..15644a404
Binary files /dev/null and b/launcher/icons/mod-download.png differ
diff --git a/launcher/icons/mod-enabled.png b/launcher/icons/mod-enabled.png
new file mode 100644
index 000000000..40204a56f
Binary files /dev/null and b/launcher/icons/mod-enabled.png differ
diff --git a/launcher/icons/mod-update.png b/launcher/icons/mod-update.png
new file mode 100644
index 000000000..b8bdbc6a2
Binary files /dev/null and b/launcher/icons/mod-update.png differ
diff --git a/launcher/launcherdirs.cpp b/launcher/launcherdirs.cpp
new file mode 100644
index 000000000..ae4b6d72a
--- /dev/null
+++ b/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";
+}
diff --git a/launcher/launcherdirs.h b/launcher/launcherdirs.h
new file mode 100644
index 000000000..2b692e039
--- /dev/null
+++ b/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();
+};
diff --git a/launcher/main.cpp b/launcher/main.cpp
new file mode 100644
index 000000000..0063be919
--- /dev/null
+++ b/launcher/main.cpp
@@ -0,0 +1,11 @@
+#include "mainwindow.h"
+#include
+
+int main(int argc, char *argv[])
+{
+ QApplication a(argc, argv);
+ MainWindow w;
+ w.show();
+
+ return a.exec();
+}
diff --git a/launcher/mainwindow.cpp b/launcher/mainwindow.cpp
new file mode 100644
index 000000000..d02393877
--- /dev/null
+++ b/launcher/mainwindow.cpp
@@ -0,0 +1,77 @@
+#include "StdInc.h"
+#include "mainwindow.h"
+#include "ui_mainwindow.h"
+
+#include
+#include
+
+#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;
+ }
+
+}
diff --git a/launcher/mainwindow.h b/launcher/mainwindow.h
new file mode 100644
index 000000000..e1c994c99
--- /dev/null
+++ b/launcher/mainwindow.h
@@ -0,0 +1,25 @@
+#pragma once
+#include
+
+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;
+};
diff --git a/launcher/mainwindow.ui b/launcher/mainwindow.ui
new file mode 100644
index 000000000..f2d65c8c9
--- /dev/null
+++ b/launcher/mainwindow.ui
@@ -0,0 +1,206 @@
+
+
+ MainWindow
+
+
+
+ 0
+ 0
+ 800
+ 480
+
+
+
+
+ 0
+ 0
+
+
+
+ VCMI Launcher
+
+
+
+ icons:menu-game.pngicons:menu-game.png
+
+
+
+ 64
+ 64
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 65
+ 16777215
+
+
+
+ Qt::ScrollBarAlwaysOff
+
+
+ Qt::ScrollBarAlwaysOff
+
+
+ QAbstractItemView::NoEditTriggers
+
+
+ false
+
+
+ QAbstractItemView::NoDragDrop
+
+
+ QAbstractItemView::SelectRows
+
+
+
+ 48
+ 64
+
+
+
+ QListView::Static
+
+
+ QListView::Fixed
+
+
+ 0
+
+
+
+ 64
+ 64
+
+
+
+ QListView::IconMode
+
+
+ true
+
+
+ true
+
+
+ false
+
+
+ -1
+
+
-
+
+ Mods
+
+
+
+ icons:menu-mods.pngicons:menu-mods.png
+
+
+ -
+
+ Settings
+
+
+
+ icons:menu-settings.pngicons:menu-settings.png
+
+
+
+
+ -
+
+
+ Play
+
+
+
+ icons:menu-game.pngicons:menu-game.png
+
+
+
+ 60
+ 60
+
+
+
+ false
+
+
+ false
+
+
+ Qt::ToolButtonIconOnly
+
+
+
+ -
+
+
+
+ 75
+ true
+
+
+
+ Start game
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+ true
+
+
+
+ 0
+ 0
+
+
+
+ 1
+
+
+
+
+
+
+
+
+
+
+
+ CModListView
+ QWidget
+ modManager/cmodlistview.h
+ 1
+
+
+ CSettingsView
+ QWidget
+ settingsView/csettingsview.h
+ 1
+
+
+
+ tabSelectList
+ startGameButon
+
+
+
+
diff --git a/launcher/modManager/cdownloadmanager.cpp b/launcher/modManager/cdownloadmanager.cpp
new file mode 100644
index 000000000..0d98dfc46
--- /dev/null
+++ b/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(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);
+}
diff --git a/launcher/modManager/cdownloadmanager.h b/launcher/modManager/cdownloadmanager.h
new file mode 100644
index 000000000..1f2cc4f7c
--- /dev/null
+++ b/launcher/modManager/cdownloadmanager.h
@@ -0,0 +1,56 @@
+#pragma once
+
+#include
+#include
+
+class QFile;
+
+class CDownloadManager: public QObject
+{
+ Q_OBJECT
+
+ struct FileEntry
+ {
+ enum Status
+ {
+ IN_PROGRESS,
+ FINISHED,
+ FAILED
+ };
+
+ QNetworkReply * reply;
+ QSharedPointer file;
+ Status status;
+ qint64 bytesReceived;
+ qint64 totalSize;
+ };
+
+ QStringList encounteredErrors;
+
+ QNetworkAccessManager manager;
+
+ QList 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);
+};
diff --git a/launcher/modManager/cmodlist.cpp b/launcher/modManager/cmodlist.cpp
new file mode 100644
index 000000000..3c503fe8b
--- /dev/null
+++ b/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 CModList::getModList() const
+{
+ QSet knownMods;
+ QVector 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;
+}
diff --git a/launcher/modManager/cmodlist.h b/launcher/modManager/cmodlist.h
new file mode 100644
index 000000000..6e2bdf12c
--- /dev/null
+++ b/launcher/modManager/cmodlist.h
@@ -0,0 +1,77 @@
+#pragma once
+
+#include
+#include
+#include
+
+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 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 getModList() const;
+};
\ No newline at end of file
diff --git a/launcher/modManager/cmodlistmodel.cpp b/launcher/modManager/cmodlistmodel.cpp
new file mode 100644
index 000000000..141f42c26
--- /dev/null
+++ b/launcher/modManager/cmodlistmodel.cpp
@@ -0,0 +1,193 @@
+#include "StdInc.h"
+#include "cmodlistmodel.h"
+
+#include
+
+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);
+}
diff --git a/launcher/modManager/cmodlistmodel.h b/launcher/modManager/cmodlistmodel.h
new file mode 100644
index 000000000..5dc705806
--- /dev/null
+++ b/launcher/modManager/cmodlistmodel.h
@@ -0,0 +1,68 @@
+#pragma once
+
+#include "cmodlist.h"
+
+#include
+#include
+
+namespace ModFields
+{
+ enum EModFields
+ {
+ STATUS_ENABLED,
+ STATUS_UPDATE,
+ TYPE,
+ NAME,
+ VERSION,
+ SIZE,
+ AUTHOR,
+ COUNT
+ };
+}
+
+class CModListModel : public QAbstractTableModel, public CModList
+{
+ Q_OBJECT
+
+ QVector 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);
+};
diff --git a/launcher/modManager/cmodlistview.cpp b/launcher/modManager/cmodlistview.cpp
new file mode 100644
index 000000000..9495ff322
--- /dev/null
+++ b/launcher/modManager/cmodlistview.cpp
@@ -0,0 +1,518 @@
+#include "StdInc.h"
+#include "cmodlistview.h"
+#include "ui_cmodlistview.h"
+
+#include
+#include
+
+#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())
+ return pattern.arg(value.toStringList().join(", "));
+
+ if (value.canConvert())
+ 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())
+ {
+ 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 = "%1: "; // shared prefix
+ QString lineTemplate = prefix + "%2
";
+ QString urlTemplate = prefix + "%2
";
+ QString textTemplate = prefix + "%2
";
+ QString noteTemplate = "%1: %2
";
+
+ QString result;
+
+ result += "";
+ 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 += ""; // 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 += "";
+ 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; iinstallMod(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());
+}
diff --git a/launcher/modManager/cmodlistview.h b/launcher/modManager/cmodlistview.h
new file mode 100644
index 000000000..1fab2f4d4
--- /dev/null
+++ b/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;
+};
diff --git a/launcher/modManager/cmodlistview.ui b/launcher/modManager/cmodlistview.ui
new file mode 100644
index 000000000..53d64b969
--- /dev/null
+++ b/launcher/modManager/cmodlistview.ui
@@ -0,0 +1,453 @@
+
+
+ CModListView
+
+
+
+ 0
+ 0
+ 596
+ 342
+
+
+
+ Form
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+ 6
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 16777215
+
+
+
+
+
+
+ Filter
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 16777215
+
+
+
+ 0
+
+
-
+
+ All mods
+
+
+ -
+
+ Downloadable
+
+
+ -
+
+ Installed
+
+
+ -
+
+ Updatable
+
+
+ -
+
+ Active
+
+
+ -
+
+ Inactive
+
+
+
+
+ -
+
+
+ QAbstractItemView::SingleSelection
+
+
+ QAbstractItemView::SelectRows
+
+
+
+ 32
+ 20
+
+
+
+ QAbstractItemView::ScrollPerItem
+
+
+ QAbstractItemView::ScrollPerPixel
+
+
+ true
+
+
+ true
+
+
+ false
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 16
+ 100
+
+
+
+
+
+
+ true
+
+
+ Qt::RightArrow
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 16777215
+
+
+
+ Qt::LeftToRight
+
+
+ false
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ true
+
+
+ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
+<html><head><meta name="qrichtext" content="1" /><style type="text/css">
+p, li { white-space: pre-wrap; }
+</style></head><body style=" font-family:'Ubuntu'; font-size:11pt; font-weight:400; font-style:normal;">
+<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p></body></html>
+
+
+ true
+
+
+ true
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::MinimumExpanding
+
+
+
+ 0
+ 20
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 51
+ 0
+
+
+
+
+ 100
+ 16777215
+
+
+
+ Enable
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 51
+ 0
+
+
+
+
+ 100
+ 16777215
+
+
+
+ Disable
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 51
+ 0
+
+
+
+
+ 100
+ 16777215
+
+
+
+ Update
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 51
+ 0
+
+
+
+
+ 100
+ 16777215
+
+
+
+ Uninstall
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 51
+ 0
+
+
+
+
+ 100
+ 16777215
+
+
+
+ Install
+
+
+
+
+
+
+ -
+
+
+ true
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+
+
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ 0
+
+
+ 0
+
+
+ true
+
+
+ false
+
+
+ %p% (%v KB out of %m KB)
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Abort
+
+
+
+
+
+
+
+
+
+ lineEdit
+ comboBox
+ allModsView
+ textBrowser
+ hideModInfoButton
+ enableButton
+ disableButton
+ updateButton
+ uninstallButton
+ installButton
+
+
+
+
diff --git a/launcher/modManager/cmodmanager.cpp b/launcher/modManager/cmodmanager.cpp
new file mode 100644
index 000000000..7c4d18fb5
--- /dev/null
+++ b/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 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(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;
+}
diff --git a/launcher/modManager/cmodmanager.h b/launcher/modManager/cmodmanager.h
new file mode 100644
index 000000000..bb787a84d
--- /dev/null
+++ b/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);
+};
diff --git a/launcher/settingsView/csettingsview.cpp b/launcher/settingsView/csettingsview.cpp
new file mode 100644
index 000000000..1dce1f976
--- /dev/null
+++ b/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);
+ }
+ }
+
+}
diff --git a/launcher/settingsView/csettingsview.h b/launcher/settingsView/csettingsview.h
new file mode 100644
index 000000000..419b91302
--- /dev/null
+++ b/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;
+};
diff --git a/launcher/settingsView/csettingsview.ui b/launcher/settingsView/csettingsview.ui
new file mode 100644
index 000000000..d28ce195c
--- /dev/null
+++ b/launcher/settingsView/csettingsview.ui
@@ -0,0 +1,367 @@
+
+
+ CSettingsView
+
+
+
+ 0
+ 0
+ 700
+ 303
+
+
+
+ Form
+
+
+ -
+
+
+ false
+
+
+
+ 150
+ 0
+
+
+
+ /usr/share/vcmi
+
+
+ true
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+ QSizePolicy::Fixed
+
+
+
+ 20
+ 8
+
+
+
+
+ -
+
+
+ Resolution
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+ QSizePolicy::Fixed
+
+
+
+ 56
+ 8
+
+
+
+
+ -
+
+
+ Fullscreen
+
+
+
+ -
+
+
+ 1024
+
+
+ 65535
+
+
+ 3030
+
+
+
+ -
+
+
-
+
+ VCAI
+
+
+
+
+ -
+
+
+ Network port
+
+
+
+ -
+
+
-
+
+ 800x600
+
+
+ -
+
+ 1024x600
+
+
+ -
+
+ 1024x768
+
+
+ -
+
+ 1280x800
+
+
+ -
+
+ 1280x960
+
+
+ -
+
+ 1280x1024
+
+
+ -
+
+ 1366x768
+
+
+ -
+
+ 1440x900
+
+
+ -
+
+ 1600x1200
+
+
+ -
+
+ 1680x1050
+
+
+ -
+
+ 1920x1080
+
+
+
+
+ -
+
+
+ User data directory
+
+
+
+ -
+
+
+ 1
+
+
-
+
+ Off
+
+
+ -
+
+ On
+
+
+
+
+ -
+
+
+
+ 75
+ true
+
+
+
+ Repositories
+
+
+
+ -
+
+
+ QPlainTextEdit::NoWrap
+
+
+ http://downloads.vcmi.eu/Mods/repository.json
+
+
+
+ -
+
+
+ Player AI
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::Fixed
+
+
+
+ 8
+ 20
+
+
+
+
+ -
+
+
+ 0
+
+
-
+
+ Off
+
+
+ -
+
+ On
+
+
+
+
+ -
+
+
+ Enable mods on install
+
+
+
+ -
+
+
+ false
+
+
+
+ 150
+ 0
+
+
+
+ /home/user/.vcmi
+
+
+ true
+
+
+
+ -
+
+
+ Neutral AI
+
+
+
+ -
+
+
-
+
+ StupidAI
+
+
+ -
+
+ BattleAI
+
+
+
+
+ -
+
+
+ Game directory
+
+
+
+ -
+
+
+
+ 75
+ true
+
+
+
+ AI Settings
+
+
+
+ -
+
+
+
+ 75
+ true
+
+
+
+ Video
+
+
+
+ -
+
+
+
+ 75
+ true
+
+
+
+ Data Directories (unchangeable)
+
+
+
+ -
+
+
+
+ 75
+ true
+
+
+
+ General
+
+
+
+
+
+
+
+