MainWindow.cpp 51 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092
  1. /*
  2. * Copyright (C) 2022 The V-Gears Team
  3. *
  4. * This file is part of V-Gears
  5. *
  6. * V-Gears is free software: you can redistribute it and/or modify it under
  7. * terms of the GNU General Public License as published by the Free Software
  8. * Foundation, version 3.0 (GPLv3) of the License.
  9. *
  10. * V-Gears is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. */
  15. #include <iostream>
  16. #include <sstream>
  17. #include <iomanip>
  18. #include <QtCore/QProcess>
  19. #include <QtWidgets/QFileDialog>
  20. #include <QtCore/QDir>
  21. #include <QtCore/QSettings>
  22. #include <QtWidgets/QMessageBox>
  23. #include <QtCore/QTimer>
  24. #include <QtWidgets/QCheckBox>
  25. #include <QtWidgets/QPushButton>
  26. #include <QtGui/QDesktopServices>
  27. #include <QtCore/QUrl>
  28. #include <QtWidgets/QComboBox>
  29. #include <QtWidgets/QDialog>
  30. #include <QtWidgets/QDialogButtonBox>
  31. #include <QtWidgets/QTextBrowser>
  32. #include <QtWidgets/QVBoxLayout>
  33. #include <QtCore/QCoreApplication>
  34. #include <QtCore/QDirIterator>
  35. #include <QtCore/QFile>
  36. #include <QtCore/QFileInfo>
  37. #include <QtCore/QStorageInfo>
  38. #include <chrono>
  39. #include <OgrePrerequisites.h>
  40. #include "common/DataDirectory.h"
  41. #include "common/OgrePlugins.h"
  42. #include "DataInstaller.h"
  43. #include "ExternalTools.h"
  44. #include "InstallLayout.h"
  45. #include "InstallLog.h"
  46. #include "InstallOptions.h"
  47. #include "InstallVerifier.h"
  48. #include "MainWindow.h"
  49. #include "MovieCamera.h"
  50. #include <ui_MainWindow.h>
  51. namespace{
  52. /**
  53. * Converts the movies in a folder to what the game plays: frames and sound. The game can't
  54. * decode the original videos. The movies that already have a converted folder are skipped,
  55. * and the originals are kept so they are not extracted again.
  56. *
  57. * @param[in] video_dir Folder with the extracted movies.
  58. */
  59. void ConvertMovies(const std::string& video_dir){
  60. const ExternalTools::Tool ffmpeg = ExternalTools::FindFfmpeg(ExternalTools::CurrentLocations());
  61. if (!ffmpeg.usable){
  62. InstallLog::Get().Add(
  63. "[WARNING] FFmpeg can't be used, so the videos are not converted and the game can't play them: "
  64. + ffmpeg.problem
  65. );
  66. return;
  67. }
  68. QDir directory(QString::fromStdString(video_dir));
  69. for (const QString& file : directory.entryList({"*.avi"}, QDir::Files)){
  70. const std::string name = QFileInfo(file).completeBaseName().toStdString();
  71. const std::string destination = video_dir + name;
  72. if (QFileInfo::exists(QString::fromStdString(destination + "/movie.xml"))) continue;
  73. InstallLog::Get().Add("Converting " + name + ".avi...");
  74. std::string error;
  75. if (ExternalTools::ConvertMovie(
  76. ffmpeg.path, video_dir + file.toStdString(), destination, name,
  77. MovieCamera::LoadCamera(video_dir + "moviecam.lgp", name), &error
  78. ))
  79. InstallLog::Get().Add("Converted " + name);
  80. else InstallLog::Get().Add("[WARNING] Could not convert " + name + ": " + error);
  81. }
  82. }
  83. }
  84. /**
  85. * Indicates if an installer has already been created.
  86. */
  87. static bool installer_created = false;
  88. /**
  89. * Finds the V-Gears "data" folder shipped with the game.
  90. *
  91. * It contains the configuration files and plugins (copied to the installation directory) and a "data" subfolder with
  92. * the game data (copied to <installation directory>/data).
  93. *
  94. * @return Path of the folder, or an empty string if it can't be found.
  95. */
  96. static QString FindGameDataDir(){
  97. // The same places the game copies its files from when the data directory is empty, then the current directory.
  98. std::vector<boost::filesystem::path> candidates = VGears::DataDirectory::TemplateCandidates(
  99. boost::filesystem::path(QCoreApplication::applicationDirPath().toStdString())
  100. );
  101. candidates.emplace_back((QDir::currentPath() + "/data").toStdString());
  102. candidates.emplace_back((QDir::currentPath() + "/../data").toStdString());
  103. const boost::filesystem::path found = VGears::DataDirectory::FindTemplate(candidates, boost::filesystem::path());
  104. return found.empty() ? QString() : QDir::cleanPath(QString::fromStdString(found.string()));
  105. }
  106. #ifndef VGEARS_OGRE_PLUGIN_DIR
  107. #define VGEARS_OGRE_PLUGIN_DIR ""
  108. #endif
  109. /**
  110. * Names of the plugins in the plugins.cfg of a folder that the game won't find.
  111. *
  112. * The game looks for them in the same places (see VGears::OgrePlugins::SearchDirectories), so they don't need to be
  113. * copied to the installation directory.
  114. */
  115. static QStringList MissingOgrePlugins(const QString& source_dir){
  116. namespace bfs = boost::filesystem;
  117. QStringList missing;
  118. const bfs::path dir(source_dir.toStdString());
  119. VGears::OgrePlugins::Config config;
  120. if (!VGears::OgrePlugins::ReadConfig(dir / "plugins.cfg", config)) return missing;
  121. const std::vector<bfs::path> directories = VGears::OgrePlugins::SearchDirectories(
  122. dir, config.folder, bfs::path(QCoreApplication::applicationDirPath().toStdString()), VGEARS_OGRE_PLUGIN_DIR
  123. );
  124. const std::string version = std::to_string(OGRE_VERSION_MAJOR) + "." + std::to_string(OGRE_VERSION_MINOR) + "."
  125. + std::to_string(OGRE_VERSION_PATCH);
  126. for (const std::string& name : config.plugins){
  127. if (VGears::OgrePlugins::Find(name, directories, version).empty()) missing << QString::fromStdString(name);
  128. }
  129. return missing;
  130. }
  131. /**
  132. * Minimum free space needed besides the ISO extraction: converted data, intermediate files and logs.
  133. */
  134. static const qint64 kRequiredFreeSpace = 3LL * 1024 * 1024 * 1024;
  135. /**
  136. * Result of the checks done before starting an installation.
  137. */
  138. struct PreflightResult{
  139. QStringList errors;
  140. QStringList warnings;
  141. };
  142. /**
  143. * Checks that an installation can succeed before anything is extracted or written.
  144. *
  145. * @param[in] disk_image The selected disk image.
  146. * @param[in] output Absolute installation directory, ending with '/'.
  147. * @param[in] copy_data True if the V-Gears data files will be copied.
  148. * @param[in] game_data_dir The V-Gears "data" folder to copy (only used if copy_data is true).
  149. */
  150. static PreflightResult RunPreflight(
  151. DiskImage& disk_image, const QString& output, bool copy_data, const QString& game_data_dir
  152. ){
  153. PreflightResult result;
  154. const QString iso_path = QString::fromStdString(disk_image.getPath());
  155. // Input.
  156. if (iso_path.isEmpty()) result.errors << QObject::tr("No ISO image selected.");
  157. else if (!QFileInfo(iso_path).isReadable()) result.errors << QObject::tr("The ISO image can't be read: %1").arg(iso_path);
  158. else if (!disk_image.isValid()) result.errors << QObject::tr("The selected ISO is not a valid Final Fantasy VII image.");
  159. else if (!disk_image.isSupported()) result.warnings << QString::fromStdString(disk_image.getWarningMessage());
  160. // Output: it must be creatable and writable.
  161. if (!QDir().mkpath(output)) result.errors << QObject::tr("The installation directory can't be created: %1").arg(output);
  162. else{
  163. QFile probe(output + ".v-gears-write-test");
  164. if (!probe.open(QIODevice::WriteOnly)) result.errors << QObject::tr("The installation directory is not writable: %1").arg(output);
  165. else{
  166. probe.close();
  167. probe.remove();
  168. }
  169. // Free space: the extracted ISO plus the converted data.
  170. QStorageInfo storage(output);
  171. const qint64 needed = kRequiredFreeSpace + (iso_path.isEmpty() ? 0 : QFileInfo(iso_path).size());
  172. if (storage.isValid() && storage.bytesAvailable() < needed){
  173. result.errors << QObject::tr("Not enough free space in %1: %2 MB available, about %3 MB needed.")
  174. .arg(storage.rootPath()).arg(storage.bytesAvailable() / (1024 * 1024)).arg(needed / (1024 * 1024));
  175. }
  176. }
  177. // Configuration and plugins.
  178. if (copy_data){
  179. if (game_data_dir.isEmpty() || !QFile::exists(game_data_dir + "/plugins.cfg") || !QDir(game_data_dir + "/data").exists())
  180. result.errors << QObject::tr("The V-Gears 'data' folder was not found (it must contain plugins.cfg and a data folder).");
  181. else{
  182. const QStringList missing = MissingOgrePlugins(game_data_dir);
  183. if (!missing.isEmpty())
  184. result.errors << QObject::tr("Ogre plugins not found in the data folder or the system: %1").arg(missing.join(", "));
  185. }
  186. }
  187. else{
  188. if (!QFile::exists(output + "plugins.cfg") || !QFile::exists(output + "resources.cfg"))
  189. result.errors << QObject::tr("The installation directory has no plugins.cfg and resources.cfg. Enable 'Copy V-Gears data files'.");
  190. else{
  191. const QStringList missing = MissingOgrePlugins(output);
  192. if (!missing.isEmpty())
  193. result.errors << QObject::tr("Ogre plugins not found in the installation directory or the system: %1").arg(missing.join(", "));
  194. }
  195. }
  196. return result;
  197. }
  198. /**
  199. * Recursively copies a directory, overwriting existing files.
  200. *
  201. * @return True if every file was copied.
  202. */
  203. static bool CopyDirectory(const QString& src, const QString& dst){
  204. const QDir src_dir(src);
  205. if (!QDir().mkpath(dst)) return false;
  206. bool ok = true;
  207. QDirIterator it(src, QDir::Files | QDir::NoDotAndDotDot, QDirIterator::Subdirectories);
  208. while (it.hasNext()){
  209. const QString file = it.next();
  210. const QString target = dst + "/" + src_dir.relativeFilePath(file);
  211. QDir().mkpath(QFileInfo(target).absolutePath());
  212. if (QFile::exists(target)) QFile::remove(target);
  213. if (!QFile::copy(file, target)){
  214. std::cout << "Could not copy " << file.toStdString() << " to " << target.toStdString() << std::endl;
  215. ok = false;
  216. }
  217. }
  218. return ok;
  219. }
  220. MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), main_window_(new Ui::MainWindow){
  221. main_window_->setupUi(this);
  222. main_window_->advancedOptions->setVisible(main_window_->chk_advanced_options->isChecked());
  223. // Presets and the rules between options. Changing an option by hand switches the preset to "Custom".
  224. for (QCheckBox* box : OptionBoxes()){
  225. connect(box, &QCheckBox::toggled, this, [this](){UpdateOptions();});
  226. connect(box, &QCheckBox::clicked, this, [this](){
  227. if (main_window_->combo_preset->currentIndex() != PRESET_CUSTOM){
  228. const QSignalBlocker blocker(main_window_->combo_preset);
  229. main_window_->combo_preset->setCurrentIndex(PRESET_CUSTOM);
  230. }
  231. });
  232. }
  233. connect(
  234. main_window_->combo_preset, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
  235. [this](int index){ApplyPreset(index);}
  236. );
  237. ApplyPreset(main_window_->combo_preset->currentIndex());
  238. InitSettings();
  239. main_window_->line_data_dst->setText(settings_->value("InstallDir").toString());
  240. // The audio tools are looked for once the window is shown.
  241. connect(main_window_->label_tools, &QLabel::linkActivated, this, [this](const QString& link){
  242. if (link == "locate-ffmpeg") LocateTool(true);
  243. else if (link == "locate-timidity") LocateTool(false);
  244. });
  245. QTimer::singleShot(0, this, [this](){RefreshToolStatus();});
  246. connect(main_window_->btn_close, &QPushButton::clicked, this, &QWidget::close);
  247. timer_ = new QTimer(this);
  248. connect(timer_, SIGNAL(timeout()), this, SLOT(DoProgress()));
  249. }
  250. MainWindow::~MainWindow(){
  251. // Stop the installation, if there is one running, before the window goes away.
  252. stop_requested_ = true;
  253. if (worker_.joinable()) worker_.join();
  254. delete main_window_;
  255. }
  256. void MainWindow::InitSettings(void){
  257. bool win = false;
  258. #ifdef Q_OS_WIN
  259. settings_ = new QSettings(
  260. QCoreApplication::applicationDirPath() + "/" + "launcherSettings.ini",
  261. QSettings::IniFormat
  262. );
  263. win = true;
  264. #else
  265. settings_ = new QSettings(QSettings::NativeFormat,QSettings::UserScope, "v-gears", "launcher", 0);
  266. #endif
  267. // There is a single installation directory: it holds the configuration (plugins.cfg, resources.cfg...) and the
  268. // game data (<dir>/data), and it is where the game runs. Older versions stored two settings, ConfigDir and
  269. // DataDir (the latter pointing to <dir>/data), so migrate them.
  270. if (settings_->value("InstallDir").isNull()){
  271. const QString default_dir = win ? QString("%1/v-gears").arg(QDir::homePath()) : QString("%1/.v-gears").arg(QDir::homePath());
  272. const QString dir = QString::fromStdString(InstallLayout::MigrateInstallDir(
  273. settings_->value("DataDir").toString().toStdString(), settings_->value("ConfigDir").toString().toStdString(),
  274. default_dir.toStdString()
  275. ));
  276. settings_->setValue("InstallDir", dir);
  277. }
  278. settings_->remove("DataDir");
  279. settings_->remove("ConfigDir");
  280. if (settings_->value("VGearsEXE").isNull()){
  281. if (win) settings_->setValue("VGearsEXE",QString("%1/v-gears.exe").arg(QCoreApplication::applicationDirPath()));
  282. else settings_->setValue("VGearsEXE", QString("/usr/games/v-gears"));
  283. }
  284. }
  285. void MainWindow::OpenInstallLog(const QString& dir, bool append){
  286. InstallLog& install_log = InstallLog::Get();
  287. install_log.Open((dir + "installer.log").toStdString(), append);
  288. install_log.SetSink([this](InstallLog::Level level, const std::string& message){
  289. const QString text = QString::fromStdString(message).toHtmlEscaped();
  290. const QString color = level == InstallLog::ERROR_LEVEL ? "#c62828" : (level == InstallLog::WARNING ? "#e08600" : "");
  291. const QString html = color.isEmpty() ? text : QString("<span style=\"color:%1\">%2</span>").arg(color, text);
  292. // The log can be written from any thread.
  293. QMetaObject::invokeMethod(this, [this, html](){main_window_->data_log->append(html);}, Qt::QueuedConnection);
  294. });
  295. }
  296. std::vector<QCheckBox*> MainWindow::OptionBoxes() const{
  297. return {
  298. main_window_->chk_battle_data, main_window_->chk_battle_models, main_window_->chk_kernel,
  299. main_window_->chk_images, main_window_->chk_sounds, main_window_->chk_music, main_window_->chk_videos,
  300. main_window_->chk_fields, main_window_->chk_field_models, main_window_->chk_wm, main_window_->chk_wm_models
  301. };
  302. }
  303. InstallOptions MainWindow::CurrentOptions() const{
  304. InstallOptions o;
  305. o.battle_data = main_window_->chk_battle_data->isChecked();
  306. o.battle_models = main_window_->chk_battle_models->isChecked();
  307. o.kernel = main_window_->chk_kernel->isChecked();
  308. o.images = main_window_->chk_images->isChecked();
  309. o.sounds = main_window_->chk_sounds->isChecked();
  310. o.music = main_window_->chk_music->isChecked();
  311. o.videos = main_window_->chk_videos->isChecked();
  312. o.fields = main_window_->chk_fields->isChecked();
  313. o.field_models = main_window_->chk_field_models->isChecked();
  314. o.wm = main_window_->chk_wm->isChecked();
  315. o.wm_models = main_window_->chk_wm_models->isChecked();
  316. return o;
  317. }
  318. void MainWindow::SetOptions(const InstallOptions& o){
  319. main_window_->chk_battle_data->setChecked(o.battle_data);
  320. main_window_->chk_battle_models->setChecked(o.battle_models);
  321. main_window_->chk_kernel->setChecked(o.kernel);
  322. main_window_->chk_images->setChecked(o.images);
  323. main_window_->chk_sounds->setChecked(o.sounds);
  324. main_window_->chk_music->setChecked(o.music);
  325. // Videos can only be chosen when the disk has them.
  326. main_window_->chk_videos->setChecked(o.videos);
  327. main_window_->chk_fields->setChecked(o.fields);
  328. main_window_->chk_field_models->setChecked(o.field_models);
  329. main_window_->chk_wm->setChecked(o.wm);
  330. main_window_->chk_wm_models->setChecked(o.wm_models);
  331. }
  332. void MainWindow::ApplyPreset(int index){
  333. if (index == PRESET_CUSTOM){
  334. UpdateOptions();
  335. return;
  336. }
  337. static const InstallPreset presets[] = {
  338. InstallPreset::EVERYTHING, InstallPreset::FIELDS, InstallPreset::WORLD_MAP, InstallPreset::BATTLE,
  339. InstallPreset::MEDIA
  340. };
  341. if (index < 0 || index >= static_cast<int>(sizeof(presets) / sizeof(presets[0]))) return;
  342. // Videos are not on the install disk: they are extracted from the game disks, which have their own flow.
  343. SetOptions(InstallRules::Preset(presets[index], false));
  344. UpdateOptions();
  345. }
  346. void MainWindow::UpdateOptions(){
  347. if (updating_options_) return;
  348. updating_options_ = true;
  349. // Options whose requirements are not met are disabled, and so unchecked.
  350. InstallOptions options = CurrentOptions();
  351. InstallRules::ApplyDependencies(options);
  352. const InstallOptions selectable = InstallRules::Selectable(options);
  353. const auto apply = [](QCheckBox* box, bool checked, bool enabled){
  354. if (box->isChecked() != checked) box->setChecked(checked);
  355. box->setEnabled(enabled);
  356. };
  357. // The options can only be changed by hand with the "Custom" preset, and then only if their requirements are met.
  358. // "Clean install" doesn't depend on what is installed, so it is always available.
  359. const bool custom = main_window_->combo_preset->currentIndex() == PRESET_CUSTOM;
  360. apply(main_window_->chk_field_models, options.field_models, custom && selectable.field_models);
  361. apply(main_window_->chk_wm_models, options.wm_models, custom && selectable.wm_models);
  362. apply(main_window_->chk_videos, main_window_->chk_videos->isChecked(), custom);
  363. for (QCheckBox* box : {
  364. main_window_->chk_battle_data, main_window_->chk_battle_models, main_window_->chk_kernel,
  365. main_window_->chk_images, main_window_->chk_sounds, main_window_->chk_music, main_window_->chk_fields,
  366. main_window_->chk_wm, main_window_->chk_copy_data, main_window_->chk_keep_original
  367. }) box->setEnabled(custom);
  368. main_window_->chk_clean_install->setEnabled(true);
  369. // Disk space.
  370. uint64_t temporary = 0;
  371. const uint64_t installed = InstallRules::EstimateMegabytes(options, &temporary);
  372. const auto size = [](uint64_t mb){
  373. return mb >= 1024 ? QString("%1 GB").arg(mb / 1024.0, 0, 'f', 1) : QString("%1 MB").arg(mb);
  374. };
  375. QString text = tr("Estimated disk space: about %1 for the converted data").arg(size(installed));
  376. if (temporary > 0) text += tr(", plus %1 of temporary files while installing").arg(size(temporary));
  377. text += tr(", plus the extracted ISO (up to about 1 GB).");
  378. if (options.videos) text += tr(" Videos are not included in the estimate.");
  379. main_window_->label_estimate->setText(text);
  380. updating_options_ = false;
  381. }
  382. void MainWindow::on_btn_verify_clicked(){
  383. QString dir = QDir::fromNativeSeparators(main_window_->line_data_dst->text());
  384. if (dir.isEmpty() || !QDir(dir).exists()){
  385. QMessageBox::warning(this, tr("Verify install"), tr("The installation directory does not exist: %1").arg(dir));
  386. return;
  387. }
  388. const VerifyResult result = VerifyInstallation(dir.toStdString(), FindGameDataDir().toStdString());
  389. // The game can start if its configuration and the V-Gears data files are there.
  390. bool can_launch = true;
  391. QString text;
  392. std::string group;
  393. for (const VerifyProblem& problem : result.problems){
  394. if (problem.group == "Configuration" || problem.group == "Game files" || problem.group == "Copied data files")
  395. can_launch = false;
  396. if (problem.group != group){
  397. group = problem.group;
  398. text += QString("\n%1:\n").arg(QString::fromStdString(group));
  399. }
  400. text += QString(" - %1\n").arg(QString::fromStdString(problem.message));
  401. }
  402. if (result.Ok()){
  403. QMessageBox::information(
  404. this, tr("Verify install"), tr("Everything the game needs is installed (%1 checks).").arg(result.checked)
  405. );
  406. }
  407. else{
  408. QMessageBox::warning(
  409. this, tr("Verify install"),
  410. tr("Some files are missing:\n%1\nIf you skipped a part of the installation on purpose (for example the "
  411. "music), the files of that part are expected to be missing.").arg(text)
  412. );
  413. }
  414. launch_available_ = can_launch;
  415. main_window_->btn_launch->setEnabled(launch_available_);
  416. }
  417. void MainWindow::on_btn_launch_clicked(){
  418. LaunchGame(QDir::fromNativeSeparators(main_window_->line_data_dst->text()));
  419. }
  420. void MainWindow::LaunchGame(const QString& dir){
  421. // The executable of the settings, or the one built next to the installer.
  422. QString exe = settings_->value("VGearsEXE").toString();
  423. if (exe.isEmpty() || !QFileInfo(exe).isExecutable()){
  424. exe.clear();
  425. for (const QString& candidate : {QCoreApplication::applicationDirPath() + "/v-gears", QString("/usr/games/v-gears")}){
  426. if (QFileInfo(candidate).isExecutable()){
  427. exe = candidate;
  428. break;
  429. }
  430. }
  431. }
  432. if (exe.isEmpty()){
  433. exe = QFileDialog::getOpenFileName(this, tr("Location of the V-Gears executable"), QDir::homePath());
  434. if (exe.isEmpty()) return;
  435. }
  436. settings_->setValue("VGearsEXE", exe);
  437. // The game runs from the installation directory, where it finds its data.
  438. QStringList args;
  439. args << QString("--data-dir=%1").arg(dir)
  440. << QString("--resources-file=%1/resources.cfg").arg(dir) << QString("--config-file=%1/v-gears.cfg").arg(dir)
  441. << QString("--plugins-file=%1/plugins.cfg").arg(dir);
  442. if (!QProcess::startDetached(exe, args, dir)){
  443. QMessageBox::critical(this, tr("Launch game"), tr("Could not start %1.").arg(exe));
  444. }
  445. }
  446. /**
  447. * Version of the installer, set by the build.
  448. */
  449. #ifndef VGEARS_INSTALLER_VERSION
  450. #define VGEARS_INSTALLER_VERSION "unknown"
  451. #endif
  452. /**
  453. * Web pages of the project.
  454. */
  455. static const char* const kHomepage = "https://v-gears.inigovalentin.com";
  456. static const char* const kRepository = "https://github.com/InigoValentin/V-Gears";
  457. void MainWindow::on_actionABout_triggered(){
  458. QMessageBox box(this);
  459. box.setWindowTitle(tr("About V-Gears Installer"));
  460. box.setIconPixmap(windowIcon().pixmap(64, 64));
  461. box.setTextFormat(Qt::RichText);
  462. box.setTextInteractionFlags(Qt::TextBrowserInteraction);
  463. box.setText(tr(
  464. "<h3>V-Gears Installer %1</h3>"
  465. "<p>Prepares the data that <b>V-Gears</b>, a free engine for Final Fantasy VII, needs: it converts the data of "
  466. "your own copy of the PC game into formats the engine can use.</p>"
  467. "<p>It doesn't include any data from the original game, and you need a legal copy of it.</p>"
  468. "<p><a href=\"%2\">Website</a> &middot; <a href=\"%3\">Source code</a></p>"
  469. "<p>Copyright &copy; The V-Gears Team.<br>"
  470. "Free software, licensed under the GNU General Public License, version 3.</p>"
  471. ).arg(VGEARS_INSTALLER_VERSION, kHomepage, kRepository));
  472. box.exec();
  473. }
  474. void MainWindow::on_actionHelp_triggered(){
  475. QDialog dialog(this);
  476. dialog.setWindowTitle(tr("V-Gears Installer Help"));
  477. dialog.resize(640, 560);
  478. QVBoxLayout* layout = new QVBoxLayout(&dialog);
  479. QTextBrowser* browser = new QTextBrowser(&dialog);
  480. browser->setOpenExternalLinks(true);
  481. browser->setHtml(tr(
  482. "<h2>Installing the game data</h2>"
  483. "<ol>"
  484. "<li><b>Select the ISO</b> of the <i>install disk</i> of the PC version of Final Fantasy VII. The installer "
  485. "tells you if the image is valid.</li>"
  486. "<li><b>Choose the installation directory</b>, for example <code>~/.v-gears</code>. It holds the "
  487. "configuration and the converted data, and it is where the game runs.</li>"
  488. "<li>Click <b>Install data</b>. The log shows the progress. Problems are shown in color and also written to "
  489. "<code>installer.log</code> in the installation directory.</li>"
  490. "</ol>"
  491. "<p>The installation can only be done once per window. To install again, close the installer and open it "
  492. "again.</p>"
  493. "<h3>Options</h3>"
  494. "<p>Select <b>Show advanced options</b> to choose what to install. <b>What to install</b> offers ready-made "
  495. "sets; choose <b>Custom</b> to pick the options by hand. Some options need others (for example the field "
  496. "models need the field maps). <b>Clean install</b> deletes the data of a previous installation first. The "
  497. "estimate under the options shows about how much disk space it needs.</p>"
  498. "<p>With <b>Copy V-Gears data files</b> the installer also copies the files that come with V-Gears (scripts, "
  499. "screens, fonts and configuration) to the installation directory.</p>"
  500. "<h3>Videos</h3>"
  501. "<p>The videos are on the PC <i>game disks</i> 1, 2 and 3, not on the install disk. Select one of those "
  502. "disks as the ISO, with the same installation directory, and click <b>Install data</b>: only its videos are "
  503. "extracted, to <code>data/video</code>. Repeat it for each disk. Videos that are already installed are "
  504. "skipped.</p>"
  505. "<h3>FFmpeg and TiMidity</h3>"
  506. "<p>The sounds and the music are converted with <b>FFmpeg</b>, and the music also with <b>TiMidity</b>. "
  507. "The installer looks for them in the folder you chose with <i>Locate…</i>, in a <code>tools</code> folder "
  508. "inside the installation directory or next to the installer (handy on Windows: put <code>ffmpeg.exe</code> "
  509. "there), and in the system <code>PATH</code>. The window shows what it found. If something is missing when "
  510. "you start, it tells you what you would lose and lets you locate the program, open its download page, check "
  511. "again, continue without it, or cancel.</p>"
  512. "<h3>When it finishes</h3>"
  513. "<p><b>Verify install</b> checks that the installation directory has everything the game needs. If you "
  514. "skipped a part on purpose, the files of that part are reported as missing. <b>Launch game</b> starts "
  515. "V-Gears from the installation directory.</p>"
  516. "<h3>More help</h3>"
  517. "<p><a href=\"%1\">Project website</a> &middot; <a href=\"%2\">Source code and documentation</a></p>"
  518. ).arg(kHomepage, kRepository));
  519. layout->addWidget(browser);
  520. QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Close, &dialog);
  521. connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
  522. layout->addWidget(buttons);
  523. dialog.exec();
  524. }
  525. void MainWindow::on_actionExit_triggered(){close();}
  526. void MainWindow::on_btn_data_src_clicked(){
  527. const QString filter = tr("ISO images (*.iso)");
  528. QString temp = QFileDialog::getOpenFileName(
  529. this, tr("Select ISO image"), settings_->value("IsoDir", QDir::homePath()).toString(), filter
  530. );
  531. std::cout << "Selected ISO: " << temp.toStdString() << std::endl;
  532. if (!temp.isNull()){
  533. main_window_->line_data_src->setText(temp);
  534. settings_->setValue("IsoDir", QFileInfo(temp).absolutePath());
  535. disk_image_ = DiskImage(temp.toStdString());
  536. main_window_->isoData->setText(QString::fromStdString(disk_image_.getId()));
  537. if (!disk_image_.isValid()){
  538. main_window_->isoError->setStyleSheet("QLabel { color : red; }");
  539. main_window_->isoError->setText(QString::fromStdString(disk_image_.getErrorMessage()));
  540. }
  541. else if (!disk_image_.isSupported()){
  542. main_window_->isoError->setStyleSheet("QLabel { color : orange; }");
  543. main_window_->isoError->setText(QString::fromStdString(disk_image_.getWarningMessage()));
  544. }
  545. else if (disk_image_.isVideoDisk()){
  546. main_window_->isoError->setStyleSheet("QLabel { color : green; }");
  547. main_window_->isoError->setText(
  548. tr("Game disk: only its videos will be extracted. Install the game from the install disk.")
  549. );
  550. }
  551. else{
  552. main_window_->isoError->setStyleSheet("QLabel { color : green; }");
  553. main_window_->isoError->setText(tr("ISO is valid and supported."));
  554. }
  555. // A game disk only has videos, so there is nothing else to choose.
  556. main_window_->advancedOptions->setEnabled(!disk_image_.isVideoDisk());
  557. main_window_->combo_preset->setEnabled(!disk_image_.isVideoDisk());
  558. }
  559. }
  560. void MainWindow::on_line_data_dst_editingFinished(){
  561. settings_->setValue("InstallDir", main_window_->line_data_dst->text());
  562. // The installation directory can have a "tools" folder.
  563. RefreshToolStatus();
  564. }
  565. void MainWindow::on_btn_data_dst_clicked(){
  566. QString temp = QFileDialog::getExistingDirectory(
  567. this, tr("V-Gears installation directory"), settings_->value("InstallDir").toString()
  568. );
  569. if (!temp.isNull()){
  570. settings_->setValue("InstallDir", temp);
  571. main_window_->line_data_dst->setText(temp);
  572. RefreshToolStatus();
  573. }
  574. }
  575. void MainWindow::on_btn_data_run_clicked(){
  576. if (main_window_->line_data_dst->text().isEmpty()){
  577. QMessageBox::critical(this, tr("Output error"), tr("No output path provided"));
  578. return;
  579. }
  580. // Normalize the output path.
  581. QString output = QDir::fromNativeSeparators(main_window_->line_data_dst->text());
  582. if (!output.endsWith("/")) output += "/";
  583. // The working directory changes during installation, so the path must be absolute.
  584. if (QDir::isRelativePath(output)) output = QDir(output).absolutePath() + "/";
  585. // A game disk only has videos: extract them, without the rest of the installation.
  586. if (disk_image_.isValid() && disk_image_.isVideoDisk()){
  587. StartVideoExtraction(output);
  588. return;
  589. }
  590. // Locate the V-Gears data folder to copy, asking if it can't be found.
  591. const bool copy_data = main_window_->chk_copy_data->checkState() == Qt::Checked;
  592. QString game_data;
  593. if (copy_data){
  594. game_data = FindGameDataDir();
  595. if (game_data.isEmpty()){
  596. game_data = QFileDialog::getExistingDirectory(
  597. this, tr("Select the V-Gears 'data' folder to copy"), QCoreApplication::applicationDirPath()
  598. );
  599. if (game_data.isEmpty()) return;
  600. }
  601. }
  602. // Check everything that can be checked before touching the disk.
  603. PreflightResult preflight = RunPreflight(disk_image_, output, copy_data, game_data);
  604. for (const std::string& problem : InstallRules::Problems(CurrentOptions(), QFile::exists(output + "data/fields/_fields.xml")))
  605. preflight.errors << QString::fromStdString(problem);
  606. if (!preflight.errors.isEmpty()){
  607. QMessageBox::critical(
  608. this, tr("The installation can't start"),
  609. tr("Fix these problems and try again:\n\n- %1").arg(preflight.errors.join("\n- "))
  610. );
  611. return;
  612. }
  613. if (!preflight.warnings.isEmpty()){
  614. const auto answer = QMessageBox::warning(
  615. this, tr("Warnings"), tr("%1\n\nContinue anyway?").arg(preflight.warnings.join("\n")),
  616. QMessageBox::Yes | QMessageBox::No, QMessageBox::No
  617. );
  618. if (answer != QMessageBox::Yes) return;
  619. }
  620. // The audio is converted with external programs: say now, before anything is extracted, if they are missing.
  621. if (!ResolveMissingTools(CurrentOptions())) return;
  622. {
  623. bool extraction_success = disk_image_.extractImage(output.toStdString());
  624. if (!extraction_success) {
  625. QMessageBox::critical(this, tr("Extraction error"), tr("Failed to extract ISO file."));
  626. return;
  627. }
  628. QString input = QDir::fromNativeSeparators(QString::fromStdString(disk_image_.getContentPath()));
  629. if (input.isEmpty()) {
  630. QMessageBox::critical(this, tr("Extraction error"), tr("Extracted data path is invalid."));
  631. return;
  632. }
  633. if (!input.endsWith("/")) input += "/";
  634. // TODO: Enumerate files or find some better way to do this.
  635. const std::vector<std::string> required_files = {
  636. "data/field/char.lgp",
  637. "data/field/flevel.lgp",
  638. "data/kernel/KERNEL.BIN",
  639. "data/menu/menu_us.lgp",
  640. "data/sound/audio.fmt",
  641. "data/sound/audio.dat",
  642. "data/music/music.idx",
  643. "data/midi/midi.lgp",
  644. "data/battle/scene.bin",
  645. "data/battle/battle.lgp",
  646. "data/battle/magic.lgp",
  647. "data/wm/WM0.MAP",
  648. "data/wm/WM2.MAP",
  649. "data/wm/WM3.MAP",
  650. "data/wm/world_us.lgp",
  651. "ff7.exe"
  652. };
  653. // Ensure required files are in the input dir
  654. for (auto& file : required_files){
  655. std::cout << "Checking for required file: " << file << std::endl;
  656. const auto matched_file = disk_image_.filePath(file);
  657. if (!matched_file){
  658. QMessageBox::critical(
  659. this, tr("Missing input file"),
  660. tr("Some of the required files were not found among the extracted data.\n\nMake sure that the file '")
  661. + file.c_str() + tr("' is in the extracted data.")
  662. );
  663. return;
  664. }
  665. }
  666. // Copy the V-Gears data files (scripts, screens, fonts...) to the installation directory.
  667. if (copy_data){
  668. main_window_->data_log->append(tr("Copying data files from %1...").arg(game_data));
  669. // Everything in the folder goes to the installation directory, and its "data" subfolder to <dir>/data.
  670. if (!CopyDirectory(game_data, output)){
  671. QMessageBox::critical(
  672. this, tr("Copy error"), tr("Failed to copy the V-Gears data files to the installation directory.")
  673. );
  674. return;
  675. }
  676. main_window_->data_log->append(tr("Data files copied."));
  677. }
  678. // The installer's Ogre setup loads plugins.cfg and resources.cfg from the working directory (and writes
  679. // install.log there), so run from the installation directory. The plugins themselves are looked for in the
  680. // same places as the game does (see VGears::OgrePlugins::SearchDirectories).
  681. if (!QFile::exists(output + "plugins.cfg") || !QFile::exists(output + "resources.cfg")){
  682. QMessageBox::critical(
  683. this, tr("Missing configuration files"),
  684. tr("The installation directory must contain plugins.cfg and resources.cfg.\n\n"
  685. "Enable 'Copy V-Gears data files' to put them there.")
  686. );
  687. return;
  688. }
  689. if (!QDir::setCurrent(output)){
  690. QMessageBox::critical(this, tr("Output error"), tr("Could not enter the installation directory."));
  691. return;
  692. }
  693. if (installer_created){
  694. // Due to the use of singletons, the installer can't be re-run.
  695. // TODO: Verify that this is true, and if so, try to fix it.
  696. QMessageBox::critical(
  697. this, tr("Error"), tr("Please, fix the errors, and then close the installer before trying again.")
  698. );
  699. return;
  700. }
  701. // Collect every message of the installation in one place: the log file, the log window and the summary.
  702. UpdateToolLocations(output);
  703. OpenInstallLog(output, false);
  704. // Start data conversion
  705. try{
  706. // Parse advanced options.
  707. DataInstaller::AdvancedOptions options;
  708. options.battle_data = (Qt::Checked == main_window_->chk_battle_data->checkState());
  709. options.battle_models = (Qt::Checked == main_window_->chk_battle_models->checkState());
  710. options.kernel = (Qt::Checked == main_window_->chk_kernel->checkState());
  711. options.images = (Qt::Checked == main_window_->chk_images->checkState());
  712. options.sounds = (Qt::Checked == main_window_->chk_sounds->checkState());
  713. options.music = (Qt::Checked == main_window_->chk_music->checkState());
  714. options.videos = false;
  715. if (Qt::Checked == main_window_->chk_videos->checkState()){
  716. InstallLog::Get().Add(
  717. "[WARNING] The install disk has no videos. To extract them, select game disk 1, 2 or 3 as the "
  718. "ISO and install again (with a new installer window)."
  719. );
  720. }
  721. options.fields = (Qt::Checked == main_window_->chk_fields->checkState());
  722. options.field_models = options.fields && (Qt::Checked == main_window_->chk_field_models->checkState());
  723. options.wm = (Qt::Checked == main_window_->chk_wm->checkState());
  724. options.wm_models = options.wm && (Qt::Checked == main_window_->chk_wm_models->checkState());
  725. options.keep_originals = (Qt::Checked == main_window_->chk_keep_original->checkState());
  726. installer_created = true;
  727. install_started_ = true;
  728. const std::string output_path = QDir::toNativeSeparators(output).toStdString();
  729. const auto write_line = [this](const std::string log_line, int level, bool as_progress = false){
  730. InstallLog::Get().Add(log_line);
  731. if (as_progress){
  732. const QString text = QString::fromStdString(log_line);
  733. QMetaObject::invokeMethod(
  734. this, [this, text](){main_window_->label_progress->setText(text);}, Qt::QueuedConnection
  735. );
  736. }
  737. };
  738. // The installation runs in a worker thread so the window stays responsive. Ogre, and the OpenGL context
  739. // it creates, belong to the thread that creates them, so the installer is created, run and destroyed
  740. // there. Setting "BackgroundInstall" to false runs it in the UI thread, step by step from a timer.
  741. background_ = settings_->value("BackgroundInstall", true).toBool();
  742. if (background_){
  743. stop_requested_ = false;
  744. OnInstallStarted();
  745. worker_ = std::thread([this, options, output_path, write_line](){RunInstallation(options, output_path, write_line);});
  746. }
  747. else{
  748. installer_ = std::make_unique<DataInstaller>(disk_image_, output_path, options, write_line);
  749. OnInstallStarted();
  750. }
  751. }
  752. catch (const std::exception& ex){
  753. InstallLog::Get().Add(std::string("[ERROR] Installation failed: ") + ex.what());
  754. InstallLog::Get().Close();
  755. installer_.reset();
  756. installer_created = false;
  757. OnInstallStopped();
  758. QMessageBox::critical(this, tr("Installation error"), ex.what());
  759. }
  760. }
  761. }
  762. void MainWindow::on_chk_advanced_options_stateChanged(){
  763. main_window_->advancedOptions->setVisible(main_window_->chk_advanced_options->checkState() == Qt::Checked);
  764. }
  765. void MainWindow::EnableUi(bool enable){
  766. // The installer can't run twice, so once an installation has started the button never comes back.
  767. main_window_->btn_data_run->setEnabled(enable && !install_started_);
  768. main_window_->btn_close->setEnabled(enable);
  769. main_window_->actionExit->setEnabled(enable);
  770. main_window_->label_tools->setEnabled(enable);
  771. main_window_->btn_verify->setEnabled(enable);
  772. main_window_->btn_launch->setEnabled(enable && launch_available_);
  773. main_window_->line_data_src->setEnabled(enable);
  774. main_window_->btn_data_src->setEnabled(enable);
  775. main_window_->line_data_dst->setEnabled(enable);
  776. main_window_->btn_data_dst->setEnabled(enable);
  777. main_window_->data_progress_bar->setValue(0);
  778. // In background mode the worker thread does the work, so there is nothing to poll.
  779. if (!enable && !background_) timer_->start(0);
  780. else timer_->stop();
  781. }
  782. void MainWindow::OnInstallStarted(){EnableUi(false);}
  783. void MainWindow::OnInstallStopped(){EnableUi(true);}
  784. void MainWindow::StartVideoExtraction(const QString& output){
  785. // Only the output directory is needed: no plugins, no configuration, no Ogre.
  786. QStringList errors;
  787. if (!QDir().mkpath(output)) errors << tr("The installation directory can't be created: %1").arg(output);
  788. else{
  789. QFile probe(output + ".v-gears-write-test");
  790. if (!probe.open(QIODevice::WriteOnly)) errors << tr("The installation directory is not writable: %1").arg(output);
  791. else{
  792. probe.close();
  793. probe.remove();
  794. }
  795. const QStorageInfo storage(output);
  796. const qint64 needed = 1024LL * 1024 * 1024;
  797. if (storage.isValid() && storage.bytesAvailable() < needed){
  798. errors << tr("Not enough free space in %1: %2 MB available, about %3 MB needed.")
  799. .arg(storage.rootPath()).arg(storage.bytesAvailable() / (1024 * 1024)).arg(needed / (1024 * 1024));
  800. }
  801. }
  802. if (!errors.isEmpty()){
  803. QMessageBox::critical(
  804. this, tr("The extraction can't start"), tr("Fix these problems and try again:\n\n- %1").arg(errors.join("\n- "))
  805. );
  806. return;
  807. }
  808. // The installation log of a previous run is kept.
  809. OpenInstallLog(output, true);
  810. // It doesn't use the installer (and its singletons), so it can be done again with another disk.
  811. background_ = true;
  812. stop_requested_ = false;
  813. OnInstallStarted();
  814. InstallLog::Get().Add("Extracting the videos of " + disk_image_.getId() + "...");
  815. worker_ = std::thread([this, output](){
  816. int extracted = 0;
  817. int skipped = 0;
  818. bool ok = false;
  819. std::string error;
  820. try{
  821. ok = disk_image_.extractVideos(
  822. output.toStdString() + "data/",
  823. [this](float progress){
  824. QMetaObject::invokeMethod(this, [this, progress](){ShowProgress(progress);}, Qt::QueuedConnection);
  825. },
  826. [](const std::string& message){InstallLog::Get().Add(message);}, &extracted, &skipped
  827. );
  828. if (ok) ConvertMovies(output.toStdString() + "data/video/");
  829. }
  830. catch (const std::exception& ex){
  831. error = ex.what();
  832. }
  833. if (stop_requested_) return;
  834. QMetaObject::invokeMethod(this, [this, ok, error, extracted, skipped](){
  835. if (worker_.joinable()) worker_.join();
  836. if (ok && error.empty()){
  837. ShowProgress(100);
  838. InstallLog::Get().Add(
  839. "Videos extracted: " + std::to_string(extracted) + " new, " + std::to_string(skipped)
  840. + " already installed."
  841. );
  842. }
  843. else InstallLog::Get().Add("[ERROR] Video extraction failed" + (error.empty() ? std::string() : ": " + error));
  844. InstallLog::Get().Close();
  845. OnInstallStopped();
  846. if (!ok || !error.empty())
  847. QMessageBox::critical(this, tr("Video extraction"), tr("Some videos could not be extracted. See the log."));
  848. }, Qt::QueuedConnection);
  849. });
  850. }
  851. void MainWindow::UpdateToolLocations(const QString& install_dir){
  852. // Where to look for FFmpeg and TiMidity: what the user chose, then a "tools" folder in the installation
  853. // directory or next to the installer, then the system PATH.
  854. QString dir = QDir::fromNativeSeparators(install_dir);
  855. if (!dir.isEmpty() && !dir.endsWith("/")) dir += "/";
  856. ExternalTools::Locations& tools = ExternalTools::CurrentLocations();
  857. tools.ffmpeg = settings_->value("FFmpegPath").toString().toStdString();
  858. tools.timidity = settings_->value("TimidityPath").toString().toStdString();
  859. const QString app_dir = QCoreApplication::applicationDirPath();
  860. tools.directories.clear();
  861. if (!dir.isEmpty()) tools.directories.push_back((dir + "tools").toStdString());
  862. tools.directories.push_back((app_dir + "/tools").toStdString());
  863. tools.directories.push_back(app_dir.toStdString());
  864. }
  865. void MainWindow::RefreshToolStatus(){
  866. UpdateToolLocations(main_window_->line_data_dst->text());
  867. const ExternalTools::Locations& locations = ExternalTools::CurrentLocations();
  868. ffmpeg_tool_ = ExternalTools::FindFfmpeg(locations);
  869. timidity_tool_ = ExternalTools::FindTimidity(locations);
  870. const auto describe = [](const QString& name, const ExternalTools::Tool& tool, const QString& link){
  871. if (tool.usable)
  872. return tr("%1 \u2713 %2").arg(name, QString::fromStdString(ExternalTools::ShortVersion(tool)));
  873. return tr("%1 <span style=\"color:#c62828\">\u2717 not found</span> (<a href=\"%2\">Locate\u2026</a>)").arg(name, link);
  874. };
  875. main_window_->label_tools->setText(
  876. tr("Audio tools: %1 \u00b7 %2").arg(
  877. describe("FFmpeg", ffmpeg_tool_, "locate-ffmpeg"), describe("TiMidity", timidity_tool_, "locate-timidity")
  878. )
  879. );
  880. const auto tip = [](const QString& name, const ExternalTools::Tool& tool){
  881. return name + ": " + QString::fromStdString(tool.usable ? tool.path : tool.problem);
  882. };
  883. main_window_->label_tools->setToolTip(tip("FFmpeg", ffmpeg_tool_) + "\n" + tip("TiMidity", timidity_tool_));
  884. }
  885. bool MainWindow::LocateTool(bool ffmpeg){
  886. const QString name = ffmpeg ? "FFmpeg" : "TiMidity";
  887. const QString key = ffmpeg ? "FFmpegPath" : "TimidityPath";
  888. const QString saved = settings_->value(key).toString();
  889. #ifdef Q_OS_WIN
  890. const QString filter = tr("Programs (*.exe);;All files (*)");
  891. #else
  892. const QString filter = tr("All files (*)");
  893. #endif
  894. const QString file = QFileDialog::getOpenFileName(
  895. this, tr("Locate %1").arg(name), saved.isEmpty() ? QDir::homePath() : QFileInfo(saved).absolutePath(), filter
  896. );
  897. if (file.isEmpty()) return false;
  898. // It must really be the program, and work.
  899. const ExternalTools::Tool tool = ffmpeg ? ExternalTools::ProbeFfmpeg(file.toStdString()) : ExternalTools::ProbeTimidity(file.toStdString());
  900. if (!tool.usable){
  901. QMessageBox::warning(this, tr("This program can't be used"), QString::fromStdString(tool.problem));
  902. return false;
  903. }
  904. settings_->setValue(key, file);
  905. RefreshToolStatus();
  906. return true;
  907. }
  908. bool MainWindow::ResolveMissingTools(const InstallOptions& options){
  909. while (true){
  910. RefreshToolStatus();
  911. // What is going to be used.
  912. const InstallRules::ToolNeeds needs = InstallRules::ToolsNeeded(options);
  913. const bool want_ffmpeg = needs.ffmpeg;
  914. const bool want_timidity = needs.timidity;
  915. const bool missing_ffmpeg = want_ffmpeg && !ffmpeg_tool_.usable;
  916. const bool missing_timidity = want_timidity && !timidity_tool_.usable;
  917. const std::vector<std::string> consequences
  918. = InstallRules::ToolConsequences(options, !missing_ffmpeg, !missing_timidity);
  919. if (consequences.empty()) return true;
  920. QStringList lost;
  921. for (const std::string& line : consequences) lost << QString::fromStdString(line);
  922. QStringList reasons;
  923. if (missing_ffmpeg) reasons << "FFmpeg: " + QString::fromStdString(ffmpeg_tool_.problem);
  924. if (missing_timidity) reasons << "TiMidity: " + QString::fromStdString(timidity_tool_.problem);
  925. QMessageBox box(this);
  926. box.setIcon(QMessageBox::Warning);
  927. box.setWindowTitle(tr("Audio tools missing"));
  928. box.setText(tr("The installer needs programs it can't use:\n\n%1\n\nIf you continue without them:\n\n%2")
  929. .arg("- " + reasons.join("\n- "), "- " + lost.join("\n- ")));
  930. const QString tools_dir
  931. = QDir::toNativeSeparators(QDir::fromNativeSeparators(main_window_->line_data_dst->text()) + "/tools");
  932. QString help = tr("You can also copy the programs to %1 and choose 'Check again'.\n\n").arg(tools_dir);
  933. #if defined(Q_OS_WIN)
  934. help += tr("Windows: download a build of FFmpeg (ffmpeg.exe) from https://ffmpeg.org/download.html. TiMidity++ "
  935. "also needs its timidity.cfg and instrument files next to timidity.exe.");
  936. #elif defined(Q_OS_MAC)
  937. help += tr("macOS: brew install ffmpeg timidity");
  938. #else
  939. help += tr("Debian, Ubuntu: sudo apt install ffmpeg timidity\nFedora: sudo dnf install ffmpeg timidity++\n"
  940. "Arch: sudo pacman -S ffmpeg timidity++");
  941. #endif
  942. box.setDetailedText(help);
  943. QPushButton* locate = box.addButton(tr("Locate\u2026"), QMessageBox::ActionRole);
  944. QPushButton* get = box.addButton(tr("Get it\u2026"), QMessageBox::ActionRole);
  945. QPushButton* again = box.addButton(tr("Check again"), QMessageBox::ActionRole);
  946. QPushButton* skip = box.addButton(tr("Continue without"), QMessageBox::AcceptRole);
  947. box.addButton(QMessageBox::Cancel);
  948. box.setDefaultButton(locate);
  949. box.exec();
  950. QAbstractButton* clicked = box.clickedButton();
  951. if (clicked == skip) return true;
  952. if (clicked == locate){
  953. if (missing_ffmpeg) LocateTool(true);
  954. if (missing_timidity) LocateTool(false);
  955. }
  956. else if (clicked == get){
  957. if (missing_ffmpeg) QDesktopServices::openUrl(QUrl("https://ffmpeg.org/download.html"));
  958. if (missing_timidity) QDesktopServices::openUrl(QUrl("https://timidity.sourceforge.net/#download"));
  959. }
  960. else if (clicked != again) return false; // Cancel, or the window was closed.
  961. }
  962. }
  963. void MainWindow::ShowProgress(float progress){
  964. main_window_->data_progress_bar->setValue(std::floor(progress));
  965. std::stringstream stream;
  966. stream << std::fixed << std::setprecision(2) << progress << "%";
  967. main_window_->label_percent->setText(QString::fromUtf8(stream.str().c_str()));
  968. }
  969. void MainWindow::RunInstallation(
  970. DataInstaller::AdvancedOptions options, std::string output_path,
  971. std::function<void(std::string, int, bool)> write_line
  972. ){
  973. std::string error;
  974. bool created = false;
  975. try{
  976. installer_ = std::make_unique<DataInstaller>(disk_image_, output_path, options, write_line);
  977. created = true;
  978. float progress = 0;
  979. auto last_shown = std::chrono::steady_clock::now();
  980. while (progress < 100 && !stop_requested_){
  981. progress = installer_->Progress();
  982. // Don't flood the UI thread with an update per step.
  983. const auto now = std::chrono::steady_clock::now();
  984. if (progress >= 100 || now - last_shown > std::chrono::milliseconds(100)){
  985. last_shown = now;
  986. QMetaObject::invokeMethod(this, [this, progress](){ShowProgress(progress);}, Qt::QueuedConnection);
  987. }
  988. }
  989. }
  990. catch (const std::exception& ex){
  991. error = ex.what();
  992. }
  993. catch (...){
  994. error = "Unknown error";
  995. }
  996. const bool finished = error.empty() && !stop_requested_;
  997. // Destroy the installer in the thread that created it.
  998. try{installer_.reset();}
  999. catch (...){}
  1000. if (stop_requested_) return;
  1001. QMetaObject::invokeMethod(this, [this, finished, created, error](){
  1002. if (worker_.joinable()) worker_.join();
  1003. if (finished){
  1004. launch_available_ = true;
  1005. OnInstallStopped();
  1006. InstallLog::Get().Close();
  1007. }
  1008. else{
  1009. InstallLog::Get().Add("[ERROR] Installation failed: " + error);
  1010. InstallLog::Get().Close();
  1011. // If the installer could not even be created it can be tried again.
  1012. if (!created) installer_created = false;
  1013. OnInstallStopped();
  1014. QMessageBox::critical(this, created ? tr("Data conversion exception") : tr("Installation error"), QString::fromStdString(error));
  1015. }
  1016. }, Qt::QueuedConnection);
  1017. }
  1018. void MainWindow::DoProgress(){
  1019. try{
  1020. const float progress = installer_->Progress();
  1021. ShowProgress(progress);
  1022. if (progress >= 100){
  1023. launch_available_ = true;
  1024. OnInstallStopped();
  1025. InstallLog::Get().Close();
  1026. }
  1027. }
  1028. catch (const std::exception& ex){
  1029. InstallLog::Get().Add(std::string("[ERROR] Installation failed: ") + ex.what());
  1030. InstallLog::Get().Close();
  1031. OnInstallStopped();
  1032. QMessageBox::critical(this, tr("Data conversion exception"), ex.what());
  1033. }
  1034. }