mirror of
https://github.com/huderlem/porymap.git
synced 2026-03-21 17:45:44 -05:00
Merge pull request #731 from GriffinRichards/soften-map-loading
Soften map/layout loading, add Back/Forward
This commit is contained in:
commit
2a7927ae82
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 = "";
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ protected:
|
|||
QIcon mapGrayIcon;
|
||||
QIcon mapIcon;
|
||||
QIcon mapEditedIcon;
|
||||
QIcon mapErroredIcon;
|
||||
QIcon mapOpenedIcon;
|
||||
QIcon mapFolderIcon;
|
||||
QIcon emptyMapFolderIcon;
|
||||
|
|
|
|||
BIN
resources/icons/map_errored.ico
Normal file
BIN
resources/icons/map_errored.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 606 B |
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
463
src/project.cpp
463
src/project.cpp
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user