From 9efe67a72f8952ba4fd6dd8dcff50f06565d2c9f Mon Sep 17 00:00:00 2001 From: GriffinR Date: Tue, 16 Jul 2024 14:19:47 -0400 Subject: [PATCH] Add sanity check to project opening --- include/mainwindow.h | 7 +- include/project.h | 3 + src/config.cpp | 2 + src/mainwindow.cpp | 165 +++++++++++++++++++++---------------------- src/project.cpp | 54 ++++++++++++++ 5 files changed, 143 insertions(+), 88 deletions(-) diff --git a/include/mainwindow.h b/include/mainwindow.h index bb4d4b91..e65605e0 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -344,9 +344,9 @@ private: bool setMap(QString, bool scrollTreeView = false); void redrawMapScene(); void refreshMapScene(); - bool loadDataStructures(); - bool loadProjectCombos(); - bool populateMapList(); + bool checkProjectSanity(); + bool loadProjectData(); + bool setProjectUI(); void sortMapList(); void openSubWindow(QWidget * window); QString getExistingDirectory(QString); @@ -376,7 +376,6 @@ private: void initMapSortOrder(); void initShortcuts(); void initExtraShortcuts(); - void setProjectSpecificUI(); void loadUserSettings(); void applyMapListFilter(QString filterText); void restoreWindowState(); diff --git a/include/project.h b/include/project.h index 90172f39..c9a1a0c0 100644 --- a/include/project.h +++ b/include/project.h @@ -97,6 +97,9 @@ public: DataQualifiers healLocationDataQualifiers; QString healLocationsTableName; + bool sanityCheck(); + bool load(); + QMap mapCache; Map* loadMap(QString); Map* getMap(QString); diff --git a/src/config.cpp b/src/config.cpp index 98681264..c294a0ee 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -890,6 +890,8 @@ void ProjectConfig::init() { if (dialog.exec() == QDialog::Accepted) { this->baseGameVersion = static_cast(baseGameVersionComboBox->currentData().toInt()); + } else { + // TODO: If user closes window Porymap assumes pokeemerald; it should instead abort project opening } } this->setUnreadKeys(); // Initialize version-specific options diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 3b48da1e..88c12103 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -413,35 +413,6 @@ void MainWindow::markMapEdited() { } } -// Update the UI using information we've read from the user's project files. -void MainWindow::setProjectSpecificUI() -{ - // Wild Encounters tab - // TODO: This index should come from an enum - ui->mainTabBar->setTabEnabled(4, editor->project->wildEncountersLoaded); - - bool hasFlags = projectConfig.mapAllowFlagsEnabled; - ui->checkBox_AllowRunning->setVisible(hasFlags); - ui->checkBox_AllowBiking->setVisible(hasFlags); - ui->checkBox_AllowEscaping->setVisible(hasFlags); - ui->label_AllowRunning->setVisible(hasFlags); - ui->label_AllowBiking->setVisible(hasFlags); - ui->label_AllowEscaping->setVisible(hasFlags); - - ui->newEventToolButton->newWeatherTriggerAction->setVisible(projectConfig.eventWeatherTriggerEnabled); - ui->newEventToolButton->newSecretBaseAction->setVisible(projectConfig.eventSecretBaseEnabled); - ui->newEventToolButton->newCloneObjectAction->setVisible(projectConfig.eventCloneObjectEnabled); - - bool floorNumEnabled = projectConfig.floorNumberEnabled; - ui->spinBox_FloorNumber->setVisible(floorNumEnabled); - ui->label_FloorNumber->setVisible(floorNumEnabled); - - Event::setIcons(); - editor->setCollisionGraphics(); - ui->spinBox_SelectedElevation->setMaximum(Block::getMaxElevation()); - ui->spinBox_SelectedCollision->setMaximum(Block::getMaxCollision()); -} - void MainWindow::mapSortOrder_changed(QAction *action) { QList items = ui->toolButton_MapSortOrder->menu()->actions(); @@ -528,13 +499,13 @@ void MainWindow::setTheme(QString theme) { } bool MainWindow::openProject(const QString &dir, bool initial) { - if (!this->closeProject()) { - logInfo("Aborted project open."); - return false; - } - if (dir.isNull() || dir.length() <= 0) { - if (!initial) setWindowDisabled(true); + // If this happened on startup it's because the user has no recent projects, which is fine. + // This shouldn't happen otherwise, but if it does then display an error. + if (!initial) { + logError("Failed to open project: Directory name cannot be empty"); + showProjectOpenFailure(); + } return false; } @@ -553,6 +524,13 @@ bool MainWindow::openProject(const QString &dir, bool initial) { return false; } + // The above checks can fail and the user will be allowed to continue with their currently-opened project (if there is one). + // We close the current project below, after which either the new project will open successfully or the window will be disabled. + if (!this->closeProject()) { + logInfo("Aborted project open."); + return false; + } + const QString openMessage = QString("Opening %1").arg(projectString); this->statusBar()->showMessage(openMessage); logInfo(openMessage); @@ -577,8 +555,14 @@ bool MainWindow::openProject(const QString &dir, bool initial) { }); this->editor->project->set_root(dir); + // Make sure project looks reasonable before attempting to load it + if (!checkProjectSanity()) { + delete this->editor->project; + return false; + } + // Load the project - if (!(loadDataStructures() && populateMapList() && setInitialMap())) { + if (!(loadProjectData() && setProjectUI() && setInitialMap())) { this->statusBar()->showMessage(QString("Failed to open %1").arg(projectString)); showProjectOpenFailure(); delete this->editor->project; @@ -606,12 +590,39 @@ bool MainWindow::openProject(const QString &dir, bool initial) { return true; } +bool MainWindow::loadProjectData() { + bool success = editor->project->load(); + Scripting::populateGlobalObject(this); + return success; +} + +bool MainWindow::checkProjectSanity() { + if (editor->project->sanityCheck()) + return true; + + QMessageBox msgBox; + msgBox.setIcon(QMessageBox::Critical); + msgBox.setText(QString("The selected directory appears to be invalid.")); + msgBox.setInformativeText(QString("The directory '%1' is missing key files.\n\n" + "Make sure you selected the correct project directory " + "(the one used to make your .gba file, e.g. 'pokeemerald').").arg(editor->project->root)); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setDefaultButton(QMessageBox::Ok); + auto tryAnyway = msgBox.addButton("Try Anyway", QMessageBox::ActionRole); + msgBox.exec(); + if (msgBox.clickedButton() == tryAnyway) { + // The user has chosen to try to load this project anyway. + // This will almost certainly fail, but they'll get a more specific error message. + return true; + } + return false; +} + void MainWindow::showProjectOpenFailure() { QString errorMsg = QString("There was an error opening the project. Please see %1 for full error details.").arg(getLogPath()); QMessageBox error(QMessageBox::Critical, "porymap", errorMsg, QMessageBox::Ok, this); error.setDetailedText(getMostRecentError()); error.exec(); - setWindowDisabled(true); } bool MainWindow::isProjectOpen() { @@ -975,45 +986,8 @@ void MainWindow::on_spinBox_FloorNumber_valueChanged(int offset) } } -bool MainWindow::loadDataStructures() { - Project *project = editor->project; - bool success = project->readMapLayouts() - && project->readRegionMapSections() - && project->readItemNames() - && project->readFlagNames() - && project->readVarNames() - && project->readMovementTypes() - && project->readInitialFacingDirections() - && project->readMapTypes() - && project->readMapBattleScenes() - && project->readWeatherNames() - && project->readCoordEventWeatherNames() - && project->readSecretBaseIds() - && project->readBgEventFacingDirections() - && project->readTrainerTypes() - && project->readMetatileBehaviors() - && project->readFieldmapProperties() - && project->readFieldmapMasks() - && project->readTilesetLabels() - && project->readTilesetMetatileLabels() - && project->readHealLocations() - && project->readMiscellaneousConstants() - && project->readSpeciesIconPaths() - && project->readWildMonData() - && project->readEventScriptLabels() - && project->readObjEventGfxConstants() - && project->readEventGraphics() - && project->readSongNames(); - - project->applyParsedLimits(); - setProjectSpecificUI(); - Scripting::populateGlobalObject(this); - - return success && loadProjectCombos(); -} - -bool MainWindow::loadProjectCombos() { - // set up project ui comboboxes +// Update the UI using information we've read from the user's project files. +bool MainWindow::setProjectUI() { Project *project = editor->project; // Block signals to the comboboxes while they are being modified @@ -1025,6 +999,7 @@ bool MainWindow::loadProjectCombos() { const QSignalBlocker blocker6(ui->comboBox_BattleScene); const QSignalBlocker blocker7(ui->comboBox_Type); + // Set up project comboboxes ui->comboBox_Song->clear(); ui->comboBox_Song->addItems(project->songNames); ui->comboBox_Location->clear(); @@ -1040,15 +1015,36 @@ bool MainWindow::loadProjectCombos() { ui->comboBox_Type->clear(); ui->comboBox_Type->addItems(project->mapTypes); - return true; -} + sortMapList(); -bool MainWindow::populateMapList() { - bool success = editor->project->readMapGroups(); - if (success) { - sortMapList(); - } - return success; + // Show/hide parts of the UI that are dependent on the user's project settings + + // Wild Encounters tab + // TODO: This index should come from an enum + ui->mainTabBar->setTabEnabled(4, editor->project->wildEncountersLoaded); + + bool hasFlags = projectConfig.mapAllowFlagsEnabled; + ui->checkBox_AllowRunning->setVisible(hasFlags); + ui->checkBox_AllowBiking->setVisible(hasFlags); + ui->checkBox_AllowEscaping->setVisible(hasFlags); + ui->label_AllowRunning->setVisible(hasFlags); + ui->label_AllowBiking->setVisible(hasFlags); + ui->label_AllowEscaping->setVisible(hasFlags); + + ui->newEventToolButton->newWeatherTriggerAction->setVisible(projectConfig.eventWeatherTriggerEnabled); + ui->newEventToolButton->newSecretBaseAction->setVisible(projectConfig.eventSecretBaseEnabled); + ui->newEventToolButton->newCloneObjectAction->setVisible(projectConfig.eventCloneObjectEnabled); + + bool floorNumEnabled = projectConfig.floorNumberEnabled; + ui->spinBox_FloorNumber->setVisible(floorNumEnabled); + ui->label_FloorNumber->setVisible(floorNumEnabled); + + Event::setIcons(); + editor->setCollisionGraphics(); + ui->spinBox_SelectedElevation->setMaximum(Block::getMaxElevation()); + ui->spinBox_SelectedCollision->setMaximum(Block::getMaxCollision()); + + return true; } void MainWindow::sortMapList() { @@ -3019,6 +3015,7 @@ bool MainWindow::closeProject() { } } editor->closeProject(); + setWindowDisabled(true); return true; } diff --git a/src/project.cpp b/src/project.cpp index d50c305e..7048812a 100644 --- a/src/project.cpp +++ b/src/project.cpp @@ -91,6 +91,60 @@ void Project::set_root(QString dir) { this->parser.set_root(dir); } +// Before attempting the initial project load we should check for a few notable files. +// If all are missing then we can warn the user, they may have accidentally selected the wrong folder. +bool Project::sanityCheck() { + // The goal with the file selection is to pick files that are important enough that any reasonable project would have + // at least 1 in the expected location, but unique enough that they're unlikely to overlap with a completely unrelated + // directory (e.g. checking for 'data/maps/' is a bad choice because it's too generic, pokeyellow would pass for instance) + static const QSet pathsToCheck = { + ProjectFilePath::json_map_groups, + ProjectFilePath::json_layouts, + ProjectFilePath::tilesets_headers, + ProjectFilePath::global_fieldmap, + }; + for (auto pathId : pathsToCheck) { + const QString path = QString("%1/%2").arg(this->root).arg(projectConfig.getFilePath(pathId)); + QFileInfo fileInfo(path); + if (fileInfo.exists() && fileInfo.isFile()) + return true; + } + return false; +} + +bool Project::load() { + bool success = readMapLayouts() + && readRegionMapSections() + && readItemNames() + && readFlagNames() + && readVarNames() + && readMovementTypes() + && readInitialFacingDirections() + && readMapTypes() + && readMapBattleScenes() + && readWeatherNames() + && readCoordEventWeatherNames() + && readSecretBaseIds() + && readBgEventFacingDirections() + && readTrainerTypes() + && readMetatileBehaviors() + && readFieldmapProperties() + && readFieldmapMasks() + && readTilesetLabels() + && readTilesetMetatileLabels() + && readHealLocations() + && readMiscellaneousConstants() + && readSpeciesIconPaths() + && readWildMonData() + && readEventScriptLabels() + && readObjEventGfxConstants() + && readEventGraphics() + && readSongNames() + && readMapGroups(); + applyParsedLimits(); + return success; +} + QString Project::getProjectTitle() { if (!root.isNull()) { return root.section('/', -1);