diff --git a/CHANGELOG.md b/CHANGELOG.md index 6de13cb4..62d2b68e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project somewhat adheres to [Semantic Versioning](https://semver.org/sp The **"Breaking Changes"** listed below are changes that have been made in the decompilation projects (e.g. pokeemerald), which porymap requires in order to work properly. It also includes changes to the scripting API that may change the behavior of existing porymap scripts. If porymap is used with a project or API script that is not up-to-date with the breaking changes, then porymap will likely break or behave improperly. ## [Unreleased] +### Added +- Support for 8BPP tileset tile images. + ### Changed - The Palette Editor now remembers the Bit Depth setting. - The min/max levels on the Wild Pokémon tab will now adjust automatically if they invalidate each other. diff --git a/include/core/tileset.h b/include/core/tileset.h index 3f50f41b..b120b2c5 100644 --- a/include/core/tileset.h +++ b/include/core/tileset.h @@ -38,6 +38,8 @@ public: QList> palettes; QList> palettePreviews; + bool hasUnsavedTilesImage; + static Tileset* getMetatileTileset(int, Tileset*, Tileset*); static Tileset* getTileTileset(int, Tileset*, Tileset*); static Metatile* getMetatile(int, Tileset*, Tileset*); diff --git a/include/ui/imageproviders.h b/include/ui/imageproviders.h index db04bdb2..fefa5546 100644 --- a/include/ui/imageproviders.h +++ b/include/ui/imageproviders.h @@ -13,6 +13,7 @@ QImage getMetatileImage(Metatile*, Tileset*, Tileset*, QList, QList, QImage getTileImage(uint16_t, Tileset*, Tileset*); QImage getPalettedTileImage(uint16_t, Tileset*, Tileset*, int, bool useTruePalettes = false); QImage getGreyscaleTileImage(uint16_t tile, Tileset *primaryTileset, Tileset *secondaryTileset); +void flattenTo4bppImage(QImage * image); static QList greyscalePalette({ qRgb(0, 0, 0), diff --git a/include/ui/tilemaptileselector.h b/include/ui/tilemaptileselector.h index 9d419d63..867f6302 100644 --- a/include/ui/tilemaptileselector.h +++ b/include/ui/tilemaptileselector.h @@ -4,6 +4,7 @@ #include "selectablepixmapitem.h" #include "paletteutil.h" +#include "imageproviders.h" #include using std::shared_ptr; @@ -127,10 +128,7 @@ public: this->tileset = QImage(tilesetFilepath); this->format = format; if (this->tileset.format() == QImage::Format::Format_Indexed8 && this->format == TilemapFormat::BPP_4) { - // Squash pixel data to fit 4BPP. Allows project repo to use 8BPP images for 4BPP tilemaps - uchar * pixel = this->tileset.bits(); - for (int i = 0; i < this->tileset.sizeInBytes(); i++, pixel++) - *pixel %= 16; + flattenTo4bppImage(&this->tileset); } bool err; if (!palFilepath.isEmpty()) { diff --git a/src/core/tileset.cpp b/src/core/tileset.cpp index 4809ba6d..31d623d5 100644 --- a/src/core/tileset.cpp +++ b/src/core/tileset.cpp @@ -22,7 +22,8 @@ Tileset::Tileset(const Tileset &other) palettePaths(other.palettePaths), metatileLabels(other.metatileLabels), palettes(other.palettes), - palettePreviews(other.palettePreviews) + palettePreviews(other.palettePreviews), + hasUnsavedTilesImage(false) { for (auto tile : other.tiles) { tiles.append(tile.copy()); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 52f210aa..02ea47d3 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -18,6 +18,7 @@ #include "mapparser.h" #include "prefab.h" #include "montabwidget.h" +#include "imageexport.h" #include #include @@ -1280,7 +1281,7 @@ void MainWindow::on_actionNew_Tileset_triggered() { } newSet.palettes[0][1] = qRgb(255,0,255); newSet.palettePreviews[0][1] = qRgb(255,0,255); - editor->project->saveTilesetTilesImage(&newSet); + exportIndexed4BPPPng(newSet.tilesImage, newSet.tilesImagePath); editor->project->saveTilesetMetatiles(&newSet); editor->project->saveTilesetMetatileAttributes(&newSet); editor->project->saveTilesetPalettes(&newSet); diff --git a/src/project.cpp b/src/project.cpp index b0e3d43c..92a71e2f 100644 --- a/src/project.cpp +++ b/src/project.cpp @@ -6,7 +6,6 @@ #include "paletteutil.h" #include "tile.h" #include "tileset.h" -#include "imageexport.h" #include "map.h" #include "orderedjson.h" @@ -1008,7 +1007,15 @@ void Project::saveTilesetMetatiles(Tileset *tileset) { } void Project::saveTilesetTilesImage(Tileset *tileset) { - exportIndexed4BPPPng(tileset->tilesImage, tileset->tilesImagePath); + // Only write the tiles image if it was changed. + // Porymap will only ever change an existing tiles image by importing a new one. + if (tileset->hasUnsavedTilesImage) { + if (!tileset->tilesImage.save(tileset->tilesImagePath, "PNG")) { + logError(QString("Failed to save tiles image '%1'").arg(tileset->tilesImagePath)); + return; + } + tileset->hasUnsavedTilesImage = false; + } } void Project::saveTilesetPalettes(Tileset *tileset) { @@ -1350,6 +1357,7 @@ void Project::loadTilesetAssets(Tileset* tileset) { QImage image; if (QFile::exists(tileset->tilesImagePath)) { image = QImage(tileset->tilesImagePath).convertToFormat(QImage::Format_Indexed8, Qt::ThresholdDither); + flattenTo4bppImage(&image); } else { image = QImage(8, 8, QImage::Format_Indexed8); } diff --git a/src/ui/imageproviders.cpp b/src/ui/imageproviders.cpp index 838f4db7..891cfbba 100644 --- a/src/ui/imageproviders.cpp +++ b/src/ui/imageproviders.cpp @@ -169,3 +169,12 @@ QImage getPalettedTileImage(uint16_t tileId, Tileset *primaryTileset, Tileset *s QImage getGreyscaleTileImage(uint16_t tileId, Tileset *primaryTileset, Tileset *secondaryTileset) { return getColoredTileImage(tileId, primaryTileset, secondaryTileset, greyscalePalette); } + +// gbagfx allows 4bpp image data to be represented with 8bpp .png files by considering only the lower 4 bits of each pixel. +// Reproduce that here to support this type of image use. +void flattenTo4bppImage(QImage * image) { + if (!image) return; + uchar * pixel = image->bits(); + for (int i = 0; i < image->sizeInBytes(); i++, pixel++) + *pixel %= 16; +} diff --git a/src/ui/tileseteditor.cpp b/src/ui/tileseteditor.cpp index 9240d0d1..e5ca4b67 100644 --- a/src/ui/tileseteditor.cpp +++ b/src/ui/tileseteditor.cpp @@ -701,15 +701,6 @@ void TilesetEditor::importTilesetTiles(Tileset *tileset, bool primary) { msgBox.setIcon(QMessageBox::Icon::Critical); msgBox.exec(); return; - } else if (palette.length() != 16) { - QMessageBox msgBox(this); - msgBox.setText("Failed to import palette."); - QString message = QString("The palette must have exactly 16 colors, but it has %1.").arg(palette.length()); - msgBox.setInformativeText(message); - msgBox.setDefaultButton(QMessageBox::Ok); - msgBox.setIcon(QMessageBox::Icon::Critical); - msgBox.exec(); - return; } QVector colorTable = palette.toVector(); @@ -717,20 +708,21 @@ void TilesetEditor::importTilesetTiles(Tileset *tileset, bool primary) { } // Validate image is properly indexed to 16 colors. - if (image.colorCount() != 16) { - QMessageBox msgBox(this); - msgBox.setText("Failed to import tiles."); - msgBox.setInformativeText(QString("The image must be indexed and contain 16 total colors, or it must be un-indexed. The provided image has %1 indexed colors.") - .arg(image.colorCount())); - msgBox.setDefaultButton(QMessageBox::Ok); - msgBox.setIcon(QMessageBox::Icon::Critical); - msgBox.exec(); - return; + int colorCount = image.colorCount(); + if (colorCount > 16) { + flattenTo4bppImage(&image); + } else if (colorCount < 16) { + QVector colorTable = image.colorTable(); + for (int i = colorTable.length(); i < 16; i++) { + colorTable.append(Qt::black); + } + image.setColorTable(colorTable); } this->project->loadTilesetTiles(tileset, image); this->refresh(); this->hasUnsavedChanges = true; + tileset->hasUnsavedTilesImage = true; } void TilesetEditor::closeEvent(QCloseEvent *event)