Sturdier pokemon icon search, add icon override settings

This commit is contained in:
GriffinR 2023-12-10 03:49:54 -05:00
parent d6dacf039e
commit 06ff213691
10 changed files with 235 additions and 30 deletions

View file

@ -39,7 +39,7 @@
<x>0</x>
<y>0</y>
<width>531</width>
<height>805</height>
<height>916</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_7">
@ -386,6 +386,63 @@
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_PokemonIcons">
<property name="title">
<string>Pokémon Icons</string>
</property>
<layout class="QGridLayout" name="gridLayout_8">
<item row="0" column="0">
<widget class="QLabel" name="label_IconSpecies">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Species</string>
</property>
</widget>
</item>
<item row="1" column="5">
<widget class="QToolButton" name="button_PokemonIcon">
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../resources/images.qrc">
<normaloff>:/icons/folder.ico</normaloff>:/icons/folder.ico</iconset>
</property>
</widget>
</item>
<item row="0" column="2" colspan="4">
<widget class="NoScrollComboBox" name="comboBox_IconSpecies">
<property name="editable">
<bool>true</bool>
</property>
<property name="maxVisibleItems">
<number>20</number>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_PokemonIcon">
<property name="text">
<string>Image Path</string>
</property>
</widget>
</item>
<item row="1" column="2" colspan="3">
<widget class="QLineEdit" name="lineEdit_PokemonIcon">
<property name="clearButtonEnabled">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QFrame" name="frame_Warning">
<property name="styleSheet">

View file

@ -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<QString, QString> getPokemonIconPaths();
void setCollisionSheetPath(const QString &path);
QString getCollisionSheetPath();
void setCollisionSheetWidth(int width);
@ -361,6 +367,7 @@ private:
uint32_t metatileLayerTypeMask;
bool enableMapAllowFlags;
QMap<Event::Group, QString> eventIconPaths;
QHash<QString, QString> pokemonIconPaths;
QString collisionSheetPath;
int collisionSheetWidth;
int collisionSheetHeight;

View file

@ -170,7 +170,6 @@ public:
void saveTilesetPalettes(Tileset*);
QString defaultSong;
QStringList getVisibilities();
void appendTilesetLabel(QString label, QString isSecondaryStr);
bool readTilesetLabels();
bool readTilesetProperties();

View file

@ -31,6 +31,8 @@ private:
bool projectNeedsReload = false;
bool refreshing = false;
const QString baseDir;
QHash<QString, QString> 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);
};

View file

@ -63,6 +63,7 @@
<file>images/collisions.png</file>
<file>images/collisions_unknown.png</file>
<file>images/Entities_16x16.png</file>
<file>images/pokemon_icon_placeholder.png</file>
<file>icons/clipboard.ico</file>
</qresource>
</RCC>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -58,9 +58,11 @@ const QMap<ProjectFilePath, std::pair<QString, QString>> 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<QString, QString> 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<QString, QString> ProjectConfig::getPokemonIconPaths() {
return this->pokemonIconPaths;
}
void ProjectConfig::setCollisionSheetPath(const QString &path) {
this->collisionSheetPath = path;
this->save();

View file

@ -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<QString, QString> 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<QString, QString> monIconNames = parser.readNamedIndexCArray(srcfilename, "gMonIconTable");
QMap<QString, QString> 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<QString, QString> 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<QString, int> 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;
}

View file

@ -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);

View file

@ -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<NoScrollComboBox *>())
connect(combo, &QComboBox::currentTextChanged, this, &ProjectSettingsEditor::markEdited);
for (auto combo : ui->centralwidget->findChildren<NoScrollComboBox *>()){
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<QCheckBox *>())
connect(checkBox, &QCheckBox::stateChanged, this, &ProjectSettingsEditor::markEdited);
for (auto lineEdit : ui->centralwidget->findChildren<QLineEdit *>())
@ -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.