diff --git a/include/config.h b/include/config.h index f7ff9a9d..5dd4f38e 100644 --- a/include/config.h +++ b/include/config.h @@ -36,6 +36,7 @@ protected: virtual void setUnreadKeys() = 0; bool getConfigBool(QString key, QString value); int getConfigInteger(QString key, QString value, int min, int max, int defaultValue); + long getConfigLong(QString key, QString value, long min, long max, long defaultValue); }; class PorymapConfig: public KeyValueConfigBase @@ -264,6 +265,11 @@ public: bool getTilesetsHaveCallback(); void setTilesetsHaveIsCompressed(bool has); bool getTilesetsHaveIsCompressed(); + int getMetatileAttributesSize(); + uint32_t getMetatileBehaviorMask(); + uint32_t getMetatileTerrainTypeMask(); + uint32_t getMetatileEncounterTypeMask(); + uint32_t getMetatileLayerTypeMask(); protected: virtual QString getConfigFilepath() override; virtual void parseConfigKeyValue(QString key, QString value) override; @@ -295,6 +301,11 @@ private: bool prefabImportPrompted; bool tilesetsHaveCallback; bool tilesetsHaveIsCompressed; + int metatileAttributesSize; + uint32_t metatileBehaviorMask; + uint32_t metatileTerrainTypeMask; + uint32_t metatileEncounterTypeMask; + uint32_t metatileLayerTypeMask; }; extern ProjectConfig projectConfig; diff --git a/include/core/metatile.h b/include/core/metatile.h index eb315ab8..2f8d6091 100644 --- a/include/core/metatile.h +++ b/include/core/metatile.h @@ -40,19 +40,34 @@ public: public: QList tiles; - uint16_t behavior; // 8 bits RSE, 9 bits FRLG - uint8_t layerType; - uint8_t encounterType; // FRLG only - uint8_t terrainType; // FRLG only + uint32_t behavior; + uint32_t layerType; + uint32_t encounterType; + uint32_t terrainType; uint32_t unusedAttributes; QString label; + uint32_t getAttributes(); + void setAttributes(uint32_t data); void setAttributes(uint32_t data, BaseGameVersion version); - uint32_t getAttributes(BaseGameVersion version); static int getIndexInTileset(int); static QPoint coordFromPixmapCoord(const QPointF &pixelCoord); static int getAttributesSize(BaseGameVersion version); + static void calculateAttributeLayout(); + +private: + static uint32_t behaviorMask; + static uint32_t terrainTypeMask; + static uint32_t encounterTypeMask; + static uint32_t layerTypeMask; + static uint32_t unusedAttrMask; + static int behaviorShift; + static int terrainTypeShift; + static int encounterTypeShift; + static int layerTypeShift; + + static int getShiftValue(uint32_t mask); }; inline bool operator==(const Metatile &a, const Metatile &b) { diff --git a/src/config.cpp b/src/config.cpp index a9ce994e..6ab6d633 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -145,7 +145,17 @@ bool KeyValueConfigBase::getConfigBool(QString key, QString value) { int KeyValueConfigBase::getConfigInteger(QString key, QString value, int min, int max, int defaultValue) { bool ok; - int result = value.toInt(&ok); + int result = value.toInt(&ok, 0); + if (!ok) { + logWarn(QString("Invalid config value for %1: '%2'. Must be an integer.").arg(key).arg(value)); + return defaultValue; + } + return qMin(max, qMax(min, result)); +} + +long KeyValueConfigBase::getConfigLong(QString key, QString value, long min, long max, long defaultValue) { + bool ok; + long result = value.toLong(&ok, 0); if (!ok) { logWarn(QString("Invalid config value for %1: '%2'. Must be an integer.").arg(key).arg(value)); return defaultValue; @@ -549,6 +559,21 @@ void ProjectConfig::parseConfigKeyValue(QString key, QString value) { this->defaultPrimaryTileset = value; } else if (key == "default_secondary_tileset") { this->defaultSecondaryTileset = value; + } else if (key == "metatile_attributes_size") { + int size = getConfigInteger(key, value, 1, 4, 2); + if (size & (size - 1)) { + logWarn(QString("Invalid config value for %1: must be 1, 2, or 4").arg(key)); + size = 2; + } + this->metatileAttributesSize = size; + } else if (key == "metatile_behavior_mask") { + this->metatileBehaviorMask = getConfigLong(key, value, 0, 0xFFFFFFFF, 0); + } else if (key == "metatile_terrain_type_mask") { + this->metatileTerrainTypeMask = getConfigLong(key, value, 0, 0xFFFFFFFF, 0); + } else if (key == "metatile_encounter_type_mask") { + this->metatileEncounterTypeMask = getConfigLong(key, value, 0, 0xFFFFFFFF, 0); + } else if (key == "metatile_layer_type_mask") { + this->metatileLayerTypeMask = getConfigLong(key, value, 0, 0xFFFFFFFF, 0); #ifdef CONFIG_BACKWARDS_COMPATABILITY } else if (key == "recent_map") { userConfig.setRecentMap(value); @@ -599,6 +624,11 @@ void ProjectConfig::setUnreadKeys() { if (!readKeys.contains("create_map_text_file")) this->createMapTextFile = (this->baseGameVersion != BaseGameVersion::pokeemerald); if (!readKeys.contains("new_map_border_metatiles")) this->newMapBorderMetatileIds = isPokefirered ? DEFAULT_BORDER_FRLG : DEFAULT_BORDER_RSE; if (!readKeys.contains("default_secondary_tileset")) this->defaultSecondaryTileset = isPokefirered ? "gTileset_PalletTown" : "gTileset_Petalburg"; + if (!readKeys.contains("metatile_attributes_size")) this->metatileAttributesSize = isPokefirered ? 4 : 2; + if (!readKeys.contains("metatile_behavior_mask")) this->metatileBehaviorMask = isPokefirered ? 0x000001FF : 0x00FF; + if (!readKeys.contains("metatile_terrain_type_mask")) this->metatileTerrainTypeMask = isPokefirered ? 0x00003E00 : 0; + if (!readKeys.contains("metatile_encounter_type_mask")) this->metatileEncounterTypeMask = isPokefirered ? 0x07000000 : 0; + if (!readKeys.contains("metatile_layer_type_mask")) this-> metatileLayerTypeMask = isPokefirered ? 0x60000000 : 0xF000; } QMap ProjectConfig::getKeyValueMap() { @@ -630,6 +660,11 @@ QMap ProjectConfig::getKeyValueMap() { } map.insert("tilesets_have_callback", QString::number(this->tilesetsHaveCallback)); map.insert("tilesets_have_is_compressed", QString::number(this->tilesetsHaveIsCompressed)); + 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()); + map.insert("metatile_encounter_type_mask", "0x" + QString::number(this->metatileEncounterTypeMask, 16).toUpper()); + map.insert("metatile_layer_type_mask", "0x" + QString::number(this->metatileLayerTypeMask, 16).toUpper()); return map; } @@ -879,6 +914,26 @@ bool ProjectConfig::getTilesetsHaveIsCompressed() { return this->tilesetsHaveIsCompressed; } +int ProjectConfig::getMetatileAttributesSize() { + return this->metatileAttributesSize; +} + +uint32_t ProjectConfig::getMetatileBehaviorMask() { + return this->metatileBehaviorMask; +} + +uint32_t ProjectConfig::getMetatileTerrainTypeMask() { + return this->metatileTerrainTypeMask; +} + +uint32_t ProjectConfig::getMetatileEncounterTypeMask() { + return this->metatileEncounterTypeMask; +} + +uint32_t ProjectConfig::getMetatileLayerTypeMask() { + return this->metatileLayerTypeMask; +} + UserConfig userConfig; diff --git a/src/core/metatile.cpp b/src/core/metatile.cpp index 6264fda2..4f9f444b 100644 --- a/src/core/metatile.cpp +++ b/src/core/metatile.cpp @@ -2,6 +2,17 @@ #include "tileset.h" #include "project.h" +uint32_t Metatile::behaviorMask = 0; +uint32_t Metatile::terrainTypeMask = 0; +uint32_t Metatile::encounterTypeMask = 0; +uint32_t Metatile::layerTypeMask = 0; +uint32_t Metatile::unusedAttrMask = 0; + +int Metatile::behaviorShift = 0; +int Metatile::terrainTypeShift = 0; +int Metatile::encounterTypeShift = 0; +int Metatile::layerTypeShift = 0; + Metatile::Metatile() : behavior(0), layerType(0), @@ -37,50 +48,115 @@ QPoint Metatile::coordFromPixmapCoord(const QPointF &pixelCoord) { return QPoint(x, y); } +// Returns the position of the rightmost set bit +int Metatile::getShiftValue(uint32_t mask) { + return log2(mask & ~(mask - 1));; +} + + +void Metatile::calculateAttributeLayout() { + // Get the maximum size of any attribute mask + const QHash maxMasks = { + {1, 0xFF}, + {2, 0xFFFF}, + {4, 0xFFFFFFFF}, + }; + const uint32_t maxMask = maxMasks.value(projectConfig.getMetatileAttributesSize(), 0); + + // Behavior + uint32_t mask = projectConfig.getMetatileBehaviorMask(); + if (mask > maxMask) { + logWarn(QString("Metatile behavior mask '%1' exceeds maximum size '%2'").arg(mask).arg(maxMask)); + mask &= maxMask; + } + Metatile::behaviorMask = mask; + Metatile::behaviorShift = getShiftValue(mask); + + // Terrain Type + mask = projectConfig.getMetatileTerrainTypeMask(); + if (mask > maxMask) { + logWarn(QString("Metatile terrain type mask '%1' exceeds maximum size '%2'").arg(mask).arg(maxMask)); + mask &= maxMask; + } + Metatile::terrainTypeMask = mask; + Metatile::terrainTypeShift = getShiftValue(mask); + + // Encounter Type + mask = projectConfig.getMetatileEncounterTypeMask(); + if (mask > maxMask) { + logWarn(QString("Metatile encounter type mask '%1' exceeds maximum size '%2'").arg(mask).arg(maxMask)); + mask &= maxMask; + } + Metatile::encounterTypeMask = mask; + Metatile::encounterTypeShift = getShiftValue(mask); + + // Layer Type + mask = projectConfig.getMetatileLayerTypeMask(); + if (mask > maxMask) { + logWarn(QString("Metatile layer type mask '%1' exceeds maximum size '%2'").arg(mask).arg(maxMask)); + mask &= maxMask; + } + Metatile::layerTypeMask = mask; + Metatile::layerTypeShift = getShiftValue(mask); + + Metatile::unusedAttrMask = ~(Metatile::behaviorMask | Metatile::terrainTypeMask | Metatile::layerTypeMask | Metatile::encounterTypeMask); + Metatile::unusedAttrMask &= maxMask; + + // Warn user if any mask overlaps + if (Metatile::behaviorMask & Metatile::terrainTypeMask + || Metatile::behaviorMask & Metatile::encounterTypeMask + || Metatile::behaviorMask & Metatile::layerTypeMask + || Metatile::terrainTypeMask & Metatile::encounterTypeMask + || Metatile::terrainTypeMask & Metatile::layerTypeMask + || Metatile::encounterTypeMask & Metatile::layerTypeMask) { + logWarn("Metatile attribute masks are overlapping."); + } +} + +uint32_t Metatile::getAttributes() { + uint32_t attributes = this->unusedAttributes & Metatile::unusedAttrMask; + attributes |= (behavior << Metatile::behaviorShift) & Metatile::behaviorMask; + attributes |= (terrainType << Metatile::terrainTypeShift) & Metatile::terrainTypeMask; + attributes |= (encounterType << Metatile::encounterTypeShift) & Metatile::encounterTypeMask; + attributes |= (layerType << Metatile::layerTypeShift) & Metatile::layerTypeMask; + return attributes; +} + +void Metatile::setAttributes(uint32_t data) { + this->behavior = (data & Metatile::behaviorMask) >> Metatile::behaviorShift; + this->terrainType = (data & Metatile::terrainTypeMask) >> Metatile::terrainTypeShift; + this->encounterType = (data & Metatile::encounterTypeMask) >> Metatile::encounterTypeShift; + this->layerType = (data & Metatile::layerTypeMask) >> Metatile::layerTypeShift; + this->unusedAttributes = data & Metatile::unusedAttrMask; +} + +// Get the vanilla attribute sizes based on version. For AdvanceMap import int Metatile::getAttributesSize(BaseGameVersion version) { return (version == BaseGameVersion::pokefirered) ? 4 : 2; } -// RSE attributes -const uint16_t behaviorMask_RSE = 0x00FF; -const uint16_t layerTypeMask_RSE = 0xF000; -const int behaviorShift_RSE = 0; -const int layerTypeShift_RSE = 12; - -// FRLG attributes -const uint32_t behaviorMask_FRLG = 0x000001FF; -const uint32_t terrainTypeMask = 0x00003E00; -const uint32_t encounterTypeMask = 0x07000000; -const uint32_t layerTypeMask_FRLG = 0x60000000; -const int behaviorShift_FRLG = 0; -const int terrainTypeShift = 9; -const int encounterTypeShift = 24; -const int layerTypeShift_FRLG = 29; - -uint32_t Metatile::getAttributes(BaseGameVersion version) { - uint32_t attributes = this->unusedAttributes; - if (version == BaseGameVersion::pokefirered) { - attributes |= (behavior << behaviorShift_FRLG) & behaviorMask_FRLG; - attributes |= (terrainType << terrainTypeShift) & terrainTypeMask; - attributes |= (encounterType << encounterTypeShift) & encounterTypeMask; - attributes |= (layerType << layerTypeShift_FRLG) & layerTypeMask_FRLG; - } else { - attributes |= (behavior << behaviorShift_RSE) & behaviorMask_RSE; - attributes |= (layerType << layerTypeShift_RSE) & layerTypeMask_RSE; - } - return attributes; -} - +// Set the attributes using the vanilla layout based on version. For AdvanceMap import void Metatile::setAttributes(uint32_t data, BaseGameVersion version) { if (version == BaseGameVersion::pokefirered) { - this->behavior = (data & behaviorMask_FRLG) >> behaviorShift_FRLG; - this->terrainType = (data & terrainTypeMask) >> terrainTypeShift; - this->encounterType = (data & encounterTypeMask) >> encounterTypeShift; - this->layerType = (data & layerTypeMask_FRLG) >> layerTypeShift_FRLG; - this->unusedAttributes = data & ~(behaviorMask_FRLG | terrainTypeMask | layerTypeMask_FRLG | encounterTypeMask); + const uint32_t behaviorMask = 0x000001FF; + const uint32_t terrainTypeMask = 0x00003E00; + const uint32_t encounterTypeMask = 0x07000000; + const uint32_t layerTypeMask = 0x60000000; + + this->behavior = data & behaviorMask; + this->terrainType = (data & terrainTypeMask) >> 9; + this->encounterType = (data & encounterTypeMask) >> 24; + this->layerType = (data & layerTypeMask) >> 29; + this->unusedAttributes = data & ~(behaviorMask | terrainTypeMask | layerTypeMask | encounterTypeMask); } else { - this->behavior = (data & behaviorMask_RSE) >> behaviorShift_RSE; - this->layerType = (data & layerTypeMask_RSE) >> layerTypeShift_RSE; - this->unusedAttributes = data & ~(behaviorMask_RSE | layerTypeMask_RSE); + const uint16_t behaviorMask = 0x00FF; + const uint16_t layerTypeMask = 0xF000; + + this->behavior = data & behaviorMask; + this->layerType = (data & layerTypeMask) >> 12; + this->unusedAttributes = data & ~(behaviorMask | layerTypeMask); } + + // Clean data to fit the user's custom masks + this->setAttributes(this->getAttributes()); } diff --git a/src/core/tileset.cpp b/src/core/tileset.cpp index adfb0a13..4b31e22e 100644 --- a/src/core/tileset.cpp +++ b/src/core/tileset.cpp @@ -233,8 +233,8 @@ bool Tileset::appendToMetatiles(QString root, QString friendlyName, bool usingAs } else { // Append to C file dataString.append(QString("const u16 gMetatiles_%1[] = INCBIN_U16(\"%2\");\n").arg(friendlyName, metatilesPath)); - QString attrSize = (projectConfig.getBaseGameVersion() == BaseGameVersion::pokefirered) ? "32" : "16"; - dataString.append(QString("const u%1 gMetatileAttributes_%2[] = INCBIN_U%1(\"%3\");\n").arg(attrSize, friendlyName, metatileAttrsPath)); + QString numBits = QString::number(projectConfig.getMetatileAttributesSize() * 8); + dataString.append(QString("const u%1 gMetatileAttributes_%2[] = INCBIN_U%1(\"%3\");\n").arg(numBits, friendlyName, metatileAttrsPath)); } file.write(dataString.toUtf8()); file.flush(); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index cdbb0734..d3f436e7 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -528,6 +528,7 @@ bool MainWindow::openProject(QString dir) { userConfig.load(); projectConfig.setProjectDir(dir); projectConfig.load(); + Metatile::calculateAttributeLayout(); this->closeSupplementaryWindows(); this->setProjectSpecificUIVisibility(); diff --git a/src/project.cpp b/src/project.cpp index 4b68be4a..c6a1dd5c 100644 --- a/src/project.cpp +++ b/src/project.cpp @@ -980,10 +980,9 @@ void Project::saveTilesetMetatileAttributes(Tileset *tileset) { QFile attrs_file(tileset->metatile_attrs_path); if (attrs_file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { QByteArray data; - BaseGameVersion version = projectConfig.getBaseGameVersion(); - int attrSize = Metatile::getAttributesSize(version); + int attrSize = projectConfig.getMetatileAttributesSize(); for (Metatile *metatile : tileset->metatiles) { - uint32_t attributes = metatile->getAttributes(version); + uint32_t attributes = metatile->getAttributes(); for (int i = 0; i < attrSize; i++) data.append(static_cast(attributes >> (8 * i))); } @@ -1538,9 +1537,7 @@ void Project::loadTilesetMetatiles(Tileset* tileset) { if (attrs_file.open(QIODevice::ReadOnly)) { QByteArray data = attrs_file.readAll(); int num_metatiles = tileset->metatiles.count(); - - BaseGameVersion version = projectConfig.getBaseGameVersion(); - int attrSize = Metatile::getAttributesSize(version); + int attrSize = projectConfig.getMetatileAttributesSize(); int num_metatileAttrs = data.length() / attrSize; if (num_metatiles != num_metatileAttrs) { logWarn(QString("Metatile count %1 does not match metatile attribute count %2 in %3").arg(num_metatiles).arg(num_metatileAttrs).arg(tileset->name)); @@ -1552,7 +1549,7 @@ void Project::loadTilesetMetatiles(Tileset* tileset) { uint32_t attributes = 0; for (int j = 0; j < attrSize; j++) attributes |= static_cast(data.at(i * attrSize + j)) << (8 * j); - tileset->metatiles.at(i)->setAttributes(attributes, version); + tileset->metatiles.at(i)->setAttributes(attributes); } } else { logError(QString("Could not open tileset metatile attributes file '%1'").arg(tileset->metatile_attrs_path)); diff --git a/src/scriptapi/apimap.cpp b/src/scriptapi/apimap.cpp index b133a74b..ee352536 100644 --- a/src/scriptapi/apimap.cpp +++ b/src/scriptapi/apimap.cpp @@ -694,15 +694,14 @@ int MainWindow::getMetatileAttributes(int metatileId) { Metatile * metatile = this->getMetatile(metatileId); if (!metatile) return -1; - return metatile->getAttributes(projectConfig.getBaseGameVersion()); + return metatile->getAttributes(); } void MainWindow::setMetatileAttributes(int metatileId, int attributes) { Metatile * metatile = this->getMetatile(metatileId); - uint32_t u_attributes = static_cast(attributes); if (!metatile) return; - metatile->setAttributes(u_attributes, projectConfig.getBaseGameVersion()); + metatile->setAttributes(attributes); this->saveMetatileAttributesByMetatileId(metatileId); }