From 06ff21369121e3aca6e6c095f661d5e6987b1319 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Sun, 10 Dec 2023 03:49:54 -0500 Subject: [PATCH] Sturdier pokemon icon search, add icon override settings --- forms/projectsettingseditor.ui | 59 ++++++++++- include/config.h | 7 ++ include/project.h | 1 - include/ui/projectsettingseditor.h | 4 + resources/images.qrc | 1 + resources/images/pokemon_icon_placeholder.png | Bin 0 -> 1025 bytes src/config.cpp | 21 ++++ src/project.cpp | 95 ++++++++++++++---- src/ui/encountertabledelegates.cpp | 21 +++- src/ui/projectsettingseditor.cpp | 56 +++++++++-- 10 files changed, 235 insertions(+), 30 deletions(-) create mode 100644 resources/images/pokemon_icon_placeholder.png diff --git a/forms/projectsettingseditor.ui b/forms/projectsettingseditor.ui index fcb35e73..c89a2cd3 100644 --- a/forms/projectsettingseditor.ui +++ b/forms/projectsettingseditor.ui @@ -39,7 +39,7 @@ 0 0 531 - 805 + 916 @@ -386,6 +386,63 @@ + + + + Pokémon Icons + + + + + + + 0 + 0 + + + + Species + + + + + + + ... + + + + :/icons/folder.ico:/icons/folder.ico + + + + + + + true + + + 20 + + + + + + + Image Path + + + + + + + true + + + + + + diff --git a/include/config.h b/include/config.h index 96cc4911..835ee47d 100644 --- a/include/config.h +++ b/include/config.h @@ -212,9 +212,11 @@ enum ProjectFilePath { constants_region_map_sections, constants_metatile_labels, constants_metatile_behaviors, + constants_species, constants_fieldmap, initial_facing_table, pokemon_icon_table, + pokemon_gfx, }; class ProjectConfig: public KeyValueConfigBase @@ -238,6 +240,7 @@ public: this->tilesetsHaveIsCompressed = true; this->filePaths.clear(); this->eventIconPaths.clear(); + this->pokemonIconPaths.clear(); this->collisionSheetPath = QString(); this->collisionSheetWidth = 2; this->collisionSheetHeight = 16; @@ -315,6 +318,9 @@ public: void setMapAllowFlagsEnabled(bool enabled); void setEventIconPath(Event::Group group, const QString &path); QString getEventIconPath(Event::Group group); + void setPokemonIconPath(const QString &species, const QString &path); + QString getPokemonIconPath(const QString & species); + QHash getPokemonIconPaths(); void setCollisionSheetPath(const QString &path); QString getCollisionSheetPath(); void setCollisionSheetWidth(int width); @@ -361,6 +367,7 @@ private: uint32_t metatileLayerTypeMask; bool enableMapAllowFlags; QMap eventIconPaths; + QHash pokemonIconPaths; QString collisionSheetPath; int collisionSheetWidth; int collisionSheetHeight; diff --git a/include/project.h b/include/project.h index d9365f23..e9f41267 100644 --- a/include/project.h +++ b/include/project.h @@ -170,7 +170,6 @@ public: void saveTilesetPalettes(Tileset*); QString defaultSong; - QStringList getVisibilities(); void appendTilesetLabel(QString label, QString isSecondaryStr); bool readTilesetLabels(); bool readTilesetProperties(); diff --git a/include/ui/projectsettingseditor.h b/include/ui/projectsettingseditor.h index 5670cb90..4e383275 100644 --- a/include/ui/projectsettingseditor.h +++ b/include/ui/projectsettingseditor.h @@ -31,6 +31,8 @@ private: bool projectNeedsReload = false; bool refreshing = false; const QString baseDir; + QHash editedPokemonIconPaths; + QString prevIconSpecies; void initUi(); void connectSignals(); @@ -51,11 +53,13 @@ private: void choosePrefabsFile(); void chooseImageFile(QLineEdit * filepathEdit); void chooseFile(QLineEdit * filepathEdit, const QString &description, const QString &extensions); + QString stripProjectDir(QString s); private slots: void dialogButtonClicked(QAbstractButton *button); void importDefaultPrefabsClicked(bool); void updateAttributeLimits(const QString &attrSize); + void updatePokemonIconPath(const QString &species); void markEdited(); void on_mainTabs_tabBarClicked(int index); }; diff --git a/resources/images.qrc b/resources/images.qrc index 35280608..86d56cb2 100644 --- a/resources/images.qrc +++ b/resources/images.qrc @@ -63,6 +63,7 @@ images/collisions.png images/collisions_unknown.png images/Entities_16x16.png + images/pokemon_icon_placeholder.png icons/clipboard.ico diff --git a/resources/images/pokemon_icon_placeholder.png b/resources/images/pokemon_icon_placeholder.png new file mode 100644 index 0000000000000000000000000000000000000000..439576681d8be93eeb508ffe6af35e0fffb3a9b3 GIT binary patch literal 1025 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdzmUKs7M+SzC{oH>NSwWJ?9znhg z3{`3j3=J&|48MRv4KElNN(~qoUL`OvSj}Ky5HFasE6|34fr&daB%&n3*T*V3KUXg? zB|j-uuOhbqsDpvQrosxy%uOvxRH(?!$t$+1uvG%9umZ9{!um=IU?nBlwn`Dc0SeCf zMX3s=dM0`XN_Jcd3JNwwDQQ+gE^bimK%T8qMoCG5mA-y?dAVM>v0i>ry1t>MrKP@s zk-m|UE>MMTab;dfVufyAu`tKo-FP#GNIX zX$YJ0ilN>DdQcxEqi?8Zpbs}2sMQ7}Y88}PoS#z)GCnjfv&7EG2CNue7-1Vkmveq@ zK~ZXPF;JVGAyf;p7`l4@qRdpF|3KEj)gz0cs}IP?FUbdnbx~%rXI@Ea5zsQIZkPjc ztALtiqYnx$q~L?Z4OkQyId)t&`tXRhZumMj=_a3dq zd%s%0o#AuxM$Pisw$Gw9&%NLjZ(WjM?j6Wopc44xZs(#mC3mlFf4K%nGATA)(nVC3YJ;w*84buH9#2zQQd|SrAV!Tw~kY(=X zhvN2{j7QWpyY-9j^?d!+9{s?ws;ukdwdik?823u8Rfs*lJHb~uUXVdRYC6NK-VKi` za&|4VdG>l@xJz>hPY{FhUmc${huciIiCO;fVPx6R(!g*ueQ%qtQ_{FVdQ&MBb@07iOp2LJ#7 literal 0 HcmV?d00001 diff --git a/src/config.cpp b/src/config.cpp index de4499a0..1c393f3f 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -58,9 +58,11 @@ const QMap> ProjectConfig::defaultP {ProjectFilePath::constants_region_map_sections, { "constants_region_map_sections", "include/constants/region_map_sections.h"}}, {ProjectFilePath::constants_metatile_labels, { "constants_metatile_labels", "include/constants/metatile_labels.h"}}, {ProjectFilePath::constants_metatile_behaviors, { "constants_metatile_behaviors", "include/constants/metatile_behaviors.h"}}, + {ProjectFilePath::constants_species, { "constants_species", "include/constants/species.h"}}, {ProjectFilePath::constants_fieldmap, { "constants_fieldmap", "include/fieldmap.h"}}, {ProjectFilePath::pokemon_icon_table, { "pokemon_icon_table", "src/pokemon_icon.c"}}, {ProjectFilePath::initial_facing_table, { "initial_facing_table", "src/event_object_movement.c"}}, + {ProjectFilePath::pokemon_gfx, { "pokemon_gfx", "graphics/pokemon/"}}, }; ProjectFilePath reverseDefaultPaths(QString str) { @@ -722,6 +724,8 @@ void ProjectConfig::parseConfigKeyValue(QString key, QString value) { this->eventIconPaths[Event::Group::Bg] = value; } else if (key == "event_icon_path_heal") { this->eventIconPaths[Event::Group::Heal] = value; + } else if (key.startsWith("pokemon_icon_path/")) { + this->pokemonIconPaths.insert(key.mid(18).toUpper(), value); } else if (key == "collision_sheet_path") { this->collisionSheetPath = value; } else if (key == "collision_sheet_width") { @@ -802,6 +806,10 @@ QMap ProjectConfig::getKeyValueMap() { map.insert("event_icon_path_coord", this->eventIconPaths[Event::Group::Coord]); map.insert("event_icon_path_bg", this->eventIconPaths[Event::Group::Bg]); map.insert("event_icon_path_heal", this->eventIconPaths[Event::Group::Heal]); + for (auto i = this->pokemonIconPaths.cbegin(), end = this->pokemonIconPaths.cend(); i != end; i++){ + const QString path = i.value(); + if (!path.isEmpty()) map.insert("pokemon_icon_path/" + i.key(), path); + } map.insert("collision_sheet_path", this->collisionSheetPath); map.insert("collision_sheet_width", QString::number(this->collisionSheetWidth)); map.insert("collision_sheet_height", QString::number(this->collisionSheetHeight)); @@ -1163,6 +1171,19 @@ QString ProjectConfig::getEventIconPath(Event::Group group) { return this->eventIconPaths.value(group); } +void ProjectConfig::setPokemonIconPath(const QString &species, const QString &path) { + this->pokemonIconPaths[species] = path; + this->save(); +} + +QString ProjectConfig::getPokemonIconPath(const QString &species) { + return this->pokemonIconPaths.value(species); +} + +QHash ProjectConfig::getPokemonIconPaths() { + return this->pokemonIconPaths; +} + void ProjectConfig::setCollisionSheetPath(const QString &path) { this->collisionSheetPath = path; this->save(); diff --git a/src/project.cpp b/src/project.cpp index 37ea7e23..69778540 100644 --- a/src/project.cpp +++ b/src/project.cpp @@ -1775,15 +1775,6 @@ QString Project::getNewMapName() { return newMapName; } -QStringList Project::getVisibilities() { - // TODO - QStringList names; - for (int i = 0; i < 16; i++) { - names.append(QString("%1").arg(i)); - } - return names; -} - Project::DataQualifiers Project::getDataQualifiers(QString text, QString label) { Project::DataQualifiers qualifiers; @@ -2477,17 +2468,87 @@ bool Project::readEventGraphics() { } bool Project::readSpeciesIconPaths() { - speciesToIconPath.clear(); - QString srcfilename = projectConfig.getFilePath(ProjectFilePath::pokemon_icon_table); - QString incfilename = projectConfig.getFilePath(ProjectFilePath::data_pokemon_gfx); + this->speciesToIconPath.clear(); + + // Read map of species constants to icon names + const QString srcfilename = projectConfig.getFilePath(ProjectFilePath::pokemon_icon_table); fileWatcher.addPath(root + "/" + srcfilename); + const QMap monIconNames = parser.readNamedIndexCArray(srcfilename, "gMonIconTable"); + + // Read map of icon names to filepaths. These are spread between two different files + const QString incfilename = projectConfig.getFilePath(ProjectFilePath::data_pokemon_gfx); fileWatcher.addPath(root + "/" + incfilename); - QMap monIconNames = parser.readNamedIndexCArray(srcfilename, "gMonIconTable"); - QMap iconIncbins = parser.readCIncbinMulti(incfilename); - for (QString species : monIconNames.keys()) { - QString path = iconIncbins[monIconNames.value(species)]; - speciesToIconPath.insert(species, root + "/" + path.replace("4bpp", "png")); + 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). + static const QStringList prefixes("\\bSPECIES_"); + const QString constantsFilename = projectConfig.getFilePath(ProjectFilePath::constants_species); + fileWatcher.addPath(root + "/" + constantsFilename); + const QMap defines = parser.readCDefines(constantsFilename, prefixes); // TODO: Suppress errors + const QStringList speciesNames = defines.isEmpty() ? monIconNames.keys() : defines.keys(); + + 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(8).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 + } + permutedNames.prepend(dir.remove("_")); + } + 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; + } + } + 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; } diff --git a/src/ui/encountertabledelegates.cpp b/src/ui/encountertabledelegates.cpp index 44229822..dc975c14 100644 --- a/src/ui/encountertabledelegates.cpp +++ b/src/ui/encountertabledelegates.cpp @@ -16,9 +16,24 @@ void SpeciesComboDelegate::paint(QPainter *painter, const QStyleOptionViewItem & QPixmap pm; if (!QPixmapCache::find(species, &pm)) { - QImage img(this->project->speciesToIconPath.value(species)); - img.setColor(0, qRgba(0, 0, 0, 0)); - pm = QPixmap::fromImage(img); + // Prefer path from config. If not present, use the path parsed from project files + QString path = projectConfig.getPokemonIconPath(species); + if (path.isEmpty()) { + path = this->project->speciesToIconPath.value(species); + } else { + QFileInfo info(path); + if (info.isRelative()) + path = QDir::cleanPath(projectConfig.getProjectDir() + QDir::separator() + path); + } + + QImage img(path); + if (img.isNull()) { + // No icon for this species, use placeholder + pm = QPixmap(":images/pokemon_icon_placeholder.png"); + } else { + img.setColor(0, qRgba(0, 0, 0, 0)); + pm = QPixmap::fromImage(img); + } QPixmapCache::insert(species, pm); } QPixmap monIcon = pm.copy(0, 0, 32, 32); diff --git a/src/ui/projectsettingseditor.cpp b/src/ui/projectsettingseditor.cpp index 95660a1f..ecd7de55 100644 --- a/src/ui/projectsettingseditor.cpp +++ b/src/ui/projectsettingseditor.cpp @@ -36,6 +36,7 @@ void ProjectSettingsEditor::connectSignals() { connect(ui->button_ImportDefaultPrefabs, &QAbstractButton::clicked, this, &ProjectSettingsEditor::importDefaultPrefabsClicked); connect(ui->comboBox_BaseGameVersion, &QComboBox::currentTextChanged, this, &ProjectSettingsEditor::promptRestoreDefaults); connect(ui->comboBox_AttributesSize, &QComboBox::currentTextChanged, this, &ProjectSettingsEditor::updateAttributeLimits); + connect(ui->comboBox_IconSpecies, &QComboBox::currentTextChanged, this, &ProjectSettingsEditor::updatePokemonIconPath); connect(ui->checkBox_EnableCustomBorderSize, &QCheckBox::stateChanged, [this](int state) { bool customSize = (state == Qt::Checked); // When switching between the spin boxes or line edit for border metatiles we set @@ -52,10 +53,13 @@ void ProjectSettingsEditor::connectSignals() { connect(ui->button_TriggersIcon, &QAbstractButton::clicked, [this](bool) { this->chooseImageFile(ui->lineEdit_TriggersIcon); }); connect(ui->button_BGsIcon, &QAbstractButton::clicked, [this](bool) { this->chooseImageFile(ui->lineEdit_BGsIcon); }); connect(ui->button_HealspotsIcon, &QAbstractButton::clicked, [this](bool) { this->chooseImageFile(ui->lineEdit_HealspotsIcon); }); + connect(ui->button_PokemonIcon, &QAbstractButton::clicked, [this](bool) { this->chooseImageFile(ui->lineEdit_PokemonIcon); }); // Record that there are unsaved changes if any of the settings are modified - for (auto combo : ui->centralwidget->findChildren()) - connect(combo, &QComboBox::currentTextChanged, this, &ProjectSettingsEditor::markEdited); + for (auto combo : ui->centralwidget->findChildren()){ + if (combo != ui->comboBox_IconSpecies) // Changes to the icon species combo box are just for info display, don't mark as unsaved + connect(combo, &QComboBox::currentTextChanged, this, &ProjectSettingsEditor::markEdited); + } for (auto checkBox : ui->centralwidget->findChildren()) connect(checkBox, &QCheckBox::stateChanged, this, &ProjectSettingsEditor::markEdited); for (auto lineEdit : ui->centralwidget->findChildren()) @@ -79,8 +83,12 @@ void ProjectSettingsEditor::on_mainTabs_tabBarClicked(int index) { void ProjectSettingsEditor::initUi() { // Populate combo boxes - if (project) ui->comboBox_DefaultPrimaryTileset->addItems(project->primaryTilesetLabels); - if (project) ui->comboBox_DefaultSecondaryTileset->addItems(project->secondaryTilesetLabels); + if (project) { + ui->comboBox_DefaultPrimaryTileset->addItems(project->primaryTilesetLabels); + ui->comboBox_DefaultSecondaryTileset->addItems(project->secondaryTilesetLabels); + ui->comboBox_IconSpecies->addItems(project->speciesToIconPath.keys()); + ui->comboBox_IconSpecies->setEditable(false); + } ui->comboBox_BaseGameVersion->addItems(ProjectConfig::versionStrings); ui->comboBox_AttributesSize->addItems({"1", "2", "4"}); @@ -153,6 +161,26 @@ void ProjectSettingsEditor::updateAttributeLimits(const QString &attrSize) { ui->spinBox_TerrainTypeMask->setMaximum(max); } +// Only one icon path is displayed at a time, so we need to keep track of the rest, +// and update the path edit when the user changes the selected species. +// The existing icon path map in ProjectConfig is left alone to allow unsaved changes. +void ProjectSettingsEditor::updatePokemonIconPath(const QString &species) { + 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(species)) { + this->editedPokemonIconPaths[this->prevIconSpecies] = ui->lineEdit_PokemonIcon->text(); + } + + QString editedPath = this->editedPokemonIconPaths.value(species); + QString defaultPath = this->project->speciesToIconPath.value(species); + + const QSignalBlocker blocker(ui->lineEdit_PokemonIcon); + ui->lineEdit_PokemonIcon->setText(this->stripProjectDir(editedPath)); + ui->lineEdit_PokemonIcon->setPlaceholderText(this->stripProjectDir(defaultPath)); + this->prevIconSpecies = species; +} + void ProjectSettingsEditor::createProjectPathsTable() { auto pathPairs = ProjectConfig::defaultPaths.values(); for (auto pathPair : pathPairs) { @@ -226,6 +254,8 @@ void ProjectSettingsEditor::refresh() { ui->comboBox_BaseGameVersion->setTextItem(projectConfig.getBaseGameVersionString()); ui->comboBox_AttributesSize->setTextItem(QString::number(projectConfig.getMetatileAttributesSize())); this->updateAttributeLimits(ui->comboBox_AttributesSize->currentText()); + this->editedPokemonIconPaths = projectConfig.getPokemonIconPaths(); + this->updatePokemonIconPath(ui->comboBox_IconSpecies->currentText()); // Set check box states ui->checkBox_UsePoryscript->setChecked(projectConfig.getUsePoryScript()); @@ -329,6 +359,11 @@ void ProjectSettingsEditor::save() { // Save border metatile IDs projectConfig.setNewMapBorderMetatileIds(this->getBorderMetatileIds(ui->checkBox_EnableCustomBorderSize->isChecked())); + // Save pokemon icon paths + this->editedPokemonIconPaths.insert(ui->comboBox_IconSpecies->currentText(), ui->lineEdit_PokemonIcon->text()); + for (auto i = this->editedPokemonIconPaths.cbegin(), end = this->editedPokemonIconPaths.cend(); i != end; i++) + projectConfig.setPokemonIconPath(i.key(), i.value()); + projectConfig.setSaveDisabled(false); projectConfig.save(); this->hasUnsavedChanges = false; @@ -353,13 +388,18 @@ void ProjectSettingsEditor::chooseFile(QLineEdit * filepathEdit, const QString & return; this->project->setImportExportPath(filepath); - // Display relative path if this file is in the project folder - if (filepath.startsWith(this->baseDir)) - filepath.remove(0, this->baseDir.length()); - if (filepathEdit) filepathEdit->setText(filepath); + if (filepathEdit) + filepathEdit->setText(this->stripProjectDir(filepath)); this->hasUnsavedChanges = true; } +// Display relative path if this file is in the project folder +QString ProjectSettingsEditor::stripProjectDir(QString s) { + if (s.startsWith(this->baseDir)) + s.remove(0, this->baseDir.length()); + return s; +} + void ProjectSettingsEditor::importDefaultPrefabsClicked(bool) { // If the prompt is accepted the prefabs file will be created and its filepath will be saved in the config. // No need to set hasUnsavedChanges here.