diff --git a/CHANGELOG.md b/CHANGELOG.md index 0cc8c25f..8bce96f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,19 +12,17 @@ The **"Breaking Changes"** listed below are changes that have been made in the d ### Added - Add Copy/Paste for metatiles in the Tileset Editor. -- Add ability to set the opacity of the scripting overlay. -- Add ability to get/set map header properties and read tile pixel data via the API. -- Add ability to display message boxes and user input windows via the API. +- Add new features to the scripting API, including the ability to display message boxes and user input windows, set overlay opacity, get/set map header properties, read tile pixel data, and set blocks or metatile attributes using a raw value. - Add button to copy the full metatile label to the clipboard in the Tileset Editor. - Add option to not open the most recent project on launch. - Add color picker to palette editor for taking colors from the screen. ### Changed +- Overhauled the region map editor, adding support for tilemaps, and significant customization. Also now supports pokefirered. - If an object event is inanimate, it will always render using its first frame. - Only log "Unknown custom script function" when a registered script function is not present in any script. - Unused metatile attribute bits that are set are preserved instead of being cleared. - The wild encounter editor is automatically disabled if the encounter JSON data cannot be read -- Overhauled the region map editor, adding support for tilemaps, and significant customization. Also now supports pokefirered. - Metatiles are always rendered accurately with 3 layers, and the unused layer is not assumed to be transparent. - `object_event_graphics_info.h` can now be parsed correctly if it uses structs with attributes. - The selection is no longer reset when pasting events. The newly pasted events are selected instead. @@ -35,9 +33,11 @@ The **"Breaking Changes"** listed below are changes that have been made in the d - Fix cursor tile and player view outlines exiting map bounds while painting. - Fix cursor tile and player view outlines not updating immediately when toggled in Collision view. - Fix selected space not updating while painting in Collision view. +- Fix collision values of 2 or 3 not rendering properly. - Fix the map music dropdown being empty when importing a map from Advance Map. - Fix object events added by pasting ignoring the map event limit. - Fixed a bug where saving the tileset editor would reselect the main editor's first selected metatile. +- Fix crashes / unexpected behavior if certain scripting API functions are given invalid palette or tile numbers. ## [4.5.0] - 2021-12-26 ### Added diff --git a/docsrc/manual/scripting-capabilities.rst b/docsrc/manual/scripting-capabilities.rst index 630634a3..55c7e903 100644 --- a/docsrc/manual/scripting-capabilities.rst +++ b/docsrc/manual/scripting-capabilities.rst @@ -214,11 +214,21 @@ The following functions are related to editing the map's blocks or retrieving in :param number x: x coordinate of the block :param number y: y coordinate of the block :param number metatileId: the metatile id of the block - :param number collision: the collision of the block (``0`` = passable, ``1`` = impassable) + :param number collision: the collision of the block (``0`` = passable, ``1-3`` = impassable) :param number elevation: the elevation of the block :param boolean forceRedraw: Force the map view to refresh. Defaults to ``true``. Redrawing the map view is expensive, so set to ``false`` when making many consecutive map edits, and then redraw the map once using ``map.redraw()``. :param boolean commitChanges: Commit the changes to the map's edit/undo history. Defaults to ``true``. When making many related map edits, it can be useful to set this to ``false``, and then commit all of them together with ``map.commit()``. +.. js:function:: map.setBlock(x, y, rawValue, forceRedraw = true, commitChanges = true) + + Sets a block in the currently-opened map. This is an overloaded function that takes the raw value of a block instead of each of the block's properties individually. + + :param number x: x coordinate of the block + :param number y: y coordinate of the block + :param number rawValue: the 16 bit value of the block. Bits ``0-9`` will be the metatile id, bits ``10-11`` will be the collision, and bits ``12-15`` will be the elevation. + :param boolean forceRedraw: Force the map view to refresh. Defaults to ``true``. Redrawing the map view is expensive, so set to ``false`` when making many consecutive map edits, and then redraw the map once using ``map.redraw()``. + :param boolean commitChanges: Commit the changes to the map's edit/undo history. Defaults to ``true``. When making many related map edits, it can be useful to set this to ``false``, and then commit all of them together with ``map.commit()``. + .. js:function:: map.getMetatileId(x, y) Gets the metatile id of a block in the currently-opened map. @@ -257,7 +267,7 @@ The following functions are related to editing the map's blocks or retrieving in .. js:function:: map.getCollision(x, y) - Gets the collision of a block in the currently-opened map. (``0`` = passable, ``1`` = impassable) + Gets the collision of a block in the currently-opened map. (``0`` = passable, ``1-3`` = impassable) :param number x: x coordinate of the block :param number y: y coordinate of the block @@ -265,7 +275,7 @@ The following functions are related to editing the map's blocks or retrieving in .. js:function:: map.setCollision(x, y, collision, forceRedraw = true, commitChanges = true) - Sets the collision of a block in the currently-opened map. (``0`` = passable, ``1`` = impassable) + Sets the collision of a block in the currently-opened map. (``0`` = passable, ``1-3`` = impassable) :param number x: x coordinate of the block :param number y: y coordinate of the block @@ -1123,6 +1133,22 @@ The following functions are related to tilesets and how they are rendered. The f :param number metatileId: id of target metatile :param number behavior: the behavior +.. js:function:: map.getMetatileAttributes(metatileId) + + Gets the raw attributes value for the specified metatile. + + :param number metatileId: id of target metatile + :returns number: the raw attributes value + +.. js:function:: map.setMetatileAttributes(metatileId, attributes) + + Sets the raw attributes value for the specified metatile. + + **Warning:** This function writes directly to the tileset. There is no undo for this. Porymap will not limit the value of existing attributes to their usual range. + + :param number metatileId: id of target metatile + :param number attributes: the raw attributes value + .. js:function:: map.getMetatileTile(metatileId, tileIndex) Gets the tile at the specified index of the metatile. @@ -1193,7 +1219,7 @@ The following functions are related to tilesets and how they are rendered. The f :param number tileEnd: index of the last tile to set. Defaults to ``-1`` (the last tile) :param boolean forceRedraw: Force the map view to refresh. Defaults to ``true``. Redrawing the map view is expensive, so set to ``false`` when making many consecutive map edits, and then redraw the map once using ``map.redraw()``. -..js:function:: map.getTilePixels(tileId) +.. js:function:: map.getTilePixels(tileId) Gets the pixel data for the specified tile. The pixel data is an array of indexes indicating which palette color each pixel uses. Tiles are 8x8, so the pixel array will be 64 elements long. diff --git a/include/core/tile.h b/include/core/tile.h index 1a862987..5d85066a 100644 --- a/include/core/tile.h +++ b/include/core/tile.h @@ -8,15 +8,14 @@ class Tile { public: Tile(); - Tile(int tileId, bool xflip, bool yflip, int palette); + Tile(uint16_t tileId, uint16_t xflip, uint16_t yflip, uint16_t palette); Tile(uint16_t raw); public: - int tileId; - bool xflip; - bool yflip; - int palette; - + uint16_t tileId:10; + uint16_t xflip:1; + uint16_t yflip:1; + uint16_t palette:4; uint16_t rawValue() const; static int getIndexInTileset(int); diff --git a/include/mainwindow.h b/include/mainwindow.h index 55fb14e8..338db5d7 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -46,7 +46,8 @@ public: Q_INVOKABLE QJSValue getBlock(int x, int y); void tryRedrawMapArea(bool forceRedraw); void tryCommitMapChanges(bool commitChanges); - Q_INVOKABLE void setBlock(int x, int y, int tile, int collision, int elevation, bool forceRedraw = true, bool commitChanges = true); + Q_INVOKABLE void setBlock(int x, int y, int metatileId, int collision, int elevation, bool forceRedraw = true, bool commitChanges = true); + Q_INVOKABLE void setBlock(int x, int y, int rawValue, bool forceRedraw = true, bool commitChanges = true); Q_INVOKABLE void setBlocksFromSelection(int x, int y, bool forceRedraw = true, bool commitChanges = true); Q_INVOKABLE int getMetatileId(int x, int y); Q_INVOKABLE void setMetatileId(int x, int y, int metatileId, bool forceRedraw = true, bool commitChanges = true); @@ -182,6 +183,8 @@ public: Q_INVOKABLE void setMetatileTerrainType(int metatileId, int terrainType); Q_INVOKABLE int getMetatileBehavior(int metatileId); Q_INVOKABLE void setMetatileBehavior(int metatileId, int behavior); + Q_INVOKABLE int getMetatileAttributes(int metatileId); + Q_INVOKABLE void setMetatileAttributes(int metatileId, int attributes); Q_INVOKABLE QJSValue getMetatileTile(int metatileId, int tileIndex); Q_INVOKABLE void setMetatileTile(int metatileId, int tileIndex, int tileId, bool xflip, bool yflip, int palette, bool forceRedraw = true); Q_INVOKABLE void setMetatileTile(int metatileId, int tileIndex, QJSValue tileObj, bool forceRedraw = true); diff --git a/src/core/map.cpp b/src/core/map.cpp index 95bd6def..c7f33d92 100644 --- a/src/core/map.cpp +++ b/src/core/map.cpp @@ -350,6 +350,7 @@ bool Map::getBlock(int x, int y, Block *out) { } void Map::setBlock(int x, int y, Block block, bool enableScriptCallback) { + if (!isWithinBounds(x, y)) return; int i = y * getWidth() + x; if (i < layout->blockdata.size()) { Block prevBlock = layout->blockdata.at(i); diff --git a/src/core/tile.cpp b/src/core/tile.cpp index 56465160..cc89c2a9 100644 --- a/src/core/tile.cpp +++ b/src/core/tile.cpp @@ -8,7 +8,7 @@ palette(0) { } - Tile::Tile(int tileId, bool xflip, bool yflip, int palette) : + Tile::Tile(uint16_t tileId, uint16_t xflip, uint16_t yflip, uint16_t palette) : tileId(tileId), xflip(xflip), yflip(yflip), diff --git a/src/core/tileset.cpp b/src/core/tileset.cpp index b87cfa69..9a055690 100644 --- a/src/core/tileset.cpp +++ b/src/core/tileset.cpp @@ -120,6 +120,12 @@ QList Tileset::getPalette(int paletteId, Tileset *primaryTileset, Tileset ? primaryTileset : secondaryTileset; auto palettes = useTruePalettes ? tileset->palettes : tileset->palettePreviews; + + if (paletteId < 0 || paletteId >= palettes.length()){ + logError(QString("Invalid tileset palette id '%1' requested.").arg(paletteId)); + return paletteTable; + } + for (int i = 0; i < palettes.at(paletteId).length(); i++) { paletteTable.append(palettes.at(paletteId).at(i)); } diff --git a/src/editor.cpp b/src/editor.cpp index 6b2c54cd..df417ce1 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -1075,7 +1075,7 @@ QString Editor::getMovementPermissionText(uint16_t collision, uint16_t elevation } else if (collision == 0) { message = QString("Collision: Passable, Elevation: %1").arg(elevation); } else { - message = QString("Collision: Impassable, Elevation: %1").arg(elevation); + message = QString("Collision: Impassable (%1), Elevation: %2").arg(collision).arg(elevation); } return message; } diff --git a/src/mainwindow_scriptapi.cpp b/src/mainwindow_scriptapi.cpp index c249236b..136233f1 100644 --- a/src/mainwindow_scriptapi.cpp +++ b/src/mainwindow_scriptapi.cpp @@ -55,10 +55,18 @@ void MainWindow::tryCommitMapChanges(bool commitChanges) { } } -void MainWindow::setBlock(int x, int y, int tile, int collision, int elevation, bool forceRedraw, bool commitChanges) { +void MainWindow::setBlock(int x, int y, int metatileId, int collision, int elevation, bool forceRedraw, bool commitChanges) { if (!this->editor || !this->editor->map) return; - this->editor->map->setBlock(x, y, Block(tile, collision, elevation)); + this->editor->map->setBlock(x, y, Block(metatileId, collision, elevation)); + this->tryCommitMapChanges(commitChanges); + this->tryRedrawMapArea(forceRedraw); +} + +void MainWindow::setBlock(int x, int y, int rawValue, bool forceRedraw, bool commitChanges) { + if (!this->editor || !this->editor->map) + return; + this->editor->map->setBlock(x, y, Block(static_cast(rawValue))); this->tryCommitMapChanges(commitChanges); this->tryRedrawMapArea(forceRedraw); } @@ -1073,6 +1081,22 @@ void MainWindow::setMetatileBehavior(int metatileId, int behavior) { this->saveMetatileAttributesByMetatileId(metatileId); } +int MainWindow::getMetatileAttributes(int metatileId) { + Metatile * metatile = this->getMetatile(metatileId); + if (!metatile) + return -1; + return metatile->getAttributes(projectConfig.getBaseGameVersion()); +} + +void MainWindow::setMetatileAttributes(int metatileId, int attributes) { + Metatile * metatile = this->getMetatile(metatileId); + uint32_t u_attributes = static_cast(attributes); + if (!metatile) + return; + metatile->setAttributes(u_attributes, projectConfig.getBaseGameVersion()); + this->saveMetatileAttributesByMetatileId(metatileId); +} + int MainWindow::calculateTileBounds(int * tileStart, int * tileEnd) { int maxNumTiles = this->getNumTilesInMetatile(); if (*tileEnd >= maxNumTiles || *tileEnd < 0) diff --git a/src/scripting.cpp b/src/scripting.cpp index 3b16feac..3f6337e8 100644 --- a/src/scripting.cpp +++ b/src/scripting.cpp @@ -275,17 +275,17 @@ QJSValue Scripting::position(int x, int y) { } Tile Scripting::toTile(QJSValue obj) { - if (!obj.hasProperty("tileId") - || !obj.hasProperty("xflip") - || !obj.hasProperty("yflip") - || !obj.hasProperty("palette")) { - return Tile(); - } Tile tile = Tile(); - tile.tileId = obj.property("tileId").toInt(); - tile.xflip = obj.property("xflip").toBool(); - tile.yflip = obj.property("yflip").toBool(); - tile.palette = obj.property("palette").toInt(); + + if (obj.hasProperty("tileId")) + tile.tileId = obj.property("tileId").toInt(); + if (obj.hasProperty("xflip")) + tile.xflip = obj.property("xflip").toBool(); + if (obj.hasProperty("yflip")) + tile.yflip = obj.property("yflip").toBool(); + if (obj.hasProperty("palette")) + tile.palette = obj.property("palette").toInt(); + return tile; } diff --git a/src/ui/imageproviders.cpp b/src/ui/imageproviders.cpp index 200d478b..6a246cc9 100644 --- a/src/ui/imageproviders.cpp +++ b/src/ui/imageproviders.cpp @@ -8,7 +8,7 @@ QImage getCollisionMetatileImage(Block block) { } QImage getCollisionMetatileImage(int collision, int elevation) { - int x = collision * 16; + int x = (collision != 0) * 16; int y = elevation * 16; QPixmap collisionImage = QPixmap(":/images/collisions.png").copy(x, y, 16, 16); return collisionImage.toImage(); @@ -66,13 +66,13 @@ QImage getMetatileImage( tile = metatile->tiles.value(tileOffset + (l * 4)); } else { // "Vanilla" metatiles only have 8 tiles, but render 12. - // The remaining 4 tiles are rendered either as tile 0 or 0x3014 (invalid) depending on layer type. + // The remaining 4 tiles are rendered either as tile 0 or 0x3014 (tile 20, palette 3) depending on layer type. switch (layerType) { default: case METATILE_LAYER_MIDDLE_TOP: if (l == 0) - tile = Tile(0x3014, false, false, 0); + tile = Tile(0x3014); else // Tiles are on layers 1 and 2 tile = metatile->tiles.value(tileOffset + ((l - 1) * 4)); break; diff --git a/src/ui/movementpermissionsselector.cpp b/src/ui/movementpermissionsselector.cpp index da6d7178..9bad2a0f 100644 --- a/src/ui/movementpermissionsselector.cpp +++ b/src/ui/movementpermissionsselector.cpp @@ -16,7 +16,7 @@ uint16_t MovementPermissionsSelector::getSelectedElevation() { } void MovementPermissionsSelector::select(uint16_t collision, uint16_t elevation) { - SelectablePixmapItem::select(collision, elevation, 0, 0); + SelectablePixmapItem::select(collision != 0, elevation, 0, 0); } void MovementPermissionsSelector::hoverMoveEvent(QGraphicsSceneHoverEvent *event) { diff --git a/src/ui/overlay.cpp b/src/ui/overlay.cpp index 635d76f9..996edb8e 100644 --- a/src/ui/overlay.cpp +++ b/src/ui/overlay.cpp @@ -26,9 +26,11 @@ void OverlayImage::render(QPainter *painter, int x, int y) { void Overlay::renderItems(QPainter *painter) { if (this->hidden) return; + qreal oldOpacity = painter->opacity(); painter->setOpacity(this->opacity); for (auto item : this->items) item->render(painter, this->x, this->y); + painter->setOpacity(oldOpacity); } void Overlay::clearItems() {