From e260c642b0ddeac9ceb47f432d01bdacc7f7357f Mon Sep 17 00:00:00 2001 From: GriffinR Date: Thu, 27 Feb 2025 14:44:19 -0500 Subject: [PATCH] Speed improvements for Project::readSpeciesIconPaths --- include/project.h | 8 +- src/project.cpp | 167 ++++++++++++++++------------- src/ui/encountertabledelegates.cpp | 2 +- src/ui/projectsettingseditor.cpp | 8 +- src/ui/wildmonsearch.cpp | 2 +- 5 files changed, 106 insertions(+), 81 deletions(-) diff --git a/include/project.h b/include/project.h index 73bb5eeb..5330b792 100644 --- a/include/project.h +++ b/include/project.h @@ -51,6 +51,7 @@ public: QStringList itemNames; QStringList flagNames; QStringList varNames; + QStringList speciesNames; QStringList movementTypes; QStringList mapTypes; QStringList mapBattleScenes; @@ -142,8 +143,8 @@ public: QVector extraEncounterGroups; bool readSpeciesIconPaths(); - QPixmap getSpeciesIcon(const QString &species) const; - QMap speciesToIconPath; + QString getDefaultSpeciesIconPath(const QString &species); + QPixmap getSpeciesIcon(const QString &species); void addNewMapsec(const QString &idName); void removeMapsec(const QString &idName); @@ -255,6 +256,7 @@ private: QMap mapSectionDisplayNames; QMap modifiedFileTimestamps; QMap facingDirections; + QMap speciesToIconPath; struct EventGraphics { @@ -275,6 +277,8 @@ private: void ignoreWatchedFileTemporarily(QString filepath); void recordFileChange(const QString &filepath); + QString findSpeciesIconPath(const QStringList &names) const; + int maxEventsPerGroup; int maxObjectEvents; static int num_tiles_primary; diff --git a/src/project.cpp b/src/project.cpp index 749e2676..5ee65254 100644 --- a/src/project.cpp +++ b/src/project.cpp @@ -2924,101 +2924,122 @@ QPixmap Project::getEventPixmap(Event::Group group) { bool Project::readSpeciesIconPaths() { this->speciesToIconPath.clear(); + this->speciesNames.clear(); // Read map of species constants to icon names const QString srcfilename = projectConfig.getFilePath(ProjectFilePath::pokemon_icon_table); - fileWatcher.addPath(root + "/" + srcfilename); + fileWatcher.addPath(this->root + "/" + srcfilename); const QString tableName = projectConfig.getIdentifier(ProjectIdentifier::symbol_pokemon_icon_table); const QMap monIconNames = parser.readNamedIndexCArray(srcfilename, tableName); - // Read map of icon names to filepaths - const QString incfilename = projectConfig.getFilePath(ProjectFilePath::data_pokemon_gfx); - fileWatcher.addPath(root + "/" + incfilename); - const QMap iconIncbins = parser.readCIncbinMulti(incfilename); - // Read species constants. If this fails we can get them from the icon table (but we shouldn't rely on it). const QString speciesPrefix = projectConfig.getIdentifier(ProjectIdentifier::define_species_prefix); const QString constantsFilename = projectConfig.getFilePath(ProjectFilePath::constants_species); - fileWatcher.addPath(root + "/" + constantsFilename); - QStringList speciesNames = parser.readCDefineNames(constantsFilename, {QString("\\b%1").arg(speciesPrefix)}); - if (speciesNames.isEmpty()) - speciesNames = monIconNames.keys(); + fileWatcher.addPath(this->root + "/" + constantsFilename); + this->speciesNames = parser.readCDefineNames(constantsFilename, {QString("\\b%1").arg(speciesPrefix)}); + if (this->speciesNames.isEmpty()) { + this->speciesNames = monIconNames.keys(); + } + this->speciesNames.sort(); - // For each species, use the information gathered above to find the icon image. - bool missingIcons = false; - for (auto species : speciesNames) { - QString path = QString(); - if (monIconNames.contains(species) && iconIncbins.contains(monIconNames.value(species))) { - // We have the icon filepath from the icon table - path = QString("%1/%2").arg(root).arg(this->fixGraphicPath(iconIncbins[monIconNames.value(species)])); - } else { - // Failed to read icon filepath from the icon table, check filepaths where icons are normally located. - // Try to use the icon name (if we have it) to determine the directory, then try the species name. - // The name permuting is overkill, but it's making up for some of the fragility in the way we find icon paths. - QStringList possibleDirNames; - if (monIconNames.contains(species)) { - // Ex: For 'gMonIcon_QuestionMark' try 'question_mark' - static const QRegularExpression re("([a-z])([A-Z0-9])"); - QString iconName = monIconNames.value(species); - iconName = iconName.mid(iconName.indexOf("_") + 1); // jump past prefix ('gMonIcon') - possibleDirNames.append(iconName.replace(re, "\\1_\\2").toLower()); - } - - // Ex: For 'SPECIES_FOO_BAR_BAZ' try 'foo_bar_baz' - possibleDirNames.append(species.mid(speciesPrefix.length()).toLower()); - - // Permute paths with underscores. - // Ex: Try 'foo_bar/baz', 'foo/bar_baz', 'foobarbaz', 'foo_bar', and 'foo' - QStringList permutedNames; - for (auto dir : possibleDirNames) { - if (!dir.contains("_")) continue; - for (int i = dir.indexOf("_"); i > -1; i = dir.indexOf("_", i + 1)) { - QString temp = dir; - permutedNames.prepend(temp.replace(i, 1, "/")); - permutedNames.append(dir.left(i)); // Prepend the others so the most generic name ('foo') ends up last + // If we successfully found the species icon table we can use this data to get the filepath for each species icon. + // For any species not in the table, or if we failed to find the table at all, we will have to predict where the icon file is. + // That can require checking a lot of files (especially for projects with many species), so to save time on startup we only + // do this on request in Project::getDefaultSpeciesIconPath. + if (!monIconNames.isEmpty()) { + const QString iconGraphicsFile = projectConfig.getFilePath(ProjectFilePath::data_pokemon_gfx); + fileWatcher.addPath(this->root + "/" + iconGraphicsFile); + QMap iconNameToFilepath = parser.readCIncbinMulti(iconGraphicsFile); + + for (auto i = monIconNames.constBegin(); i != monIconNames.constEnd(); i++) { + QString path; + QString species = i.key(); + QString iconName = i.value(); + if (iconNameToFilepath.contains(iconName)) { + path = fixGraphicPath(iconNameToFilepath.value(iconName)); + } else { + // We have an icon name for this species, but we haven't found its filepath. + // Try to find the icon file using the full icon name, and the icon name if we assume it has a prefix. + // Ex: For 'gMonIcon_QuestionMark' search for files by permuting through directories using 'question_mark' and 'g_mon_icon_question_mark. + static const QRegularExpression re_caseChange("([a-z])([A-Z0-9])"); + QStringList dirNames; + if (iconName.contains("_")) { + QString iconNameNoPrefix = iconName.mid(iconName.indexOf("_") + 1); + dirNames.append(iconNameNoPrefix.replace(re_caseChange, "\\1_\\2").toLower()); } - permutedNames.prepend(dir.remove("_")); + QString iconNameWithPrefix = iconName; // Leave iconName unchanged by .replace + dirNames.append(iconNameWithPrefix.replace(re_caseChange, "\\1_\\2").toLower()); + path = iconNameToFilepath[iconName] = findSpeciesIconPath(dirNames); } - possibleDirNames.append(permutedNames); - - possibleDirNames.removeDuplicates(); - for (auto dir : possibleDirNames) { - if (dir.isEmpty()) continue; - const QString stdPath = QString("%1/%2%3/icon.png") - .arg(root) - .arg(projectConfig.getFilePath(ProjectFilePath::pokemon_gfx)) - .arg(dir); - if (QFile::exists(stdPath)) { - // Icon found at a normal filepath - path = stdPath; - break; - } - } - - if (path.isEmpty() && projectConfig.getPokemonIconPath(species).isEmpty()) { - // Failed to find icon, this species will use a placeholder icon. - logWarn(QString("Failed to find Pokémon icon for '%1'").arg(species)); - missingIcons = true; + if (!path.isEmpty()) { + this->speciesToIconPath.insert(species, QString("%1/%2").arg(this->root).arg(path)); } } - this->speciesToIconPath.insert(species, path); } - - // Logging this alongside every warning (if there are multiple) is obnoxious, just do it once at the end. - if (missingIcons) logInfo("Pokémon icon filepaths can be specified under 'Options->Project Settings'"); - return true; } -QPixmap Project::getSpeciesIcon(const QString &species) const { +QString Project::getDefaultSpeciesIconPath(const QString &species) { + if (this->speciesToIconPath.contains(species)) { + // We already know the icon path for this species (either because we read it from the project, or we found it already). + return this->speciesToIconPath.value(species); + } + if (!this->speciesNames.contains(species)) { + // Don't bother searching for a path if we don't recognize the species name. + return QString(); + } + + // Ex: For 'SPECIES_FOO_BAR_BAZ' search for files by permuting through directories using 'foo_bar_baz'. + const QString speciesPrefix = projectConfig.getIdentifier(ProjectIdentifier::define_species_prefix); + const QString path = findSpeciesIconPath({species.mid(speciesPrefix.length()).toLower()}); + this->speciesToIconPath.insert(species, path); + + // We failed to find a default icon path, this species will use a placeholder icon. + // If the user has no custom icon path for this species, tell them they can provide one. + if (path.isEmpty() && projectConfig.getPokemonIconPath(species).isEmpty()) { + logWarn(QString("Failed to find Pokémon icon for '%1'. The filepath can be specified under 'Options->Project Settings'").arg(species)); + } + return path; +} + +// The name permuting in here is overkill, but it's making up for some of the fragility in the way we find pokémon icon paths. +// For pokeemerald-expansion in particular this function is solely responsible for finding pokémon icons, because they have no icon table. +QString Project::findSpeciesIconPath(const QStringList &names) const { + QStringList possibleDirNames = names; + + // Permute paths with underscores. + // Ex: For a base name of 'foo_bar_baz', try 'foo_bar/baz', 'foo/bar_baz', 'foobarbaz', 'foo_bar', and 'foo'. + QStringList permutedNames; + for (auto dir : possibleDirNames) { + if (!dir.contains("_")) continue; + for (int i = dir.indexOf("_"); i > -1; i = dir.indexOf("_", i + 1)) { + QString temp = dir; + permutedNames.prepend(temp.replace(i, 1, "/")); + permutedNames.append(dir.left(i)); // Prepend the others so the most generic name ('foo') ends up last + } + permutedNames.prepend(dir.remove("_")); + } + possibleDirNames.append(permutedNames); + possibleDirNames.removeDuplicates(); + + const QString basePath = QString("%1/%2").arg(this->root).arg(projectConfig.getFilePath(ProjectFilePath::pokemon_gfx)); + for (const auto &dir : possibleDirNames) { + if (dir.isEmpty()) continue; + + const QString path = QString("%1%2/icon.png").arg(basePath).arg(dir); + if (QFile::exists(path)) + return path; + } + return QString(); +} + +QPixmap Project::getSpeciesIcon(const QString &species) { QPixmap pixmap; if (!QPixmapCache::find(species, &pixmap)) { // Prefer path from config. If not present, use the path parsed from project files - QString path = projectConfig.getPokemonIconPath(species); + QString path = Project::getExistingFilepath(projectConfig.getPokemonIconPath(species)); if (path.isEmpty()) { - path = this->speciesToIconPath.value(species); - } else { - path = Project::getExistingFilepath(path); + path = getDefaultSpeciesIconPath(species); } QImage img(path); diff --git a/src/ui/encountertabledelegates.cpp b/src/ui/encountertabledelegates.cpp index 4ffe9e0d..2d46f54f 100644 --- a/src/ui/encountertabledelegates.cpp +++ b/src/ui/encountertabledelegates.cpp @@ -21,7 +21,7 @@ void SpeciesComboDelegate::paint(QPainter *painter, const QStyleOptionViewItem & QWidget *SpeciesComboDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &, const QModelIndex &) const { NoScrollComboBox *editor = new NoScrollComboBox(parent); editor->setFrame(false); - editor->addItems(this->project->speciesToIconPath.keys()); + editor->addItems(this->project->speciesNames); return editor; } diff --git a/src/ui/projectsettingseditor.cpp b/src/ui/projectsettingseditor.cpp index 54aeada2..fa84f3e0 100644 --- a/src/ui/projectsettingseditor.cpp +++ b/src/ui/projectsettingseditor.cpp @@ -104,7 +104,7 @@ void ProjectSettingsEditor::initUi() { if (project) { ui->comboBox_DefaultPrimaryTileset->addItems(project->primaryTilesetLabels); ui->comboBox_DefaultSecondaryTileset->addItems(project->secondaryTilesetLabels); - ui->comboBox_IconSpecies->addItems(project->speciesToIconPath.keys()); + ui->comboBox_IconSpecies->addItems(project->speciesNames); ui->comboBox_WarpBehaviors->addItems(project->metatileBehaviorMap.keys()); } ui->comboBox_BaseGameVersion->addItems(ProjectConfig::versionStrings); @@ -278,11 +278,11 @@ void ProjectSettingsEditor::updatePokemonIconPath(const QString &newSpecies) { if (!project) return; // If user was editing a path for a valid species, record filepath text before we wipe it. - if (!this->prevIconSpecies.isEmpty() && this->project->speciesToIconPath.contains(this->prevIconSpecies)) + if (!this->prevIconSpecies.isEmpty() && this->project->speciesNames.contains(this->prevIconSpecies)) this->editedPokemonIconPaths[this->prevIconSpecies] = ui->lineEdit_PokemonIcon->text(); QString editedPath = this->editedPokemonIconPaths.value(newSpecies); - QString defaultPath = this->project->speciesToIconPath.value(newSpecies); + QString defaultPath = this->project->getDefaultSpeciesIconPath(newSpecies); ui->lineEdit_PokemonIcon->setText(this->stripProjectDir(editedPath)); ui->lineEdit_PokemonIcon->setPlaceholderText(this->stripProjectDir(defaultPath)); @@ -567,7 +567,7 @@ void ProjectSettingsEditor::save() { // Save pokemon icon paths const QString species = ui->comboBox_IconSpecies->currentText(); - if (this->project->speciesToIconPath.contains(species)) + if (this->project->speciesNames.contains(species)) this->editedPokemonIconPaths.insert(species, ui->lineEdit_PokemonIcon->text()); for (auto i = this->editedPokemonIconPaths.cbegin(), end = this->editedPokemonIconPaths.cend(); i != end; i++) projectConfig.setPokemonIconPath(i.key(), i.value()); diff --git a/src/ui/wildmonsearch.cpp b/src/ui/wildmonsearch.cpp index f43a0dab..056e7565 100644 --- a/src/ui/wildmonsearch.cpp +++ b/src/ui/wildmonsearch.cpp @@ -22,7 +22,7 @@ WildMonSearch::WildMonSearch(Project *project, QWidget *parent) : ui->setupUi(this); // Set up species combo box - ui->comboBox_Search->addItems(project->speciesToIconPath.keys()); + ui->comboBox_Search->addItems(project->speciesNames); ui->comboBox_Search->setCurrentText(QString()); ui->comboBox_Search->lineEdit()->setPlaceholderText(Project::getEmptySpeciesName()); connect(ui->comboBox_Search, &QComboBox::currentTextChanged, this, &WildMonSearch::updateResults);