diff --git a/forms/mainwindow.ui b/forms/mainwindow.ui index c431d469..602ff620 100644 --- a/forms/mainwindow.ui +++ b/forms/mainwindow.ui @@ -2928,6 +2928,7 @@ + diff --git a/forms/newtilesetdialog.ui b/forms/newtilesetdialog.ui index c582eba0..1fed2558 100644 --- a/forms/newtilesetdialog.ui +++ b/forms/newtilesetdialog.ui @@ -6,238 +6,152 @@ 0 0 - 400 - 216 + 450 + 209 - - - 0 - 0 - - Add new Tileset - - - - 0 - 0 - 400 - 216 - - - - - 0 - 0 - - - - - 10 - - - 10 - - - 10 - - - 10 - - - 6 - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - 0 - 0 - - - - - 380 - 161 - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - 0 - 0 - 380 - 155 - - - - - 6 + + + + + QFrame::Shape::StyledPanel + + + QFrame::Shadow::Raised + + + + + + Name - - 10 + + + + + + true - - 10 + + + + + + false - - 10 + + color: rgb(255, 0, 0) - - - - Name - - + + + + + + + + + Symbol Name + + + + + + + + + + + + + + Type + + + + + + + false + + + + Primary + - - - - true - - + + + Secondary + - - - - Type - - - - - - - - Primary - - - - - Secondary - - - - - - - - Path - - - - - - - false - - - true - - - - - - - Symbol Name - - - - - - - false - - - true - - - - - - - Checkerboard Fill - - - - - - - - - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - false - - - - - + + + + + + Checkerboard Fill + + + + + + + + + + Qt::Orientation::Vertical + + + + 20 + 1 + + + + + + + + + + + false + + + color: rgb(255, 0, 0) + + + + + + true + + + + + + + Qt::Orientation::Horizontal + + + QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok + + + false + + + + + + + NoScrollComboBox + QComboBox +
noscrollcombobox.h
+
+
- - - buttonBox - accepted() - NewTilesetDialog - accept() - - - 248 - 254 - - - 157 - 274 - - - - - buttonBox - rejected() - NewTilesetDialog - reject() - - - 316 - 260 - - - 286 - 274 - - - - + diff --git a/include/core/tileset.h b/include/core/tileset.h index bb7631d7..999e731d 100644 --- a/include/core/tileset.h +++ b/include/core/tileset.h @@ -38,8 +38,6 @@ public: QList> palettes; QList> palettePreviews; - bool hasUnsavedTilesImage; - static Tileset* getMetatileTileset(int, Tileset*, Tileset*); static Tileset* getTileTileset(int, Tileset*, Tileset*); static Metatile* getMetatile(int, Tileset*, Tileset*); @@ -56,10 +54,25 @@ public: static QHash getHeaderMemberMap(bool usingAsm); static QString getExpectedDir(QString tilesetName, bool isSecondary); QString getExpectedDir(); + + void load(); + void loadMetatiles(); + void loadMetatileAttributes(); + void loadTilesImage(QImage *importedImage = nullptr); + void loadPalettes(); + + void save(); + void saveMetatileAttributes(); + void saveMetatiles(); + void saveTilesImage(); + void savePalettes(); + bool appendToHeaders(QString root, QString friendlyName, bool usingAsm); bool appendToGraphics(QString root, QString friendlyName, bool usingAsm); bool appendToMetatiles(QString root, QString friendlyName, bool usingAsm); + void setTilesImage(const QImage &image); + void setMetatiles(const QList &metatiles); void addMetatile(Metatile* metatile); @@ -72,6 +85,7 @@ public: private: QList m_metatiles; + bool m_hasUnsavedTilesImage = false; }; #endif // TILESET_H diff --git a/include/mainwindow.h b/include/mainwindow.h index 22f64a81..c51b10cf 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -22,7 +22,6 @@ #include "mapimageexporter.h" #include "filterchildrenproxymodel.h" #include "maplistmodels.h" -#include "newtilesetdialog.h" #include "shortcutseditor.h" #include "preferenceeditor.h" #include "projectsettingseditor.h" @@ -191,6 +190,7 @@ private slots: void onNewMapGroupCreated(const QString &groupName); void onNewMapSectionCreated(const QString &idName); void onNewLayoutCreated(Layout *layout); + void onNewTilesetCreated(Tileset *tileset); void onMapLoaded(Map *map); void onMapRulerStatusChanged(const QString &); void applyUserShortcuts(); @@ -412,7 +412,6 @@ private: QObjectList shortcutableObjects() const; void addCustomHeaderValue(QString key, QJsonValue value, bool isNew = false); - int insertTilesetLabel(QStringList * list, QString label); void checkForUpdates(bool requestedByUser); void setDivingMapsVisible(bool visible); diff --git a/include/project.h b/include/project.h index 2b84d8d4..8a08cef6 100644 --- a/include/project.h +++ b/include/project.h @@ -139,6 +139,7 @@ public: Map *createNewMap(const Project::NewMapSettings &mapSettings, const Map* toDuplicate = nullptr); Layout *createNewLayout(const Layout::Settings &layoutSettings, const Layout* toDuplicate = nullptr); + Tileset *createNewTileset(const QString &friendlyName, bool secondary, bool checkerboardFill); bool isIdentifierUnique(const QString &identifier) const; QString getProjectTitle(); @@ -167,10 +168,7 @@ public: bool loadMapLayout(Map*); bool loadLayoutTilesets(Layout *); void loadTilesetAssets(Tileset*); - void loadTilesetTiles(Tileset*, QImage); - void loadTilesetMetatiles(Tileset*); void loadTilesetMetatileLabels(Tileset*); - void loadTilesetPalettes(Tileset*); void readTilesetPaths(Tileset* tileset); void saveLayout(Layout *); @@ -188,10 +186,6 @@ public: void saveHealLocations(Map*); void saveTilesets(Tileset*, Tileset*); void saveTilesetMetatileLabels(Tileset*, Tileset*); - void saveTilesetMetatileAttributes(Tileset*); - void saveTilesetMetatiles(Tileset*); - void saveTilesetTilesImage(Tileset*); - void saveTilesetPalettes(Tileset*); void appendTilesetLabel(const QString &label, const QString &isSecondaryStr); bool readTilesetLabels(); bool readTilesetMetatileLabels(); @@ -282,6 +276,7 @@ signals: void mapLoaded(Map *map); void mapCreated(Map *newMap, const QString &groupName); void layoutCreated(Layout *newLayout); + void tilesetCreated(Tileset *newTileset); void mapGroupAdded(const QString &groupName); void mapSectionAdded(const QString &idName); void mapSectionIdNamesChanged(const QStringList &idNames); diff --git a/include/ui/newlayoutdialog.h b/include/ui/newlayoutdialog.h index 5fdb780f..b1a2c47b 100644 --- a/include/ui/newlayoutdialog.h +++ b/include/ui/newlayoutdialog.h @@ -39,7 +39,6 @@ private: void refresh(); void saveSettings(); - bool isExistingLayout() const; private slots: void dialogButtonClicked(QAbstractButton *button); diff --git a/include/ui/newtilesetdialog.h b/include/ui/newtilesetdialog.h index c2563f21..06012708 100644 --- a/include/ui/newtilesetdialog.h +++ b/include/ui/newtilesetdialog.h @@ -2,7 +2,10 @@ #define NEWTILESETDIALOG_H #include -#include "project.h" +#include + +class Project; +class Tileset; namespace Ui { class NewTilesetDialog; @@ -15,20 +18,17 @@ class NewTilesetDialog : public QDialog public: explicit NewTilesetDialog(Project *project, QWidget *parent = nullptr); ~NewTilesetDialog(); - QString path; - QString fullSymbolName; - QString friendlyName; - bool isSecondary; - bool checkerboardFill; -private slots: - void NameOrSecondaryChanged(); - void SecondaryChanged(); - void FillChanged(); + virtual void accept() override; private: Ui::NewTilesetDialog *ui; Project *project = nullptr; + const QString symbolPrefix; + + bool validateName(bool allowEmpty = false); + void onFriendlyNameChanged(const QString &friendlyName); + void dialogButtonClicked(QAbstractButton *button); }; #endif // NEWTILESETDIALOG_H diff --git a/src/core/tileset.cpp b/src/core/tileset.cpp index be3a04d4..cf21a675 100644 --- a/src/core/tileset.cpp +++ b/src/core/tileset.cpp @@ -3,6 +3,7 @@ #include "project.h" #include "log.h" #include "config.h" +#include "imageproviders.h" #include #include @@ -23,7 +24,7 @@ Tileset::Tileset(const Tileset &other) metatileLabels(other.metatileLabels), palettes(other.palettes), palettePreviews(other.palettePreviews), - hasUnsavedTilesImage(false) + m_hasUnsavedTilesImage(other.m_hasUnsavedTilesImage) { for (auto tile : other.tiles) { tiles.append(tile.copy()); @@ -397,3 +398,184 @@ QHash Tileset::getHeaderMemberMap(bool usingAsm) map.insert(metatileAttrPosition, "metatileAttributes"); return map; } + +void Tileset::loadMetatiles() { + clearMetatiles(); + + QFile metatiles_file(this->metatiles_path); + if (!metatiles_file.open(QIODevice::ReadOnly)) { + logError(QString("Could not open '%1' for reading.").arg(this->metatiles_path)); + return; + } + + QByteArray data = metatiles_file.readAll(); + int tilesPerMetatile = projectConfig.getNumTilesInMetatile(); + int bytesPerMetatile = 2 * tilesPerMetatile; + int num_metatiles = data.length() / bytesPerMetatile; + for (int i = 0; i < num_metatiles; i++) { + auto metatile = new Metatile; + int index = i * bytesPerMetatile; + for (int j = 0; j < tilesPerMetatile; j++) { + uint16_t tileRaw = static_cast(data[index++]); + tileRaw |= static_cast(data[index++]) << 8; + metatile->tiles.append(Tile(tileRaw)); + } + m_metatiles.append(metatile); + } +} + +void Tileset::saveMetatiles() { + QFile metatiles_file(this->metatiles_path); + if (!metatiles_file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + logError(QString("Could not open '%1' for writing.").arg(this->metatiles_path)); + return; + } + + QByteArray data; + int numTiles = projectConfig.getNumTilesInMetatile(); + for (const auto &metatile : m_metatiles) { + for (int i = 0; i < numTiles; i++) { + uint16_t tile = metatile->tiles.at(i).rawValue(); + data.append(static_cast(tile)); + data.append(static_cast(tile >> 8)); + } + } + metatiles_file.write(data); +} + +void Tileset::loadMetatileAttributes() { + QFile attrs_file(this->metatile_attrs_path); + if (!attrs_file.open(QIODevice::ReadOnly)) { + logError(QString("Could not open '%1' for reading.").arg(this->metatile_attrs_path)); + return; + } + + QByteArray data = attrs_file.readAll(); + int attrSize = projectConfig.metatileAttributesSize; + int numMetatiles = m_metatiles.length(); + int numMetatileAttrs = data.length() / attrSize; + if (numMetatiles != numMetatileAttrs) { + logWarn(QString("Metatile count %1 does not match metatile attribute count %2 in %3").arg(numMetatiles).arg(numMetatileAttrs).arg(this->name)); + } + + for (int i = 0; i < qMin(numMetatiles, numMetatileAttrs); i++) { + uint32_t attributes = 0; + for (int j = 0; j < attrSize; j++) + attributes |= static_cast(data.at(i * attrSize + j)) << (8 * j); + m_metatiles.at(i)->setAttributes(attributes); + } +} + +void Tileset::saveMetatileAttributes() { + QFile attrs_file(this->metatile_attrs_path); + if (!attrs_file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + logError(QString("Could not open '%1' for writing.").arg(this->metatile_attrs_path)); + return; + } + + QByteArray data; + for (const auto &metatile : m_metatiles) { + uint32_t attributes = metatile->getAttributes(); + for (int i = 0; i < projectConfig.metatileAttributesSize; i++) + data.append(static_cast(attributes >> (8 * i))); + } + attrs_file.write(data); +} + +void Tileset::loadTilesImage(QImage *importedImage) { + QImage image; + if (importedImage) { + image = *importedImage; + m_hasUnsavedTilesImage = true; + } else if (QFile::exists(this->tilesImagePath)) { + // No image provided, load from file path. + image = QImage(this->tilesImagePath).convertToFormat(QImage::Format_Indexed8, Qt::ThresholdDither); + } else { + // Use default image + image = QImage(8, 8, QImage::Format_Indexed8); + } + + // Validate image contains 16 colors. + int colorCount = image.colorCount(); + if (colorCount > 16) { + flattenTo4bppImage(&image); + } else if (colorCount < 16) { + QVector colorTable = image.colorTable(); + for (int i = colorTable.length(); i < 16; i++) { + colorTable.append(Qt::black); + } + image.setColorTable(colorTable); + } + + QList tiles; + int w = 8; + int h = 8; + for (int y = 0; y < image.height(); y += h) + for (int x = 0; x < image.width(); x += w) { + QImage tile = image.copy(x, y, w, h); + tiles.append(tile); + } + this->tilesImage = image; + this->tiles = tiles; +} + +void Tileset::saveTilesImage() { + // Only write the tiles image if it was changed. + // Porymap will only ever change an existing tiles image by importing a new one. + if (!m_hasUnsavedTilesImage) + return; + + if (!this->tilesImage.save(this->tilesImagePath, "PNG")) { + logError(QString("Failed to save tiles image '%1'").arg(this->tilesImagePath)); + return; + } + + m_hasUnsavedTilesImage = false; +} + +void Tileset::loadPalettes() { + this->palettes.clear(); + this->palettePreviews.clear(); + + for (int i = 0; i < Project::getNumPalettesTotal(); i++) { + QList palette; + QString path = this->palettePaths.value(i); + if (!path.isEmpty()) { + bool error = false; + palette = PaletteUtil::parse(path, &error); + if (error) palette.clear(); + } + if (palette.isEmpty()) { + // Either the palette failed to load, or no palette exists. + // We expect tilesets to have a certain number of palettes, + // so fill this palette with dummy colors. + for (int j = 0; j < 16; j++) { + palette.append(qRgb(j * 16, j * 16, j * 16)); + } + } + this->palettes.append(palette); + this->palettePreviews.append(palette); + } +} + +void Tileset::savePalettes() { + int numPalettes = qMin(this->palettePaths.length(), this->palettes.length()); + for (int i = 0; i < numPalettes; i++) { + PaletteUtil::writeJASC(this->palettePaths.at(i), this->palettes.at(i).toVector(), 0, 16); + } +} + +void Tileset::load() { + loadMetatiles(); + loadMetatileAttributes(); + loadTilesImage(); + loadPalettes(); +} + +// Because metatile labels are global (and handled by the project) we don't save them here. +void Tileset::save() { + saveMetatiles(); + saveMetatileAttributes(); + saveTilesImage(); + savePalettes(); +} diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index ff39fef7..3c16b0f4 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -25,6 +25,7 @@ #include "filedialog.h" #include "newmapdialog.h" #include "newlayoutdialog.h" +#include "newtilesetdialog.h" #include #include @@ -407,8 +408,8 @@ void MainWindow::initMapList() { layout->setContentsMargins(0, 0, 0, 0); // Create add map/layout button - // TODO: Tool tip QPushButton *buttonAdd = new QPushButton(QIcon(":/icons/add.ico"), ""); + buttonAdd->setToolTip("Create New Map"); connect(buttonAdd, &QPushButton::clicked, this, &MainWindow::openNewMapDialog); layout->addWidget(buttonAdd); @@ -623,6 +624,7 @@ bool MainWindow::openProject(QString dir, bool initial) { connect(project, &Project::mapLoaded, this, &MainWindow::onMapLoaded); connect(project, &Project::mapCreated, this, &MainWindow::onNewMapCreated); connect(project, &Project::layoutCreated, this, &MainWindow::onNewLayoutCreated); + connect(project, &Project::tilesetCreated, this, &MainWindow::onNewTilesetCreated); connect(project, &Project::mapGroupAdded, this, &MainWindow::onNewMapGroupCreated); connect(project, &Project::mapSectionAdded, this, &MainWindow::onNewMapSectionCreated); connect(project, &Project::mapSectionIdNamesChanged, this->mapHeaderForm, &MapHeaderForm::setLocations); @@ -925,7 +927,6 @@ void MainWindow::setLayoutOnlyMode(bool layoutOnly) { // setLayout, but with a visible error message in case of failure. // Use when the user is specifically requesting a layout to open. -// TODO: Update the various functions taking layout IDs to take layout names (to mirror the equivalent map functions, this discrepancy is confusing atm) bool MainWindow::userSetLayout(QString layoutId) { if (!setLayout(layoutId)) { QMessageBox msgBox(this); @@ -1439,6 +1440,21 @@ void MainWindow::onNewMapSectionCreated(const QString &idName) { // TODO: Refresh Region Map Editor's map section dropdown, if it's open } +void MainWindow::onNewTilesetCreated(Tileset *tileset) { + QString message = QString("Created a new tileset named %1.").arg(tileset->name); + logInfo(message); + statusBar()->showMessage(message); + + // Refresh tileset combo boxes + if (!tileset->is_secondary) { + int index = this->editor->project->primaryTilesetLabels.indexOf(tileset->name); + ui->comboBox_PrimaryTileset->insertItem(index, tileset->name); + } else { + int index = this->editor->project->secondaryTilesetLabels.indexOf(tileset->name); + ui->comboBox_SecondaryTileset->insertItem(index, tileset->name); + } +} + void MainWindow::openNewMapDialog() { auto dialog = new NewMapDialog(this->editor->project, this); dialog->open(); @@ -1450,119 +1466,9 @@ void MainWindow::openNewLayoutDialog() { dialog->open(); } -// Insert label for newly-created tileset into sorted list of existing labels -int MainWindow::insertTilesetLabel(QStringList * list, QString label) { - int i = 0; - for (; i < list->length(); i++) - if (list->at(i) > label) break; - list->insert(i, label); - return i; -} - void MainWindow::on_actionNew_Tileset_triggered() { - NewTilesetDialog *createTilesetDialog = new NewTilesetDialog(editor->project, this); - if(createTilesetDialog->exec() == QDialog::Accepted){ - if(createTilesetDialog->friendlyName.isEmpty()) { - logError(QString("Tried to create a directory with an empty name.")); - QMessageBox msgBox(this); - msgBox.setText("Failed to add new tileset."); - QString message = QString("The given name was empty."); - msgBox.setInformativeText(message); - msgBox.setDefaultButton(QMessageBox::Ok); - msgBox.setIcon(QMessageBox::Icon::Critical); - msgBox.exec(); - return; - } - QString fullDirectoryPath = editor->project->root + "/" + createTilesetDialog->path; - QDir directory; - if(directory.exists(fullDirectoryPath)) { - logError(QString("Could not create tileset \"%1\", the folder \"%2\" already exists.").arg(createTilesetDialog->friendlyName, fullDirectoryPath)); - QMessageBox msgBox(this); - msgBox.setText("Failed to add new tileset."); - QString message = QString("The folder for tileset \"%1\" already exists. View porymap.log for specific errors.").arg(createTilesetDialog->friendlyName); - msgBox.setInformativeText(message); - msgBox.setDefaultButton(QMessageBox::Ok); - msgBox.setIcon(QMessageBox::Icon::Critical); - msgBox.exec(); - return; - } - if (editor->project->tilesetLabelsOrdered.contains(createTilesetDialog->fullSymbolName)) { - logError(QString("Could not create tileset \"%1\", the symbol \"%2\" already exists.").arg(createTilesetDialog->friendlyName, createTilesetDialog->fullSymbolName)); - QMessageBox msgBox(this); - msgBox.setText("Failed to add new tileset."); - QString message = QString("The symbol for tileset \"%1\" (\"%2\") already exists.").arg(createTilesetDialog->friendlyName, createTilesetDialog->fullSymbolName); - msgBox.setInformativeText(message); - msgBox.setDefaultButton(QMessageBox::Ok); - msgBox.setIcon(QMessageBox::Icon::Critical); - msgBox.exec(); - return; - } - directory.mkdir(fullDirectoryPath); - directory.mkdir(fullDirectoryPath + "/palettes"); - Tileset newSet; - newSet.name = createTilesetDialog->fullSymbolName; - newSet.tilesImagePath = fullDirectoryPath + "/tiles.png"; - newSet.metatiles_path = fullDirectoryPath + "/metatiles.bin"; - newSet.metatile_attrs_path = fullDirectoryPath + "/metatile_attributes.bin"; - newSet.is_secondary = createTilesetDialog->isSecondary; - int numMetatiles = createTilesetDialog->isSecondary ? (Project::getNumMetatilesTotal() - Project::getNumMetatilesPrimary()) : Project::getNumMetatilesPrimary(); - QImage tilesImage(":/images/blank_tileset.png"); - editor->project->loadTilesetTiles(&newSet, tilesImage); - int tilesPerMetatile = projectConfig.getNumTilesInMetatile(); - for(int i = 0; i < numMetatiles; ++i) { - Metatile *mt = new Metatile(); - for(int j = 0; j < tilesPerMetatile; ++j){ - Tile tile = Tile(); - if (createTilesetDialog->checkerboardFill) { - // Create a checkerboard-style dummy tileset - if (((i / 8) % 2) == 0) - tile.tileId = ((i % 2) == 0) ? 1 : 2; - else - tile.tileId = ((i % 2) == 1) ? 1 : 2; - } - mt->tiles.append(tile); - } - newSet.addMetatile(mt); - } - for(int i = 0; i < 16; ++i) { - QList currentPal; - for(int i = 0; i < 16;++i) { - currentPal.append(qRgb(0,0,0)); - } - newSet.palettes.append(currentPal); - newSet.palettePreviews.append(currentPal); - QString fileName = QString("%1.pal").arg(i, 2, 10, QLatin1Char('0')); - newSet.palettePaths.append(fullDirectoryPath+"/palettes/" + fileName); - } - newSet.palettes[0][1] = qRgb(255,0,255); - newSet.palettePreviews[0][1] = qRgb(255,0,255); - exportIndexed4BPPPng(newSet.tilesImage, newSet.tilesImagePath); - editor->project->saveTilesetMetatiles(&newSet); - editor->project->saveTilesetMetatileAttributes(&newSet); - editor->project->saveTilesetPalettes(&newSet); - - //append to tileset specific files - newSet.appendToHeaders(editor->project->root, createTilesetDialog->friendlyName, editor->project->usingAsmTilesets); - newSet.appendToGraphics(editor->project->root, createTilesetDialog->friendlyName, editor->project->usingAsmTilesets); - newSet.appendToMetatiles(editor->project->root, createTilesetDialog->friendlyName, editor->project->usingAsmTilesets); - - if (!createTilesetDialog->isSecondary) { - int index = insertTilesetLabel(&editor->project->primaryTilesetLabels, createTilesetDialog->fullSymbolName); - this->ui->comboBox_PrimaryTileset->insertItem(index, createTilesetDialog->fullSymbolName); - } else { - int index = insertTilesetLabel(&editor->project->secondaryTilesetLabels, createTilesetDialog->fullSymbolName); - this->ui->comboBox_SecondaryTileset->insertItem(index, createTilesetDialog->fullSymbolName); - } - editor->project->tilesetLabelsOrdered.append(createTilesetDialog->fullSymbolName); - - QMessageBox msgBox(this); - msgBox.setText("Successfully created tileset."); - QString message = QString("Tileset \"%1\" was created successfully.").arg(createTilesetDialog->friendlyName); - msgBox.setInformativeText(message); - msgBox.setDefaultButton(QMessageBox::Ok); - msgBox.setIcon(QMessageBox::Icon::Information); - msgBox.exec(); - } + auto dialog = new NewTilesetDialog(editor->project, this); + dialog->open(); } void MainWindow::updateTilesetEditor() { diff --git a/src/project.cpp b/src/project.cpp index 22e1924e..e7fc3ab2 100644 --- a/src/project.cpp +++ b/src/project.cpp @@ -122,19 +122,13 @@ QString Project::getProjectTitle() { } void Project::clearMapCache() { - for (auto *map : mapCache.values()) { - if (map) - delete map; - } - mapCache.clear(); + qDeleteAll(this->mapCache); + this->mapCache.clear(); } void Project::clearTilesetCache() { - for (auto *tileset : tilesetCache.values()) { - if (tileset) - delete tileset; - } - tilesetCache.clear(); + qDeleteAll(this->tilesetCache); + this->tilesetCache.clear(); } Map* Project::loadMap(QString mapName) { @@ -1005,25 +999,21 @@ void Project::saveHealLocationsConstants() { void Project::saveTilesets(Tileset *primaryTileset, Tileset *secondaryTileset) { saveTilesetMetatileLabels(primaryTileset, secondaryTileset); - saveTilesetMetatileAttributes(primaryTileset); - saveTilesetMetatileAttributes(secondaryTileset); - saveTilesetMetatiles(primaryTileset); - saveTilesetMetatiles(secondaryTileset); - saveTilesetTilesImage(primaryTileset); - saveTilesetTilesImage(secondaryTileset); - saveTilesetPalettes(primaryTileset); - saveTilesetPalettes(secondaryTileset); + if (primaryTileset) + primaryTileset->save(); + if (secondaryTileset) + secondaryTileset->save(); } void Project::updateTilesetMetatileLabels(Tileset *tileset) { // Erase old labels, then repopulate with new labels const QString prefix = tileset->getMetatileLabelPrefix(); - metatileLabelsMap[tileset->name].clear(); + this->metatileLabelsMap[tileset->name].clear(); for (int metatileId : tileset->metatileLabels.keys()) { if (tileset->metatileLabels[metatileId].isEmpty()) continue; QString label = prefix + tileset->metatileLabels[metatileId]; - metatileLabelsMap[tileset->name][label] = metatileId; + this->metatileLabelsMap[tileset->name][label] = metatileId; } } @@ -1082,59 +1072,6 @@ void Project::saveTilesetMetatileLabels(Tileset *primaryTileset, Tileset *second saveTextFile(root + "/" + filename, outputText); } -void Project::saveTilesetMetatileAttributes(Tileset *tileset) { - QFile attrs_file(tileset->metatile_attrs_path); - if (attrs_file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { - QByteArray data; - for (const auto &metatile : tileset->metatiles()) { - uint32_t attributes = metatile->getAttributes(); - for (int i = 0; i < projectConfig.metatileAttributesSize; i++) - data.append(static_cast(attributes >> (8 * i))); - } - attrs_file.write(data); - } else { - logError(QString("Could not save tileset metatile attributes file '%1'").arg(tileset->metatile_attrs_path)); - } -} - -void Project::saveTilesetMetatiles(Tileset *tileset) { - QFile metatiles_file(tileset->metatiles_path); - if (metatiles_file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { - QByteArray data; - int numTiles = projectConfig.getNumTilesInMetatile(); - for (const auto &metatile : tileset->metatiles()) { - for (int i = 0; i < numTiles; i++) { - uint16_t tile = metatile->tiles.at(i).rawValue(); - data.append(static_cast(tile)); - data.append(static_cast(tile >> 8)); - } - } - metatiles_file.write(data); - } else { - tileset->clearMetatiles(); - logError(QString("Could not open tileset metatiles file '%1'").arg(tileset->metatiles_path)); - } -} - -void Project::saveTilesetTilesImage(Tileset *tileset) { - // Only write the tiles image if it was changed. - // Porymap will only ever change an existing tiles image by importing a new one. - if (tileset->hasUnsavedTilesImage) { - if (!tileset->tilesImage.save(tileset->tilesImagePath, "PNG")) { - logError(QString("Failed to save tiles image '%1'").arg(tileset->tilesImagePath)); - return; - } - tileset->hasUnsavedTilesImage = false; - } -} - -void Project::saveTilesetPalettes(Tileset *tileset) { - for (int i = 0; i < Project::getNumPalettesTotal(); i++) { - QString filepath = tileset->palettePaths.at(i); - PaletteUtil::writeJASC(filepath, tileset->palettes.at(i).toVector(), 0, 16); - } -} - bool Project::loadLayoutTilesets(Layout *layout) { layout->tileset_primary = getTileset(layout->tileset_primary_label); if (!layout->tileset_primary) { @@ -1465,18 +1402,9 @@ void Project::loadTilesetAssets(Tileset* tileset) { if (tileset->name.isNull()) { return; } - this->readTilesetPaths(tileset); - QImage image; - if (QFile::exists(tileset->tilesImagePath)) { - image = QImage(tileset->tilesImagePath).convertToFormat(QImage::Format_Indexed8, Qt::ThresholdDither); - flattenTo4bppImage(&image); - } else { - image = QImage(8, 8, QImage::Format_Indexed8); - } - this->loadTilesetTiles(tileset, image); - this->loadTilesetMetatiles(tileset); - this->loadTilesetMetatileLabels(tileset); - this->loadTilesetPalettes(tileset); + readTilesetPaths(tileset); + loadTilesetMetatileLabels(tileset); + tileset->load(); } void Project::readTilesetPaths(Tileset* tileset) { @@ -1536,84 +1464,89 @@ void Project::readTilesetPaths(Tileset* tileset) { } } -void Project::loadTilesetPalettes(Tileset* tileset) { - QList> palettes; - QList> palettePreviews; - for (int i = 0; i < tileset->palettePaths.length(); i++) { - QString path = tileset->palettePaths.value(i); - bool error = false; - QList palette = PaletteUtil::parse(path, &error); - if (error) { - for (int j = 0; j < 16; j++) { - palette.append(qRgb(j * 16, j * 16, j * 16)); +Tileset *Project::createNewTileset(const QString &friendlyName, bool secondary, bool checkerboardFill) { + auto tileset = new Tileset(); + tileset->name = projectConfig.getIdentifier(ProjectIdentifier::symbol_tilesets_prefix) + friendlyName; + tileset->is_secondary = secondary; + + // Create tileset directories + const QString fullDirectoryPath = QString("%1/%2").arg(this->root).arg(tileset->getExpectedDir()); + QDir directory; + if (!directory.mkpath(fullDirectoryPath)) { + logError(QString("Failed to create directory '%1' for new tileset '%2'").arg(fullDirectoryPath).arg(tileset->name)); + delete tileset; + return nullptr; + } + const QString palettesPath = fullDirectoryPath + "/palettes"; + if (!directory.mkpath(palettesPath)) { + logError(QString("Failed to create palettes directory '%1' for new tileset '%2'").arg(palettesPath).arg(tileset->name)); + delete tileset; + return nullptr; + } + + tileset->tilesImagePath = fullDirectoryPath + "/tiles.png"; + tileset->metatiles_path = fullDirectoryPath + "/metatiles.bin"; + tileset->metatile_attrs_path = fullDirectoryPath + "/metatile_attributes.bin"; + + // Set default tiles image + QImage tilesImage(":/images/blank_tileset.png"); + tileset->loadTilesImage(&tilesImage); + //exportIndexed4BPPPng(tileset->tilesImage, tileset->tilesImagePath); // TODO: Make sure we can now properly handle the 8bpp images that get written without this. + + // Create default metatiles + const int numMetatiles = tileset->is_secondary ? (Project::getNumMetatilesTotal() - Project::getNumMetatilesPrimary()) : Project::getNumMetatilesPrimary(); + const int tilesPerMetatile = projectConfig.getNumTilesInMetatile(); + for (int i = 0; i < numMetatiles; ++i) { + auto metatile = new Metatile(); + for(int j = 0; j < tilesPerMetatile; ++j){ + Tile tile = Tile(); + if (checkerboardFill) { + // Create a checkerboard-style dummy tileset + if (((i / 8) % 2) == 0) + tile.tileId = ((i % 2) == 0) ? 1 : 2; + else + tile.tileId = ((i % 2) == 1) ? 1 : 2; } + metatile->tiles.append(tile); } - - palettes.append(palette); - palettePreviews.append(palette); - } - tileset->palettes = palettes; - tileset->palettePreviews = palettePreviews; -} - -void Project::loadTilesetTiles(Tileset *tileset, QImage image) { - QList tiles; - int w = 8; - int h = 8; - for (int y = 0; y < image.height(); y += h) - for (int x = 0; x < image.width(); x += w) { - QImage tile = image.copy(x, y, w, h); - tiles.append(tile); - } - tileset->tilesImage = image; - tileset->tiles = tiles; -} - -void Project::loadTilesetMetatiles(Tileset* tileset) { - QFile metatiles_file(tileset->metatiles_path); - if (metatiles_file.open(QIODevice::ReadOnly)) { - QByteArray data = metatiles_file.readAll(); - int tilesPerMetatile = projectConfig.getNumTilesInMetatile(); - int bytesPerMetatile = 2 * tilesPerMetatile; - int num_metatiles = data.length() / bytesPerMetatile; - QList metatiles; - for (int i = 0; i < num_metatiles; i++) { - Metatile *metatile = new Metatile; - int index = i * bytesPerMetatile; - for (int j = 0; j < tilesPerMetatile; j++) { - uint16_t tileRaw = static_cast(data[index++]); - tileRaw |= static_cast(data[index++]) << 8; - metatile->tiles.append(Tile(tileRaw)); - } - metatiles.append(metatile); - } - tileset->setMetatiles(metatiles); - } else { - tileset->clearMetatiles(); - logError(QString("Could not open tileset metatiles file '%1'").arg(tileset->metatiles_path)); + tileset->addMetatile(metatile); } - QFile attrs_file(tileset->metatile_attrs_path); - if (attrs_file.open(QIODevice::ReadOnly)) { - QByteArray data = attrs_file.readAll(); - int num_metatiles = tileset->numMetatiles(); - int attrSize = projectConfig.metatileAttributesSize; - 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)); - if (num_metatileAttrs > num_metatiles) - num_metatileAttrs = num_metatiles; + // Create default palettes + for(int i = 0; i < 16; ++i) { + QList currentPal; + for(int i = 0; i < 16;++i) { + currentPal.append(qRgb(0,0,0)); } - - for (int i = 0; i < num_metatileAttrs; i++) { - uint32_t attributes = 0; - for (int j = 0; j < attrSize; j++) - attributes |= static_cast(data.at(i * attrSize + j)) << (8 * j); - tileset->metatileAt(i)->setAttributes(attributes); - } - } else { - logError(QString("Could not open tileset metatile attributes file '%1'").arg(tileset->metatile_attrs_path)); + tileset->palettes.append(currentPal); + tileset->palettePreviews.append(currentPal); + tileset->palettePaths.append(QString("%1/%2.pal").arg(palettesPath).arg(i, 2, 10, QLatin1Char('0'))); } + tileset->palettes[0][1] = qRgb(255,0,255); + tileset->palettePreviews[0][1] = qRgb(255,0,255); + + // Update tileset label arrays + QStringList *labelList = tileset->is_secondary ? &this->secondaryTilesetLabels : &this->primaryTilesetLabels; + for (int i = 0; i < labelList->length(); i++) { + if (labelList->at(i) > tileset->name) { + labelList->insert(i, tileset->name); + break; + } + } + this->tilesetLabelsOrdered.append(tileset->name); + + // TODO: Ideally we wouldn't save new Tilesets immediately + // Append to tileset specific files + tileset->appendToHeaders(this->root, friendlyName, this->usingAsmTilesets); + tileset->appendToGraphics(this->root, friendlyName, this->usingAsmTilesets); + tileset->appendToMetatiles(this->root, friendlyName, this->usingAsmTilesets); + + tileset->save(); + + this->tilesetCache.insert(tileset->name, tileset); + + emit tilesetCreated(tileset); + return tileset; } QString Project::findMetatileLabelsTileset(QString label) { @@ -1658,9 +1591,9 @@ void Project::loadTilesetMetatileLabels(Tileset* tileset) { QString metatileLabelPrefix = tileset->getMetatileLabelPrefix(); // Reverse map for faster lookup by metatile id - for (QString labelName : metatileLabelsMap[tileset->name].keys()) { - auto metatileId = metatileLabelsMap[tileset->name][labelName]; - tileset->metatileLabels[metatileId] = labelName.replace(metatileLabelPrefix, ""); + for (auto it = this->metatileLabelsMap[tileset->name].constBegin(); it != this->metatileLabelsMap[tileset->name].constEnd(); it++) { + QString labelName = it.key(); + tileset->metatileLabels[it.value()] = labelName.replace(metatileLabelPrefix, ""); } } diff --git a/src/scriptapi/apimap.cpp b/src/scriptapi/apimap.cpp index f041b7b1..91cc4e67 100644 --- a/src/scriptapi/apimap.cpp +++ b/src/scriptapi/apimap.cpp @@ -337,7 +337,7 @@ void MainWindow::refreshAfterPaletteChange(Tileset *tileset) { this->editor->map_item->draw(true); this->editor->updateMapBorder(); this->editor->updateMapConnections(); - this->editor->project->saveTilesetPalettes(tileset); + tileset->savePalettes(); } void MainWindow::setTilesetPalette(Tileset *tileset, int paletteIndex, QList> colors) { @@ -576,14 +576,14 @@ void MainWindow::setSecondaryTileset(QString tileset) { void MainWindow::saveMetatilesByMetatileId(int metatileId) { Tileset * tileset = Tileset::getMetatileTileset(metatileId, this->editor->layout->tileset_primary, this->editor->layout->tileset_secondary); - if (this->editor->project && tileset) - this->editor->project->saveTilesetMetatiles(tileset); + if (tileset) + tileset->saveMetatiles(); } void MainWindow::saveMetatileAttributesByMetatileId(int metatileId) { Tileset * tileset = Tileset::getMetatileTileset(metatileId, this->editor->layout->tileset_primary, this->editor->layout->tileset_secondary); - if (this->editor->project && tileset) - this->editor->project->saveTilesetMetatileAttributes(tileset); + if (tileset) + tileset->saveMetatileAttributes(); // If the tileset editor is open it needs to be refreshed with the new changes. // Rather than do a full refresh (which is costly) we tell the editor it will need @@ -811,9 +811,6 @@ QJSValue MainWindow::getTilePixels(int tileId) { // Editing map header //===================== -// TODO: Connect signals from new function calls to update UI -// TODO: Is the error-checking for known constant names still reasonable / needed? (you can type anything after all) - QString MainWindow::getSong() { if (!this->editor || !this->editor->map) return QString(); @@ -823,10 +820,6 @@ QString MainWindow::getSong() { void MainWindow::setSong(QString song) { if (!this->editor || !this->editor->map || !this->editor->project) return; - if (!this->editor->project->songNames.contains(song)) { - logError(QString("Unknown song '%1'").arg(song)); - return; - } this->editor->map->header()->setSong(song); } @@ -867,10 +860,6 @@ QString MainWindow::getWeather() { void MainWindow::setWeather(QString weather) { if (!this->editor || !this->editor->map || !this->editor->project) return; - if (!this->editor->project->weatherNames.contains(weather)) { - logError(QString("Unknown weather '%1'").arg(weather)); - return; - } this->editor->map->header()->setWeather(weather); } @@ -883,10 +872,6 @@ QString MainWindow::getType() { void MainWindow::setType(QString type) { if (!this->editor || !this->editor->map || !this->editor->project) return; - if (!this->editor->project->mapTypes.contains(type)) { - logError(QString("Unknown map type '%1'").arg(type)); - return; - } this->editor->map->header()->setType(type); } @@ -899,10 +884,6 @@ QString MainWindow::getBattleScene() { void MainWindow::setBattleScene(QString battleScene) { if (!this->editor || !this->editor->map || !this->editor->project) return; - if (!this->editor->project->mapBattleScenes.contains(battleScene)) { - logError(QString("Unknown battle scene '%1'").arg(battleScene)); - return; - } this->editor->map->header()->setBattleScene(battleScene); } diff --git a/src/ui/newtilesetdialog.cpp b/src/ui/newtilesetdialog.cpp index 8cb4c5f9..1ddd975d 100644 --- a/src/ui/newtilesetdialog.cpp +++ b/src/ui/newtilesetdialog.cpp @@ -1,50 +1,87 @@ #include "newtilesetdialog.h" #include "ui_newtilesetdialog.h" #include "project.h" +#include "imageexport.h" + +const QString lineEdit_ErrorStylesheet = "QLineEdit { background-color: rgba(255, 0, 0, 25%) }"; NewTilesetDialog::NewTilesetDialog(Project* project, QWidget *parent) : QDialog(parent), - ui(new Ui::NewTilesetDialog) + ui(new Ui::NewTilesetDialog), + symbolPrefix(projectConfig.getIdentifier(ProjectIdentifier::symbol_tilesets_prefix)) { + setAttribute(Qt::WA_DeleteOnClose); + setModal(true); ui->setupUi(this); - this->setFixedSize(this->width(), this->height()); this->project = project; + + ui->checkBox_CheckerboardFill->setChecked(porymapConfig.tilesetCheckerboardFill); + ui->label_SymbolNameDisplay->setText(this->symbolPrefix); + ui->comboBox_Type->setMinimumContentsLength(12); + //only allow characters valid for a symbol - static const QRegularExpression expression("[_A-Za-z0-9]+$"); // TODO: Incorrect, allows digits at beginning - QRegularExpressionValidator *validator = new QRegularExpressionValidator(expression); - this->ui->nameLineEdit->setValidator(validator); + static const QRegularExpression expression("[A-Za-z_]+[\\w]*"); + QRegularExpressionValidator *validator = new QRegularExpressionValidator(expression, this); + ui->lineEdit_FriendlyName->setValidator(validator); - bool checkerboard = porymapConfig.tilesetCheckerboardFill; - this->ui->fillCheckBox->setChecked(checkerboard); - this->checkerboardFill = checkerboard; + connect(ui->lineEdit_FriendlyName, &QLineEdit::textChanged, this, &NewTilesetDialog::onFriendlyNameChanged); + connect(ui->buttonBox, &QDialogButtonBox::clicked, this, &NewTilesetDialog::dialogButtonClicked); - connect(this->ui->nameLineEdit, &QLineEdit::textChanged, this, &NewTilesetDialog::NameOrSecondaryChanged); - connect(this->ui->typeComboBox, &QComboBox::currentTextChanged, this, &NewTilesetDialog::SecondaryChanged); - connect(this->ui->fillCheckBox, &QCheckBox::stateChanged, this, &NewTilesetDialog::FillChanged); - //connect(this->ui->toolButton, &QToolButton::clicked, this, &NewTilesetDialog::ChangeFilePath); - this->SecondaryChanged(); + adjustSize(); } NewTilesetDialog::~NewTilesetDialog() { + porymapConfig.tilesetCheckerboardFill = ui->checkBox_CheckerboardFill->isChecked(); delete ui; } -void NewTilesetDialog::SecondaryChanged(){ - this->isSecondary = (this->ui->typeComboBox->currentIndex() == 1); - NameOrSecondaryChanged(); +void NewTilesetDialog::onFriendlyNameChanged(const QString &friendlyName) { + // When the tileset name is changed, update this label to display the full symbol name. + ui->label_SymbolNameDisplay->setText(this->symbolPrefix + friendlyName); + + validateName(true); } -// TODO: No validation -void NewTilesetDialog::NameOrSecondaryChanged() { - this->friendlyName = this->ui->nameLineEdit->text(); - this->fullSymbolName = projectConfig.getIdentifier(ProjectIdentifier::symbol_tilesets_prefix) + this->friendlyName; - this->ui->symbolNameLineEdit->setText(this->fullSymbolName); - this->path = Tileset::getExpectedDir(this->fullSymbolName, this->isSecondary); - this->ui->pathLineEdit->setText(this->path); +bool NewTilesetDialog::validateName(bool allowEmpty) { + const QString friendlyName = ui->lineEdit_FriendlyName->text(); + const QString symbolName = ui->label_SymbolNameDisplay->text(); + + QString errorText; + if (friendlyName.isEmpty()) { + if (!allowEmpty) errorText = QString("%1 cannot be empty.").arg(ui->label_FriendlyName->text()); + } else if (!this->project->isIdentifierUnique(symbolName)) { + errorText = QString("%1 '%2' is not unique.").arg(ui->label_SymbolName->text()).arg(symbolName); + } + + bool isValid = errorText.isEmpty(); + ui->label_NameError->setText(errorText); + ui->label_NameError->setVisible(!isValid); + ui->lineEdit_FriendlyName->setStyleSheet(!isValid ? lineEdit_ErrorStylesheet : ""); + return isValid; } -void NewTilesetDialog::FillChanged() { - this->checkerboardFill = this->ui->fillCheckBox->isChecked(); - porymapConfig.tilesetCheckerboardFill = this->checkerboardFill; +void NewTilesetDialog::dialogButtonClicked(QAbstractButton *button) { + auto role = ui->buttonBox->buttonRole(button); + if (role == QDialogButtonBox::RejectRole){ + reject(); + } else if (role == QDialogButtonBox::AcceptRole) { + accept(); + } +} + +void NewTilesetDialog::accept() { + if (!validateName()) + return; + + bool secondary = ui->comboBox_Type->currentIndex() == 1; + Tileset *tileset = this->project->createNewTileset(ui->lineEdit_FriendlyName->text(), secondary, ui->checkBox_CheckerboardFill->isChecked()); + if (!tileset) { + ui->label_GenericError->setText(QString("Failed to create tileset. See %1 for details.").arg(getLogPath())); + ui->label_GenericError->setVisible(true); + return; + } + ui->label_GenericError->setVisible(false); + + QDialog::accept(); } diff --git a/src/ui/tileseteditor.cpp b/src/ui/tileseteditor.cpp index 940761ad..f3c4a1c9 100644 --- a/src/ui/tileseteditor.cpp +++ b/src/ui/tileseteditor.cpp @@ -716,22 +716,9 @@ void TilesetEditor::importTilesetTiles(Tileset *tileset, bool primary) { image = image.convertToFormat(QImage::Format::Format_Indexed8, colorTable); } - // Validate image is properly indexed to 16 colors. - int colorCount = image.colorCount(); - if (colorCount > 16) { - flattenTo4bppImage(&image); - } else if (colorCount < 16) { - QVector colorTable = image.colorTable(); - for (int i = colorTable.length(); i < 16; i++) { - colorTable.append(Qt::black); - } - image.setColorTable(colorTable); - } - - this->project->loadTilesetTiles(tileset, image); + tileset->loadTilesImage(&image); this->refresh(); this->hasUnsavedChanges = true; - tileset->hasUnsavedTilesImage = true; } void TilesetEditor::closeEvent(QCloseEvent *event)