Merge pull request #731 from GriffinRichards/soften-map-loading

Soften map/layout loading, add Back/Forward
This commit is contained in:
GriffinR 2025-05-11 13:48:34 -04:00 committed by GitHub
commit 2a7927ae82
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 587 additions and 401 deletions

View File

@ -2892,6 +2892,9 @@
<addaction name="actionZoom_In"/>
<addaction name="actionZoom_Out"/>
<addaction name="separator"/>
<addaction name="actionBack"/>
<addaction name="actionForward"/>
<addaction name="separator"/>
<addaction name="actionCursor_Tile_Outline"/>
<addaction name="actionPlayer_View_Rectangle"/>
<addaction name="actionBetter_Cursors"/>
@ -3248,6 +3251,22 @@
<string>Duplicate Current Map/Layout...</string>
</property>
</action>
<action name="actionBack">
<property name="text">
<string>Back</string>
</property>
<property name="shortcut">
<string>Alt+Left</string>
</property>
</action>
<action name="actionForward">
<property name="text">
<string>Forward</string>
</property>
<property name="shortcut">
<string>Alt+Right</string>
</property>
</action>
</widget>
<layoutdefault spacing="6" margin="11"/>
<customwidgets>

View File

@ -48,6 +48,9 @@ public:
void setLayout(Layout *layout);
Layout* layout() const { return m_layout; }
void setLayoutId(const QString &layoutId) { m_layoutId = layoutId; }
QString layoutId() const { return layout() ? layout()->id : m_layoutId; }
int getWidth() const;
int getHeight() const;
int getBorderWidth() const;
@ -110,6 +113,7 @@ public:
private:
QString m_name;
QString m_constantName;
QString m_layoutId; // Only needed if layout fails to load.
QString m_sharedEventsMap = "";
QString m_sharedScriptsMap = "";

View File

@ -122,6 +122,9 @@ public:
bool saveBorder(const QString &root);
bool saveBlockdata(const QString &root);
bool loadBorder(const QString &root);
bool loadBlockdata(const QString &root);
bool layoutBlockChanged(int i, const Blockdata &cache);
uint16_t getBorderMetatileId(int x, int y);
@ -147,6 +150,7 @@ private:
void setNewDimensionsBlockdata(int newWidth, int newHeight);
void setNewBorderDimensionsBlockdata(int newWidth, int newHeight);
bool writeBlockdata(const QString &path, const Blockdata &blockdata) const;
static Blockdata readBlockdata(const QString &path, QString *error);
static int getBorderDrawDistance(int dimension, qreal minimum);

View File

@ -331,6 +331,14 @@ private:
QAction *redoAction = nullptr;
QPointer<QUndoView> undoView = nullptr;
struct MapNavigation {
QStack<QString> stack;
QPointer<QToolButton> button;
};
MapNavigation backNavigation;
MapNavigation forwardNavigation;
bool ignoreNavigationRecords = false;
QAction *copyAction = nullptr;
QAction *pasteAction = nullptr;
@ -342,11 +350,11 @@ private:
bool tilesetNeedsRedraw = false;
bool setLayout(QString layoutId);
bool setMap(QString);
bool setLayout(const QString &layoutId);
bool setMap(const QString &mapName);
void unsetMap();
bool userSetLayout(QString layoutId);
bool userSetMap(QString);
bool userSetLayout(const QString &layoutId);
bool userSetMap(const QString &mapName);
void redrawMapScene();
void refreshMapScene();
void refreshMetatileViews();
@ -380,7 +388,6 @@ private:
bool closeProject();
void showRecentError(const QString &baseMessage);
void showProjectOpenFailure();
void showMapsExcludedAlert(const QStringList &excludedMapNames);
bool setInitialMap();
void saveGlobalConfigs();
@ -392,6 +399,11 @@ private:
void updateMapList();
void openMapListItem(const QModelIndex &index);
void onMapListTabChanged(int index);
QString getActiveItemName();
void recordNavigation(const QString &itemName);
void openMapFromHistory(bool previous);
void openPreviousMap();
void openNextMap();
void displayMapProperties();
void checkToolButtons();

View File

@ -31,17 +31,11 @@ public:
public:
QString root;
QStringList mapNames;
QStringList groupNames;
QMap<QString, QStringList> groupNameToMapNames;
QStringList healLocationSaveOrder;
QMap<QString, QList<HealLocationEvent*>> healLocations;
QMap<QString, QString> mapConstantsToMapNames;
QString layoutsLabel;
QStringList layoutIds;
QStringList layoutIdsMaster;
QMap<QString, Layout*> mapLayouts;
QMap<QString, Layout*> mapLayoutsMaster;
QMap<QString, int> gfxDefines;
QString defaultSong;
QStringList songNames;
@ -78,6 +72,27 @@ public:
void setRoot(const QString&);
const QStringList& mapNames() const { return this->alphabeticalMapNames; }
bool isKnownMap(const QString &mapName) const { return this->maps.contains(mapName); }
bool isErroredMap(const QString &mapName) const { return this->erroredMaps.contains(mapName); }
bool isLoadedMap(const QString &mapName) const { return this->loadedMapNames.contains(mapName); }
bool isUnsavedMap(const QString &mapName) const;
// Note: This does not guarantee the map is loaded.
Map* getMap(const QString &mapName) { return this->maps.value(mapName); }
Map* loadMap(const QString &mapName);
const QStringList& layoutIds() const { return this->alphabeticalLayoutIds; }
bool isKnownLayout(const QString &layoutId) const { return this->mapLayouts.contains(layoutId); }
bool isLoadedLayout(const QString &layoutId) const { return this->loadedLayoutIds.contains(layoutId); }
bool isUnsavedLayout(const QString &layoutId) const;
QString getLayoutName(const QString &layoutId) const;
QStringList getLayoutNames() const;
// Note: This does not guarantee the layout is loaded.
Layout* getLayout(const QString &layoutId) const { return this->mapLayouts.value(layoutId); }
Layout* loadLayout(const QString &layoutId);
void clearMaps();
void clearTilesetCache();
void clearMapLayouts();
@ -88,16 +103,6 @@ public:
int getSupportedMajorVersion(QString *errorOut = nullptr);
bool load();
Map* loadMap(const QString &mapName);
// Note: This does not guarantee the map is loaded.
Map* getMap(const QString &mapName) { return this->maps.value(mapName); }
bool isMapLoaded(const Map *map) const { return map && isMapLoaded(map->name()); }
bool isMapLoaded(const QString &mapName) const { return this->loadedMapNames.contains(mapName); }
bool isLayoutLoaded(const Layout *layout) const { return layout && isLayoutLoaded(layout->id); }
bool isLayoutLoaded(const QString &layoutId) const { return this->loadedLayoutIds.contains(layoutId); }
QMap<QString, Tileset*> tilesetCache;
Tileset* loadTileset(QString, Tileset *tileset = nullptr);
Tileset* getTileset(QString, bool forceLoad = false);
@ -105,10 +110,6 @@ public:
QStringList secondaryTilesetLabels;
QStringList tilesetLabelsOrdered;
Blockdata readBlockdata(QString, bool *ok = nullptr);
bool loadBlockdata(Layout *);
bool loadLayoutBorder(Layout *);
bool readMapGroups();
void addNewMapGroup(const QString &groupName);
QString mapNameToMapGroup(const QString &mapName) const;
@ -158,13 +159,9 @@ public:
bool hasUnsavedChanges();
bool hasUnsavedDataChanges = false;
bool readMapJson(const QString &mapName, QJsonDocument * out);
bool loadMapEvent(Map *map, QJsonObject json, Event::Type defaultType = Event::Type::None);
bool loadMapData(Map*);
bool readMapLayouts();
Layout *loadLayout(QString layoutId);
bool loadLayout(Layout *);
bool loadMapLayout(Map*);
bool loadLayoutTilesets(Layout *);
bool loadTilesetAssets(Tileset*);
void loadTilesetMetatileLabels(Tileset*);
@ -265,6 +262,14 @@ private:
QMap<QString, QString> facingDirections;
QHash<QString, QString> speciesToIconPath;
QHash<QString, Map*> maps;
QHash<QString, QString> erroredMaps;
QStringList alphabeticalMapNames;
QString layoutsLabel;
QStringList alphabeticalLayoutIds;
QStringList orderedLayoutIds;
QStringList orderedLayoutIdsMaster;
QHash<QString, Layout*> mapLayouts;
QHash<QString, Layout*> mapLayoutsMaster;
// Fields for preserving top-level JSON data that Porymap isn't expecting.
QJsonObject customLayoutsData;
@ -284,6 +289,10 @@ private:
QSet<QString> loadedMapNames;
QSet<QString> loadedLayoutIds;
// Data for layouts that failed to load at launch.
// We can't display these layouts to the user, but we want to preserve the data when they save.
QList<QJsonObject> failedLayoutsData;
const QRegularExpression re_gbapalExtension;
const QRegularExpression re_bppExtension;
@ -307,6 +316,8 @@ private:
};
QHash<QString, LocationData> locationData;
QJsonDocument readMapJson(const QString &mapName, QString *error = nullptr);
void setNewLayoutBlockdata(Layout *layout);
void setNewLayoutBorder(Layout *layout);
@ -349,7 +360,6 @@ signals:
void mapSectionAdded(const QString &idName);
void mapSectionDisplayNameChanged(const QString &idName, const QString &displayName);
void mapSectionIdNamesChanged(const QStringList &idNames);
void mapsExcluded(const QStringList &excludedMapNames);
void eventScriptLabelsRead();
};

View File

