diff --git a/include/core/metatile.h b/include/core/metatile.h index c5536016..db1f6e6f 100644 --- a/include/core/metatile.h +++ b/include/core/metatile.h @@ -41,9 +41,9 @@ public: public: QList tiles; uint32_t behavior; - uint32_t layerType; - uint32_t encounterType; uint32_t terrainType; + uint32_t encounterType; + uint32_t layerType; uint32_t unusedAttributes; QString label; @@ -61,22 +61,28 @@ public: static const QHash defaultLayoutFRLG; static const QHash defaultLayoutRSE; + static QHash customLayout; uint32_t getAttributes(); void setAttributes(uint32_t data); void convertAttributes(uint32_t data, BaseGameVersion version); + void setBehavior(uint32_t); + void setTerrainType(uint32_t); + void setEncounterType(uint32_t); + void setLayerType(uint32_t); + static int getIndexInTileset(int); static QPoint coordFromPixmapCoord(const QPointF &pixelCoord); static int getDefaultAttributesSize(BaseGameVersion version); static void setCustomLayout(); private: - static QHash customLayout; static uint32_t unusedAttrMask; void setAttributes(uint32_t, const QHash*); - static void setCustomAttributeLayout(Metatile::AttrLayout *, uint32_t, uint32_t, QString); + static void setCustomAttributeLayout(Metatile::AttrLayout *, uint32_t, uint32_t); + static bool isMaskTooSmall(Metatile::AttrLayout * layout, int n); static bool doMasksOverlap(QList); }; diff --git a/include/ui/tileseteditor.h b/include/ui/tileseteditor.h index 29912e4d..e671564f 100644 --- a/include/ui/tileseteditor.h +++ b/include/ui/tileseteditor.h @@ -111,9 +111,7 @@ private slots: private: void initUi(); - void setMetatileBehaviors(); - void setMetatileLayersUi(); - void setVersionSpecificUi(); + void setAttributesUi(); void setMetatileLabelValidator(); void initMetatileSelector(); void initTileSelector(); diff --git a/src/core/metatile.cpp b/src/core/metatile.cpp index 6fb8c413..44b71fa3 100644 --- a/src/core/metatile.cpp +++ b/src/core/metatile.cpp @@ -21,17 +21,17 @@ const QHash Metatile::defaultLayoutRSE = { Metatile::Metatile() : behavior(0), - layerType(0), - encounterType(0), terrainType(0), + encounterType(0), + layerType(0), unusedAttributes(0) { } Metatile::Metatile(const int numTiles) : behavior(0), - layerType(0), - encounterType(0), terrainType(0), + encounterType(0), + layerType(0), unusedAttributes(0) { Tile tile = Tile(); @@ -60,15 +60,25 @@ QPoint Metatile::coordFromPixmapCoord(const QPointF &pixelCoord) { return QPoint(x, y); } -void Metatile::setCustomAttributeLayout(Metatile::AttrLayout * layout, uint32_t mask, uint32_t max, QString errName) { +void Metatile::setCustomAttributeLayout(Metatile::AttrLayout * layout, uint32_t mask, uint32_t max) { if (mask > max) { - logWarn(QString("Metatile %1 mask '%2' exceeds maximum size '%3'").arg(errName).arg(mask).arg(max)); + uint32_t oldMask = mask; mask &= max; + logWarn(QString("Metatile attribute mask '0x%1' has been truncated to '0x%2'") + .arg(QString::number(oldMask, 16).toUpper()) + .arg(QString::number(mask, 16).toUpper())); } layout->mask = mask; layout->shift = log2(mask & ~(mask - 1)); // Get the position of the rightmost set bit } +// For checking whether a metatile attribute mask can contain all the hard-coded values +bool Metatile::isMaskTooSmall(Metatile::AttrLayout * layout, int n) { + if (!layout->mask) return false; + uint32_t maxValue = n - 1; + return (maxValue & (layout->mask >> layout->shift)) != maxValue; +} + bool Metatile::doMasksOverlap(QList masks) { for (int i = 0; i < masks.length(); i++) for (int j = i + 1; j < masks.length(); j++) { @@ -88,10 +98,10 @@ void Metatile::setCustomLayout() { const uint32_t maxMask = maxMasks.value(projectConfig.getMetatileAttributesSize(), 0); // Set custom attribute masks from the config file - setCustomAttributeLayout(&customLayout[Attr::Behavior], projectConfig.getMetatileBehaviorMask(), maxMask, "behavior"); - setCustomAttributeLayout(&customLayout[Attr::TerrainType], projectConfig.getMetatileTerrainTypeMask(), maxMask, "terrain type"); - setCustomAttributeLayout(&customLayout[Attr::EncounterType], projectConfig.getMetatileEncounterTypeMask(), maxMask, "encounter type"); - setCustomAttributeLayout(&customLayout[Attr::LayerType], projectConfig.getMetatileLayerTypeMask(), maxMask, "layer type"); + setCustomAttributeLayout(&customLayout[Attr::Behavior], projectConfig.getMetatileBehaviorMask(), maxMask); + setCustomAttributeLayout(&customLayout[Attr::TerrainType], projectConfig.getMetatileTerrainTypeMask(), maxMask); + setCustomAttributeLayout(&customLayout[Attr::EncounterType], projectConfig.getMetatileEncounterTypeMask(), maxMask); + setCustomAttributeLayout(&customLayout[Attr::LayerType], projectConfig.getMetatileLayerTypeMask(), maxMask); // Set mask for preserving any attribute bits not used by Porymap Metatile::unusedAttrMask = ~(customLayout[Attr::Behavior].mask @@ -100,13 +110,24 @@ void Metatile::setCustomLayout() { | customLayout[Attr::LayerType].mask); Metatile::unusedAttrMask &= maxMask; - // Overlapping masks are legal, but probably not intended + // Overlapping masks are technically ok, but probably not intended. + // Additionally, Porymap will not properly reflect that the values are linked. if (doMasksOverlap({customLayout[Attr::Behavior].mask, customLayout[Attr::TerrainType].mask, customLayout[Attr::EncounterType].mask, customLayout[Attr::LayerType].mask})) { logWarn("Metatile attribute masks are overlapping."); } + + // The available options in the Tileset Editor for Terrain Type, Encounter Type, and Layer Type are hard-coded. + // Warn the user if they have set a nonzero mask that is too small to contain these options. + // They'll be allowed to select them, but they'll be truncated to a different value when revisited. + if (isMaskTooSmall(&customLayout[Attr::TerrainType], NUM_METATILE_TERRAIN_TYPES)) + logWarn(QString("Metatile Terrain Type mask is too small to contain all %1 available options.").arg(NUM_METATILE_TERRAIN_TYPES)); + if (isMaskTooSmall(&customLayout[Attr::EncounterType], NUM_METATILE_ENCOUNTER_TYPES)) + logWarn(QString("Metatile Encounter Type mask is too small to contain all %1 available options.").arg(NUM_METATILE_ENCOUNTER_TYPES)); + if (isMaskTooSmall(&customLayout[Attr::LayerType], NUM_METATILE_LAYER_TYPES)) + logWarn(QString("Metatile Layer Type mask is too small to contain all %1 available options.").arg(NUM_METATILE_LAYER_TYPES)); } uint32_t Metatile::getAttributes() { @@ -165,3 +186,19 @@ void Metatile::convertAttributes(uint32_t data, BaseGameVersion version) { // Clean data to fit the user's custom masks this->setAttributes(this->getAttributes()); } + +void Metatile::setBehavior(uint32_t value) { + this->behavior = value & Metatile::customLayout[Attr::Behavior].mask; +} + +void Metatile::setTerrainType(uint32_t value) { + this->terrainType = value & Metatile::customLayout[Attr::TerrainType].mask; +} + +void Metatile::setEncounterType(uint32_t value) { + this->encounterType = value & Metatile::customLayout[Attr::EncounterType].mask; +} + +void Metatile::setLayerType(uint32_t value) { + this->layerType = value & Metatile::customLayout[Attr::LayerType].mask; +} diff --git a/src/scriptapi/apimap.cpp b/src/scriptapi/apimap.cpp index ee352536..0adbded8 100644 --- a/src/scriptapi/apimap.cpp +++ b/src/scriptapi/apimap.cpp @@ -635,10 +635,9 @@ int MainWindow::getMetatileLayerType(int metatileId) { void MainWindow::setMetatileLayerType(int metatileId, int layerType) { Metatile * metatile = this->getMetatile(metatileId); - uint8_t u_layerType = static_cast(layerType); - if (!metatile || metatile->layerType == u_layerType || u_layerType >= NUM_METATILE_LAYER_TYPES) + if (!metatile) return; - metatile->layerType = u_layerType; + metatile->setLayerType(layerType); this->saveMetatileAttributesByMetatileId(metatileId); } @@ -651,10 +650,9 @@ int MainWindow::getMetatileEncounterType(int metatileId) { void MainWindow::setMetatileEncounterType(int metatileId, int encounterType) { Metatile * metatile = this->getMetatile(metatileId); - uint8_t u_encounterType = static_cast(encounterType); - if (!metatile || metatile->encounterType == u_encounterType || u_encounterType >= NUM_METATILE_ENCOUNTER_TYPES) + if (!metatile) return; - metatile->encounterType = u_encounterType; + metatile->setEncounterType(encounterType); this->saveMetatileAttributesByMetatileId(metatileId); } @@ -667,10 +665,9 @@ int MainWindow::getMetatileTerrainType(int metatileId) { void MainWindow::setMetatileTerrainType(int metatileId, int terrainType) { Metatile * metatile = this->getMetatile(metatileId); - uint8_t u_terrainType = static_cast(terrainType); - if (!metatile || metatile->terrainType == u_terrainType || u_terrainType >= NUM_METATILE_TERRAIN_TYPES) + if (!metatile) return; - metatile->terrainType = u_terrainType; + metatile->setTerrainType(terrainType); this->saveMetatileAttributesByMetatileId(metatileId); } @@ -683,10 +680,9 @@ int MainWindow::getMetatileBehavior(int metatileId) { void MainWindow::setMetatileBehavior(int metatileId, int behavior) { Metatile * metatile = this->getMetatile(metatileId); - uint16_t u_behavior = static_cast(behavior); - if (!metatile || metatile->behavior == u_behavior) + if (!metatile) return; - metatile->behavior = u_behavior; + metatile->setBehavior(behavior); this->saveMetatileAttributesByMetatileId(metatileId); } diff --git a/src/ui/tileseteditor.cpp b/src/ui/tileseteditor.cpp index 14e9d441..5532334a 100644 --- a/src/ui/tileseteditor.cpp +++ b/src/ui/tileseteditor.cpp @@ -99,9 +99,7 @@ void TilesetEditor::initUi() { this->ui->spinBox_paletteSelector->setMinimum(0); this->ui->spinBox_paletteSelector->setMaximum(Project::getNumPalettesTotal() - 1); - this->setMetatileBehaviors(); - this->setMetatileLayersUi(); - this->setVersionSpecificUi(); + this->setAttributesUi(); this->setMetatileLabelValidator(); this->initMetatileSelector(); @@ -113,43 +111,55 @@ void TilesetEditor::initUi() { this->restoreWindowState(); } -void TilesetEditor::setMetatileBehaviors() { - for (int num : project->metatileBehaviorMapInverse.keys()) { - this->ui->comboBox_metatileBehaviors->addItem(project->metatileBehaviorMapInverse[num], num); - } -} - -void TilesetEditor::setMetatileLayersUi() { - if (!projectConfig.getTripleLayerMetatilesEnabled()) { - this->ui->comboBox_layerType->addItem("Normal - Middle/Top", METATILE_LAYER_MIDDLE_TOP); - this->ui->comboBox_layerType->addItem("Covered - Bottom/Middle", METATILE_LAYER_BOTTOM_MIDDLE); - this->ui->comboBox_layerType->addItem("Split - Bottom/Top", METATILE_LAYER_BOTTOM_TOP); +void TilesetEditor::setAttributesUi() { + // Behavior + if (Metatile::customLayout[Metatile::Attr::Behavior].mask) { + for (int num : project->metatileBehaviorMapInverse.keys()) { + this->ui->comboBox_metatileBehaviors->addItem(project->metatileBehaviorMapInverse[num], num); + } } else { - this->ui->comboBox_layerType->setVisible(false); - this->ui->label_layerType->setVisible(false); - this->ui->label_BottomTop->setText("Bottom/Middle/Top"); + this->ui->comboBox_metatileBehaviors->setVisible(false); + this->ui->label_metatileBehavior->setVisible(false); } -} -void TilesetEditor::setVersionSpecificUi() { - if (projectConfig.getBaseGameVersion() == BaseGameVersion::pokefirered) { - this->ui->comboBox_encounterType->setVisible(true); - this->ui->label_encounterType->setVisible(true); - this->ui->comboBox_encounterType->addItem("None", ENCOUNTER_NONE); - this->ui->comboBox_encounterType->addItem("Land", ENCOUNTER_LAND); - this->ui->comboBox_encounterType->addItem("Water", ENCOUNTER_WATER); - this->ui->comboBox_terrainType->setVisible(true); - this->ui->label_terrainType->setVisible(true); + // Terrain Type + if (Metatile::customLayout[Metatile::Attr::TerrainType].mask) { this->ui->comboBox_terrainType->addItem("Normal", TERRAIN_NONE); this->ui->comboBox_terrainType->addItem("Grass", TERRAIN_GRASS); this->ui->comboBox_terrainType->addItem("Water", TERRAIN_WATER); this->ui->comboBox_terrainType->addItem("Waterfall", TERRAIN_WATERFALL); } else { - this->ui->comboBox_encounterType->setVisible(false); - this->ui->label_encounterType->setVisible(false); this->ui->comboBox_terrainType->setVisible(false); this->ui->label_terrainType->setVisible(false); } + + // Encounter Type + if (Metatile::customLayout[Metatile::Attr::EncounterType].mask) { + this->ui->comboBox_encounterType->addItem("None", ENCOUNTER_NONE); + this->ui->comboBox_encounterType->addItem("Land", ENCOUNTER_LAND); + this->ui->comboBox_encounterType->addItem("Water", ENCOUNTER_WATER); + } else { + this->ui->comboBox_encounterType->setVisible(false); + this->ui->label_encounterType->setVisible(false); + } + + // Layer Type + if (!projectConfig.getTripleLayerMetatilesEnabled()) { + this->ui->comboBox_layerType->addItem("Normal - Middle/Top", METATILE_LAYER_MIDDLE_TOP); + this->ui->comboBox_layerType->addItem("Covered - Bottom/Middle", METATILE_LAYER_BOTTOM_MIDDLE); + this->ui->comboBox_layerType->addItem("Split - Bottom/Top", METATILE_LAYER_BOTTOM_TOP); + if (!Metatile::customLayout[Metatile::Attr::LayerType].mask) { + // User doesn't have triple layer metatiles, but has no layer type attribute. + // Porymap is still using the layer type value to render metatiles, and with + // no mask set every metatile will be "Middle/Top", so just display the combo + // box but prevent the user from changing the value. + this->ui->comboBox_layerType->setEnabled(false); + } + } else { + this->ui->comboBox_layerType->setVisible(false); + this->ui->label_layerType->setVisible(false); + this->ui->label_BottomTop->setText("Bottom/Middle/Top"); + } } void TilesetEditor::setMetatileLabelValidator() { @@ -373,13 +383,9 @@ void TilesetEditor::onSelectedMetatileChanged(uint16_t metatileId) { this->ui->graphicsView_metatileLayers->setFixedSize(this->metatileLayersItem->pixmap().width() + 2, this->metatileLayersItem->pixmap().height() + 2); this->ui->lineEdit_metatileLabel->setText(this->metatile->label); setComboValue(this->ui->comboBox_metatileBehaviors, this->metatile->behavior); - if (!projectConfig.getTripleLayerMetatilesEnabled()) { - setComboValue(this->ui->comboBox_layerType, this->metatile->layerType); - } - if (projectConfig.getBaseGameVersion() == BaseGameVersion::pokefirered) { - setComboValue(this->ui->comboBox_encounterType, this->metatile->encounterType); - setComboValue(this->ui->comboBox_terrainType, this->metatile->terrainType); - } + setComboValue(this->ui->comboBox_layerType, this->metatile->layerType); + setComboValue(this->ui->comboBox_encounterType, this->metatile->encounterType); + setComboValue(this->ui->comboBox_terrainType, this->metatile->terrainType); } void TilesetEditor::onHoveredTileChanged(uint16_t tile) { @@ -501,7 +507,7 @@ void TilesetEditor::on_comboBox_metatileBehaviors_textActivated(const QString &m { if (this->metatile) { Metatile *prevMetatile = new Metatile(*this->metatile); - this->metatile->behavior = static_cast(project->metatileBehaviorMap[metatileBehavior]); + this->metatile->setBehavior(project->metatileBehaviorMap[metatileBehavior]); MetatileHistoryItem *commit = new MetatileHistoryItem(this->getSelectedMetatileId(), prevMetatile, new Metatile(*this->metatile)); metatileHistory.push(commit); @@ -537,7 +543,7 @@ void TilesetEditor::on_comboBox_layerType_activated(int layerType) { if (this->metatile) { Metatile *prevMetatile = new Metatile(*this->metatile); - this->metatile->layerType = static_cast(layerType); + this->metatile->setLayerType(layerType); MetatileHistoryItem *commit = new MetatileHistoryItem(this->getSelectedMetatileId(), prevMetatile, new Metatile(*this->metatile)); metatileHistory.push(commit); @@ -550,7 +556,7 @@ void TilesetEditor::on_comboBox_encounterType_activated(int encounterType) { if (this->metatile) { Metatile *prevMetatile = new Metatile(*this->metatile); - this->metatile->encounterType = static_cast(encounterType); + this->metatile->setEncounterType(encounterType); MetatileHistoryItem *commit = new MetatileHistoryItem(this->getSelectedMetatileId(), prevMetatile, new Metatile(*this->metatile)); metatileHistory.push(commit); @@ -562,7 +568,7 @@ void TilesetEditor::on_comboBox_terrainType_activated(int terrainType) { if (this->metatile) { Metatile *prevMetatile = new Metatile(*this->metatile); - this->metatile->terrainType = static_cast(terrainType); + this->metatile->setTerrainType(terrainType); MetatileHistoryItem *commit = new MetatileHistoryItem(this->getSelectedMetatileId(), prevMetatile, new Metatile(*this->metatile)); metatileHistory.push(commit);