From 7cbfdd509c14429e1462b96bcd5fc4e35bdac593 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 22 Aug 2013 14:22:49 +0000 Subject: [PATCH] VCMI Launcher/Mod manager. See forum post for details. --- config/schemas/settings.json | 8 +- launcher/CMakeLists.txt | 58 +++ launcher/StdInc.cpp | 1 + launcher/StdInc.h | 11 + launcher/icons/menu-game.png | Bin 0 -> 5482 bytes launcher/icons/menu-mods.png | Bin 0 -> 2986 bytes launcher/icons/menu-settings.png | Bin 0 -> 4474 bytes launcher/icons/mod-delete.png | Bin 0 -> 1449 bytes launcher/icons/mod-disabled.png | Bin 0 -> 1604 bytes launcher/icons/mod-download.png | Bin 0 -> 895 bytes launcher/icons/mod-enabled.png | Bin 0 -> 1104 bytes launcher/icons/mod-update.png | Bin 0 -> 1341 bytes launcher/launcherdirs.cpp | 26 ++ launcher/launcherdirs.h | 13 + launcher/main.cpp | 11 + launcher/mainwindow.cpp | 77 ++++ launcher/mainwindow.h | 25 ++ launcher/mainwindow.ui | 206 +++++++++ launcher/modManager/cdownloadmanager.cpp | 114 +++++ launcher/modManager/cdownloadmanager.h | 56 +++ launcher/modManager/cmodlist.cpp | 204 +++++++++ launcher/modManager/cmodlist.h | 77 ++++ launcher/modManager/cmodlistmodel.cpp | 193 +++++++++ launcher/modManager/cmodlistmodel.h | 68 +++ launcher/modManager/cmodlistview.cpp | 518 +++++++++++++++++++++++ launcher/modManager/cmodlistview.h | 84 ++++ launcher/modManager/cmodlistview.ui | 453 ++++++++++++++++++++ launcher/modManager/cmodmanager.cpp | 256 +++++++++++ launcher/modManager/cmodmanager.h | 38 ++ launcher/settingsView/csettingsview.cpp | 112 +++++ launcher/settingsView/csettingsview.h | 34 ++ launcher/settingsView/csettingsview.ui | 367 ++++++++++++++++ 32 files changed, 3008 insertions(+), 2 deletions(-) create mode 100644 launcher/CMakeLists.txt create mode 100644 launcher/StdInc.cpp create mode 100644 launcher/StdInc.h create mode 100644 launcher/icons/menu-game.png create mode 100644 launcher/icons/menu-mods.png create mode 100644 launcher/icons/menu-settings.png create mode 100644 launcher/icons/mod-delete.png create mode 100644 launcher/icons/mod-disabled.png create mode 100644 launcher/icons/mod-download.png create mode 100644 launcher/icons/mod-enabled.png create mode 100644 launcher/icons/mod-update.png create mode 100644 launcher/launcherdirs.cpp create mode 100644 launcher/launcherdirs.h create mode 100644 launcher/main.cpp create mode 100644 launcher/mainwindow.cpp create mode 100644 launcher/mainwindow.h create mode 100644 launcher/mainwindow.ui create mode 100644 launcher/modManager/cdownloadmanager.cpp create mode 100644 launcher/modManager/cdownloadmanager.h create mode 100644 launcher/modManager/cmodlist.cpp create mode 100644 launcher/modManager/cmodlist.h create mode 100644 launcher/modManager/cmodlistmodel.cpp create mode 100644 launcher/modManager/cmodlistmodel.h create mode 100644 launcher/modManager/cmodlistview.cpp create mode 100644 launcher/modManager/cmodlistview.h create mode 100644 launcher/modManager/cmodlistview.ui create mode 100644 launcher/modManager/cmodmanager.cpp create mode 100644 launcher/modManager/cmodmanager.h create mode 100644 launcher/settingsView/csettingsview.cpp create mode 100644 launcher/settingsView/csettingsview.h create mode 100644 launcher/settingsView/csettingsview.ui 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 0000000000000000000000000000000000000000..5f632e2b71bab48cd3c01d300a8146267c7c193a GIT binary patch literal 5482 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 literal 0 HcmV?d00001 diff --git a/launcher/icons/menu-settings.png b/launcher/icons/menu-settings.png new file mode 100644 index 0000000000000000000000000000000000000000..4cce5d91ea81b0b051dfd7736f7beeda24380e0d GIT binary patch literal 4474 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 M07*qoM6N<$g8#!*Hvj+t literal 0 HcmV?d00001 diff --git a/launcher/icons/mod-delete.png b/launcher/icons/mod-delete.png new file mode 100644 index 0000000000000000000000000000000000000000..fa0c04d95bd286e97de68da3b40bc1cfd0dbcfb9 GIT binary patch literal 1449 zcmV;a1y=frP)zs}v|HDFrc-hzT#gh&~y7Ly3t7qk>Rx(HIkm z62M4;FC<_hFD4igee!`sF(?wTkwWh@P+IA3cei_X&wXb6XZCbygJOK}B>&DiGv9pQ zf0_BS0RQtt9<(^HbzRz^{&U1IUck=|!g#%$7aA>qfMMbLUm3(W*LLxb{C^U-($kYj z8ume<(|xJ6oiw?jGahMf1}za+>&&vipZXKZ{R4LS#vlX6kEBHLlh#X<_P+{Tf4sAe zaq@kly`!aJ`?feW5K_*8aLNGOuYhO-NGu7EOapg0+tX!FBUO}B31Hfz#op)huItSLA1jj5p_bmB5c(T@3-(57e*ejM+nj5I7#Do58fu;L*O1V9KZg zO9^<&87|hGlO8R zHb|Q}aSZ&~EX2}{u=UtU$Tl@A+j{gQMAK=tHgn>56)(-p%s{-cF~K<9CAoF^<1?~e zk47Pw%3|Rd5Iije3+mpw4U_L4gw{hxAkxqPTMi$s)T^cjy)D3<_xcn@$WtW?FUNcl z-cI6N8tY*Ys=gtNK%RW&xQg>$1RQdXY2*DN zRZpZ?+4Ui_WGOsx@(YM!isS*17Ok&`Z8(>0TBb03VHC*Wlq?7YnXL*4K?cl%iQ-xX z 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 literal 0 HcmV?d00001 diff --git a/launcher/icons/mod-enabled.png b/launcher/icons/mod-enabled.png new file mode 100644 index 0000000000000000000000000000000000000000..40204a56f4f2a67fae6c199ac8ac30be8dea745f GIT binary patch literal 1104 zcmV-W1h4yvP)0tFiqx|hd}<3erfn=qt&NE_Y22jAp4`vw+1+!_%=off)+BAv_0d24 zABLIve$4m(7~ua}r%1U1FMM=|PJOY{0I3sPygpr3``M8vkMQC?Ef*lw=u2Cw>l&VW zxTCVZx~98kX>z=)r)}4N0@!zG=LSZ|Tf3WEjewCsNBeHQqkT^eW%#CeMkxoN5I51% z(4_cv0o(Z#0AOV;p;Gd5*)MqhaNA~8)t=eW*kt6~2~gsLkYW5Va5nEaqO)v(kJ1m? zo0@zeOW?wW#^N|LJmh$u^JUM0Gb3dIx_Vl6=!&+hv96A#b4duHW7(GI`}LA1^ZD0G z&;E0O1W{Gi`@r^X{=A!l<7UA~6@I)hXiLrycE5Pu{M($?(nusgQ?b@pDJ6Pz#&|c8 zIN@DOPH*cY!BFt_+GrKYWG_QuLCh`NxRji5xHR5hskP<{X5;t19j&NH)W$ZxF?UnG z@7WLA;sCJstG!I2^y8h4n}c?K2CnCUkqAx={gUAlZywrzBE2eUtNm^3&=lpBJ@;%g zG?ijF8Qgwucx&Me}#_(mnD-x)f2VdR(x?lsrn8cb)W;Rz0%`O!CgG0i>sa_RSQLyIUSL%$^_ zb3&>Rl48kDA{21RL(L6F+t!+(u5;MVA_(zgEM-c|&W`pR=>PV5nOFt5x}o;LDRbe| z;{(GP$MJ)}hji8im$TSZ87Ap$3Lcl>o{Dq7CvDF8)Box>-QU}?Poc`emMzu(3O|r@ z7eNSxlpMMm!FVd?j84uS>+bA-;zj_r29N?!&wf8Oc-Q@#PS04@V+x~6BoLyGmxYuR zTI#u+$ncFl~X9VsORK!gxPN{LV?L~PqeDd3tlkw}C{ zDT$Pll+I^%BAuNY%za&bckt#}+pS-oci%br`IVmtA%YOXDW!r^DilTGnx+ZMvIGDQ zK#&#wvqIBYEXFwJR0u(f&;l5Q5Ls+WsbGu=(=<7NqA)McgRGOV(q0*9Q4lHZRrMEM WTe#;$n{L(s00005I1-UB5ex=H zbavvp!=0tNw7!5kT-MzQH|#`Tp^I8t&0DORVK8b1 zu7^DBVd?WY;e$_(BteJgh7_ez?q^WoIrlR9`3&C^$%r9C6-1Fsv#58{R5Sm_&14G9 z7bKw2=kCFAY;Y(MOC@7<8ESe=wePlRK!zT}r=BH21uTGo+;6D~2VRRM2f8EKapZdf z3T$`Si-SDnry4u$Pk=3D+0=?0Dm0z;<9rWkL*~b$fCv@Xy4h<%!ob-8Li{Px8J?=b zB#`kws9l((s=;3POW{NS`sFGW5aHM>CyALCRId-NQ7dz)huK6$Q*jg9QlJeR?B=<&zfvxfspCv`YKVaU(d7}&$OA$w3$jfc1k$R8lI=*?~`nKOgKIkVO{6RhxARh{z8xETr37;Q}oF6;4 zFrKt9Ay|@JUy|f4&z7&s>lF)sDHeMai+#$Km&%p#5AWY@Y$!K2K5TAo;*a?D!6$Vl z%H72SgH==4z~QvCw6qZfPau&|ltQ6U=?s>hp1!_;vB@^G?K^i{^LRl){NM{0V=rHc zPt47$t*x(bXl!lk9~>GP9eXuC^?G_nDwEI6%`ZW``xwhBA7d3_ZB3z2u0wn`;-nIuo0j;$-S-j9Nx{S_(*u zYEF6kh{Pjy?5uvn4DWPF0G&rwUJ6sz*I6X%|JXoYBRAXL?m4p zyJ5Z5E7lx_YGX#?j%)G>1Kct~xsiLr8*#BHrv{ + +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 + + + + + + + +