@ -84,6 +84,7 @@ protected:
QIcon mapGrayIcon;
QIcon mapIcon;
QIcon mapEditedIcon;
QIcon mapErroredIcon;
QIcon mapOpenedIcon;
QIcon mapFolderIcon;
QIcon emptyMapFolderIcon;

Binary file not shown.

After

Width:  |  Height:  |  Size: 606 B

View File

@ -29,6 +29,7 @@
<file>icons/link.ico</file>
<file>icons/magnifier.ico</file>
<file>icons/map_edited.ico</file>
<file>icons/map_errored.ico</file>
<file>icons/map_opened.ico</file>
<file>icons/map.ico</file>
<file>icons/map_grayed.ico</file>

View File

@ -56,6 +56,9 @@ void Map::setLayout(Layout *layout) {
if (layout == m_layout)
return;
m_layout = layout;
if (layout) {
m_layoutId = layout->id;
}
emit layoutChanged();
}
@ -65,19 +68,19 @@ QString Map::mapConstantFromName(const QString &name) {
}
int Map::getWidth() const {
return m_layout->getWidth();
return m_layout ? m_layout->getWidth() : 0;
}
int Map::getHeight() const {
return m_layout->getHeight();
return m_layout ? m_layout->getHeight() : 0;
}
int Map::getBorderWidth() const {
return m_layout->getBorderWidth();
return m_layout ? m_layout->getBorderWidth() : 0;
}
int Map::getBorderHeight() const {
return m_layout->getBorderHeight();
return m_layout ? m_layout->getBorderHeight() : 0;
}
// Get the portion of the map that can be rendered when rendered as a map connection.
@ -111,6 +114,9 @@ QRect Map::getConnectionRect(const QString &direction, Layout * fromLayout) cons
}
QPixmap Map::renderConnection(const QString &direction, Layout * fromLayout) {
if (!m_layout)
return QPixmap();
QRect bounds = getConnectionRect(direction, fromLayout);
if (!bounds.isValid())
return QPixmap();
@ -372,7 +378,7 @@ void Map::setClean() {
}
bool Map::hasUnsavedChanges() const {
return !m_editHistory->isClean() || m_layout->hasUnsavedChanges() || m_hasUnsavedDataChanges || !m_isPersistedToFile;
return !m_editHistory->isClean() || (m_layout && m_layout->hasUnsavedChanges()) || m_hasUnsavedDataChanges || !m_isPersistedToFile;
}
void Map::pruneEditHistory() {

View File

@ -509,7 +509,7 @@ bool Layout::saveBlockdata(const QString &root) {
bool Layout::writeBlockdata(const QString &path, const Blockdata &blockdata) const {
QFile file(path);
if (!file.open(QIODevice::WriteOnly)) {
logError(QString("Could not open '%1' for writing: %2").arg(path).arg(file.errorString()));
logError(QString("Failed to write '%1' for %2: %3").arg(path).arg(this->name).arg(file.errorString()));
return false;
}
@ -517,3 +517,94 @@ bool Layout::writeBlockdata(const QString &path, const Blockdata &blockdata) con
file.write(data);
return true;
}
bool Layout::loadBorder(const QString &root) {
if (this->border_path.isEmpty()) {
logError(QString("Failed to load border for %1: no path specified.").arg(this->name));
return false;
}
QString error;
QString path = QString("%1/%2").arg(root).arg(this->border_path);
auto blockdata = readBlockdata(path, &error);
if (!error.isEmpty()) {
logError(QString("Failed to load border for %1 from '%2': %3").arg(this->name).arg(path).arg(error));
return false;
}
// 0 is an expected border width/height that should be handled, GF used it for the RS layouts in FRLG
if (this->border_width <= 0) {
this->border_width = DEFAULT_BORDER_WIDTH;
}
if (this->border_height <= 0) {
this->border_height = DEFAULT_BORDER_HEIGHT;
}
this->border = blockdata;
this->lastCommitBlocks.border = blockdata;
this->lastCommitBlocks.borderDimensions = QSize(this->border_width, this->border_height);
int expectedSize = this->border_width * this->border_height;
if (this->border.count() != expectedSize) {
logWarn(QString("%1 border blockdata length %2 does not match dimensions %3x%4 (should be %5). Resizing border blockdata.")
.arg(this->name)
.arg(this->border.count())
.arg(this->border_width)
.arg(this->border_height)
.arg(expectedSize));
this->border.resize(expectedSize);
}
return true;
}
bool Layout::loadBlockdata(const QString &root) {
if (this->blockdata_path.isEmpty()) {
logError(QString("Failed to load blockdata for %1: no path specified.").arg(this->name));
return false;
}
QString error;
QString path = QString("%1/%2").arg(root).arg(this->blockdata_path);
auto blockdata = readBlockdata(path, &error);
if (!error.isEmpty()) {
logError(QString("Failed to load blockdata for %1 from '%2': %3").arg(this->name).arg(path).arg(error));
return false;
}
this->blockdata = blockdata;
this->lastCommitBlocks.blocks = blockdata;
this->lastCommitBlocks.layoutDimensions = QSize(this->width, this->height);
int expectedSize = this->width * this->height;
if (expectedSize <= 0) {
logError(QString("Failed to load blockdata for %1: invalid dimensions %2x%3").arg(this->name).arg(this->width).arg(this->height));
return false;
}
if (this->blockdata.count() != expectedSize) {
logWarn(QString("%1 blockdata length %2 does not match dimensions %3x%4 (should be %5). Resizing blockdata.")
.arg(this->name)
.arg(this->blockdata.count())
.arg(this->width)
.arg(this->height)
.arg(expectedSize));
this->blockdata.resize(expectedSize);
}
return true;
}
Blockdata Layout::readBlockdata(const QString &path, QString *error) {
Blockdata blockdata;
QFile file(path);
if (file.open(QIODevice::ReadOnly)) {
QByteArray data = file.readAll();
for (int i = 0; (i + 1) < data.length(); i += 2) {
uint16_t word = static_cast<uint16_t>((data[i] & 0xff) + ((data[i + 1] & 0xff) << 8));
blockdata.append(word);
}
} else {
if (error) *error = file.errorString();
}
return blockdata;
}

View File

@ -779,7 +779,7 @@ void Editor::displayConnection(MapConnection *connection) {
connect(pixmapItem, &ConnectionPixmapItem::positionChanged, this, &Editor::maskNonVisibleConnectionTiles);
// Create item for the list panel
auto listItem = new ConnectionsListItem(ui->scrollAreaContents_ConnectionsList, pixmapItem->connection, project->mapNames);
auto listItem = new ConnectionsListItem(ui->scrollAreaContents_ConnectionsList, pixmapItem->connection, project->mapNames());
ui->layout_ConnectionsList->insertWidget(ui->layout_ConnectionsList->count() - 1, listItem); // Insert above the vertical spacer
// Double clicking the pixmap or clicking the list item's map button opens the connected map
@ -939,7 +939,7 @@ void Editor::removeDivingMapPixmap(MapConnection *connection) {
}
bool Editor::setDivingMapName(const QString &mapName, const QString &direction) {
if (!mapName.isEmpty() && !this->project->mapNames.contains(mapName))
if (!mapName.isEmpty() && !this->project->isKnownMap(mapName))
return false;
if (!MapConnection::isDiving(direction))
return false;
@ -977,7 +977,7 @@ void Editor::onDivingMapEditingFinished(NoScrollComboBox *combo, const QString &
}
void Editor::updateDivingMapButton(QToolButton* button, const QString &mapName) {
if (this->project) button->setDisabled(!this->project->mapNames.contains(mapName));
if (this->project) button->setDisabled(!this->project->isKnownMap(mapName));
}
void Editor::updateDivingMapsVisibility() {
@ -1227,7 +1227,7 @@ bool Editor::setMap(QString map_name) {
unsetMap();
this->map = loadedMap;
setLayout(map->layout()->id);
setLayout(map->layoutId());
editGroup.addStack(map->editHistory());
editGroup.setActiveStack(map->editHistory());

View File

@ -432,25 +432,53 @@ void MainWindow::initMapList() {
QFrame *buttonFrame = new QFrame(this->ui->mapListContainer);
buttonFrame->setFrameShape(QFrame::NoFrame);
QHBoxLayout *layout = new QHBoxLayout(buttonFrame);
layout->setSpacing(0);
layout->setContentsMargins(0, 0, 0, 0);
QHBoxLayout *buttonLayout = new QHBoxLayout(buttonFrame);
buttonLayout->setSpacing(0);
buttonLayout->setContentsMargins(0, 0, 0, 0);
// Create add map/layout button
QPushButton *buttonAdd = new QPushButton(QIcon(":/icons/add.ico"), "");
buttonAdd->setToolTip("Create New Map");
buttonAdd->setToolTip("Create new map");
connect(buttonAdd, &QPushButton::clicked, this, &MainWindow::openNewMapDialog);
layout->addWidget(buttonAdd);
buttonLayout->addWidget(buttonAdd);
/* TODO: Remove button disabled, no current support for deleting maps/layouts
// Create remove map/layout button
QPushButton *buttonRemove = new QPushButton(QIcon(":/icons/delete.ico"), "");
connect(buttonRemove, &QPushButton::clicked, this, &MainWindow::deleteCurrentMapOrLayout);
layout->addWidget(buttonRemove);
buttonLayout->addWidget(buttonRemove);
*/
ui->mapListContainer->setCornerWidget(buttonFrame, Qt::TopRightCorner);
// Navigation arrows
auto navigationFrame = new QFrame(ui->mapListContainer);
navigationFrame->setFrameShape(QFrame::NoFrame);
auto navigationLayout = new QHBoxLayout(navigationFrame);
navigationLayout->setSpacing(0);
navigationLayout->setContentsMargins(0, 0, 0, 0);
auto backArrow = new QToolButton(navigationFrame);
backArrow->setArrowType(Qt::LeftArrow);
backArrow->setToolTip("Open previous map");
backArrow->setEnabled(false);
connect(backArrow, &QToolButton::clicked, this, &MainWindow::openPreviousMap);
connect(ui->actionBack, &QAction::triggered, this, &MainWindow::openPreviousMap);
navigationLayout->addWidget(backArrow);
this->backNavigation.button = backArrow;
auto forwardArrow = new QToolButton(navigationFrame);
forwardArrow->setArrowType(Qt::RightArrow);
forwardArrow->setToolTip("Open next map");
forwardArrow->setEnabled(false);
connect(forwardArrow, &QToolButton::clicked, this, &MainWindow::openNextMap);
connect(ui->actionForward, &QAction::triggered, this, &MainWindow::openNextMap);
navigationLayout->addWidget(forwardArrow);
this->forwardNavigation.button = forwardArrow;
ui->mapListContainer->setCornerWidget(navigationFrame, Qt::TopLeftCorner);
// Connect tool bars to lists
ui->mapListToolBar_Groups->setList(ui->mapList);
ui->mapListToolBar_Locations->setList(ui->locationList);
@ -708,7 +736,6 @@ bool MainWindow::openProject(QString dir, bool initial) {
connect(project, &Project::mapGroupAdded, this, &MainWindow::onNewMapGroupCreated);
connect(project, &Project::mapSectionAdded, this, &MainWindow::onNewMapSectionCreated);
connect(project, &Project::mapSectionDisplayNameChanged, this, &MainWindow::onMapSectionDisplayNameChanged);
connect(project, &Project::mapsExcluded, this, &MainWindow::showMapsExcludedAlert);
this->editor->setProject(project);
// Make sure project looks reasonable before attempting to load it
@ -826,19 +853,6 @@ void MainWindow::showProjectOpenFailure() {
RecentErrorMessage::show(QStringLiteral("There was an error opening the project."), this);
}
// Alert the user that one or more maps have been excluded while loading the project.
void MainWindow::showMapsExcludedAlert(const QStringList &excludedMapNames) {
auto msgBox = new RecentErrorMessage("", this);
msgBox->setAttribute(Qt::WA_DeleteOnClose);
if (excludedMapNames.length() == 1) {
msgBox->setText(QString("Failed to load map '%1'. Saving will exclude this map from your project.").arg(excludedMapNames.first()));
} else {
msgBox->setText(QStringLiteral("Failed to load the maps listed below. Saving will exclude these maps from your project."));
msgBox->setDetailedText(excludedMapNames.join("\n")); // Overwrites error details text, user will need to check the log.
}
msgBox->open();
}
bool MainWindow::isProjectOpen() {
return editor && editor->project;
}
@ -847,22 +861,22 @@ bool MainWindow::setInitialMap() {
porysplash->showMessage("Opening initial map");
const QString recent = userConfig.recentMapOrLayout;
if (editor->project->mapNames.contains(recent)) {
if (editor->project->isKnownMap(recent)) {
// User recently had a map open that still exists.
if (setMap(recent))
return true;
} else if (editor->project->layoutIds.contains(recent)) {
} else if (editor->project->isKnownLayout(recent)) {
// User recently had a layout open that still exists.
if (setLayout(recent))
return true;
}
// Failed to open recent map/layout, or no recent map/layout. Try opening maps then layouts sequentially.
for (const auto &name : editor->project->mapNames) {
for (const auto &name : editor->project->mapNames()) {
if (name != recent && setMap(name))
return true;
}
for (const auto &id : editor->project->layoutIds) {
for (const auto &id : editor->project->layoutIds()) {
if (id != recent && setLayout(id))
return true;
}
@ -992,41 +1006,98 @@ void MainWindow::unsetMap() {
setLayoutOnlyMode(true);
}
void MainWindow::openPreviousMap() {
openMapFromHistory(true);
}
void MainWindow::openNextMap() {
openMapFromHistory(false);
}
// Either open a map/layout from the 'Back' list and put it in the 'Forward' list (i.e., previous == true) or vice versa.
void MainWindow::openMapFromHistory(bool previous) {
if (!this->editor->project)
return;
MapNavigation* popNavigation = (previous) ? &this->backNavigation : &this->forwardNavigation;
MapNavigation* pushNavigation = (previous) ? &this->forwardNavigation : &this->backNavigation;
if (popNavigation->stack.isEmpty())
return;
QString incomingItem = popNavigation->stack.top();
QString outgoingItem = getActiveItemName();
this->ignoreNavigationRecords = true;
bool success = false;
if (this->editor->project->isKnownMap(incomingItem)) {
success = userSetMap(incomingItem);
} else if (this->editor->project->isKnownLayout(incomingItem)) {
success = userSetLayout(incomingItem);
}
if (success) {
// We were successful in opening the map/layout, so we can remove it from the history.
popNavigation->stack.pop();
if (popNavigation->stack.isEmpty()) {
popNavigation->button->setEnabled(false);
}
// Save the map/layout that was previously open.
pushNavigation->stack.push(outgoingItem);
pushNavigation->button->setEnabled(true);
}
this->ignoreNavigationRecords = false;
}
void MainWindow::recordNavigation(const QString &itemName) {
if (this->ignoreNavigationRecords)
return;
this->backNavigation.stack.push(itemName);
this->backNavigation.button->setEnabled(true);
this->forwardNavigation.stack.clear();
this->forwardNavigation.button->setEnabled(false);
}
// setMap, but with a visible error message in case of failure.
// Use when the user is specifically requesting a map to open.
bool MainWindow::userSetMap(QString map_name) {
if (editor->map && editor->map->name() == map_name)
return true; // Already set
if (map_name.isEmpty()) {
bool MainWindow::userSetMap(const QString &mapName) {
if (mapName.isEmpty()) {
WarningMessage::show(QStringLiteral("Cannot open map with empty name."), this);
return false;
}
if (map_name == editor->project->getDynamicMapName()) {
auto msgBox = new WarningMessage(QString("Cannot open map '%1'.").arg(map_name), this);
if (mapName == editor->project->getDynamicMapName()) {
auto msgBox = new WarningMessage(QString("Cannot open map '%1'.").arg(mapName), this);
msgBox->setAttribute(Qt::WA_DeleteOnClose);
msgBox->setInformativeText(QStringLiteral("This map name is a placeholder to indicate that the warp's map will be set programmatically."));
msgBox->open();
return false;
}
if (!setMap(map_name)) {
RecentErrorMessage::show(QString("There was an error opening map '%1'.").arg(map_name), this);
QString prevItem = getActiveItemName();
if (prevItem == mapName)
return true; // Already set
if (!setMap(mapName)) {
RecentErrorMessage::show(QString("There was an error opening map '%1'.").arg(mapName), this);
return false;
}
recordNavigation(prevItem);
return true;
}
bool MainWindow::setMap(QString map_name) {
if (!editor || !editor->project || map_name.isEmpty() || map_name == editor->project->getDynamicMapName()) {
logWarn(QString("Ignored setting map to '%1'").arg(map_name));
bool MainWindow::setMap(const QString &mapName) {
if (!editor || !editor->project || mapName.isEmpty() || mapName == editor->project->getDynamicMapName()) {
logWarn(QString("Ignored setting map to '%1'").arg(mapName));
return false;
}
logInfo(QString("Setting map to '%1'").arg(map_name));
if (!editor->setMap(map_name)) {
logWarn(QString("Failed to set map to '%1'").arg(map_name));
logInfo(QString("Setting map to '%1'").arg(mapName));
if (!editor->setMap(mapName)) {
logWarn(QString("Failed to set map to '%1'").arg(mapName));
return false;
}
@ -1046,9 +1117,9 @@ bool MainWindow::setMap(QString map_name) {
connect(editor->layout, &Layout::needsRedrawing, this, &MainWindow::redrawMapScene, Qt::UniqueConnection);
userConfig.recentMapOrLayout = map_name;
userConfig.recentMapOrLayout = mapName;
Scripting::cb_MapOpened(map_name);
Scripting::cb_MapOpened(mapName);
prefab.updatePrefabUi(editor->layout);
updateTilesetEditor();
@ -1079,22 +1150,33 @@ 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.
bool MainWindow::userSetLayout(QString layoutId) {
bool MainWindow::userSetLayout(const QString &layoutId) {
if (layoutId.isEmpty()) {
WarningMessage::show(QStringLiteral("Cannot open layout with empty ID."), this);
return false;
}
QString prevItem = getActiveItemName();
if (prevItem == layoutId)
return true; // Already set
if (!setLayout(layoutId)) {
RecentErrorMessage::show(QString("There was an error opening layout '%1'.").arg(layoutId), this);
return false;
}
recordNavigation(prevItem);
// Only the Layouts tab of the map list shows Layouts, so if we're not already on that tab we'll open it now.
ui->mapListContainer->setCurrentIndex(MapListTab::Layouts);
return true;
}
bool MainWindow::setLayout(QString layoutId) {
bool MainWindow::setLayout(const QString &layoutId) {
// Prefer logging the name of the layout as displayed in the map list.
const Layout* layout = this->editor->project ? this->editor->project->mapLayouts.value(layoutId) : nullptr;
logInfo(QString("Setting layout to '%1'").arg(layout ? layout->name : layoutId));
QString layoutName = this->editor->project ? this->editor->project->getLayoutName(layoutId) : QString();
logInfo(QString("Setting layout to '%1'").arg(layoutName.isEmpty() ? layoutId : layoutName));
if (!this->editor->setLayout(layoutId)) {
return false;
@ -1233,7 +1315,7 @@ void MainWindow::on_comboBox_LayoutSelector_currentTextChanged(const QString &te
if (!this->editor || !this->editor->project || !this->editor->map)
return;
if (!this->editor->project->mapLayouts.contains(text)) {
if (!this->editor->project->isKnownLayout(text)) {
// User may be in the middle of typing the name of a layout, don't bother trying to load it.
return;
}
@ -1244,7 +1326,7 @@ void MainWindow::on_comboBox_LayoutSelector_currentTextChanged(const QString &te
// New layout failed to load, restore previous layout
const QSignalBlocker b(ui->comboBox_LayoutSelector);
ui->comboBox_LayoutSelector->setTextItem(this->editor->map->layout()->id);
ui->comboBox_LayoutSelector->setTextItem(this->editor->map->layoutId());
return;
}
this->editor->map->setLayout(layout);
@ -1258,7 +1340,7 @@ void MainWindow::onLayoutSelectorEditingFinished() {
// If the user left the layout selector in an invalid state, restore it so that it displays the current layout.
const QString text = ui->comboBox_LayoutSelector->currentText();
if (!this->editor->project->mapLayouts.contains(text)) {
if (!this->editor->project->isKnownLayout(text)) {
const QSignalBlocker b(ui->comboBox_LayoutSelector);
ui->comboBox_LayoutSelector->setTextItem(this->editor->layout->id);
}
@ -1283,17 +1365,17 @@ bool MainWindow::setProjectUI() {
const QSignalBlocker b_LayoutSelector(ui->comboBox_LayoutSelector);
ui->comboBox_LayoutSelector->clear();
ui->comboBox_LayoutSelector->addItems(project->layoutIds);
ui->comboBox_LayoutSelector->addItems(project->layoutIds());
const QSignalBlocker b_DiveMap(ui->comboBox_DiveMap);
ui->comboBox_DiveMap->clear();
ui->comboBox_DiveMap->addItems(project->mapNames);
ui->comboBox_DiveMap->addItems(project->mapNames());
ui->comboBox_DiveMap->setClearButtonEnabled(true);
ui->comboBox_DiveMap->setFocusedScrollingEnabled(false);
const QSignalBlocker b_EmergeMap(ui->comboBox_EmergeMap);
ui->comboBox_EmergeMap->clear();
ui->comboBox_EmergeMap->addItems(project->mapNames);
ui->comboBox_EmergeMap->addItems(project->mapNames());
ui->comboBox_EmergeMap->setClearButtonEnabled(true);
ui->comboBox_EmergeMap->setFocusedScrollingEnabled(false);
@ -1526,11 +1608,11 @@ void MainWindow::onNewMapCreated(Map *newMap, const QString &groupName) {
// Add new map to the map lists
this->mapGroupModel->insertMapItem(newMap->name(), groupName);
this->mapLocationModel->insertMapItem(newMap->name(), newMap->header()->location());
this->layoutTreeModel->insertMapItem(newMap->name(), newMap->layout()->id);
this->layoutTreeModel->insertMapItem(newMap->name(), newMap->layoutId());
// Refresh any combo box that displays map names and persists between maps
// (other combo boxes like for warp destinations are repopulated when the map changes).
int mapIndex = this->editor->project->mapNames.indexOf(newMap->name());
int mapIndex = this->editor->project->mapNames().indexOf(newMap->name());
if (mapIndex >= 0) {
ui->comboBox_DiveMap->insertItem(mapIndex, newMap->name());
ui->comboBox_EmergeMap->insertItem(mapIndex, newMap->name());
@ -1544,7 +1626,7 @@ void MainWindow::onNewLayoutCreated(Layout *layout) {
logInfo(QString("Created a new layout named %1.").arg(layout->name));
// Refresh layout combo box
int layoutIndex = this->editor->project->layoutIds.indexOf(layout->id);
int layoutIndex = this->editor->project->layoutIds().indexOf(layout->id);
if (layoutIndex >= 0) {
const QSignalBlocker b(ui->comboBox_LayoutSelector);
ui->comboBox_LayoutSelector->insertItem(layoutIndex, layout->id);
@ -1754,20 +1836,22 @@ void MainWindow::rebuildMapList_Layouts() {
resetMapListFilters();
}
QString MainWindow::getActiveItemName() {
if (this->editor->map) return this->editor->map->name();
if (this->editor->layout) return this->editor->layout->id;
return QString();
}
void MainWindow::updateMapList() {
// Get the name of the open map/layout (or clear the relevant selection if there is none).
QString activeItemName;
if (this->editor->map) {
activeItemName = this->editor->map->name();
} else {
QString activeItemName = getActiveItemName();
// Clear relevant selections
if (!this->editor->map) {
ui->mapList->clearSelection();
ui->locationList->clearSelection();
if (this->editor->layout) {
activeItemName = this->editor->layout->id;
} else {
ui->layoutList->clearSelection();
}
}
if (!this->editor->layout) {
ui->layoutList->clearSelection();
}
this->mapGroupModel->setActiveItem(activeItemName);
@ -2749,7 +2833,7 @@ void MainWindow::on_pushButton_AddConnection_clicked() {
if (!this->editor || !this->editor->map || !this->editor->project)
return;
auto dialog = new NewMapConnectionDialog(this, this->editor->map, this->editor->project->mapNames);
auto dialog = new NewMapConnectionDialog(this, this->editor->map, this->editor->project->mapNames());
connect(dialog, &NewMapConnectionDialog::newConnectionedAdded, this->editor, &Editor::addNewConnection);
connect(dialog, &NewMapConnectionDialog::connectionReplaced, this->editor, &Editor::replaceConnection);
dialog->open();

View File

@ -280,24 +280,61 @@ Map* Project::loadMap(const QString &mapName) {
return nullptr;
}
// Some maps are ignored while opening the project because they have invalid or incomplete data.
// We already logged a warning about this, but now that we're trying to load the map it's an error.
auto it = this->erroredMaps.constFind(mapName);
if (it != this->erroredMaps.constEnd()) {
logError(it.value());
return nullptr;
}
Map* map = this->maps.value(mapName);
if (!map) {
logError(QString("Unknown map name '%1'.").arg(mapName));
return nullptr;
}
if (isMapLoaded(map))
if (isLoadedMap(mapName))
return map;
if (!(loadMapData(map) && loadMapLayout(map)))
if (!loadMapData(map))
return nullptr;
// Load map layout
if (map->isPersistedToFile() && !map->hasUnsavedChanges()) {
if (!loadLayout(map->layoutId()))
return nullptr;
}
this->loadedMapNames.insert(mapName);
emit mapLoaded(map);
return map;
}
Layout *Project::loadLayout(const QString &layoutId) {
Layout *layout = this->mapLayouts.value(layoutId);
if (!layout) {
logError(QString("Unknown layout ID '%1'.").arg(layoutId));
return nullptr;
}
if (isLoadedLayout(layoutId))
return layout;
// Force these to run even if one fails
bool loadedTilesets = loadLayoutTilesets(layout);
bool loadedBlockdata = layout->loadBlockdata(this->root);
bool loadedBorder = layout->loadBorder(this->root);
if (!loadedTilesets || !loadedBlockdata || !loadedBorder) {
// Error should already be logged.
return nullptr;
}
this->loadedLayoutIds.insert(layoutId);
return layout;
}
QSet<QString> Project::getTopLevelMapFields() const {
QSet<QString> fields = {
"id",
@ -329,15 +366,17 @@ QSet<QString> Project::getTopLevelMapFields() const {
return fields;
}
bool Project::readMapJson(const QString &mapName, QJsonDocument * out) {
QJsonDocument Project::readMapJson(const QString &mapName, QString *error) {
const QString mapFilepath = Map::getJsonFilepath(mapName);
watchFile(mapFilepath);
QString error;
if (!parser.tryParseJsonFile(out, mapFilepath, &error)) {
logError(QString("Failed to read map data from '%1': %2").arg(mapFilepath).arg(error));
return false;
QJsonDocument doc;
if (!parser.tryParseJsonFile(&doc, mapFilepath, error)) {
if (error) {
error->prepend(QString("Failed to read map data from '%1': ").arg(mapFilepath));
}
}
return true;
return doc;
}
bool Project::loadMapEvent(Map *map, QJsonObject json, Event::Type defaultType) {
@ -360,9 +399,12 @@ bool Project::loadMapData(Map* map) {
return true;
}
QJsonDocument mapDoc;
if (!readMapJson(map->name(), &mapDoc))
QString error;
QJsonDocument mapDoc = readMapJson(map->name(), &error);
if (!error.isEmpty()) {
logError(error);
return false;
}
QJsonObject mapObj = mapDoc.object();
@ -452,24 +494,15 @@ Map *Project::createNewMap(const Project::NewMapSettings &settings, const Map* t
// Generate a unique MAP constant.
map->setConstantName(toUniqueIdentifier(map->expectedConstantName()));
// Make sure we keep the order of the map names the same as in the map group order.
int mapNamePos;
if (this->groupNames.contains(settings.group)) {
mapNamePos = 0;
for (const auto &name : this->groupNames) {
mapNamePos += this->groupNameToMapNames[name].length();
if (name == settings.group)
break;
}
} else if (isValidNewIdentifier(settings.group)) {
if (!this->groupNames.contains(settings.group)) {
// Adding map to a map group that doesn't exist yet.
// Create the group, and we already know the map will be last in the list.
addNewMapGroup(settings.group);
mapNamePos = this->mapNames.length();
} else {
logError(QString("Cannot create new map with invalid map group name '%1'.").arg(settings.group));
delete map;
return nullptr;
if (isValidNewIdentifier(settings.group)) {
addNewMapGroup(settings.group);
} else {
logError(QString("Cannot create new map with invalid map group name '%1'.").arg(settings.group));
delete map;
return nullptr;
}
}
Layout *layout = this->mapLayouts.value(settings.layout.id);
@ -483,16 +516,20 @@ Map *Project::createNewMap(const Project::NewMapSettings &settings, const Map* t
}
} else {
// This layout already exists. Make sure it's loaded.
loadLayout(layout);
if (!loadLayout(settings.layout.id)) {
// Layout failed to load. For now we can just record the ID.
map->setLayoutId(settings.layout.id);
}
}
map->setLayout(layout);
// Try to record the MAPSEC name in case this is a new name.
addNewMapsec(map->header()->location());
this->mapNames.insert(mapNamePos, map->name());
this->groupNameToMapNames[settings.group].append(map->name());
this->mapConstantsToMapNames.insert(map->constantName(), map->name());
this->alphabeticalMapNames.append(map->name());
Util::numericalModeSort(this->alphabeticalMapNames);
map->setIsPersistedToFile(false);
this->maps.insert(map->name(), map);
@ -503,7 +540,7 @@ Map *Project::createNewMap(const Project::NewMapSettings &settings, const Map* t
}
Layout *Project::createNewLayout(const Layout::Settings &settings, const Layout *toDuplicate) {
if (this->layoutIds.contains(settings.id))
if (this->mapLayouts.contains(settings.id))
return nullptr;
Layout *layout = toDuplicate ? new Layout(*toDuplicate) : new Layout();
@ -540,62 +577,27 @@ Layout *Project::createNewLayout(const Layout::Settings &settings, const Layout
}
this->mapLayouts.insert(layout->id, layout);
this->layoutIds.append(layout->id);
this->orderedLayoutIds.append(layout->id);
this->loadedLayoutIds.insert(layout->id);
this->alphabeticalLayoutIds.append(layout->id);
Util::numericalModeSort(this->alphabeticalLayoutIds);
emit layoutCreated(layout);
return layout;
}
bool Project::loadLayout(Layout *layout) {
if (!isLayoutLoaded(layout)) {
// Force these to run even if one fails
bool loadedTilesets = loadLayoutTilesets(layout);
bool loadedBlockdata = loadBlockdata(layout);
bool loadedBorder = loadLayoutBorder(layout);
if (loadedTilesets && loadedBlockdata && loadedBorder) {
this->loadedLayoutIds.insert(layout->id);
return true;
} else {
return false;
}
}
return true;
}
Layout *Project::loadLayout(QString layoutId) {
Layout *layout = this->mapLayouts.value(layoutId);
if (!layout) {
logError(QString("Unknown layout ID '%1'.").arg(layoutId));
return nullptr;
}
if (!loadLayout(layout)) {
// Error should already be logged.
return nullptr;
}
return layout;
}
bool Project::loadMapLayout(Map* map) {
if (!map->isPersistedToFile() || map->hasUnsavedChanges()) {
return true;
} else {
return loadLayout(map->layout());
}
}
void Project::clearMapLayouts() {
qDeleteAll(this->mapLayouts);
this->mapLayouts.clear();
qDeleteAll(this->mapLayoutsMaster);
this->mapLayoutsMaster.clear();
this->layoutIds.clear();
this->layoutIdsMaster.clear();
this->alphabeticalLayoutIds.clear();
this->orderedLayoutIds.clear();
this->orderedLayoutIdsMaster.clear();
this->loadedLayoutIds.clear();
this->customLayoutsData = QJsonObject();
this->failedLayoutsData.clear();
}
bool Project::readMapLayouts() {
@ -625,83 +627,49 @@ bool Project::readMapLayouts() {
QJsonObject layoutObj = layouts[i].toObject();
if (layoutObj.isEmpty())
continue;
Layout *layout = new Layout();
QScopedPointer<Layout> layout(new Layout());
layout->id = ParseUtil::jsonToQString(layoutObj.take("id"));
if (layout->id.isEmpty()) {
logError(QString("Missing 'id' value on layout %1 in %2").arg(i).arg(layoutsFilepath));
delete layout;
return false;
// Use name to identify it in the warning, if available.
QString name = ParseUtil::jsonToQString(layoutObj["name"]);
if (name.isEmpty()) name = QString("Layout %1 (unnamed)").arg(i);
logWarn(QString("Missing 'id' value for %1 in %2").arg(name).arg(layoutsFilepath));
this->failedLayoutsData.append(layouts[i].toObject());
continue;
}
if (mapLayouts.contains(layout->id)) {
if (this->mapLayouts.contains(layout->id)) {
logWarn(QString("Duplicate layout entry for %1 in %2 will be ignored.").arg(layout->id).arg(layoutsFilepath));
delete layout;
this->failedLayoutsData.append(layouts[i].toObject());
continue;
}
layout->name = ParseUtil::jsonToQString(layoutObj.take("name"));
if (layout->name.isEmpty()) {
logError(QString("Missing 'name' value for %1 in %2").arg(layout->id).arg(layoutsFilepath));
delete layout;
return false;
logWarn(QString("Missing 'name' value for %1 in %2").arg(layout->id).arg(layoutsFilepath));
this->failedLayoutsData.append(layouts[i].toObject());
continue;
}
int lwidth = ParseUtil::jsonToInt(layoutObj.take("width"));
if (lwidth <= 0) {
logError(QString("Invalid 'width' value '%1' for %2 in %3. Must be greater than 0.").arg(lwidth).arg(layout->id).arg(layoutsFilepath));
delete layout;
return false;
}
layout->width = lwidth;
int lheight = ParseUtil::jsonToInt(layoutObj.take("height"));
if (lheight <= 0) {
logError(QString("Invalid 'height' value '%1' for %2 in %3. Must be greater than 0.").arg(lheight).arg(layout->id).arg(layoutsFilepath));
delete layout;
return false;
}
layout->height = lheight;
layout->width = ParseUtil::jsonToInt(layoutObj.take("width"));
layout->height = ParseUtil::jsonToInt(layoutObj.take("height"));
if (projectConfig.useCustomBorderSize) {
int bwidth = ParseUtil::jsonToInt(layoutObj.take("border_width"));
if (bwidth <= 0) { // 0 is an expected border width/height that should be handled, GF used it for the RS layouts in FRLG
bwidth = DEFAULT_BORDER_WIDTH;
}
layout->border_width = bwidth;
int bheight = ParseUtil::jsonToInt(layoutObj.take("border_height"));
if (bheight <= 0) {
bheight = DEFAULT_BORDER_HEIGHT;
}
layout->border_height = bheight;
layout->border_width = ParseUtil::jsonToInt(layoutObj.take("border_width"));
layout->border_height = ParseUtil::jsonToInt(layoutObj.take("border_height"));
} else {
layout->border_width = DEFAULT_BORDER_WIDTH;
layout->border_height = DEFAULT_BORDER_HEIGHT;
}
layout->tileset_primary_label = ParseUtil::jsonToQString(layoutObj.take("primary_tileset"));
if (layout->tileset_primary_label.isEmpty()) {
logError(QString("Missing 'primary_tileset' value for %1 in %2").arg(layout->id).arg(layoutsFilepath));
delete layout;
return false;
}
layout->tileset_secondary_label = ParseUtil::jsonToQString(layoutObj.take("secondary_tileset"));
if (layout->tileset_secondary_label.isEmpty()) {
logError(QString("Missing 'secondary_tileset' value for %1 in %2").arg(layout->id).arg(layoutsFilepath));
delete layout;
return false;
}
layout->border_path = ParseUtil::jsonToQString(layoutObj.take("border_filepath"));
if (layout->border_path.isEmpty()) {
logError(QString("Missing 'border_filepath' value for %1 in %2").arg(layout->id).arg(layoutsFilepath));
delete layout;
return false;
}
layout->blockdata_path = ParseUtil::jsonToQString(layoutObj.take("blockdata_filepath"));
if (layout->blockdata_path.isEmpty()) {
logError(QString("Missing 'blockdata_filepath' value for %1 in %2").arg(layout->id).arg(layoutsFilepath));
delete layout;
return false;
}
layout->customData = layoutObj;
this->mapLayouts.insert(layout->id, layout);
this->mapLayouts.insert(layout->id, layout->copy());
this->mapLayoutsMaster.insert(layout->id, layout->copy());
this->layoutIds.append(layout->id);
this->layoutIdsMaster.append(layout->id);
this->orderedLayoutIds.append(layout->id);
this->orderedLayoutIdsMaster.append(layout->id);
}
if (this->mapLayouts.isEmpty()) {
@ -709,6 +677,9 @@ bool Project::readMapLayouts() {
return false;
}
this->alphabeticalLayoutIds = this->orderedLayoutIds;
Util::numericalModeSort(this->alphabeticalLayoutIds);
this->customLayoutsData = layoutsObj;
return true;
@ -726,7 +697,7 @@ bool Project::saveMapLayouts() {
layoutsObj["layouts_table_label"] = this->layoutsLabel;
OrderedJson::array layoutsArr;
for (const QString &layoutId : this->layoutIdsMaster) {
for (const QString &layoutId : this->orderedLayoutIdsMaster) {
Layout *layout = this->mapLayoutsMaster.value(layoutId);
OrderedJson::object layoutObj;
layoutObj["id"] = layout->id;
@ -744,6 +715,22 @@ bool Project::saveMapLayouts() {
OrderedJson::append(&layoutObj, layout->customData);
layoutsArr.push_back(layoutObj);
}
// Append any layouts that were hidden because we failed to load them at launch.
// We do a little extra work to keep the field order the same as successfully-loaded layouts.
for (QJsonObject failedData : this->failedLayoutsData) {
OrderedJson::object layoutObj;
static const QStringList expectedFields = {
"id", "name", "width", "height", "border_width", "border_height",
"primary_tileset", "secondary_tileset", "border_filepath", "blockdata_filepath"
};
for (const auto &field : expectedFields) {
if (failedData.contains(field)) {
layoutObj[field] = OrderedJson::fromQJsonValue(failedData.take(field));
}
}
OrderedJson::append(&layoutObj, failedData);
layoutsArr.push_back(layoutObj);
}
layoutsObj["layouts"] = layoutsArr;
OrderedJson::append(&layoutsObj, this->customLayoutsData);
@ -1107,6 +1094,18 @@ bool Project::saveTilesetMetatileLabels(Tileset *primaryTileset, Tileset *second
}
bool Project::loadLayoutTilesets(Layout *layout) {
// Note: Do not replace invalid tileset labels with the default tileset labels here.
// Changing the tilesets like this can require us to load tilesets unnecessarily
// in order to avoid strange behavior (e.g. tile/metatile usage counts changing).
if (layout->tileset_primary_label.isEmpty()) {
logError(QString("Failed to load %1: missing primary tileset label.").arg(layout->name));
return false;
}
if (layout->tileset_secondary_label.isEmpty()) {
logError(QString("Failed to load %1: missing secondary tileset label.").arg(layout->name));
return false;
}
layout->tileset_primary = getTileset(layout->tileset_primary_label);
layout->tileset_secondary = getTileset(layout->tileset_secondary_label);
return layout->tileset_primary && layout->tileset_secondary;
@ -1161,31 +1160,6 @@ Tileset* Project::loadTileset(QString label, Tileset *tileset) {
return tileset;
}
bool Project::loadBlockdata(Layout *layout) {
bool ok = true;
QString path = QString("%1/%2").arg(root).arg(layout->blockdata_path);
auto blockdata = readBlockdata(path, &ok);
if (!ok) {
logError(QString("Failed to load layout blockdata from '%1'").arg(path));
return false;
}
layout->blockdata = blockdata;
layout->lastCommitBlocks.blocks = blockdata;
layout->lastCommitBlocks.layoutDimensions = QSize(layout->getWidth(), layout->getHeight());
if (layout->blockdata.count() != layout->getWidth() * layout->getHeight()) {
logWarn(QString("%1 blockdata length %2 does not match dimensions %3x%4 (should be %5). Resizing blockdata.")
.arg(layout->name)
.arg(layout->blockdata.count())
.arg(layout->getWidth())
.arg(layout->getHeight())
.arg(layout->getWidth() * layout->getHeight()));
layout->blockdata.resize(layout->getWidth() * layout->getHeight());
}
return true;
}
void Project::setNewLayoutBlockdata(Layout *layout) {
layout->blockdata.clear();
int width = layout->getWidth();
@ -1198,30 +1172,6 @@ void Project::setNewLayoutBlockdata(Layout *layout) {
layout->lastCommitBlocks.layoutDimensions = QSize(width, height);
}
bool Project::loadLayoutBorder(Layout *layout) {
bool ok = true;
QString path = QString("%1/%2").arg(root).arg(layout->border_path);
auto blockdata = readBlockdata(path, &ok);
if (!ok) {
logError(QString("Failed to load layout border from '%1'").arg(path));
return false;
}
layout->border = blockdata;
layout->lastCommitBlocks.border = blockdata;
layout->lastCommitBlocks.borderDimensions = QSize(layout->getBorderWidth(), layout->getBorderHeight());
int borderLength = layout->getBorderWidth() * layout->getBorderHeight();
if (layout->border.count() != borderLength) {
logWarn(QString("%1 border blockdata length %2 must be %3. Resizing border blockdata.")
.arg(layout->name)
.arg(layout->border.count())
.arg(borderLength));
layout->border.resize(borderLength);
}
return true;
}
void Project::setNewLayoutBorder(Layout *layout) {
layout->border.clear();
int width = layout->getBorderWidth();
@ -1259,7 +1209,7 @@ bool Project::saveAll() {
}
bool Project::saveMap(Map *map, bool skipLayout) {
if (!map || !isMapLoaded(map)) return true;
if (!map || !isLoadedMap(map->name())) return true;
// Create/Modify a few collateral files for brand new maps.
const QString folderPath = projectConfig.getFilePath(ProjectFilePath::data_map_folders) + map->name();
@ -1299,7 +1249,7 @@ bool Project::saveMap(Map *map, bool skipLayout) {
// Header values.
mapObj["id"] = map->constantName();
mapObj["name"] = map->name();
mapObj["layout"] = map->layout()->id;
mapObj["layout"] = map->layoutId();
mapObj["music"] = map->header()->song();
mapObj["region_map_section"] = map->header()->location();
mapObj["requires_flash"] = map->header()->requiresFlash();
@ -1400,15 +1350,15 @@ bool Project::saveMap(Map *map, bool skipLayout) {
}
bool Project::saveLayout(Layout *layout) {
if (!layout || !isLayoutLoaded(layout))
if (!layout || !isLoadedLayout(layout->id))
return true;
if (!layout->save(this->root))
return false;
// Update global data structures with current map data.
if (!this->layoutIdsMaster.contains(layout->id)) {
this->layoutIdsMaster.append(layout->id);
if (!this->orderedLayoutIdsMaster.contains(layout->id)) {
this->orderedLayoutIdsMaster.append(layout->id);
}
if (this->mapLayoutsMaster.contains(layout->id)) {
@ -1662,24 +1612,6 @@ void Project::loadTilesetMetatileLabels(Tileset* tileset) {
}
}
Blockdata Project::readBlockdata(QString path, bool *ok) {
Blockdata blockdata;
QFile file(path);
if (file.open(QIODevice::ReadOnly)) {
QByteArray data = file.readAll();
for (int i = 0; (i + 1) < data.length(); i += 2) {
uint16_t word = static_cast<uint16_t>((data[i] & 0xff) + ((data[i + 1] & 0xff) << 8));
blockdata.append(word);
}
if (ok) *ok = true;
} else {
// Failed
if (ok) *ok = false;
}
return blockdata;
}
Tileset* Project::getTileset(QString label, bool forceLoad) {
Tileset *existingTileset = nullptr;
if (tilesetCache.contains(label)) {
@ -1906,7 +1838,7 @@ bool Project::readWildMonData() {
bool Project::readMapGroups() {
clearMaps();
this->mapConstantsToMapNames.clear();
this->mapNames.clear();
this->alphabeticalMapNames.clear();
this->groupNames.clear();
this->groupNameToMapNames.clear();
this->customMapGroupsData = QJsonObject();
@ -1927,7 +1859,6 @@ bool Project::readMapGroups() {
const QString dynamicMapConstant = getDynamicMapDefineName();
// Process the map group lists
QStringList failedMapNames;
for (int groupIndex = 0; groupIndex < mapGroupOrder.size(); groupIndex++) {
const QString groupName = ParseUtil::jsonToQString(mapGroupOrder.at(groupIndex));
if (this->groupNames.contains(groupName)) {
@ -1941,87 +1872,89 @@ bool Project::readMapGroups() {
// Process the names in this map group
for (int j = 0; j < mapNamesJson.size(); j++) {
const QString mapName = ParseUtil::jsonToQString(mapNamesJson.at(j));
if (mapName == dynamicMapName) {
logWarn(QString("Ignoring map with reserved name '%1'.").arg(mapName));
failedMapNames.append(mapName);
if (mapName.isEmpty()) {
logWarn(QString("Ignoring empty map %1 in map group '%2'.").arg(j).arg(groupName).arg(mapName));
continue;
}
if (this->mapNames.contains(mapName)) {
logWarn(QString("Ignoring repeated map name '%1'.").arg(mapName));
failedMapNames.append(mapName);
// We explicitly hide "Dynamic" from the map list, so this entry will be deleted from the file if the user changes the map list order.
if (mapName == dynamicMapName) {
logWarn(QString("Ignoring map %1 in map group '%2': Cannot use reserved map name '%3'.").arg(j).arg(groupName).arg(mapName));
continue;
}
// Excepting the disallowed map names above, we want to preserve the user's map list data,
// so this list should accept all names we find whether they have valid data or not.
this->groupNameToMapNames[groupName].append(mapName);
// We log a warning for this, but a repeated name in the map list otherwise functions fine.
// All repeated entries will refer to the same map data.
if (this->maps.contains(mapName)) {
logWarn(QString("Map %1 in map group '%2' has repeated map name '%3'.").arg(j).arg(groupName).arg(mapName));
continue;
}
// Load the map's json file so we can get its ID constant (and two other constants we use for the map list).
QJsonDocument mapDoc;
if (!readMapJson(mapName, &mapDoc)) {
failedMapNames.append(mapName);
continue; // Error message has already been logged
// If we fail to get the ID for any reason, we flag the map as 'errored'. It can still appear in the map list,
// but we won't be able to translate the map name to a map constant, so the map name can't appear elsewhere.
QString mapJsonError;
QJsonDocument mapDoc = readMapJson(mapName, &mapJsonError);
if (!mapJsonError.isEmpty()) {
this->erroredMaps.insert(mapName, mapJsonError);
logWarn(mapJsonError);
continue;
}
// Read and validate the map's ID from its JSON data.
const QJsonObject mapObj = mapDoc.object();
const QString mapConstant = ParseUtil::jsonToQString(mapObj["id"]);
if (mapConstant.isEmpty()) {
logWarn(QString("Map '%1' is missing an \"id\" value and will be ignored.").arg(mapName));
failedMapNames.append(mapName);
QString message = QString("Map '%1' is invalid: Missing \"id\" value.").arg(mapName);
this->erroredMaps.insert(mapName, message);
logWarn(message);
continue;
}
if (mapConstant == dynamicMapConstant) {
logWarn(QString("Ignoring map with reserved \"id\" value '%1'.").arg(mapName));
failedMapNames.append(mapName);
QString message = QString("Map '%1' is invalid: Cannot use reserved name '%2' for \"id\" value.").arg(mapName).arg(mapConstant);
this->erroredMaps.insert(mapName, message);
logWarn(message);
continue;
}
auto it = this->mapConstantsToMapNames.constFind(mapConstant);
if (it != this->mapConstantsToMapNames.constEnd()) {
logWarn(QString("Map '%1' has the same \"id\" value '%2' as map '%3' and will be ignored.").arg(mapName).arg(it.key()).arg(it.value()));
failedMapNames.append(mapName);
QString message = QString("Map '%1' is invalid: Cannot use the same \"id\" value '%2' as map '%3'.")
.arg(mapName)
.arg(it.key())
.arg(it.value());
this->erroredMaps.insert(mapName, message);
logWarn(message);
continue;
}
// Read layout ID for map list
const QString layoutId = ParseUtil::jsonToQString(mapObj["layout"]);
if (!this->layoutIds.contains(layoutId)) {
// If a map has an unknown layout ID it won't be able to load it at all anyway, so skip it.
// Skipping these will let us assume all the map layout IDs are valid, which simplies some handling elsewhere.
logWarn(QString("Map '%1' has unknown \"layout\" value '%2' and will be ignored.").arg(mapName).arg(layoutId));
failedMapNames.append(mapName);
continue;
}
// Read MAPSEC name for map list
const QString mapSectionName = ParseUtil::jsonToQString(mapObj["region_map_section"]);
if (!this->mapSectionIdNames.contains(mapSectionName)) {
// An unknown location is OK. Aside from that name not appearing in the dropdowns this shouldn't cause problems.
// We'll log a warning, but allow this map to be displayed.
logWarn(QString("Map '%1' has unknown \"region_map_section\" value '%2'.").arg(mapName).arg(mapSectionName));
}
// Success, create the Map object
auto map = new Map;
map->setName(mapName);
map->setConstantName(mapConstant);
map->setLayout(this->mapLayouts.value(layoutId));
map->header()->setLocation(mapSectionName);
this->maps.insert(mapName, map);
this->mapNames.append(mapName);
this->groupNameToMapNames[groupName].append(mapName);
this->maps.insert(mapName, map);
this->alphabeticalMapNames.append(mapName);
this->mapConstantsToMapNames.insert(mapConstant, mapName);
// Read layout ID for map list
const QString layoutId = ParseUtil::jsonToQString(mapObj["layout"]);
map->setLayoutId(layoutId);
map->setLayout(this->mapLayouts.value(layoutId)); // This may set layout to nullptr. Don't report anything until user tries to load this map.
// Read MAPSEC name for map list
map->header()->setLocation(ParseUtil::jsonToQString(mapObj["region_map_section"]));
}
}
// Note: Not successfully reading any maps or map groups is ok. We only require at least 1 map layout.
if (!failedMapNames.isEmpty()) {
// At least 1 map was excluded due to an error.
// User should be alerted of this, rather than just silently logging the details.
emit mapsExcluded(failedMapNames);
}
// Save special "Dynamic" constant
this->mapConstantsToMapNames.insert(dynamicMapConstant, dynamicMapName);
this->mapNames.append(dynamicMapName);
this->alphabeticalMapNames.append(dynamicMapName);
Util::numericalModeSort(this->alphabeticalMapNames);
// Chuck the "connections_include_order" field, this is only for matching.
if (!projectConfig.preserveMatchingOnlyData) {
@ -2047,8 +1980,7 @@ void Project::addNewMapGroup(const QString &groupName) {
QString Project::mapNameToMapGroup(const QString &mapName) const {
for (auto it = this->groupNameToMapNames.constBegin(); it != this->groupNameToMapNames.constEnd(); it++) {
const QStringList mapNames = it.value();
if (mapNames.contains(mapName)) {
if (it.value().contains(mapName)) {
return it.key();
}
}
@ -2064,7 +1996,7 @@ QString Project::getMapConstant(const QString &mapName, const QString &defaultVa
QString Project::getMapLayoutId(const QString &mapName, const QString &defaultValue) const {
Map* map = this->maps.value(mapName);
return (map && map->layout()) ? map->layout()->id : defaultValue;
return map ? map->layoutId() : defaultValue;
}
QString Project::getMapLocation(const QString &mapName, const QString &defaultValue) const {
@ -2072,6 +2004,29 @@ QString Project::getMapLocation(const QString &mapName, const QString &defaultVa
return map ? map->header()->location() : defaultValue;
}
QString Project::getLayoutName(const QString &layoutId) const {
Layout* layout = this->mapLayouts.value(layoutId);
return layout ? layout->name : QString();
}
QStringList Project::getLayoutNames() const {
QStringList names;
for (const auto &layoutId : this->alphabeticalLayoutIds) {
names.append(getLayoutName(layoutId));
}
return names;
}
bool Project::isUnsavedMap(const QString &mapName) const {
Map* map = this->maps.value(mapName);
return map ? map->hasUnsavedChanges() : false;
}
bool Project::isUnsavedLayout(const QString &layoutId) const {
Layout* layout = this->mapLayouts.value(layoutId);
return layout ? layout->hasUnsavedChanges() : false;
}
// Determining which map a secret base ID refers to relies on assumptions about its name.
// The default format is for a secret base ID of 'SECRET_BASE_FOO_#' to refer to a map with the constant
// 'MAP_SECRET_BASE_FOO', so we strip the `_#` suffix and add the default map prefix 'MAP_'. If this fails,
@ -2109,7 +2064,7 @@ QString Project::secretBaseIdToMapName(const QString &secretBaseId) const {
// In general this only matters to Porymap if the identifier will be added to the group it collides with,
// but name collisions are likely undesirable in the project.
bool Project::isIdentifierUnique(const QString &identifier) const {
if (this->mapNames.contains(identifier))
if (this->maps.contains(identifier) || this->erroredMaps.contains(identifier))
return false;
if (this->mapConstantsToMapNames.contains(identifier))
return false;
@ -2119,7 +2074,7 @@ bool Project::isIdentifierUnique(const QString &identifier) const {
return false;
if (this->tilesetLabelsOrdered.contains(identifier))
return false;
if (this->layoutIds.contains(identifier))
if (this->mapLayouts.contains(identifier))
return false;
for (const auto &layout : this->mapLayouts) {
if (layout->name == identifier) {

View File

@ -246,7 +246,7 @@ void ScriptUtility::setMetatileLayerOpacity(QList<float> order) {
QList<QString> ScriptUtility::getMapNames() {
if (!window || !window->editor || !window->editor->project)
return QList<QString>();
return window->editor->project->mapNames;
return window->editor->project->mapNames();
}
QList<QString> ScriptUtility::getMapConstants() {
@ -256,18 +256,15 @@ QList<QString> ScriptUtility::getMapConstants() {
}
QList<QString> ScriptUtility::getLayoutNames() {
QList<QString> names;
if (!window || !window->editor || !window->editor->project)
return names;
for (const auto &layout : window->editor->project->mapLayouts)
names.append(layout->name);
return names;
return {};
return window->editor->project->getLayoutNames();
}
QList<QString> ScriptUtility::getLayoutConstants() {
if (!window || !window->editor || !window->editor->project)
return QList<QString>();
return window->editor->project->layoutIds;
return window->editor->project->layoutIds();
}
QList<QString> ScriptUtility::getTilesetNames() {

View File

@ -227,14 +227,14 @@ void EventFrame::populateMapNameDropdown(NoScrollComboBox * combo, Project * pro
if (!project)
return;
populateDropdown(combo, project->mapNames);
populateDropdown(combo, project->mapNames());
// This frame type displays map names, so when a new map is created we need to repopulate it.
connect(project, &Project::mapCreated, this, &EventFrame::invalidateValues, Qt::UniqueConnection);
}
void EventFrame::populateIdNameDropdown(NoScrollComboBox * combo, Project * project, const QString &mapName, Event::Group group) {
if (!project || !project->mapNames.contains(mapName))
if (!project || !project->isKnownMap(mapName))
return;
Map *map = project->loadMap(mapName);

View File

@ -100,11 +100,11 @@ void MapImageExporter::setModeSpecificUi() {
const QSignalBlocker b(ui->comboBox_MapSelection);
ui->comboBox_MapSelection->clear();
if (m_map) {
ui->comboBox_MapSelection->addItems(m_project->mapNames);
ui->comboBox_MapSelection->addItems(m_project->mapNames());
ui->comboBox_MapSelection->setTextItem(m_map->name());
ui->label_MapSelection->setText(m_mode == ImageExporterMode::Stitch ? QStringLiteral("Starting Map") : QStringLiteral("Map"));
} else if (m_layout) {
ui->comboBox_MapSelection->addItems(m_project->layoutIds);
ui->comboBox_MapSelection->addItems(m_project->layoutIds());
ui->comboBox_MapSelection->setTextItem(m_layout->id);
ui->label_MapSelection->setText(QStringLiteral("Layout"));
}
@ -155,13 +155,13 @@ void MapImageExporter::updateMapSelection() {
auto oldLayout = m_layout;
const QString text = ui->comboBox_MapSelection->currentText();
if (m_project->mapNames.contains(text)) {
if (m_project->isKnownMap(text)) {
auto newMap = m_project->loadMap(text);
if (newMap) {
m_map = newMap;
m_layout = newMap->layout();
}
} else if (m_project->layoutIds.contains(text)) {
} else if (m_project->isKnownLayout(text)) {
auto newLayout = m_project->loadLayout(text);
if (newLayout) {
m_map = nullptr;

View File

@ -47,6 +47,7 @@ MapListModel::MapListModel(Project *project, QObject *parent) : QStandardItemMod
this->mapGrayIcon = QIcon(QStringLiteral(":/icons/map_grayed.ico"));
this->mapIcon = QIcon(QStringLiteral(":/icons/map.ico"));
this->mapEditedIcon = QIcon(QStringLiteral(":/icons/map_edited.ico"));
this->mapErroredIcon = QIcon(QStringLiteral(":/icons/map_errored.ico"));
this->mapOpenedIcon = QIcon(QStringLiteral(":/icons/map_opened.ico"));
this->mapFolderIcon.addFile(QStringLiteral(":/icons/folder_closed_map.ico"), QSize(), QIcon::Normal, QIcon::Off);
@ -121,7 +122,7 @@ QStandardItem *MapListModel::createMapFolderItem(const QString &folderName, QSta
}
QStandardItem *MapListModel::insertMapItem(const QString &mapName, const QString &folderName) {
if (mapName.isEmpty() || mapName == this->project->getDynamicMapName()) // Disallow adding MAP_DYNAMIC to the map list.
if (mapName.isEmpty() || folderName.isEmpty() || mapName == this->project->getDynamicMapName()) // Disallow adding MAP_DYNAMIC to the map list.
return nullptr;
QStandardItem *map = createMapItem(mapName);
@ -163,11 +164,13 @@ QVariant MapListModel::data(const QModelIndex &index, int role) const {
// Decorating map in the map list
if (name == this->activeItemName)
return this->mapOpenedIcon;
const Map* map = this->project->getMap(name);
if (!this->project->isMapLoaded(map))
return this->mapGrayIcon;
return map->hasUnsavedChanges() ? this->mapEditedIcon : this->mapIcon;
if (this->project->isErroredMap(name))
return this->mapErroredIcon;
if (this->project->isUnsavedMap(name))
return this->mapEditedIcon;
if (this->project->isLoadedMap(name))
return this->mapIcon;
return this->mapGrayIcon;
} else if (type == this->folderTypeName) {
// Decorating map folder in the map list
return item->hasChildren() ? this->mapFolderIcon : this->emptyMapFolderIcon;
@ -372,7 +375,6 @@ void MapGroupModel::updateProject() {
if (!this->project) return;
// Temporary objects in case of failure, so we won't modify the project unless it succeeds.
QStringList mapNames;
QStringList groupNames;
QMap<QString, QStringList> groupNameToMapNames;
@ -388,11 +390,9 @@ void MapGroupModel::updateProject() {
}
QString mapName = mapItem->data(MapListUserRoles::NameRole).toString();
groupNameToMapNames[groupName].append(mapName);
mapNames.append(mapName);
}
}
this->project->mapNames = mapNames;
this->project->groupNames = groupNames;
this->project->groupNameToMapNames = groupNameToMapNames;
this->project->hasUnsavedDataChanges = true;
@ -445,7 +445,7 @@ MapLocationModel::MapLocationModel(Project *project, QObject *parent) : MapListM
for (const auto &idName : this->project->mapSectionIdNames) {
insertMapFolderItem(idName);
}
for (const auto &mapName : this->project->mapNames) {
for (const auto &mapName : this->project->mapNames()) {
insertMapItem(mapName, this->project->getMapLocation(mapName));
}
}
@ -466,10 +466,10 @@ QStandardItem *MapLocationModel::createMapFolderItem(const QString &folderName,
LayoutTreeModel::LayoutTreeModel(Project *project, QObject *parent) : MapListModel(project, parent) {
this->folderTypeName = "map_layout";
for (const auto &layoutId : this->project->layoutIds) {
for (const auto &layoutId : this->project->layoutIds()) {
insertMapFolderItem(layoutId);
}
for (const auto &mapName : this->project->mapNames) {
for (const auto &mapName : this->project->mapNames()) {
insertMapItem(mapName, this->project->getMapLayoutId(mapName));
}
}
@ -483,8 +483,8 @@ QStandardItem *LayoutTreeModel::createMapFolderItem(const QString &folderName, Q
// Despite using layout IDs internally, the Layouts map list shows layouts using their file path name.
// We could handle this with Qt::DisplayRole in LayoutTreeModel::data, but then it would be sorted using the ID instead of the name.
const Layout* layout = this->project->mapLayouts.value(folderName);
if (layout) folder->setText(layout->name);
QString layoutName = this->project->getLayoutName(folderName);
if (!layoutName.isEmpty()) folder->setText(layoutName);
// The layout ID will instead be shown as a tool tip.
folder->setToolTip(folderName);
@ -501,18 +501,18 @@ QVariant LayoutTreeModel::data(const QModelIndex &index, int role) const {
const QStandardItem *item = this->itemAt(index)->child(row, col);
const QString type = item->data(MapListUserRoles::TypeRole).toString();
const QString name = item->data(MapListUserRoles::NameRole).toString();
const QString layoutId = item->data(MapListUserRoles::NameRole).toString();
if (type == this->folderTypeName) {
if (role == Qt::DecorationRole) {
// Map layouts are used as folders, but we display them with the same icons as maps.
if (name == this->activeItemName)
if (layoutId == this->activeItemName)
return this->mapOpenedIcon;
const Layout* layout = this->project->mapLayouts.value(name);
if (!this->project->isLayoutLoaded(layout))
return this->mapGrayIcon;
return layout->hasUnsavedChanges() ? this->mapEditedIcon : this->mapIcon;
if (this->project->isUnsavedLayout(layoutId))
return this->mapEditedIcon;
if (this->project->isLoadedLayout(layoutId))
return this->mapIcon;
return this->mapGrayIcon;
}
}
return MapListModel::data(index, role);

View File

@ -43,7 +43,7 @@ NewMapDialog::NewMapDialog(Project *project, const Map *mapToCopy, QWidget *pare
ui->newLayoutForm->initUi(project);
ui->comboBox_Group->addItems(project->groupNames);
ui->comboBox_LayoutID->addItems(project->layoutIds);
ui->comboBox_LayoutID->addItems(project->layoutIds());
auto validator = new IdentifierValidator(this);
ui->lineEdit_Name->setValidator(validator);
@ -128,7 +128,7 @@ void NewMapDialog::saveSettings() {
// (an older iteration of this dialog gave users an option to name new layouts, but it's extra clutter for
// something the majority of users creating a map won't need. If they want to give a specific name to a layout
// they can create the layout first, then create a new map that uses that layout.)
const Layout *layout = this->project->mapLayouts.value(settings->layout.id);
const Layout *layout = this->project->getLayout(settings->layout.id);
if (!layout) {
const QString newLayoutName = settings->name + QStringLiteral("_Layout");
settings->layout.name = this->project->toUniqueIdentifier(newLayoutName);
@ -198,7 +198,7 @@ bool NewMapDialog::validateLayoutID(bool allowEmpty) {
QString errorText;
if (layoutId.isEmpty()) {
if (!allowEmpty) errorText = QString("%1 cannot be empty.").arg(ui->label_LayoutID->text());
} else if (!this->project->layoutIds.contains(layoutId) && !this->project->isIdentifierUnique(layoutId)) {
} else if (!this->project->isKnownLayout(layoutId) && !this->project->isIdentifierUnique(layoutId)) {
errorText = QString("%1 must either be the ID for an existing layout, or a unique identifier for a new layout.").arg(ui->label_LayoutID->text());
}
@ -213,7 +213,7 @@ void NewMapDialog::on_comboBox_LayoutID_currentTextChanged(const QString &text)
validateLayoutID(true);
// Changing the layout ID to an existing layout updates the layout settings to match.
const Layout *layout = this->project->mapLayouts.value(text);
const Layout *layout = this->project->getLayout(text);
if (!layout && this->mapToCopy) {
// When duplicating a map, if a new layout ID is specified the settings will be updated
// to match the layout of the map we're duplicating.

View File

@ -1145,12 +1145,13 @@ void TilesetEditor::countMetatileUsage() {
// do not double count
this->metatileSelector->usedMetatiles.fill(0);
for (auto layout : this->project->mapLayouts) {
for (const auto &layoutId : this->project->layoutIds()) {
Layout *layout = this->project->getLayout(layoutId);
bool usesPrimary = (layout->tileset_primary_label == this->primaryTileset->name);
bool usesSecondary = (layout->tileset_secondary_label == this->secondaryTileset->name);
if (usesPrimary || usesSecondary) {
if (!this->project->loadLayout(layout))
if (!this->project->loadLayout(layoutId))
continue;
// for each block in the layout, mark in the vector that it is used
@ -1183,7 +1184,8 @@ void TilesetEditor::countTileUsage() {
QSet<Tileset*> primaryTilesets;
QSet<Tileset*> secondaryTilesets;
for (auto &layout : this->project->mapLayouts) {
for (const auto &layoutId : this->project->layoutIds()) {
Layout *layout = this->project->getLayout(layoutId);
if (layout->tileset_primary_label == this->primaryTileset->name
|| layout->tileset_secondary_label == this->secondaryTileset->name) {
// need to check metatiles