diff --git a/docsrc/manual/settings-and-options.rst b/docsrc/manual/settings-and-options.rst index e26dc08e..2c2d1e88 100644 --- a/docsrc/manual/settings-and-options.rst +++ b/docsrc/manual/settings-and-options.rst @@ -97,7 +97,7 @@ Fill Metatile Field name: ``new_map_metatile`` Elevation - This is the elevation that will be used to fill new maps. New maps will be filled with passable collision. + This is the elevation that will be used to fill new maps. It will also be the default selection on the Collision tab when a map is opened. New maps will be filled with passable collision. Defaults to ``3``. diff --git a/include/config.h b/include/config.h index 6fc02c31..c7671991 100644 --- a/include/config.h +++ b/include/config.h @@ -38,8 +38,8 @@ protected: virtual void onNewConfigFileCreated() = 0; virtual void setUnreadKeys() = 0; bool getConfigBool(QString key, QString value); - int getConfigInteger(QString key, QString value, int min, int max, int defaultValue); - uint32_t getConfigUint32(QString key, QString value, uint32_t min, uint32_t max, uint32_t defaultValue); + int getConfigInteger(QString key, QString value, int min = INT_MIN, int max = INT_MAX, int defaultValue = 0); + uint32_t getConfigUint32(QString key, QString value, uint32_t min = 0, uint32_t max = UINT_MAX, uint32_t defaultValue = 0); private: bool saveDisabled = false; }; @@ -227,6 +227,10 @@ public: this->tilesetsHaveCallback = true; this->tilesetsHaveIsCompressed = true; this->filePaths.clear(); + this->eventIconPaths.clear(); + this->collisionSheetPath = QString(); + this->collisionSheetWidth = 2; + this->collisionSheetHeight = 16; this->readKeys.clear(); } static const QMap> defaultPaths; @@ -299,8 +303,14 @@ public: void setMapAllowFlagsEnabled(bool enabled); void setEventIconPath(Event::Group group, const QString &path); QString getEventIconPath(Event::Group group); - void setCollisionMapPath(const QString &path); - QString getCollisionMapPath(); + void setCollisionIconPath(int collision, const QString &path); + QString getCollisionIconPath(int collision); + void setCollisionSheetPath(const QString &path); + QString getCollisionSheetPath(); + void setCollisionSheetWidth(int width); + int getCollisionSheetWidth(); + void setCollisionSheetHeight(int height); + int getCollisionSheetHeight(); protected: virtual QString getConfigFilepath() override; @@ -340,7 +350,9 @@ private: uint32_t metatileLayerTypeMask; bool enableMapAllowFlags; QMap eventIconPaths; - QString collisionMapPath; + QString collisionSheetPath; + int collisionSheetWidth; + int collisionSheetHeight; }; extern ProjectConfig projectConfig; diff --git a/include/core/events.h b/include/core/events.h index 4ab30231..e6427466 100644 --- a/include/core/events.h +++ b/include/core/events.h @@ -178,7 +178,7 @@ public: static QString eventGroupToString(Event::Group group); static QString eventTypeToString(Event::Type type); static Event::Type eventTypeFromString(QString type); - static void initIcons(); + static void setIcons(); // protected attributes protected: @@ -260,8 +260,6 @@ public: void setFrameFromMovement(QString movement); void setPixmapFromSpritesheet(QImage, int, int, bool); - static const QPixmap * defaultIcon; - protected: QString gfx; @@ -347,8 +345,6 @@ public: void setDestinationWarpID(QString newDestinationWarpID) { this->destinationWarpID = newDestinationWarpID; } QString getDestinationWarpID() { return this->destinationWarpID; } - static const QPixmap * defaultIcon; - private: QString destinationMap; QString destinationWarpID; @@ -375,8 +371,6 @@ public: virtual void setDefaultValues(Project *project) override = 0; virtual QSet getExpectedFields() override = 0; - - static const QPixmap * defaultIcon; }; @@ -476,8 +470,6 @@ public: virtual void setDefaultValues(Project *project) override = 0; virtual QSet getExpectedFields() override = 0; - - static const QPixmap * defaultIcon; }; @@ -633,8 +625,6 @@ public: void setRespawnNPC(uint8_t newRespawnNPC) { this->respawnNPC = newRespawnNPC; } uint8_t getRespawnNPC() { return this->respawnNPC; } - static const QPixmap * defaultIcon; - private: int index = -1; QString locationName; diff --git a/include/editor.h b/include/editor.h index 2282fba7..39272d7c 100644 --- a/include/editor.h +++ b/include/editor.h @@ -138,6 +138,7 @@ public: int scaleIndex = 2; qreal collisionOpacity = 0.5; + static QList> collisionIcons; void objectsView_onMousePress(QMouseEvent *event); @@ -151,6 +152,7 @@ public: void scaleMapView(int); static void openInTextEditor(const QString &path, int lineNum = 0); bool eventLimitReached(Event::Type type); + static void setCollisionGraphics(); public slots: void openMapScripts() const; diff --git a/include/ui/movementpermissionsselector.h b/include/ui/movementpermissionsselector.h index f2a993fd..f9a02529 100644 --- a/include/ui/movementpermissionsselector.h +++ b/include/ui/movementpermissionsselector.h @@ -6,8 +6,8 @@ class MovementPermissionsSelector: public SelectablePixmapItem { Q_OBJECT public: - MovementPermissionsSelector(QPixmap basePixmap) : - SelectablePixmapItem(32, 32, 1, 1), + MovementPermissionsSelector(int cellWidth, int cellHeight, QPixmap basePixmap) : + SelectablePixmapItem(cellWidth, cellHeight, 1, 1), basePixmap(basePixmap) { setAcceptHoverEvents(true); } diff --git a/resources/images.qrc b/resources/images.qrc index 8afb0df4..35280608 100644 --- a/resources/images.qrc +++ b/resources/images.qrc @@ -61,6 +61,7 @@ icons/ui/midnight_branch_more.png images/blank_tileset.png images/collisions.png + images/collisions_unknown.png images/Entities_16x16.png icons/clipboard.ico diff --git a/resources/images/collisions_unknown.png b/resources/images/collisions_unknown.png new file mode 100644 index 00000000..446704b3 Binary files /dev/null and b/resources/images/collisions_unknown.png differ diff --git a/src/config.cpp b/src/config.cpp index df90213b..0fe3f6e9 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -628,8 +628,10 @@ void ProjectConfig::parseConfigKeyValue(QString key, QString value) { } else if (key == "enable_triple_layer_metatiles") { this->enableTripleLayerMetatiles = getConfigBool(key, value); } else if (key == "new_map_metatile") { + // TODO: Update max this->newMapMetatileId = getConfigUint32(key, value, 0, 1023, 0); } else if (key == "new_map_elevation") { + // TODO: Update max this->newMapElevation = getConfigInteger(key, value, 0, 15, 3); } else if (key == "new_map_border_metatiles") { this->newMapBorderMetatileIds.clear(); @@ -652,13 +654,13 @@ void ProjectConfig::parseConfigKeyValue(QString key, QString value) { } this->metatileAttributesSize = size; } else if (key == "metatile_behavior_mask") { - this->metatileBehaviorMask = getConfigUint32(key, value, 0, 0xFFFFFFFF, 0); + this->metatileBehaviorMask = getConfigUint32(key, value); } else if (key == "metatile_terrain_type_mask") { - this->metatileTerrainTypeMask = getConfigUint32(key, value, 0, 0xFFFFFFFF, 0); + this->metatileTerrainTypeMask = getConfigUint32(key, value); } else if (key == "metatile_encounter_type_mask") { - this->metatileEncounterTypeMask = getConfigUint32(key, value, 0, 0xFFFFFFFF, 0); + this->metatileEncounterTypeMask = getConfigUint32(key, value); } else if (key == "metatile_layer_type_mask") { - this->metatileLayerTypeMask = getConfigUint32(key, value, 0, 0xFFFFFFFF, 0); + this->metatileLayerTypeMask = getConfigUint32(key, value); } else if (key == "enable_map_allow_flags") { this->enableMapAllowFlags = getConfigBool(key, value); #ifdef CONFIG_BACKWARDS_COMPATABILITY @@ -694,6 +696,14 @@ void ProjectConfig::parseConfigKeyValue(QString key, QString value) { this->eventIconPaths[Event::Group::Bg] = value; } else if (key == "event_icon_path_heal") { this->eventIconPaths[Event::Group::Heal] = value; + } else if (key == "collision_sheet_path") { + this->collisionSheetPath = value; + } else if (key == "collision_sheet_width") { + // Max for these two keys is if a user specifies blocks with 1 bit for metatile ID and 15 bits for collision or elevation + // TODO: Test to make sure there shouldn't be a stricter limit given the UI + this->collisionSheetWidth = getConfigInteger(key, value, 1, 0x7FFF, 2); + } else if (key == "collision_sheet_height") { + this->collisionSheetHeight = getConfigInteger(key, value, 1, 0x7FFF, 16); } else { logWarn(QString("Invalid config key found in config file %1: '%2'").arg(this->getConfigFilepath()).arg(key)); } @@ -766,6 +776,9 @@ QMap ProjectConfig::getKeyValueMap() { map.insert("event_icon_path_coord", this->eventIconPaths[Event::Group::Coord]); map.insert("event_icon_path_bg", this->eventIconPaths[Event::Group::Bg]); map.insert("event_icon_path_heal", this->eventIconPaths[Event::Group::Heal]); + map.insert("collision_sheet_path", this->collisionSheetPath); + map.insert("collision_sheet_width", QString::number(this->collisionSheetWidth)); + map.insert("collision_sheet_height", QString::number(this->collisionSheetHeight)); return map; } @@ -1116,15 +1129,35 @@ QString ProjectConfig::getEventIconPath(Event::Group group) { return this->eventIconPaths.value(group); } -void ProjectConfig::setCollisionMapPath(const QString &path) { - this->collisionMapPath = path; +// TODO: Expose to project settings editor +void ProjectConfig::setCollisionSheetPath(const QString &path) { + this->collisionSheetPath = path; this->save(); } -QString ProjectConfig::getCollisionMapPath() { - return this->collisionMapPath; +QString ProjectConfig::getCollisionSheetPath() { + return this->collisionSheetPath; } +void ProjectConfig::setCollisionSheetWidth(int width) { + this->collisionSheetWidth = width; + this->save(); +} + +int ProjectConfig::getCollisionSheetWidth() { + return this->collisionSheetWidth; +} + +void ProjectConfig::setCollisionSheetHeight(int height) { + this->collisionSheetHeight = height; + this->save(); +} + +int ProjectConfig::getCollisionSheetHeight() { + return this->collisionSheetHeight; +} + + UserConfig userConfig; QString UserConfig::getConfigFilepath() { diff --git a/src/core/events.cpp b/src/core/events.cpp index ad5a3055..05f6eb35 100644 --- a/src/core/events.cpp +++ b/src/core/events.cpp @@ -131,7 +131,7 @@ void Event::loadPixmap(Project *) { this->pixmap = pixmap ? *pixmap : QPixmap(); } -void Event::initIcons() { +void Event::setIcons() { qDeleteAll(icons); icons.clear(); @@ -159,9 +159,9 @@ void Event::initIcons() { if (customIcon.isNull()) { // Custom icon failed to load, use the default icon. icons[group] = new QPixmap(defaultIcons.copy(i * w, 0, w, h)); - logError(QString("Failed to load custom event icon '%1', using default icon.").arg(customIconPath)); + logWarn(QString("Failed to load custom event icon '%1', using default icon.").arg(customIconPath)); } else { - icons[group] = new QPixmap(customIcon); + icons[group] = new QPixmap(customIcon.scaled(w, h)); } } } diff --git a/src/editor.cpp b/src/editor.cpp index 31ead704..286ae545 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -20,6 +20,11 @@ #include static bool selectNewEvents = false; +static const QPixmap *collisionSheetPixmap = nullptr; +static const int movementPermissionsSelectorCellSize = 32; + +// 2D array mapping collision+elevation combos to an icon. +QList> Editor::collisionIcons; Editor::Editor(Ui::MainWindow* ui) { @@ -1484,12 +1489,14 @@ void Editor::displayMovementPermissionSelector() { scene_collision_metatiles = new QGraphicsScene; if (!movement_permissions_selector_item) { - movement_permissions_selector_item = new MovementPermissionsSelector(QPixmap(":/images/collisions.png").scaled(32 * 2, 32 * 16)); // TODO: Don't assume default + movement_permissions_selector_item = new MovementPermissionsSelector(movementPermissionsSelectorCellSize, + movementPermissionsSelectorCellSize, + collisionSheetPixmap ? *collisionSheetPixmap : QPixmap()); connect(movement_permissions_selector_item, &MovementPermissionsSelector::hoveredMovementPermissionChanged, this, &Editor::onHoveredMovementPermissionChanged); connect(movement_permissions_selector_item, &MovementPermissionsSelector::hoveredMovementPermissionCleared, this, &Editor::onHoveredMovementPermissionCleared); - movement_permissions_selector_item->select(0, 3); + movement_permissions_selector_item->select(0, projectConfig.getNewMapElevation()); // TODO: New map collision config? } scene_collision_metatiles->addItem(movement_permissions_selector_item); @@ -2227,3 +2234,77 @@ void Editor::objectsView_onMousePress(QMouseEvent *event) { } selectingEvent = false; } + +// TODO: Show elevation & collision spinners on collision tab +// TODO: Hide selection rect for elevation/collision combos not shown on the image +// TODO: Zoom slider +// TODO: Bug--Images with transparency allow users to paint metatiles on the Collision tab +// Custom collision graphics may be provided by the user. +void Editor::setCollisionGraphics() { + static const QImage defaultCollisionImgSheet = QImage(":/images/collisions.png"); + QString customPath = projectConfig.getCollisionSheetPath(); + + QImage imgSheet; + if (!customPath.isEmpty()) { + // Try to load custom collision image + QFileInfo info(customPath); + if (info.isRelative()) { + customPath = QDir::cleanPath(projectConfig.getProjectDir() + QDir::separator() + customPath); + } + imgSheet = QImage(customPath); + if (imgSheet.isNull()) { + // Custom collision image failed to load, use default + logWarn(QString("Failed to load custom collision image '%1', using default.").arg(customPath)); + imgSheet = defaultCollisionImgSheet; + } + } else { + // No custom collision image specified, use the default. + imgSheet = defaultCollisionImgSheet; + } + + // Like the vanilla collision image, users are not required to provide an image that gives an icon for every elevation/collision combination. + // Instead they tell us how many are provided in their image by specifying the number of columns and rows. + const int imgColumns = projectConfig.getCollisionSheetWidth(); + const int imgRows = projectConfig.getCollisionSheetHeight(); + + // Create a pixmap for the selector on the Collision tab + delete collisionSheetPixmap; + collisionSheetPixmap = new QPixmap(QPixmap::fromImage(imgSheet) + .scaled(movementPermissionsSelectorCellSize * imgColumns, + movementPermissionsSelectorCellSize * imgRows)); + + for (auto sublist : collisionIcons) + qDeleteAll(sublist); + collisionIcons.clear(); + + // Chop up the collision image sheet into separate icon images to be displayed on the map. + // Any icons for elevation/collision combinations that aren't provided by the image sheet are also created now. + const int w = 16; + const int h = 16; + const int numCollisions = 4; // TODO: Read value from elsewhere + const int numElevations = 16; // TODO: Read value from elsewhere + imgSheet = imgSheet.scaled(w * imgColumns, h * imgRows); + for (int collision = 0; collision < numCollisions; collision++) { + // If (collision >= imgColumns) here, it's a valid collision value, but it is not represented with an icon on the image sheet. + // In this case we just use the rightmost collision icon. This is mostly to support the vanilla case, where technically 0-3 + // are valid collision values, but 1-3 have the same meaning, so the vanilla collision selector image only has 2 columns. + int x = ((collision < imgColumns) ? collision : imgColumns) * w; + + QList sublist; + for (int elevation = 0; elevation < numElevations; elevation++) { + if (elevation < imgRows) { + // This elevation has an icon on the image sheet, add it to the list + int y = elevation * h; + sublist.append(new QImage(imgSheet.copy(x, y, w, h))); + } else { + // This is a valid elevation value, but it has no icon on the image sheet. + // Give it a placeholder "?" icon (white if passable, red otherwise) + static const QImage placeholder = QImage(":/images/collisions_unknown.png"); + static const QImage * placeholder_White = new QImage(placeholder.copy(0, 0, w, h)); + static const QImage * placeholder_Red = new QImage(placeholder.copy(w, 0, w, h)); + sublist.append(x == 0 ? placeholder_White : placeholder_Red); + } + } + collisionIcons.append(sublist); + } +} diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 7f9d1f82..648950be 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -391,6 +391,9 @@ void MainWindow::setProjectSpecificUIVisibility() bool floorNumEnabled = projectConfig.getFloorNumberEnabled(); ui->spinBox_FloorNumber->setVisible(floorNumEnabled); ui->label_FloorNumber->setVisible(floorNumEnabled); + + Event::setIcons(); + Editor::setCollisionGraphics(); } void MainWindow::mapSortOrder_changed(QAction *action) @@ -514,7 +517,6 @@ bool MainWindow::openProject(QString dir) { this->setProjectSpecificUIVisibility(); this->newMapDefaultsSet = false; - Event::initIcons(); Scripting::init(this); bool already_open = isProjectOpen() && (editor->project->root == dir); if (!already_open) { diff --git a/src/ui/imageproviders.cpp b/src/ui/imageproviders.cpp index 3935b196..3e17ab44 100644 --- a/src/ui/imageproviders.cpp +++ b/src/ui/imageproviders.cpp @@ -1,18 +1,16 @@ #include "config.h" #include "imageproviders.h" #include "log.h" +#include "editor.h" #include QImage getCollisionMetatileImage(Block block) { return getCollisionMetatileImage(block.collision, block.elevation); } -// TODO: QImage getCollisionMetatileImage(int collision, int elevation) { - static const QImage collisionImage(":/images/collisions.png"); - int x = (collision != 0) * 16; - int y = elevation * 16; - return collisionImage.copy(x, y, 16, 16); + const QImage * image = Editor::collisionIcons.at(collision).at(elevation); + return image ? *image : QImage(); } QImage getMetatileImage(