Add sanity check to project opening

This commit is contained in:
GriffinR 2024-07-16 14:19:47 -04:00
parent 29ed696d9e
commit 9efe67a72f
5 changed files with 143 additions and 88 deletions

View file

@ -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();

View file

@ -97,6 +97,9 @@ public:
DataQualifiers healLocationDataQualifiers;
QString healLocationsTableName;
bool sanityCheck();
bool load();
QMap<QString, Map*> mapCache;
Map* loadMap(QString);
Map* getMap(QString);

View file

@ -890,6 +890,8 @@ void ProjectConfig::init() {
if (dialog.exec() == QDialog::Accepted) {
this->baseGameVersion = static_cast<BaseGameVersion>(baseGameVersionComboBox->currentData().toInt());
} else {
// TODO: If user closes window Porymap assumes pokeemerald; it should instead abort project opening
}
}
this->setUnreadKeys(); // Initialize version-specific options

View file

@ -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<QAction*> 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 <b>.gba</b> 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;
}
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;
}

View file

@ -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<ProjectFilePath> 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);