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