From 3f88072981cb0c8e5ddea3d56a1ca6eb3b49b580 Mon Sep 17 00:00:00 2001 From: Marcus Huderle Date: Tue, 8 Jan 2019 18:04:41 -0600 Subject: [PATCH] Add ability to import metatiles from Advance Map (.bvd files) --- forms/tileseteditor.ui | 16 +++++- include/config.h | 1 + include/core/metatileparser.h | 15 ++++++ include/ui/tileseteditor.h | 5 ++ porymap.pro | 2 + src/core/metatileparser.cpp | 92 +++++++++++++++++++++++++++++++++++ src/core/paletteparser.cpp | 8 +-- src/ui/tileseteditor.cpp | 57 ++++++++++++++++++++++ 8 files changed, 190 insertions(+), 6 deletions(-) create mode 100644 include/core/metatileparser.h create mode 100644 src/core/metatileparser.cpp diff --git a/forms/tileseteditor.ui b/forms/tileseteditor.ui index a0687826..626351cb 100644 --- a/forms/tileseteditor.ui +++ b/forms/tileseteditor.ui @@ -477,6 +477,8 @@ + + @@ -505,12 +507,12 @@ - Import Primary Tiles... + Import Primary Tiles Image... - Import Secondary Tiles... + Import Secondary Tiles Image... @@ -549,6 +551,16 @@ Export Secondary Tiles Image... + + + Import Primary Metatiles from Advance Map 1.92... + + + + + Import Secondary Metatiles from Advance Map 1.92... + + diff --git a/include/config.h b/include/config.h index efc76e5a..b7672746 100644 --- a/include/config.h +++ b/include/config.h @@ -70,6 +70,7 @@ extern PorymapConfig porymapConfig; enum BaseGameVersion { pokeruby, + pokefirered, pokeemerald, }; diff --git a/include/core/metatileparser.h b/include/core/metatileparser.h new file mode 100644 index 00000000..1d423a72 --- /dev/null +++ b/include/core/metatileparser.h @@ -0,0 +1,15 @@ +#ifndef METATILEPARSER_H +#define METATILEPARSER_H + +#include "metatile.h" +#include +#include + +class MetatileParser +{ +public: + MetatileParser(); + QList *parse(QString filepath, bool *error, bool primaryTileset); +}; + +#endif // METATILEPARSER_H diff --git a/include/ui/tileseteditor.h b/include/ui/tileseteditor.h index 0a58c30c..6e3ed3df 100644 --- a/include/ui/tileseteditor.h +++ b/include/ui/tileseteditor.h @@ -79,6 +79,10 @@ private slots: void on_actionExport_Secondary_Tiles_Image_triggered(); + void on_actionImport_Primary_Metatiles_triggered(); + + void on_actionImport_Secondary_Metatiles_triggered(); + private: void closeEvent(QCloseEvent*); void initMetatileSelector(); @@ -87,6 +91,7 @@ private: void initMetatileLayersItem(); void drawSelectedTiles(); void importTilesetTiles(Tileset*, bool); + void importTilesetMetatiles(Tileset*, bool); void refresh(); Ui::TilesetEditor *ui; History metatileHistory; diff --git a/porymap.pro b/porymap.pro index 2f440ee3..6bee230d 100644 --- a/porymap.pro +++ b/porymap.pro @@ -22,6 +22,7 @@ SOURCES += src/core/block.cpp \ src/core/map.cpp \ src/core/maplayout.cpp \ src/core/metatile.cpp \ + src/core/metatileparser.cpp \ src/core/paletteparser.cpp \ src/core/parseutil.cpp \ src/core/tile.cpp \ @@ -67,6 +68,7 @@ HEADERS += include/core/block.h \ include/core/mapconnection.h \ include/core/maplayout.h \ include/core/metatile.h \ + include/core/metatileparser.h \ include/core/paletteparser.h \ include/core/parseutil.h \ include/core/tile.h \ diff --git a/src/core/metatileparser.cpp b/src/core/metatileparser.cpp new file mode 100644 index 00000000..da7de6b1 --- /dev/null +++ b/src/core/metatileparser.cpp @@ -0,0 +1,92 @@ +#include "metatileparser.h" +#include "config.h" +#include "log.h" +#include "project.h" + +MetatileParser::MetatileParser() +{ + +} + +QList *MetatileParser::parse(QString filepath, bool *error, bool primaryTileset) +{ + QFile file(filepath); + if (!file.open(QIODevice::ReadOnly)) { + *error = true; + logError(QString("Could not open Advance Map 1.92 Metatile .bvd file '%1': ").arg(filepath) + file.errorString()); + return nullptr; + } + + QByteArray in = file.readAll(); + file.close(); + + if (in.length() < 9 || in.length() % 2 != 0) { + *error = true; + logError(QString("Advance Map 1.92 Metatile .bvd file '%1' is an unexpected size.").arg(filepath)); + return nullptr; + } + + int projIdOffset = in.length() - 4; + int metatileSize = 16; + int attrSize; + BaseGameVersion version; + if (in.at(projIdOffset + 0) == 'R' + && in.at(projIdOffset + 1) == 'S' + && in.at(projIdOffset + 2) == 'E' + && in.at(projIdOffset + 3) == ' ') { + // ruby and emerald are handled equally here. + version = BaseGameVersion::pokeemerald; + attrSize = 2; + } else { + *error = true; + logError(QString("Detected unsupported game type from .bvd file. Last 4 bytes of file must be 'RSE '.")); + return nullptr; + } + + int maxMetatiles = primaryTileset ? Project::getNumMetatilesPrimary() : Project::getNumMetatilesTotal() - Project::getNumMetatilesPrimary(); + int numMetatiles = static_cast(in.at(0)) | + (static_cast(in.at(1)) << 8) | + (static_cast(in.at(2)) << 16) | + (static_cast(in.at(3)) << 24); + if (numMetatiles > maxMetatiles) { + *error = true; + logError(QString(".bvd file contains data for %1 metatiles, but the maximum number of metatiles is %2.").arg(numMetatiles).arg(maxMetatiles)); + return nullptr; + } + if (numMetatiles < 1) { + *error = true; + logError(QString(".bvd file contains no data for metatiles.")); + return nullptr; + } + + int expectedFileSize = 4 + (metatileSize * numMetatiles) + (attrSize * numMetatiles) + 4; + if (in.length() != expectedFileSize) { + *error = true; + logError(QString(".bvd file is an unexpected size. Expected %1 bytes, but it has %2 bytes.").arg(expectedFileSize).arg(in.length())); + return nullptr; + } + + QList *metatiles = new QList(); + for (int i = 0; i < numMetatiles; i++) { + Metatile *metatile = new Metatile(); + QList *tiles = new QList(); + for (int j = 0; j < 8; j++) { + int metatileOffset = 4 + i * metatileSize + j * 2; + uint16_t word = static_cast( + static_cast(in.at(metatileOffset)) | + (static_cast(in.at(metatileOffset + 1)) << 8)); + Tile tile(word & 0x3ff, (word >> 10) & 1, (word >> 11) & 1, (word >> 12) & 0xf); + tiles->append(tile); + } + + int attrOffset = 4 + (numMetatiles * metatileSize) + (i * attrSize); + int value = static_cast(in.at(attrOffset)) | + (static_cast(in.at(attrOffset + 1)) << 8); + metatile->behavior = value & 0xFF; + metatile->layerType = (value & 0xF000) >> 12; + metatile->tiles = tiles; + metatiles->append(metatile); + } + + return metatiles; +} diff --git a/src/core/paletteparser.cpp b/src/core/paletteparser.cpp index 6d2661d1..5a024628 100644 --- a/src/core/paletteparser.cpp +++ b/src/core/paletteparser.cpp @@ -124,7 +124,7 @@ QList PaletteParser::parseAdvanceMapPal(QString filepath, bool *error) { QFile file(filepath); if (!file.open(QIODevice::ReadOnly)) { *error = true; - logError(QString("Could not open Advance Map palette file '%1': ").arg(filepath) + file.errorString()); + logError(QString("Could not open Advance Map 1.92 palette file '%1': ").arg(filepath) + file.errorString()); return QList(); } @@ -133,16 +133,16 @@ QList PaletteParser::parseAdvanceMapPal(QString filepath, bool *error) { if (in.length() % 4 != 0) { *error = true; - logError(QString("Advance Map palette file '%1' had an unexpected format. File's length must be a multiple of 4, but the length is %2.").arg(filepath).arg(in.length())); + logError(QString("Advance Map 1.92 palette file '%1' had an unexpected format. File's length must be a multiple of 4, but the length is %2.").arg(filepath).arg(in.length())); return QList(); } QList palette; int i = 0; while (i < in.length()) { - unsigned char blue = static_cast(in.at(i)); + unsigned char red = static_cast(in.at(i)); unsigned char green = static_cast(in.at(i + 1)); - unsigned char red = static_cast(in.at(i + 2)); + unsigned char blue = static_cast(in.at(i + 2)); palette.append(qRgb(this->clampColorValue(red), this->clampColorValue(green), this->clampColorValue(blue))); diff --git a/src/ui/tileseteditor.cpp b/src/ui/tileseteditor.cpp index df85deda..f8904511 100644 --- a/src/ui/tileseteditor.cpp +++ b/src/ui/tileseteditor.cpp @@ -2,6 +2,7 @@ #include "ui_tileseteditor.h" #include "log.h" #include "imageproviders.h" +#include "metatileparser.h" #include "paletteparser.h" #include #include @@ -626,3 +627,59 @@ void TilesetEditor::on_actionExport_Secondary_Tiles_Image_triggered() image.save(filepath); } } + +void TilesetEditor::on_actionImport_Primary_Metatiles_triggered() +{ + this->importTilesetMetatiles(this->primaryTileset, true); +} + +void TilesetEditor::on_actionImport_Secondary_Metatiles_triggered() +{ + this->importTilesetMetatiles(this->secondaryTileset, false); +} + +void TilesetEditor::importTilesetMetatiles(Tileset *tileset, bool primary) +{ + QString descriptor = primary ? "primary" : "secondary"; + QString descriptorCaps = primary ? "Primary" : "Secondary"; + + QString filepath = QFileDialog::getOpenFileName( + this, + QString("Import %1 Tileset Metatiles from Advance Map 1.92").arg(descriptorCaps), + this->project->root, + "Advance Map 1.92 Metatile Files (*.bvd)"); + if (filepath.isEmpty()) { + return; + } + + MetatileParser parser; + bool error = false; + QList *metatiles = parser.parse(filepath, &error, primary); + if (error) { + QMessageBox msgBox(this); + msgBox.setText("Failed to import metatiles from Advance Map 1.92 .bvd file."); + QString message = QString("The .bvd file could not be processed. View porymap.log for specific errors."); + msgBox.setInformativeText(message); + msgBox.setDefaultButton(QMessageBox::Ok); + msgBox.setIcon(QMessageBox::Icon::Critical); + msgBox.exec(); + return; + } +\ + // TODO: This is crude because it makes a history entry for every newly-imported metatile. + // Revisit this when tiles and num metatiles are added to tileset editory history. + int metatileIdBase = primary ? 0 : Project::getNumMetatilesPrimary(); + for (int i = 0; i < metatiles->length(); i++) { + if (i >= tileset->metatiles->length()) { + break; + } + + Metatile *prevMetatile = tileset->metatiles->at(i)->copy(); + MetatileHistoryItem *commit = new MetatileHistoryItem(static_cast(metatileIdBase + i), prevMetatile, metatiles->at(i)->copy()); + metatileHistory.push(commit); + } + + tileset->metatiles = metatiles; + this->refresh(); + this->hasUnsavedChanges = true; +}