diff --git a/include/aoapplication.h b/include/aoapplication.h index 835c7fd..90364db 100644 --- a/include/aoapplication.h +++ b/include/aoapplication.h @@ -37,6 +37,25 @@ class NetworkManager; class Lobby; class Courtroom; +class VPath : QString { + using QString::QString; + +public: + explicit VPath(const QString &str) : QString(str) {} + inline QString const &toQString() const { return *this; } + inline bool operator==(const VPath &str) const { + return this->toQString() == str.toQString(); + } + inline VPath operator+(const VPath &str) const { + return VPath(this->toQString() + str.toQString()); + } +}; + +inline uint qHash(const VPath &key, uint seed = qGlobalQHashSeed()) +{ + return qHash(key.toQString(), seed); +} + class AOApplication : public QApplication { Q_OBJECT @@ -130,24 +149,26 @@ public: // implementation in path_functions.cpp QString get_base_path(); - QString get_data_path(); - QString get_theme_path(QString p_file, QString p_theme=""); - QString get_character_path(QString p_char, QString p_file); - QString get_misc_path(QString p_misc, QString p_file); - QString get_sounds_path(QString p_file); - QString get_music_path(QString p_song); - QString get_background_path(QString p_file); - QString get_default_background_path(QString p_file); - QString get_evidence_path(QString p_file); - QStringList get_asset_paths(QString p_element, QString p_theme="", QString p_subtheme="", QString p_default_theme="", QString p_misc="", QString p_character="", QString p_placeholder=""); - QString get_asset_path(QStringList pathlist); - QString get_image_path(QStringList pathlist, bool static_image=false); - QString get_sfx_path(QStringList pathlist); + VPath get_theme_path(QString p_file, QString p_theme=""); + VPath get_character_path(QString p_char, QString p_file); + VPath get_misc_path(QString p_misc, QString p_file); + VPath get_sounds_path(QString p_file); + VPath get_music_path(QString p_song); + VPath get_background_path(QString p_file); + VPath get_default_background_path(QString p_file); + VPath get_evidence_path(QString p_file); + QVector get_asset_paths(QString p_element, QString p_theme="", QString p_subtheme="", QString p_default_theme="", QString p_misc="", QString p_character="", QString p_placeholder=""); + QString get_asset_path(QVector pathlist); + QString get_image_path(QVector pathlist, bool static_image=false); + QString get_sfx_path(QVector pathlist); QString get_config_value(QString p_identifier, QString p_config, QString p_theme="", QString p_subtheme="", QString p_default_theme="", QString p_misc=""); QString get_asset(QString p_element, QString p_theme="", QString p_subtheme="", QString p_default_theme="", QString p_misc="", QString p_character="", QString p_placeholder=""); - QString get_image(QString p_element, QString p_theme="", QString p_subtheme="", QString p_default_theme="", QString p_misc="", QString p_character="", QString p_placeholder=""); + QString get_image(QString p_element, QString p_theme="", QString p_subtheme="", QString p_default_theme="", QString p_misc="", QString p_character="", QString p_placeholder="", bool static_image=false); QString get_sfx(QString p_sfx, QString p_misc="", QString p_character=""); QString get_case_sensitive_path(QString p_file); + QString get_real_path(const VPath &vpath); + QString get_real_suffixed_path(const VPath &vpath, const QStringList &suffixes); + void invalidate_lookup_cache(); ////// Functions for reading and writing files ////// // Implementations file_functions.cpp @@ -285,6 +306,7 @@ public: QStringList get_call_words(); // returns all of the file's lines in a QStringList + QStringList get_list_file(VPath path); QStringList get_list_file(QString p_file); // Process a file and return its text as a QString @@ -308,6 +330,7 @@ public: QVector read_serverlist_txt(); // Returns the value of p_identifier in the design.ini file in p_design_path + QString read_design_ini(QString p_identifier, VPath p_design_path); QString read_design_ini(QString p_identifier, QString p_design_path); // Returns the coordinates of widget with p_identifier from p_file @@ -337,18 +360,18 @@ public: QString get_court_sfx(QString p_identifier, QString p_misc=""); // Figure out if we can opus this or if we should fall back to wav - QString get_sfx_suffix(QString sound_to_check); + QString get_sfx_suffix(VPath sound_to_check); // Can we use APNG for this? If not, WEBP? If not, GIF? If not, fall back to // PNG. - QString get_image_suffix(QString path_to_check, bool static_image=false); + QString get_image_suffix(VPath path_to_check, bool static_image=false); // Returns the value of p_search_line within target_tag and terminator_tag QString read_char_ini(QString p_char, QString p_search_line, QString target_tag); // Returns a QStringList of all key=value definitions on a given tag. - QStringList read_ini_tags(QString p_file, QString target_tag = ""); + QStringList read_ini_tags(VPath p_file, QString target_tag = ""); // Sets the char.ini p_search_line key under tag target_tag to value. void set_char_ini(QString p_char, QString value, QString p_search_line, @@ -492,6 +515,9 @@ public: // Get the default scaling method QString get_default_scaling(); + // Get a list of custom mount paths + QStringList get_mount_paths(); + // Currently defined subtheme QString subtheme; @@ -527,6 +553,9 @@ private: QVector server_list; QVector favorite_list; + QHash asset_lookup_cache; + QHash dir_listing_cache; + QSet dir_listing_exist_cache; private slots: void ms_connect_finished(bool connected, bool will_retry); diff --git a/include/aolayer.h b/include/aolayer.h index f3ecebf..7a8e2fd 100644 --- a/include/aolayer.h +++ b/include/aolayer.h @@ -9,6 +9,7 @@ #include class AOApplication; +class VPath; // "Brief" explanation of what the hell this is: // diff --git a/include/aooptionsdialog.h b/include/aooptionsdialog.h index a6c8b8e..4568b1e 100644 --- a/include/aooptionsdialog.h +++ b/include/aooptionsdialog.h @@ -23,6 +23,7 @@ #include #include +#include #include class Lobby; @@ -174,6 +175,19 @@ private: QLabel *ui_log_lbl; QCheckBox *ui_log_cb; + QWidget *ui_assets_tab; + QVBoxLayout *ui_assets_tab_layout; + QGridLayout *ui_mount_buttons_layout; + QLabel *ui_asset_lbl; + QListWidget *ui_mount_list; + QPushButton *ui_mount_add; + QPushButton *ui_mount_remove; + QPushButton *ui_mount_up; + QPushButton *ui_mount_down; + QPushButton *ui_mount_clear_cache; + + bool asset_cache_dirty = false; + bool needs_default_audiodev(); void update_values(); diff --git a/src/aoapplication.cpp b/src/aoapplication.cpp index dd8d9ae..7485759 100644 --- a/src/aoapplication.cpp +++ b/src/aoapplication.cpp @@ -18,7 +18,8 @@ AOApplication::AOApplication(int &argc, char **argv) : QApplication(argc, argv) discord = new AttorneyOnline::Discord(); QObject::connect(net_manager, SIGNAL(ms_connect_finished(bool, bool)), SLOT(ms_connect_finished(bool, bool))); - qApp->setStyleSheet("QFrame {background-color:transparent;} QAbstractItemView {background-color: transparent; color: black;}; QLineEdit {background-color:transparent;}"); + + asset_lookup_cache.reserve(2048); } AOApplication::~AOApplication() diff --git a/src/aobutton.cpp b/src/aobutton.cpp index 7f8c13f..242fe79 100644 --- a/src/aobutton.cpp +++ b/src/aobutton.cpp @@ -20,14 +20,9 @@ void AOButton::set_image(QString p_path, QString p_misc) { movie->stop(); QString p_image; - // Check if the user wants animated themes - if (ao_app->get_animated_theme()) - // We want an animated image - p_image = ao_app->get_image(p_path, ao_app->current_theme, ao_app->get_subtheme(), ao_app->default_theme, p_misc); - else - // Grab a static variant of the image - p_image = ao_app->get_image_path(ao_app->get_asset_paths(p_path, ao_app->current_theme, ao_app->get_subtheme(), ao_app->default_theme, p_misc), true); - if (!file_exists(p_image)) { + p_image = ao_app->get_image(p_path, ao_app->current_theme, ao_app->get_subtheme(), + ao_app->default_theme, p_misc, "", "", !ao_app->get_animated_theme()); + if (p_image.isEmpty()) { this->setIcon(QIcon()); this->setIconSize(this->size()); this->setStyleSheet(""); diff --git a/src/aoevidencebutton.cpp b/src/aoevidencebutton.cpp index fee7327..ae67b82 100644 --- a/src/aoevidencebutton.cpp +++ b/src/aoevidencebutton.cpp @@ -32,7 +32,7 @@ AOEvidenceButton::AOEvidenceButton(QWidget *p_parent, AOApplication *p_ao_app, void AOEvidenceButton::set_image(QString p_image) { - QString image_path = ao_app->get_evidence_path(p_image); + QString image_path = ao_app->get_real_path(ao_app->get_evidence_path(p_image)); if (file_exists(p_image)) { this->setText(""); this->setStyleSheet( @@ -57,8 +57,10 @@ void AOEvidenceButton::set_image(QString p_image) void AOEvidenceButton::set_theme_image(QString p_image) { - QString theme_image_path = ao_app->get_theme_path(p_image); - QString default_image_path = ao_app->get_theme_path(p_image, ao_app->default_theme); + QString theme_image_path = ao_app->get_real_path( + ao_app->get_theme_path(p_image)); + QString default_image_path = ao_app->get_real_path( + ao_app->get_theme_path(p_image, ao_app->default_theme)); QString final_image_path; diff --git a/src/aoevidencedisplay.cpp b/src/aoevidencedisplay.cpp index ba1c170..c635a02 100644 --- a/src/aoevidencedisplay.cpp +++ b/src/aoevidencedisplay.cpp @@ -35,7 +35,8 @@ void AOEvidenceDisplay::show_evidence(QString p_evidence_image, gif_name = "evidence_appear_right"; } - QString f_evidence_path = ao_app->get_evidence_path(p_evidence_image); + QString f_evidence_path = ao_app->get_real_path( + ao_app->get_evidence_path(p_evidence_image)); QPixmap f_pixmap(f_evidence_path); pos_size_type icon_dimensions = diff --git a/src/aoimage.cpp b/src/aoimage.cpp index 656528e..82e17b9 100644 --- a/src/aoimage.cpp +++ b/src/aoimage.cpp @@ -24,21 +24,16 @@ AOImage::AOImage(QWidget *parent, AOApplication *p_ao_app, bool make_static) : Q AOImage::~AOImage() {} -bool AOImage::set_image(QString p_path, QString p_misc) +bool AOImage::set_image(QString p_image, QString p_misc) { - // Check if the user wants animated themes - if (!is_static && ao_app->get_animated_theme()) - // We want an animated image - p_path = ao_app->get_image(p_path, ao_app->current_theme, ao_app->get_subtheme(), ao_app->default_theme, p_misc); - else - // Grab a static variant of the image - p_path = ao_app->get_image_path(ao_app->get_asset_paths(p_path, ao_app->current_theme, ao_app->get_subtheme(), ao_app->default_theme, p_misc), true); + p_image = ao_app->get_image(p_image, ao_app->current_theme, ao_app->get_subtheme(), + ao_app->default_theme, p_misc, "", "", is_static || !ao_app->get_animated_theme()); - if (!file_exists(p_path)) { - qDebug() << "Warning: Image" << p_path << "not found! Can't set!"; + if (!file_exists(p_image)) { + qDebug() << "Warning: Image" << p_image << "not found! Can't set!"; return false; } - path = p_path; + path = p_image; if (!is_static) { movie->stop(); movie->setFileName(path); diff --git a/src/aolayer.cpp b/src/aolayer.cpp index 2c6c911..3791d66 100644 --- a/src/aolayer.cpp +++ b/src/aolayer.cpp @@ -138,7 +138,7 @@ void BackgroundLayer::load_image(QString p_filename) { play_once = false; cull_image = false; - QString design_path = ao_app->get_background_path("design.ini"); + VPath design_path = ao_app->get_background_path("design.ini"); transform_mode = ao_app->get_scaling(ao_app->read_design_ini("scaling", design_path)); stretch = ao_app->read_design_ini("stretch", design_path).startsWith("true"); @@ -197,7 +197,7 @@ void CharLayer::load_image(QString p_filename, QString p_charname, << current_emote << " from character: " << p_charname << " continuous: " << continuous; #endif - QStringList pathlist = { + QVector pathlist { ao_app->get_character_path( p_charname, prefix + current_emote), // Default path ao_app->get_character_path( @@ -207,7 +207,7 @@ void CharLayer::load_image(QString p_filename, QString p_charname, ao_app->get_character_path( p_charname, current_emote), // Just use the non-prefixed image, animated or not - current_emote, // The path by itself after the above fail + VPath(current_emote), // The path by itself after the above fail ao_app->get_theme_path("placeholder"), // Theme placeholder path ao_app->get_theme_path( "placeholder", ao_app->default_theme)}; // Default theme placeholder path diff --git a/src/aomusicplayer.cpp b/src/aomusicplayer.cpp index e8bd690..b0f3fa3 100644 --- a/src/aomusicplayer.cpp +++ b/src/aomusicplayer.cpp @@ -19,7 +19,7 @@ int AOMusicPlayer::play(QString p_song, int channel, bool loop, channel = channel % m_channelmax; if (channel < 0) // wtf? return BASS_ERROR_NOCHAN; - QString f_path = ao_app->get_music_path(p_song); + QString f_path = ao_app->get_real_path(ao_app->get_music_path(p_song)); unsigned int flags = BASS_STREAM_PRESCAN | BASS_STREAM_AUTOFREE | BASS_UNICODE | BASS_ASYNCFILE; diff --git a/src/aooptionsdialog.cpp b/src/aooptionsdialog.cpp index 1db0dec..5f3dbf0 100644 --- a/src/aooptionsdialog.cpp +++ b/src/aooptionsdialog.cpp @@ -4,6 +4,8 @@ #include "lobby.h" #include "bass.h" +#include + AOOptionsDialog::AOOptionsDialog(QWidget *parent, AOApplication *p_ao_app) : QDialog(parent, Qt::WindowTitleHint | Qt::WindowSystemMenuHint) { @@ -12,7 +14,7 @@ AOOptionsDialog::AOOptionsDialog(QWidget *parent, AOApplication *p_ao_app) // Setting up the basics. setWindowFlag(Qt::WindowCloseButtonHint); setWindowTitle(tr("Settings")); - resize(400, 408); + resize(450, 408); ui_settings_buttons = new QDialogButtonBox(this); @@ -73,12 +75,19 @@ AOOptionsDialog::AOOptionsDialog(QWidget *parent, AOApplication *p_ao_app) ui_theme_combobox = new QComboBox(ui_form_layout_widget); // Fill the combobox with the names of the themes. - QDirIterator it(ao_app->get_base_path() + "themes", QDir::Dirs, - QDirIterator::NoIteratorFlags); - while (it.hasNext()) { - QString actualname = QDir(it.next()).dirName(); - if (actualname != "." && actualname != "..") - ui_theme_combobox->addItem(actualname); + QSet themes; + QStringList bases = ao_app->get_mount_paths(); + bases.push_front(ao_app->get_base_path()); + for (const QString &base : bases) { + QDirIterator it(base + "/themes", QDir::Dirs | QDir::NoDotAndDotDot, + QDirIterator::NoIteratorFlags); + while (it.hasNext()) { + QString actualname = QDir(it.next()).dirName(); + if (!themes.contains(actualname)) { + ui_theme_combobox->addItem(actualname); + themes.insert(actualname); + } + } } QObject::connect(ui_theme_combobox, SIGNAL(currentIndexChanged(int)), this, @@ -902,7 +911,131 @@ AOOptionsDialog::AOOptionsDialog(QWidget *parent, AOApplication *p_ao_app) ui_casing_layout->setWidget(row, QFormLayout::FieldRole, ui_log_cb); + // Assets tab + ui_assets_tab = new QWidget(this); + ui_assets_tab_layout = new QVBoxLayout(ui_assets_tab); + ui_assets_tab->setLayout(ui_assets_tab_layout); + ui_settings_tabs->addTab(ui_assets_tab, tr("Assets")); + + ui_asset_lbl = new QLabel(ui_assets_tab); + ui_asset_lbl->setText( + tr("Add or remove base folders for use by assets. " + "Base folders will be searched in the order provided.")); + ui_asset_lbl->setWordWrap(true); + ui_assets_tab_layout->addWidget(ui_asset_lbl); + + ui_mount_list = new QListWidget(ui_assets_tab); + ui_assets_tab_layout->addWidget(ui_mount_list); + + ui_mount_buttons_layout = new QGridLayout(ui_assets_tab); + ui_assets_tab_layout->addLayout(ui_mount_buttons_layout); + + QSizePolicy stretch_btns(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); + stretch_btns.setHorizontalStretch(4); + + ui_mount_add = new QPushButton(tr("Add…"), ui_assets_tab); + ui_mount_add->setSizePolicy(stretch_btns); + ui_mount_buttons_layout->addWidget(ui_mount_add, 0, 0, 1, 1); + connect(ui_mount_add, &QPushButton::clicked, this, [this] { + QString dir = QFileDialog::getExistingDirectory(this, tr("Select a base folder"), + QApplication::applicationDirPath(), + QFileDialog::ShowDirsOnly); + if (dir.isEmpty()) + return; + QListWidgetItem *dir_item = new QListWidgetItem(dir); + ui_mount_list->addItem(dir_item); + ui_mount_list->setCurrentItem(dir_item); + + // quick hack to update buttons + emit ui_mount_list->itemSelectionChanged(); + }); + + ui_mount_remove = new QPushButton(tr("Remove"), ui_assets_tab); + ui_mount_remove->setSizePolicy(stretch_btns); + ui_mount_remove->setEnabled(false); + ui_mount_buttons_layout->addWidget(ui_mount_remove, 0, 1, 1, 1); + connect(ui_mount_remove, &QPushButton::clicked, this, [=] { + auto selected = ui_mount_list->selectedItems(); + if (selected.isEmpty()) + return; + delete selected[0]; + emit ui_mount_list->itemSelectionChanged(); + asset_cache_dirty = true; + }); + + auto *mount_buttons_spacer = new QSpacerItem(40, 20, QSizePolicy::Expanding, + QSizePolicy::Minimum); + ui_mount_buttons_layout->addItem(mount_buttons_spacer, 0, 2, 1, 1); + + ui_mount_up = new QPushButton(tr("↑"), ui_assets_tab); + ui_mount_up->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed); + ui_mount_up->setMaximumWidth(40); + ui_mount_up->setEnabled(false); + ui_mount_buttons_layout->addWidget(ui_mount_up, 0, 3, 1, 1); + connect(ui_mount_up, &QPushButton::clicked, this, [=] { + auto selected = ui_mount_list->selectedItems(); + if (selected.isEmpty()) + return; + auto *item = selected[0]; + int row = ui_mount_list->row(item); + ui_mount_list->takeItem(row); + int new_row = qMax(1, row - 1); + ui_mount_list->insertItem(new_row, item); + ui_mount_list->setCurrentRow(new_row); + asset_cache_dirty = true; + }); + + ui_mount_down = new QPushButton(tr("↓"), ui_assets_tab); + ui_mount_down->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed); + ui_mount_down->setMaximumWidth(40); + ui_mount_down->setEnabled(false); + ui_mount_buttons_layout->addWidget(ui_mount_down, 0, 4, 1, 1); + connect(ui_mount_down, &QPushButton::clicked, this, [=] { + auto selected = ui_mount_list->selectedItems(); + if (selected.isEmpty()) + return; + auto *item = selected[0]; + int row = ui_mount_list->row(item); + ui_mount_list->takeItem(row); + int new_row = qMin(ui_mount_list->count() + 1, row + 1); + ui_mount_list->insertItem(new_row, item); + ui_mount_list->setCurrentRow(new_row); + asset_cache_dirty = true; + }); + + auto *mount_buttons_spacer_2 = new QSpacerItem(40, 20, QSizePolicy::Expanding, + QSizePolicy::Minimum); + ui_mount_buttons_layout->addItem(mount_buttons_spacer_2, 0, 5, 1, 1); + + ui_mount_clear_cache = new QPushButton(tr("Clear Cache"), ui_assets_tab); + ui_mount_clear_cache->setToolTip(tr("Clears the lookup cache for assets. " + "Use this when you have added an asset that takes precedence over another " + "existing asset.")); + ui_mount_buttons_layout->addWidget(ui_mount_clear_cache, 0, 6, 1, 1); + connect(ui_mount_clear_cache, &QPushButton::clicked, this, [=] { + asset_cache_dirty = true; + ui_mount_clear_cache->setEnabled(false); + }); + + connect(ui_mount_list, &QListWidget::itemSelectionChanged, this, [=] { + auto selected_items = ui_mount_list->selectedItems(); + bool row_selected = !ui_mount_list->selectedItems().isEmpty(); + ui_mount_remove->setEnabled(row_selected); + ui_mount_up->setEnabled(row_selected); + ui_mount_down->setEnabled(row_selected); + + if (!row_selected) + return; + + int row = ui_mount_list->row(selected_items[0]); + if (row <= 1) + ui_mount_up->setEnabled(false); + if (row >= ui_mount_list->count() - 1) + ui_mount_down->setEnabled(false); + }); + update_values(); + // When we're done, we should continue the updates! setUpdatesEnabled(true); } @@ -976,6 +1109,12 @@ void AOOptionsDialog::update_values() { ui_blips_volume_spinbox->setValue(ao_app->get_default_blip()); ui_bliprate_spinbox->setValue(ao_app->read_blip_rate()); ui_default_showname_textbox->setText(ao_app->get_default_showname()); + + auto *defaultMount = new QListWidgetItem(tr("%1 (default)") + .arg(ao_app->get_base_path())); + defaultMount->setFlags(Qt::ItemFlag::NoItemFlags); + ui_mount_list->addItem(defaultMount); + ui_mount_list->addItems(ao_app->get_mount_paths()); } void AOOptionsDialog::save_pressed() @@ -1050,9 +1189,17 @@ void AOOptionsDialog::save_pressed() configini->setValue("casing_can_host_cases", ui_casing_cm_cases_textbox->text()); + QStringList mountPaths; + for (int i = 1; i < ui_mount_list->count(); i++) + mountPaths.append(ui_mount_list->item(i)->text()); + configini->setValue("mount_paths", mountPaths); + if (audioChanged) ao_app->initBASS(); + if (asset_cache_dirty) + ao_app->invalidate_lookup_cache(); + callwordsini->close(); // We most probably pressed "Restore defaults" at some point. Since we're saving our settings, remove the temporary file. diff --git a/src/charselect.cpp b/src/charselect.cpp index 53269b5..472938d 100644 --- a/src/charselect.cpp +++ b/src/charselect.cpp @@ -164,8 +164,8 @@ void Courtroom::char_clicked(int n_char) { if (n_char != -1) { - QString char_ini_path = - ao_app->get_character_path(char_list.at(n_char).name, "char.ini"); + QString char_ini_path = ao_app->get_real_path( + ao_app->get_character_path(char_list.at(n_char).name, "char.ini")); qDebug() << "char_ini_path" << char_ini_path; diff --git a/src/courtroom.cpp b/src/courtroom.cpp index b2fdc3c..18c6f01 100644 --- a/src/courtroom.cpp +++ b/src/courtroom.cpp @@ -612,7 +612,7 @@ void Courtroom::set_widgets() { QString filename = "courtroom_design.ini"; // Update the default theme from the courtroom_design.ini, if it's not defined it will be 'default'. - QSettings settings(ao_app->get_theme_path(filename, ao_app->current_theme), QSettings::IniFormat); + QSettings settings(ao_app->get_real_path(ao_app->get_theme_path(filename, ao_app->current_theme)), QSettings::IniFormat); ao_app->default_theme = settings.value("default_theme", "default").toString(); set_fonts(); @@ -1222,6 +1222,12 @@ void Courtroom::set_stylesheet(QWidget *widget) void Courtroom::set_stylesheets() { set_stylesheet(this); + this->setStyleSheet( + "QFrame { background-color:transparent; } " + "QAbstractItemView { background-color: transparent; color: black; } " + "QLineEdit { background-color:transparent; }" + + this->styleSheet() + ); } void Courtroom::set_window_title(QString p_title) @@ -1492,11 +1498,11 @@ void Courtroom::update_character(int p_cid) custom_obj_menu->setDefaultAction(action); objection_custom = ""; } - if (dir_exists( - ao_app->get_character_path(current_char, "custom_objections"))) { - ui_custom_objection->show(); - QDir directory( + QString custom_objection_dir = ao_app->get_real_path( ao_app->get_character_path(current_char, "custom_objections")); + if (dir_exists(custom_objection_dir)) { + ui_custom_objection->show(); + QDir directory(custom_objection_dir); QStringList custom_obj = directory.entryList(QStringList() << "*.png" << "*.gif" << "*.apng" @@ -1631,7 +1637,7 @@ void Courtroom::list_music() treeItem->setText(0, i_song_listname); treeItem->setText(1, i_song); - QString song_path = ao_app->get_music_path(i_song); + QString song_path = ao_app->get_real_path(ao_app->get_music_path(i_song)); if (file_exists(song_path)) treeItem->setBackground(0, found_brush); @@ -4431,8 +4437,9 @@ void Courtroom::set_iniswap_dropdown() ui_iniswap_remove->hide(); return; } - QStringList iniswaps = ao_app->get_list_file( - ao_app->get_character_path(char_list.at(m_cid).name, "iniswaps.ini")) + ao_app->get_list_file(ao_app->get_base_path() + "iniswaps.ini"); + QStringList iniswaps = + ao_app->get_list_file(ao_app->get_character_path(char_list.at(m_cid).name, "iniswaps.ini")) + + ao_app->get_list_file(ao_app->get_base_path() + "iniswaps.ini"); iniswaps.removeDuplicates(); iniswaps.prepend(char_list.at(m_cid).name); if (iniswaps.size() <= 0) { @@ -4488,7 +4495,8 @@ void Courtroom::on_iniswap_context_menu_requested(const QPoint &pos) menu->setAttribute(Qt::WA_DeleteOnClose); menu->addSeparator(); - if (file_exists(ao_app->get_character_path(current_char, "char.ini"))) + if (file_exists(ao_app->get_real_path( + ao_app->get_character_path(current_char, "char.ini")))) menu->addAction(QString("Edit " + current_char + "/char.ini"), this, SLOT(on_iniswap_edit_requested())); if (ui_iniswap_dropdown->itemText(ui_iniswap_dropdown->currentIndex()) != @@ -4499,7 +4507,7 @@ void Courtroom::on_iniswap_context_menu_requested(const QPoint &pos) } void Courtroom::on_iniswap_edit_requested() { - QString p_path = ao_app->get_character_path(current_char, "char.ini"); + QString p_path = ao_app->get_real_path(ao_app->get_character_path(current_char, "char.ini")); if (!file_exists(p_path)) return; QDesktopServices::openUrl(QUrl::fromLocalFile(p_path)); @@ -4576,7 +4584,8 @@ void Courtroom::on_sfx_context_menu_requested(const QPoint &pos) menu->setAttribute(Qt::WA_DeleteOnClose); menu->addSeparator(); - if (file_exists(ao_app->get_character_path(current_char, "soundlist.ini"))) + if (file_exists(ao_app->get_real_path( + ao_app->get_character_path(current_char, "soundlist.ini")))) menu->addAction(QString("Edit " + current_char + "/soundlist.ini"), this, SLOT(on_sfx_edit_requested())); else @@ -4589,10 +4598,10 @@ void Courtroom::on_sfx_context_menu_requested(const QPoint &pos) void Courtroom::on_sfx_edit_requested() { - QString p_path = ao_app->get_character_path(current_char, "soundlist.ini"); + QString p_path = ao_app->get_real_path(ao_app->get_character_path(current_char, "soundlist.ini")); if (!file_exists(p_path)) { p_path = ao_app->get_base_path() + "soundlist.ini"; - } + } QDesktopServices::openUrl(QUrl::fromLocalFile(p_path)); } @@ -4625,12 +4634,11 @@ void Courtroom::set_effects_dropdown() // ICON-MAKING HELL QString p_effect = ao_app->read_char_ini(current_char, "effects", "Options"); - QString custom_path = - ao_app->get_base_path() + "misc/" + p_effect + "/icons/"; - QString theme_path = ao_app->get_theme_path("effects/icons/"); - QString default_path = ao_app->get_theme_path("effects/icons/", "default"); + VPath custom_path("misc/" + p_effect + "/icons/"); + VPath theme_path = ao_app->get_theme_path("effects/icons/"); + VPath default_path = ao_app->get_theme_path("effects/icons/", "default"); for (int i = 0; i < ui_effects_dropdown->count(); ++i) { - QString entry = ui_effects_dropdown->itemText(i); + VPath entry = VPath(ui_effects_dropdown->itemText(i)); QString iconpath = ao_app->get_image_suffix(custom_path + entry); if (!file_exists(iconpath)) { iconpath = ao_app->get_image_suffix(theme_path + entry); @@ -4664,9 +4672,9 @@ void Courtroom::on_effects_context_menu_requested(const QPoint &pos) } void Courtroom::on_effects_edit_requested() { - QString p_path = ao_app->get_theme_path("effects/"); + QString p_path = ao_app->get_real_path(ao_app->get_theme_path("effects/")); if (!dir_exists(p_path)) { - p_path = ao_app->get_theme_path("effects/", "default"); + p_path = ao_app->get_real_path(ao_app->get_theme_path("effects/", "default")); if (!dir_exists(p_path)) { return; } @@ -4676,7 +4684,7 @@ void Courtroom::on_effects_edit_requested() void Courtroom::on_character_effects_edit_requested() { QString p_effect = ao_app->read_char_ini(current_char, "effects", "Options"); - QString p_path = ao_app->get_base_path() + "misc/" + p_effect + "/"; + QString p_path = ao_app->get_real_path(VPath("misc/" + p_effect + "/")); if (!dir_exists(p_path)) return; diff --git a/src/debug_functions.cpp b/src/debug_functions.cpp index 1613a7d..456aee8 100644 --- a/src/debug_functions.cpp +++ b/src/debug_functions.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include diff --git a/src/file_functions.cpp b/src/file_functions.cpp index e64a46b..37cdc32 100644 --- a/src/file_functions.cpp +++ b/src/file_functions.cpp @@ -2,6 +2,9 @@ bool file_exists(QString file_path) { + if (file_path.isEmpty()) + return false; + QFileInfo check_file(file_path); return check_file.exists() && check_file.isFile(); diff --git a/src/lobby.cpp b/src/lobby.cpp index 5cdb94a..31895d1 100644 --- a/src/lobby.cpp +++ b/src/lobby.cpp @@ -235,6 +235,12 @@ void Lobby::set_stylesheet(QWidget *widget) void Lobby::set_stylesheets() { set_stylesheet(this); + this->setStyleSheet( + "QFrame { background-color:transparent; } " + "QAbstractItemView { background-color: transparent; color: black; } " + "QLineEdit { background-color:transparent; }" + + this->styleSheet() + ); } void Lobby::set_font(QWidget *widget, QString p_identifier) diff --git a/src/path_functions.cpp b/src/path_functions.cpp index dbf1cc4..2f5aaec 100644 --- a/src/path_functions.cpp +++ b/src/path_functions.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #ifdef BASE_OVERRIDE #include "base_override.h" @@ -39,72 +40,57 @@ QString AOApplication::get_base_path() return base_path; } -QString AOApplication::get_data_path() { return get_base_path() + "data/"; } - -QString AOApplication::get_theme_path(QString p_file, QString p_theme) +VPath AOApplication::get_theme_path(QString p_file, QString p_theme) { if (p_theme == "") p_theme = current_theme; - QString path = get_base_path() + "themes/" + p_theme + "/" + p_file; - return get_case_sensitive_path(path); + return VPath("themes/" + p_theme + "/" + p_file); } -QString AOApplication::get_character_path(QString p_char, QString p_file) +VPath AOApplication::get_character_path(QString p_char, QString p_file) { - QString path = get_base_path() + "characters/" + p_char + "/" + p_file; - return get_case_sensitive_path(path); + return VPath("characters/" + p_char + "/" + p_file); } -QString AOApplication::get_misc_path(QString p_misc, QString p_file) +VPath AOApplication::get_misc_path(QString p_misc, QString p_file) { - QString path = get_base_path() + "misc/" + p_misc + "/" + p_file; -#ifndef CASE_SENSITIVE_FILESYSTEM - return path; -#else - return get_case_sensitive_path(path); -#endif + return VPath("misc/" + p_misc + "/" + p_file); } -QString AOApplication::get_sounds_path(QString p_file) +VPath AOApplication::get_sounds_path(QString p_file) { - QString path = get_base_path() + "sounds/general/" + p_file; - return get_case_sensitive_path(path); + return VPath("sounds/general/" + p_file); } -QString AOApplication::get_music_path(QString p_song) +VPath AOApplication::get_music_path(QString p_song) { if (p_song.startsWith("http")) { - return p_song; // url + return VPath(p_song); // url } - QString path = get_base_path() + "sounds/music/" + p_song; - return get_case_sensitive_path(path); + return VPath("sounds/music/" + p_song); } -QString AOApplication::get_background_path(QString p_file) +VPath AOApplication::get_background_path(QString p_file) { - QString path = get_base_path() + "background/" + - w_courtroom->get_current_background() + "/" + p_file; if (courtroom_constructed) { - return get_case_sensitive_path(path); + return VPath("background/" + w_courtroom->get_current_background() + "/" + p_file); } return get_default_background_path(p_file); } -QString AOApplication::get_default_background_path(QString p_file) +VPath AOApplication::get_default_background_path(QString p_file) { - QString path = get_base_path() + "background/default/" + p_file; - return get_case_sensitive_path(path); + return VPath("background/default/" + p_file); } -QString AOApplication::get_evidence_path(QString p_file) +VPath AOApplication::get_evidence_path(QString p_file) { - QString path = get_base_path() + "evidence/" + p_file; - return get_case_sensitive_path(path); + return VPath("evidence/" + p_file); } -QStringList AOApplication::get_asset_paths(QString p_element, QString p_theme, QString p_subtheme, QString p_default_theme, QString p_misc, QString p_character, QString p_placeholder) +QVector AOApplication::get_asset_paths(QString p_element, QString p_theme, QString p_subtheme, QString p_default_theme, QString p_misc, QString p_character, QString p_placeholder) { - QStringList pathlist; + QVector pathlist; if (p_character != "") pathlist += get_character_path(p_character, p_element); // Character folder if (p_misc != "" && p_theme != "" && p_subtheme != "") @@ -119,7 +105,7 @@ QStringList AOApplication::get_asset_paths(QString p_element, QString p_theme, Q pathlist += get_theme_path(p_element, p_theme); // Theme path if (p_default_theme != "") pathlist += get_theme_path(p_element, p_default_theme); // Default theme path - pathlist += p_element; // The path by itself + pathlist += VPath(p_element); // The path by itself if (p_placeholder != "" && p_theme != "") pathlist += get_theme_path(p_placeholder, p_theme); // Placeholder path if (p_placeholder != "" && p_default_theme != "") @@ -127,52 +113,48 @@ QStringList AOApplication::get_asset_paths(QString p_element, QString p_theme, Q return pathlist; } -QString AOApplication::get_asset_path(QStringList pathlist) +QString AOApplication::get_asset_path(QVector pathlist) { - QString path; - for (QString p : pathlist) { - p = get_case_sensitive_path(p); - if (file_exists(p)) { - path = p; - break; + for (const VPath &p : pathlist) { + QString path = get_real_path(p); + if (!path.isEmpty()) { + return path; } } - return path; + return QString(); } -QString AOApplication::get_image_path(QStringList pathlist, bool static_image) +QString AOApplication::get_image_path(QVector pathlist, bool static_image) { - QString path; - for (QString p : pathlist) { - p = get_case_sensitive_path(get_image_suffix(p, static_image)); - if (file_exists(p)) { - path = p; - break; - } - } - return path; -} - -QString AOApplication::get_sfx_path(QStringList pathlist) -{ - QString path; - for (QString p : pathlist) { - p = get_case_sensitive_path(get_sfx_suffix(p)); - if (file_exists(p)) { - path = p; - break; + for (const VPath &p : pathlist) { + QString path = get_image_suffix(p, static_image); + if (!path.isEmpty()) { + return path; } } - return path; + return QString(); } + +QString AOApplication::get_sfx_path(QVector pathlist) +{ + for (const VPath &p : pathlist) { + QString path = get_sfx_suffix(p); + if (!path.isEmpty()) { + return path; + } + } + return QString(); +} + QString AOApplication::get_config_value(QString p_identifier, QString p_config, QString p_theme, QString p_subtheme, QString p_default_theme, QString p_misc) { QString path; // qDebug() << "got request for" << p_identifier << "in" << p_config; - for (QString p : get_asset_paths(p_config, p_theme, p_subtheme, p_default_theme, p_misc)) { - p = get_case_sensitive_path(p); - if (file_exists(p)) { - QSettings settings(p, QSettings::IniFormat); + const auto paths = get_asset_paths(p_config, p_theme, p_subtheme, p_default_theme, p_misc); + for (const VPath &p : paths) { + path = get_real_path(p); + if (!path.isEmpty()) { + QSettings settings(path, QSettings::IniFormat); settings.setIniCodec("UTF-8"); QVariant value = settings.value(p_identifier); if (value.type() == QVariant::StringList) { @@ -193,25 +175,22 @@ QString AOApplication::get_asset(QString p_element, QString p_theme, QString p_s return get_asset_path(get_asset_paths(p_element, p_theme, p_subtheme, p_default_theme, p_misc, p_character, p_placeholder)); } -QString AOApplication::get_image(QString p_element, QString p_theme, QString p_subtheme, QString p_default_theme, QString p_misc, QString p_character, QString p_placeholder) +QString AOApplication::get_image(QString p_element, QString p_theme, QString p_subtheme, QString p_default_theme, QString p_misc, QString p_character, QString p_placeholder, + bool static_image) { - return get_image_path(get_asset_paths(p_element, p_theme, p_subtheme, p_default_theme, p_misc, p_character, p_placeholder)); + return get_image_path(get_asset_paths(p_element, p_theme, p_subtheme, p_default_theme, p_misc, p_character, p_placeholder), static_image); } QString AOApplication::get_sfx(QString p_sfx, QString p_misc, QString p_character) { - QStringList pathlist = get_asset_paths(p_sfx, current_theme, get_subtheme(), default_theme, p_misc, p_character); + QVector pathlist = get_asset_paths(p_sfx, current_theme, get_subtheme(), default_theme, p_misc, p_character); pathlist += get_sounds_path(p_sfx); // Sounds folder path return get_sfx_path(pathlist); } QString AOApplication::get_case_sensitive_path(QString p_file) { - // no path traversal above base folder - if (!(p_file.startsWith(get_base_path()))) - return get_base_path() + p_file; - - #ifdef CASE_SENSITIVE_FILESYSTEM +#ifdef CASE_SENSITIVE_FILESYSTEM // first, check to see if it's actually there (also serves as base case for // recursion) QFileInfo file(p_file); @@ -219,6 +198,7 @@ QString AOApplication::get_case_sensitive_path(QString p_file) if (exists(p_file)) return p_file; + QString file_parent_dir = get_case_sensitive_path(file.absolutePath()); // second, does it exist in the new parent dir? @@ -226,14 +206,20 @@ QString AOApplication::get_case_sensitive_path(QString p_file) return file_parent_dir + "/" + file_basename; // last resort, dirlist parent dir and find case insensitive match - QRegExp file_rx = - QRegExp(file_basename, Qt::CaseInsensitive, QRegExp::FixedString); - QStringList files = QDir(file_parent_dir).entryList(); - int result = files.indexOf(file_rx); + if (!dir_listing_exist_cache.contains(qHash(file_parent_dir))) { + QStringList files = QDir(file_parent_dir).entryList(); + for (const QString &file : files) { + dir_listing_cache.insert(qHash(file_parent_dir % QChar('/') % file.toLower()), file); + } + dir_listing_exist_cache.insert(qHash(file_parent_dir)); + } + QString found_file = dir_listing_cache.value( + qHash(file_parent_dir % QChar('/') % file_basename.toLower())); - if (result != -1) - return file_parent_dir + "/" + files.at(result); + if (!found_file.isEmpty()) { + return file_parent_dir + "/" + found_file; + } // if nothing is found, let the caller handle the missing file return file_parent_dir + "/" + file_basename; @@ -241,3 +227,72 @@ QString AOApplication::get_case_sensitive_path(QString p_file) return p_file; #endif } + +QString AOApplication::get_real_path(const VPath &vpath) { + // Try cache first + QString phys_path = asset_lookup_cache.value(qHash(vpath)); + if (!phys_path.isEmpty() && exists(phys_path)) { + return phys_path; + } + + // Cache miss; try all known mount paths + QStringList bases = get_mount_paths(); + bases.push_front(get_base_path()); + + for (const QString &base : bases) { + QDir baseDir(base); + QString path = baseDir.absoluteFilePath(vpath.toQString()); + if (!path.startsWith(baseDir.absolutePath())) { + qWarning() << "invalid path" << path << "(path is outside vfs)"; + break; + } + path = get_case_sensitive_path(path); + if (exists(path)) { + asset_lookup_cache.insert(qHash(vpath), path); + return path; + } + } + + // File or directory not found + return QString(); +} + +// Special case of get_real_path where multiple suffixes need to be tried +// on each mount path. +QString AOApplication::get_real_suffixed_path(const VPath &vpath, + const QStringList &suffixes) { + // Try cache first + QString phys_path = asset_lookup_cache.value(qHash(vpath)); + if (!phys_path.isEmpty() && exists(phys_path)) { + return phys_path; + } + + // Cache miss; try each suffix on all known mount paths + QStringList bases = get_mount_paths(); + bases.push_front(get_base_path()); + + for (const QString &base : bases) { + for (const QString &suffix : suffixes) { + QDir baseDir(base); + QString path = baseDir.absoluteFilePath(vpath.toQString() + suffix); + if (!path.startsWith(baseDir.absolutePath())) { + qWarning() << "invalid path" << path << "(path is outside vfs)"; + break; + } + path = get_case_sensitive_path(path); + if (exists(path)) { + asset_lookup_cache.insert(qHash(vpath), path); + return path; + } + } + } + + // File or directory not found + return QString(); +} + +void AOApplication::invalidate_lookup_cache() { + asset_lookup_cache.clear(); + dir_listing_cache.clear(); + dir_listing_exist_cache.clear(); +} diff --git a/src/text_file_functions.cpp b/src/text_file_functions.cpp index 1d36689..fbe8279 100644 --- a/src/text_file_functions.cpp +++ b/src/text_file_functions.cpp @@ -130,6 +130,11 @@ QStringList AOApplication::get_call_words() return get_list_file(get_base_path() + "callwords.ini"); } +QStringList AOApplication::get_list_file(VPath path) +{ + return get_list_file(get_real_path(path)); +} + QStringList AOApplication::get_list_file(QString p_file) { QStringList return_value; @@ -279,6 +284,12 @@ QVector AOApplication::read_serverlist_txt() return f_server_list; } +QString AOApplication::read_design_ini(QString p_identifier, + VPath p_design_path) +{ + return read_design_ini(p_identifier, get_real_path(p_design_path)); +} + QString AOApplication::read_design_ini(QString p_identifier, QString p_design_path) { @@ -448,12 +459,14 @@ QString AOApplication::get_chat_markup(QString p_identifier, QString p_chat) return value.toUtf8(); // Backwards ass compatibility - QStringList backwards_paths{get_theme_path("misc/" + p_chat + "/config.ini"), - get_base_path() + "misc/" + p_chat + - "/config.ini", - get_theme_path("misc/default/config.ini"), - get_base_path() + "misc/default/config.ini"}; - for (const QString &p : backwards_paths) { + QVector backwards_paths { + get_theme_path("misc/" + p_chat + "/config.ini"), + VPath("misc/" + p_chat + "/config.ini"), + get_theme_path("misc/default/config.ini"), + VPath("misc/default/config.ini") + }; + + for (const VPath &p : backwards_paths) { QString value = read_design_ini(p_identifier, p); if (!value.isEmpty()) { return value.toUtf8(); @@ -490,36 +503,21 @@ QString AOApplication::get_court_sfx(QString p_identifier, QString p_misc) return ""; } -QString AOApplication::get_sfx_suffix(QString sound_to_check) +QString AOApplication::get_sfx_suffix(VPath sound_to_check) { - if (file_exists(sound_to_check)) - return sound_to_check; - if (file_exists(sound_to_check + ".opus")) - return sound_to_check + ".opus"; - if (file_exists(sound_to_check + ".ogg")) - return sound_to_check + ".ogg"; - if (file_exists(sound_to_check + ".mp3")) - return sound_to_check + ".mp3"; - if (file_exists(sound_to_check + ".mp4")) - return sound_to_check + ".mp4"; - return sound_to_check + ".wav"; + return get_real_suffixed_path(sound_to_check, + { "", ".opus", ".ogg", ".mp3", ".wav" }); } -QString AOApplication::get_image_suffix(QString path_to_check, bool static_image) +QString AOApplication::get_image_suffix(VPath path_to_check, bool static_image) { - if (file_exists(path_to_check)) - return path_to_check; - // A better method would to actually use AOImageReader and see if these images have more than 1 frame. - // However, that might not be performant. + QStringList suffixes { "" }; if (!static_image) { - if (file_exists(path_to_check + ".webp")) - return path_to_check + ".webp"; - if (file_exists(path_to_check + ".apng")) - return path_to_check + ".apng"; - if (file_exists(path_to_check + ".gif")) - return path_to_check + ".gif"; + suffixes.append({ ".webp", ".apng", ".gif" }); } - return path_to_check + ".png"; + suffixes.append(".png"); + + return get_real_suffixed_path(path_to_check, suffixes); } // returns whatever is to the right of "search_line =" within target_tag and @@ -528,7 +526,7 @@ QString AOApplication::get_image_suffix(QString path_to_check, bool static_image QString AOApplication::read_char_ini(QString p_char, QString p_search_line, QString target_tag) { - QSettings settings(get_character_path(p_char, "char.ini"), + QSettings settings(get_real_path(get_character_path(p_char, "char.ini")), QSettings::IniFormat); settings.beginGroup(target_tag); settings.setIniCodec("UTF-8"); @@ -540,7 +538,7 @@ QString AOApplication::read_char_ini(QString p_char, QString p_search_line, void AOApplication::set_char_ini(QString p_char, QString value, QString p_search_line, QString target_tag) { - QSettings settings(get_character_path(p_char, "char.ini"), + QSettings settings(get_real_path(get_character_path(p_char, "char.ini")), QSettings::IniFormat); settings.beginGroup(target_tag); settings.setValue(p_search_line, value); @@ -548,10 +546,10 @@ void AOApplication::set_char_ini(QString p_char, QString value, } // returns all the values of target_tag -QStringList AOApplication::read_ini_tags(QString p_path, QString target_tag) +QStringList AOApplication::read_ini_tags(VPath p_path, QString target_tag) { QStringList r_values; - QSettings settings(p_path, QSettings::IniFormat); + QSettings settings(get_real_path(p_path), QSettings::IniFormat); settings.setIniCodec("UTF-8"); if (!target_tag.isEmpty()) settings.beginGroup(target_tag); @@ -1095,3 +1093,8 @@ QString AOApplication::get_default_scaling() { return configini->value("default_scaling", "fast").value(); } + +QStringList AOApplication::get_mount_paths() +{ + return configini->value("mount_paths").value(); +}