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 00000000..43957668
Binary files /dev/null and b/resources/images/pokemon_icon_placeholder.png differ
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.