1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-08-13 19:54:17 +02:00

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

This commit is contained in:
Ivan Savenko
2013-08-22 14:22:49 +00:00
parent cf4b3c91cb
commit 7cbfdd509c
32 changed files with 3008 additions and 2 deletions

View File

@@ -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
launcher/CMakeLists.txt Normal file
View File

@@ -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
launcher/StdInc.cpp Normal file
View File

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

11
launcher/StdInc.h Normal file
View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 895 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

26
launcher/launcherdirs.cpp Normal file
View File

@@ -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
launcher/launcherdirs.h Normal file
View File

@@ -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
launcher/main.cpp Normal file
View File

@@ -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
launcher/mainwindow.cpp Normal file
View File

@@ -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
launcher/mainwindow.h Normal file
View File

@@ -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
launcher/mainwindow.ui Normal file
View File

@@ -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>

View File

@@ -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);
}

View File

@@ -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);
};

View File

@@ -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;
}

View File

@@ -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;
};

View File

@@ -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);
}

View File

@@ -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);
};

View File

@@ -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());
}

View File

@@ -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;
};

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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);
};

View File

@@ -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);
}
}
}

View File

@@ -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;
};

View File

@@ -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>