diff --git a/rpcs3/Crypto/unpkg.h b/rpcs3/Crypto/unpkg.h index d253561509..0b7971c07e 100644 --- a/rpcs3/Crypto/unpkg.h +++ b/rpcs3/Crypto/unpkg.h @@ -347,6 +347,7 @@ public: }; bool is_valid() const { return m_is_valid; } + const PKGHeader& get_header() const { return m_header; } package_install_result check_target_app_version() const; static package_install_result extract_data(std::deque& readers, std::deque& bootable_paths); const psf::registry& get_psf() const { return m_psf; } diff --git a/rpcs3/Emu/system_utils.cpp b/rpcs3/Emu/system_utils.cpp index e840887bac..3cf886d06d 100644 --- a/rpcs3/Emu/system_utils.cpp +++ b/rpcs3/Emu/system_utils.cpp @@ -184,6 +184,11 @@ namespace rpcs3::utils return g_cfg_vfs.get(g_cfg_vfs.dev_bdvd, get_emu_dir()); } + std::string get_hdd0_game_dir() + { + return get_hdd0_dir() + "game/"; + } + u64 get_cache_disk_usage() { if (const u64 data_size = fs::get_dir_size(rpcs3::utils::get_cache_dir(), 1); data_size != umax) diff --git a/rpcs3/Emu/system_utils.hpp b/rpcs3/Emu/system_utils.hpp index b4142dacb9..c2825abb24 100644 --- a/rpcs3/Emu/system_utils.hpp +++ b/rpcs3/Emu/system_utils.hpp @@ -34,6 +34,8 @@ namespace rpcs3::utils std::string get_flash3_dir(); std::string get_bdvd_dir(); + std::string get_hdd0_game_dir(); + // Cache directories and disk usage u64 get_cache_disk_usage(); std::string get_cache_dir(); diff --git a/rpcs3/rpcs3qt/game_compatibility.cpp b/rpcs3/rpcs3qt/game_compatibility.cpp index 2d901b6642..2845659716 100644 --- a/rpcs3/rpcs3qt/game_compatibility.cpp +++ b/rpcs3/rpcs3qt/game_compatibility.cpp @@ -272,6 +272,10 @@ compat::package_info game_compatibility::GetPkgInfo(const QString& pkg_path, gam info.category = QString::fromStdString(std::string(psf::get_string(psf, "CATEGORY"))); info.version = QString::fromStdString(std::string(psf::get_string(psf, "APP_VER"))); + // Technically, there is no specific package's header info providing its installation size on disk. + // We use "data_size" header as an approximation (a bit larger) for this purpose + info.data_size = reader.get_header().data_size.value(); + if (!info.category.isEmpty()) { const Localized localized; diff --git a/rpcs3/rpcs3qt/game_compatibility.h b/rpcs3/rpcs3qt/game_compatibility.h index f24ebfbe1f..664bafea34 100644 --- a/rpcs3/rpcs3qt/game_compatibility.h +++ b/rpcs3/rpcs3qt/game_compatibility.h @@ -1,5 +1,7 @@ #pragma once +#include "util/types.hpp" + #include #include @@ -108,6 +110,7 @@ namespace compat QString version; // May be empty QString category; // HG, DG, GD etc. QString local_cat; // Localized category + u64 data_size = 0; // Installation size package_type type = package_type::other; // The type of package (Update, DLC or other) }; diff --git a/rpcs3/rpcs3qt/main_window.cpp b/rpcs3/rpcs3qt/main_window.cpp index 5509a6f69c..80d71a44c2 100644 --- a/rpcs3/rpcs3qt/main_window.cpp +++ b/rpcs3/rpcs3qt/main_window.cpp @@ -860,11 +860,28 @@ bool main_window::InstallPackages(QStringList file_paths, bool from_boot) info.changelog = tr("Changelog:\n%0", "Block for Changelog").arg(info.changelog); } - const QString info_string = QStringLiteral("%0\n\n%1%2%3%4").arg(file_info.fileName()).arg(info.title).arg(info.local_cat).arg(info.title_id).arg(info.version); - QString message = tr("Do you want to install this package?\n\n%0").arg(info_string); + u64 free_space = 0; - QMessageBox mb(QMessageBox::Icon::Question, tr("PKG Decrypter / Installer"), message, QMessageBox::Yes | QMessageBox::No, this); + // Retrieve disk space info on data path's drive + if (fs::device_stat stat{}; fs::statfs(rpcs3::utils::get_hdd0_dir(), stat)) + { + free_space = stat.avail_free; + } + + const QString installation_info = + tr("Installation path: %0\nAvailable disk space: %1%2\nRequired disk space: %3") + .arg(rpcs3::utils::get_hdd0_game_dir()) + .arg(gui::utils::format_byte_size(free_space)) + .arg(info.data_size <= free_space ? QString() : tr(" - NOT ENOUGH SPACE")) + .arg(gui::utils::format_byte_size(info.data_size)); + + const QString info_string = QStringLiteral("%0\n\n%1%2%3%4").arg(file_info.fileName()).arg(info.title).arg(info.local_cat).arg(info.title_id).arg(info.version); + QString message = tr("Do you want to install this package?\n\n%0\n\n%1").arg(info_string).arg(installation_info); + + QMessageBox mb(QMessageBox::Icon::Question, tr("PKG Decrypter / Installer"), gui::utils::make_paragraph(message), QMessageBox::Yes | QMessageBox::No, this); mb.setDefaultButton(QMessageBox::No); + mb.setTextFormat(Qt::RichText); // Support HTML tags + mb.button(QMessageBox::Yes)->setEnabled(info.data_size <= free_space); if (!info.changelog.isEmpty()) { diff --git a/rpcs3/rpcs3qt/pkg_install_dialog.cpp b/rpcs3/rpcs3qt/pkg_install_dialog.cpp index 11431b2ad4..26fb7515c4 100644 --- a/rpcs3/rpcs3qt/pkg_install_dialog.cpp +++ b/rpcs3/rpcs3qt/pkg_install_dialog.cpp @@ -2,6 +2,10 @@ #include "game_compatibility.h" #include "numbered_widget_item.h" #include "richtext_item_delegate.h" +#include "qt_utils.h" + +#include "Emu/system_utils.hpp" +#include "Utilities/File.h" #include #include @@ -17,6 +21,7 @@ enum Roles TitleRole = Qt::UserRole + 2, TitleIdRole = Qt::UserRole + 3, VersionRole = Qt::UserRole + 4, + DataSizeRole = Qt::UserRole + 5, }; pkg_install_dialog::pkg_install_dialog(const QStringList& paths, game_compatibility* compat, QWidget* parent) @@ -89,7 +94,8 @@ pkg_install_dialog::pkg_install_dialog(const QStringList& paths, game_compatibil append_comma(); accumulated_info += file_info.fileName(); - const QString text = tr("%0 (%2)", "Package text").arg(info.title.simplified()).arg(accumulated_info); + const QString text = tr("%0 (%1) - %2", "Package text").arg(info.title.simplified()) + .arg(accumulated_info).arg(gui::utils::format_byte_size(info.data_size)); QListWidgetItem* item = new numbered_widget_item(text, m_dir_list); item->setData(Roles::FullPathRole, info.path); @@ -97,6 +103,7 @@ pkg_install_dialog::pkg_install_dialog(const QStringList& paths, game_compatibil item->setData(Roles::TitleRole, info.title); item->setData(Roles::TitleIdRole, info.title_id); item->setData(Roles::VersionRole, info.version); + item->setData(Roles::DataSizeRole, static_cast(info.data_size)); item->setToolTip(tooltip); item->setFlags(item->flags() | Qt::ItemIsUserCheckable); item->setCheckState(Qt::Checked); @@ -106,6 +113,10 @@ pkg_install_dialog::pkg_install_dialog(const QStringList& paths, game_compatibil m_dir_list->setCurrentRow(0); m_dir_list->setMinimumWidth((m_dir_list->sizeHintForColumn(0) * 125) / 100); + // Create contextual label (updated in connect(m_dir_list, &QListWidget::itemChanged ...)) + QLabel* installation_info = new QLabel(); + installation_info->setTextFormat(Qt::RichText); // Support HTML tags + // Create buttons QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Cancel | QDialogButtonBox::Ok); buttons->button(QDialogButtonBox::Ok)->setText(tr("Install")); @@ -123,19 +134,9 @@ pkg_install_dialog::pkg_install_dialog(const QStringList& paths, game_compatibil } }); - connect(m_dir_list, &QListWidget::itemChanged, this, [this, buttons](QListWidgetItem*) + connect(m_dir_list, &QListWidget::itemChanged, this, [this, installation_info, buttons](QListWidgetItem*) { - bool any_checked = false; - for (int i = 0; i < m_dir_list->count(); i++) - { - if (m_dir_list->item(i)->checkState() == Qt::Checked) - { - any_checked = true; - break; - } - } - - buttons->button(QDialogButtonBox::Ok)->setEnabled(any_checked); + UpdateInfo(installation_info, buttons); }); QToolButton* move_up = new QToolButton; @@ -159,11 +160,41 @@ pkg_install_dialog::pkg_install_dialog(const QStringList& paths, game_compatibil vbox->addWidget(description); vbox->addLayout(hbox); vbox->addWidget(m_dir_list); + vbox->addWidget(installation_info); vbox->addWidget(buttons); setLayout(vbox); setWindowTitle(tr("Batch PKG Installation")); setObjectName("pkg_install_dialog"); + UpdateInfo(installation_info, buttons); // Just to show and check available and required size +} + +void pkg_install_dialog::UpdateInfo(QLabel* installation_info, QDialogButtonBox* buttons) const +{ + u64 data_size = 0; + u64 free_space = 0; + + // Retrieve disk space info on data path's drive + if (fs::device_stat stat{}; fs::statfs(rpcs3::utils::get_hdd0_game_dir(), stat)) + { + free_space = stat.avail_free; + } + + for (int i = 0; i < m_dir_list->count(); i++) + { + if (m_dir_list->item(i)->checkState() == Qt::Checked) + { + data_size += m_dir_list->item(i)->data(Roles::DataSizeRole).toULongLong(); + } + } + + installation_info->setText(gui::utils::make_paragraph( + tr("Installation path: %0\nAvailable disk space: %1%2\nRequired disk space: %3") + .arg(rpcs3::utils::get_hdd0_game_dir()) + .arg(gui::utils::format_byte_size(free_space)) + .arg(data_size <= free_space ? QString() : tr(" - NOT ENOUGH SPACE")) + .arg(gui::utils::format_byte_size(data_size)))); + buttons->button(QDialogButtonBox::Ok)->setEnabled(data_size && (data_size <= free_space)); } void pkg_install_dialog::MoveItem(int offset) const diff --git a/rpcs3/rpcs3qt/pkg_install_dialog.h b/rpcs3/rpcs3qt/pkg_install_dialog.h index bb566b1b67..2122259a1d 100644 --- a/rpcs3/rpcs3qt/pkg_install_dialog.h +++ b/rpcs3/rpcs3qt/pkg_install_dialog.h @@ -2,6 +2,8 @@ #include #include +#include +#include namespace compat { @@ -19,6 +21,7 @@ public: std::vector GetPathsToInstall() const; private: + void UpdateInfo(QLabel* installation_info, QDialogButtonBox* buttons) const; void MoveItem(int offset) const; QListWidget* m_dir_list;