Use custom attribute masks in API and Tileset Editor

This commit is contained in:
GriffinR 2022-10-26 02:42:55 -04:00
parent 1641ac00b0
commit 1283f5c19d
5 changed files with 113 additions and 70 deletions

View file

@ -41,9 +41,9 @@ public:
public:
QList<Tile> 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<Metatile::Attr, Metatile::AttrLayout> defaultLayoutFRLG;
static const QHash<Metatile::Attr, Metatile::AttrLayout> defaultLayoutRSE;
static QHash<Metatile::Attr, Metatile::AttrLayout> 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<Metatile::Attr, Metatile::AttrLayout> customLayout;
static uint32_t unusedAttrMask;
void setAttributes(uint32_t, const QHash<Metatile::Attr, Metatile::AttrLayout>*);
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<uint32_t>);
};

View file

@ -111,9 +111,7 @@ private slots:
private:
void initUi();
void setMetatileBehaviors();
void setMetatileLayersUi();
void setVersionSpecificUi();
void setAttributesUi();
void setMetatileLabelValidator();
void initMetatileSelector();
void initTileSelector();

View file

@ -21,17 +21,17 @@ const QHash<Metatile::Attr, Metatile::AttrLayout> 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<uint32_t> 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;
}

View file

@ -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<uint8_t>(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<uint8_t>(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<uint8_t>(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<uint16_t>(behavior);
if (!metatile || metatile->behavior == u_behavior)
if (!metatile)
return;
metatile->behavior = u_behavior;
metatile->setBehavior(behavior);
this->saveMetatileAttributesByMetatileId(metatileId);
}

View file

@ -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() {
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);
}
}
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);
} 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,14 +383,10 @@ 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);
}
}
void TilesetEditor::onHoveredTileChanged(uint16_t tile) {
QString message = QString("Tile: 0x%1")
@ -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<uint16_t>(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<uint8_t>(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<uint8_t>(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<uint8_t>(terrainType);
this->metatile->setTerrainType(terrainType);
MetatileHistoryItem *commit = new MetatileHistoryItem(this->getSelectedMetatileId(),
prevMetatile, new Metatile(*this->metatile));
metatileHistory.push(commit);