From 486d1b7335b63ea75ae86108ceab40fac5274117 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Thu, 19 Dec 2024 13:58:13 -0500 Subject: [PATCH] Add config settings for rendering transparency --- forms/projectsettingseditor.ui | 107 +++++++++++++++++++++++++++---- include/config.h | 8 +++ include/core/tile.h | 2 + include/ui/imageproviders.h | 4 +- src/config.cpp | 12 ++++ src/core/tile.cpp | 29 ++++++--- src/project.cpp | 28 +++++--- src/ui/imageproviders.cpp | 32 ++++----- src/ui/projectsettingseditor.cpp | 18 ++++++ src/ui/tileseteditor.cpp | 8 ++- 10 files changed, 201 insertions(+), 47 deletions(-) diff --git a/forms/projectsettingseditor.ui b/forms/projectsettingseditor.ui index 9aa521a8..fc156a72 100644 --- a/forms/projectsettingseditor.ui +++ b/forms/projectsettingseditor.ui @@ -369,7 +369,7 @@ 0 0 559 - 548 + 560 @@ -602,7 +602,7 @@ - + The mask used to read/write metatile IDs in map data. @@ -616,7 +616,7 @@ - + The mask used to read/write collision values in map data. @@ -630,7 +630,7 @@ - + The mask used to read/write elevation values in map data. @@ -742,7 +742,7 @@ 0 0 559 - 568 + 798 @@ -775,6 +775,86 @@ + + + + Transparent Pixel Rendering + + + + + + Fully transparent pixels will be rendered as black pixels (the Pokémon games do this by default) + + + Render as black + + + + + + + Fully transparent pixels will be rendered using the first palette color (this the default behavior for the GBA) + + + Render using first palette color + + + + + + + + + + Unused Layer Rendering + + + + + + Normal + + + + + + + This raw tile value will be used to fill the unused bottom layer of Normal metatiles + + + + + + + Covered + + + + + + + This raw tile value will be used to fill the unused top layer of Covered metatiles + + + + + + + Split + + + + + + + This raw tile value will be used to fill the unused middle layer of Split metatiles + + + + + + @@ -810,14 +890,14 @@ - + The mask used to read/write Layer Type from the metatile's attributes data. If 0, this attribute is disabled. - + The mask used to read/write Metatile Behavior from the metatile's attributes data. If 0, this attribute is disabled. @@ -864,7 +944,7 @@ - + The mask used to read/write Terrain Type from the metatile's attributes data. If 0, this attribute is disabled. @@ -891,7 +971,7 @@ - + The mask used to read/write Encounter Type from the metatile's attributes data. If 0, this attribute is disabled. @@ -1549,10 +1629,15 @@
noscrollspinbox.h
- UIntHexSpinBox - QWidget + UIntSpinBox + QAbstractSpinBox
uintspinbox.h
+ + UIntHexSpinBox + UIntSpinBox +
uintspinbox.h
+
diff --git a/include/config.h b/include/config.h index 47bc2010..020808b6 100644 --- a/include/config.h +++ b/include/config.h @@ -301,6 +301,7 @@ public: this->prefabImportPrompted = false; this->tilesetsHaveCallback = true; this->tilesetsHaveIsCompressed = true; + this->setTransparentPixelsBlack = true; this->filePaths.clear(); this->eventIconPaths.clear(); this->pokemonIconPaths.clear(); @@ -310,6 +311,9 @@ public: this->blockMetatileIdMask = 0x03FF; this->blockCollisionMask = 0x0C00; this->blockElevationMask = 0xF000; + this->unusedTileNormal = 0x3014; + this->unusedTileCovered = 0x0000; + this->unusedTileSplit = 0x0000; this->identifiers.clear(); this->readKeys.clear(); } @@ -362,6 +366,7 @@ public: bool prefabImportPrompted; bool tilesetsHaveCallback; bool tilesetsHaveIsCompressed; + bool setTransparentPixelsBlack; int metatileAttributesSize; uint32_t metatileBehaviorMask; uint32_t metatileTerrainTypeMask; @@ -370,6 +375,9 @@ public: uint16_t blockMetatileIdMask; uint16_t blockCollisionMask; uint16_t blockElevationMask; + uint16_t unusedTileNormal; + uint16_t unusedTileCovered; + uint16_t unusedTileSplit; bool mapAllowFlagsEnabled; QString collisionSheetPath; int collisionSheetWidth; diff --git a/include/core/tile.h b/include/core/tile.h index 5d85066a..b58a187d 100644 --- a/include/core/tile.h +++ b/include/core/tile.h @@ -19,6 +19,8 @@ public: uint16_t rawValue() const; static int getIndexInTileset(int); + + static const uint16_t maxValue; }; inline bool operator==(const Tile &a, const Tile &b) { diff --git a/include/ui/imageproviders.h b/include/ui/imageproviders.h index fefa5546..806ffd5b 100644 --- a/include/ui/imageproviders.h +++ b/include/ui/imageproviders.h @@ -8,8 +8,8 @@ QImage getCollisionMetatileImage(Block); QImage getCollisionMetatileImage(int, int); -QImage getMetatileImage(uint16_t, Tileset*, Tileset*, QList, QList, bool useTruePalettes = false); -QImage getMetatileImage(Metatile*, Tileset*, Tileset*, QList, QList, bool useTruePalettes = false); +QImage getMetatileImage(uint16_t, Tileset*, Tileset*, const QList&, const QList&, bool useTruePalettes = false); +QImage getMetatileImage(Metatile*, Tileset*, Tileset*, const QList&, const QList&, bool useTruePalettes = false); 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); diff --git a/src/config.cpp b/src/config.cpp index c0c00194..6d23977f 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -720,6 +720,12 @@ void ProjectConfig::parseConfigKeyValue(QString key, QString value) { this->blockCollisionMask = getConfigUint32(key, value, 0, Block::maxValue); } else if (key == "block_elevation_mask") { this->blockElevationMask = getConfigUint32(key, value, 0, Block::maxValue); + } else if (key == "unused_tile_normal") { + this->unusedTileNormal = getConfigUint32(key, value, 0, Tile::maxValue); + } else if (key == "unused_tile_covered") { + this->unusedTileCovered = getConfigUint32(key, value, 0, Tile::maxValue); + } else if (key == "unused_tile_split") { + this->unusedTileSplit = getConfigUint32(key, value, 0, Tile::maxValue); } else if (key == "enable_map_allow_flags") { this->mapAllowFlagsEnabled = getConfigBool(key, value); #ifdef CONFIG_BACKWARDS_COMPATABILITY @@ -752,6 +758,8 @@ void ProjectConfig::parseConfigKeyValue(QString key, QString value) { this->tilesetsHaveCallback = getConfigBool(key, value); } else if (key == "tilesets_have_is_compressed") { this->tilesetsHaveIsCompressed = getConfigBool(key, value); + } else if (key == "set_transparent_pixels_black") { + this->setTransparentPixelsBlack = getConfigBool(key, value); } else if (key == "event_icon_path_object") { this->eventIconPaths[Event::Group::Object] = value; } else if (key == "event_icon_path_warp") { @@ -839,6 +847,7 @@ QMap ProjectConfig::getKeyValueMap() { } map.insert("tilesets_have_callback", QString::number(this->tilesetsHaveCallback)); map.insert("tilesets_have_is_compressed", QString::number(this->tilesetsHaveIsCompressed)); + map.insert("set_transparent_pixels_black", QString::number(this->setTransparentPixelsBlack)); map.insert("metatile_attributes_size", QString::number(this->metatileAttributesSize)); map.insert("metatile_behavior_mask", "0x" + QString::number(this->metatileBehaviorMask, 16).toUpper()); map.insert("metatile_terrain_type_mask", "0x" + QString::number(this->metatileTerrainTypeMask, 16).toUpper()); @@ -847,6 +856,9 @@ QMap ProjectConfig::getKeyValueMap() { map.insert("block_metatile_id_mask", "0x" + QString::number(this->blockMetatileIdMask, 16).toUpper()); map.insert("block_collision_mask", "0x" + QString::number(this->blockCollisionMask, 16).toUpper()); map.insert("block_elevation_mask", "0x" + QString::number(this->blockElevationMask, 16).toUpper()); + map.insert("unused_tile_normal", "0x" + QString::number(this->unusedTileNormal, 16).toUpper()); + map.insert("unused_tile_covered", "0x" + QString::number(this->unusedTileCovered, 16).toUpper()); + map.insert("unused_tile_split", "0x" + QString::number(this->unusedTileSplit, 16).toUpper()); map.insert("enable_map_allow_flags", QString::number(this->mapAllowFlagsEnabled)); map.insert("event_icon_path_object", this->eventIconPaths[Event::Group::Object]); map.insert("event_icon_path_warp", this->eventIconPaths[Event::Group::Warp]); diff --git a/src/core/tile.cpp b/src/core/tile.cpp index cc89c2a9..1aab7a9b 100644 --- a/src/core/tile.cpp +++ b/src/core/tile.cpp @@ -1,5 +1,17 @@ #include "tile.h" #include "project.h" +#include "bitpacker.h" + +// Upper limit for raw value (i.e., uint16_t max). +const uint16_t Tile::maxValue = 0xFFFF; + +// At the moment these are fixed, and not exposed to the user. +// We're only using them for convenience when converting between raw values. +// The actual job of clamping Tile's members to correct values is handled by the widths in the bit field. +const BitPacker bitsTileId = BitPacker(0x03FF); +const BitPacker bitsXFlip = BitPacker(0x0400); +const BitPacker bitsYFlip = BitPacker(0x0800); +const BitPacker bitsPalette = BitPacker(0xF000); Tile::Tile() : tileId(0), @@ -16,18 +28,17 @@ { } Tile::Tile(uint16_t raw) : - tileId(raw & 0x3FF), - xflip((raw >> 10) & 1), - yflip((raw >> 11) & 1), - palette((raw >> 12) & 0xF) + tileId(bitsTileId.unpack(raw)), + xflip(bitsXFlip.unpack(raw)), + yflip(bitsYFlip.unpack(raw)), + palette(bitsPalette.unpack(raw)) { } uint16_t Tile::rawValue() const { - return static_cast( - (this->tileId & 0x3FF) - | ((this->xflip & 1) << 10) - | ((this->yflip & 1) << 11) - | ((this->palette & 0xF) << 12)); + return bitsTileId.pack(this->tileId) + | bitsXFlip.pack(this->xflip) + | bitsYFlip.pack(this->yflip) + | bitsPalette.pack(this->palette); } int Tile::getIndexInTileset(int tileId) { diff --git a/src/project.cpp b/src/project.cpp index 43b62ecf..1791bcdf 100644 --- a/src/project.cpp +++ b/src/project.cpp @@ -2096,19 +2096,29 @@ bool Project::readFieldmapProperties() { fileWatcher.addPath(root + "/" + filename); const QMap defines = parser.readCDefinesByName(filename, names); - auto loadDefine = [defines](const QString name, int * dest) { + auto loadDefine = [defines](const QString name, int * dest, int min, int max) { auto it = defines.find(name); if (it != defines.end()) { *dest = it.value(); + if (*dest < min) { + logWarn(QString("Value for tileset property '%1' (%2) is below the minimum (%3). Defaulting to minimum.").arg(name).arg(*dest).arg(min)); + *dest = min; + } else if (*dest > max) { + logWarn(QString("Value for tileset property '%1' (%2) is above the maximum (%3). Defaulting to maximum.").arg(name).arg(*dest).arg(max)); + *dest = max; + } } else { logWarn(QString("Value for tileset property '%1' not found. Using default (%2) instead.").arg(name).arg(*dest)); } }; - loadDefine(numTilesPrimaryName, &Project::num_tiles_primary); - loadDefine(numTilesTotalName, &Project::num_tiles_total); - loadDefine(numMetatilesPrimaryName, &Project::num_metatiles_primary); - loadDefine(numPalsPrimaryName, &Project::num_pals_primary); - loadDefine(numPalsTotalName, &Project::num_pals_total); + loadDefine(numPalsTotalName, &Project::num_pals_total, 2, INT_MAX); // In reality the max would be 16, but as far as Porymap is concerned it doesn't matter. + loadDefine(numTilesTotalName, &Project::num_tiles_total, 2, 1024); // 1024 is fixed because we store tile IDs in a 10-bit field. + loadDefine(numPalsPrimaryName, &Project::num_pals_primary, 1, Project::num_pals_total - 1); + loadDefine(numTilesPrimaryName, &Project::num_tiles_primary, 1, Project::num_tiles_total - 1); + + // This maximum is overly generous, because until we parse the appropriate masks from the project + // we don't actually know what the maximum number of metatiles is. + loadDefine(numMetatilesPrimaryName, &Project::num_metatiles_primary, 1, 0xFFFF - 1); auto it = defines.find(maxMapSizeName); if (it != defines.end()) { @@ -3020,12 +3030,12 @@ void Project::applyParsedLimits() { Block::setLayout(); Metatile::setLayout(this); - Project::num_metatiles_primary = qMin(Project::num_metatiles_primary, Block::getMaxMetatileId() + 1); + Project::num_metatiles_primary = qMin(qMax(Project::num_metatiles_primary, 1), Block::getMaxMetatileId() + 1); projectConfig.defaultMetatileId = qMin(projectConfig.defaultMetatileId, Block::getMaxMetatileId()); projectConfig.defaultElevation = qMin(projectConfig.defaultElevation, Block::getMaxElevation()); projectConfig.defaultCollision = qMin(projectConfig.defaultCollision, Block::getMaxCollision()); - projectConfig.collisionSheetHeight = qMin(projectConfig.collisionSheetHeight, Block::getMaxElevation() + 1); - projectConfig.collisionSheetWidth = qMin(projectConfig.collisionSheetWidth, Block::getMaxCollision() + 1); + projectConfig.collisionSheetHeight = qMin(qMax(projectConfig.collisionSheetHeight, 1), Block::getMaxElevation() + 1); + projectConfig.collisionSheetWidth = qMin(qMax(projectConfig.collisionSheetWidth, 1), Block::getMaxCollision() + 1); } bool Project::hasUnsavedChanges() { diff --git a/src/ui/imageproviders.cpp b/src/ui/imageproviders.cpp index d4cdb71a..8fa72699 100644 --- a/src/ui/imageproviders.cpp +++ b/src/ui/imageproviders.cpp @@ -17,8 +17,8 @@ QImage getMetatileImage( uint16_t metatileId, Tileset *primaryTileset, Tileset *secondaryTileset, - QList layerOrder, - QList layerOpacity, + const QList &layerOrder, + const QList &layerOpacity, bool useTruePalettes) { Metatile* metatile = Tileset::getMetatile(metatileId, primaryTileset, secondaryTileset); @@ -34,8 +34,8 @@ QImage getMetatileImage( Metatile *metatile, Tileset *primaryTileset, Tileset *secondaryTileset, - QList layerOrder, - QList layerOpacity, + const QList &layerOrder, + const QList &layerOpacity, bool useTruePalettes) { QImage metatile_image(16, 16, QImage::Format_RGBA8888); @@ -44,13 +44,15 @@ QImage getMetatileImage( return metatile_image; } - // The GBA renders transparent pixels using palette 0 color 0. We have access to that color (palettes.value(0).value(0)) - // but all 3 games actually overwrite this color with black when loading the tileset palettes, so we fill the metatile - // image with black so that any pixels we don't render will reveal the correct color. - metatile_image.fill(Qt::black); - QList> palettes = Tileset::getBlockPalettes(primaryTileset, secondaryTileset, useTruePalettes); + // We need to fill the metatile image with something so that if any transparent + // tile pixels line up across layers we will still have something to render. + // The GBA renders transparent pixels using palette 0 color 0. We have this color, + // but all 3 games actually overwrite it with black when loading the tileset palettes, + // so we have a setting to choose between these two behaviors. + metatile_image.fill(projectConfig.setTransparentPixelsBlack ? QColor("black") : QColor(palettes.value(0).value(0))); + QPainter metatile_painter(&metatile_image); const int numLayers = 3; // When rendering, metatiles always have 3 layers uint32_t layerType = metatile->layerType(); @@ -66,25 +68,25 @@ QImage getMetatileImage( tile = metatile->tiles.value(tileOffset + (l * 4)); } else { // "Vanilla" metatiles only have 8 tiles, but render 12. - // The remaining 4 tiles are rendered either as tile 0 or 0x3014 (tile 20, palette 3) depending on layer type. + // The remaining 4 tiles are rendered using user-specified tiles depending on layer type. switch (layerType) { default: case METATILE_LAYER_MIDDLE_TOP: if (l == 0) - tile = Tile(0x3014); + tile = Tile(projectConfig.unusedTileNormal); else // Tiles are on layers 1 and 2 tile = metatile->tiles.value(tileOffset + ((l - 1) * 4)); break; case METATILE_LAYER_BOTTOM_MIDDLE: if (l == 2) - tile = Tile(); + tile = Tile(projectConfig.unusedTileCovered); else // Tiles are on layers 0 and 1 tile = metatile->tiles.value(tileOffset + (l * 4)); break; case METATILE_LAYER_BOTTOM_TOP: if (l == 1) - tile = Tile(); + tile = Tile(projectConfig.unusedTileSplit); else // Tiles are on layers 0 and 2 tile = metatile->tiles.value(tileOffset + ((l == 0 ? 0 : 1) * 4)); break; @@ -101,7 +103,7 @@ QImage getMetatileImage( // Colorize the metatile tiles with its palette. if (tile.palette < palettes.length()) { - QList palette = palettes.value(tile.palette); + const QList palette = palettes.value(tile.palette); for (int j = 0; j < palette.length(); j++) { tile_image.setColor(j, palette.value(j)); } @@ -141,7 +143,7 @@ QImage getTileImage(uint16_t tileId, Tileset *primaryTileset, Tileset *secondary return tileset->tiles.value(index, QImage()); } -QImage getColoredTileImage(uint16_t tileId, Tileset *primaryTileset, Tileset *secondaryTileset, QList palette) { +QImage getColoredTileImage(uint16_t tileId, Tileset *primaryTileset, Tileset *secondaryTileset, const QList &palette) { QImage tileImage = getTileImage(tileId, primaryTileset, secondaryTileset); if (tileImage.isNull()) { tileImage = QImage(8, 8, QImage::Format_RGBA8888); diff --git a/src/ui/projectsettingseditor.cpp b/src/ui/projectsettingseditor.cpp index 346591f4..0e787f5e 100644 --- a/src/ui/projectsettingseditor.cpp +++ b/src/ui/projectsettingseditor.cpp @@ -82,6 +82,8 @@ void ProjectSettingsEditor::connectSignals() { } for (auto checkBox : ui->centralwidget->findChildren()) connect(checkBox, &QCheckBox::stateChanged, this, &ProjectSettingsEditor::markEdited); + for (auto radioButton : ui->centralwidget->findChildren()) + connect(radioButton, &QRadioButton::toggled, this, &ProjectSettingsEditor::markEdited); for (auto lineEdit : ui->centralwidget->findChildren()) connect(lineEdit, &QLineEdit::textEdited, this, &ProjectSettingsEditor::markEdited); for (auto spinBox : ui->centralwidget->findChildren()) @@ -135,6 +137,9 @@ void ProjectSettingsEditor::initUi() { ui->spinBox_MetatileIdMask->setMaximum(Block::maxValue); ui->spinBox_CollisionMask->setMaximum(Block::maxValue); ui->spinBox_ElevationMask->setMaximum(Block::maxValue); + ui->spinBox_UnusedTileNormal->setMaximum(Tile::maxValue); + ui->spinBox_UnusedTileCovered->setMaximum(Tile::maxValue); + ui->spinBox_UnusedTileSplit->setMaximum(Tile::maxValue); // The values for some of the settings we provide in this window can be determined using constants in the user's projects. // If the user has these constants we disable these settings in the UI -- they can modify them using their constants. @@ -442,6 +447,12 @@ void ProjectSettingsEditor::refresh() { ui->checkBox_OutputIsCompressed->setChecked(projectConfig.tilesetsHaveIsCompressed); ui->checkBox_DisableWarning->setChecked(porymapConfig.warpBehaviorWarningDisabled); + // Radio buttons + if (projectConfig.setTransparentPixelsBlack) + ui->radioButton_RenderBlack->setChecked(true); + else + ui->radioButton_RenderFirstPalColor->setChecked(true); + // Set spin box values ui->spinBox_Elevation->setValue(projectConfig.defaultElevation); ui->spinBox_Collision->setValue(projectConfig.defaultCollision); @@ -455,6 +466,9 @@ void ProjectSettingsEditor::refresh() { ui->spinBox_MetatileIdMask->setValue(projectConfig.blockMetatileIdMask & ui->spinBox_MetatileIdMask->maximum()); ui->spinBox_CollisionMask->setValue(projectConfig.blockCollisionMask & ui->spinBox_CollisionMask->maximum()); ui->spinBox_ElevationMask->setValue(projectConfig.blockElevationMask & ui->spinBox_ElevationMask->maximum()); + ui->spinBox_UnusedTileNormal->setValue(projectConfig.unusedTileNormal); + ui->spinBox_UnusedTileCovered->setValue(projectConfig.unusedTileCovered); + ui->spinBox_UnusedTileSplit->setValue(projectConfig.unusedTileSplit); // Set (and sync) border metatile IDs this->setBorderMetatileIds(false, projectConfig.newMapBorderMetatileIds); @@ -511,6 +525,7 @@ void ProjectSettingsEditor::save() { projectConfig.tilesetsHaveCallback = ui->checkBox_OutputCallback->isChecked(); projectConfig.tilesetsHaveIsCompressed = ui->checkBox_OutputIsCompressed->isChecked(); porymapConfig.warpBehaviorWarningDisabled = ui->checkBox_DisableWarning->isChecked(); + projectConfig.setTransparentPixelsBlack = ui->radioButton_RenderBlack->isChecked(); // Save spin box settings projectConfig.defaultElevation = ui->spinBox_Elevation->value(); @@ -525,6 +540,9 @@ void ProjectSettingsEditor::save() { projectConfig.blockMetatileIdMask = ui->spinBox_MetatileIdMask->value(); projectConfig.blockCollisionMask = ui->spinBox_CollisionMask->value(); projectConfig.blockElevationMask = ui->spinBox_ElevationMask->value(); + projectConfig.unusedTileNormal = ui->spinBox_UnusedTileNormal->value(); + projectConfig.unusedTileCovered = ui->spinBox_UnusedTileCovered->value(); + projectConfig.unusedTileSplit = ui->spinBox_UnusedTileSplit->value(); // Save line edit settings projectConfig.prefabFilepath = ui->lineEdit_PrefabsPath->text(); diff --git a/src/ui/tileseteditor.cpp b/src/ui/tileseteditor.cpp index 49cfe147..b8da1912 100644 --- a/src/ui/tileseteditor.cpp +++ b/src/ui/tileseteditor.cpp @@ -99,7 +99,13 @@ void TilesetEditor::initUi() { this->paletteId = ui->spinBox_paletteSelector->value(); this->ui->spinBox_paletteSelector->setMinimum(0); this->ui->spinBox_paletteSelector->setMaximum(Project::getNumPalettesTotal() - 1); - this->ui->actionShow_Tileset_Divider->setChecked(porymapConfig.showTilesetEditorDivider); + + // TODO: The dividing line at the moment is only accurate if the number of primary metatiles is divisible by 8. + // If it's not, the secondary metatiles will wrap above the line. This has other problems (like skewing + // metatile groups the user may have designed) so this should be fixed by filling the primary metatiles + // image with invalid magenta metatiles until it's divisible by 8. Then the line can be re-enabled as-is. + this->ui->actionShow_Tileset_Divider->setChecked(/*porymapConfig.showTilesetEditorDivider*/false); + this->ui->actionShow_Tileset_Divider->setVisible(false); this->setAttributesUi(); this->setMetatileLabelValidator();