diff --git a/include/core/block.h b/include/core/block.h index b31f18b4..798e179c 100644 --- a/include/core/block.h +++ b/include/core/block.h @@ -8,6 +8,7 @@ class Block public: Block(); Block(uint16_t); + Block(uint16_t tile, uint16_t collision, uint16_t elevation); Block(const Block&); bool operator ==(Block); bool operator !=(Block); diff --git a/include/core/map.h b/include/core/map.h index 32eb6e70..33a9db68 100644 --- a/include/core/map.h +++ b/include/core/map.h @@ -74,8 +74,7 @@ public: void cacheBlockdata(); void cacheCollision(); Block *getBlock(int x, int y); - void setBlock(int x, int y, Block block); - void _setBlock(int x, int y, Block block); + void setBlock(int x, int y, Block block, bool invokeCallback = false); void floodFillCollisionElevation(int x, int y, uint16_t collision, uint16_t elevation); void _floodFillCollisionElevation(int x, int y, uint16_t collision, uint16_t elevation); void magicFillCollisionElevation(int x, int y, uint16_t collision, uint16_t elevation); diff --git a/include/mainwindow.h b/include/mainwindow.h index 1e5a65be..117b264e 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -37,6 +37,8 @@ public: MainWindow(const MainWindow &) = delete; MainWindow & operator = (const MainWindow &) = delete; + Q_INVOKABLE void scriptapi_setBlock(int x, int y, int tile, int collision, int elevation); + public slots: void scaleMapView(int); diff --git a/include/scripting.h b/include/scripting.h new file mode 100644 index 00000000..c5dc4323 --- /dev/null +++ b/include/scripting.h @@ -0,0 +1,31 @@ +#ifndef SCRIPTING_H +#define SCRIPTING_H + +#include "mainwindow.h" +#include "block.h" + +#include +#include + +enum CallbackType { + OnBlockChanged, +}; + +class Scripting +{ +public: + Scripting(MainWindow *mainWindow); + QJSValue newBlockObject(Block block); + static void init(MainWindow *mainWindow); + static void cb_MetatileChanged(int x, int y, Block prevBlock, Block newBlock); + +private: + QJSEngine *engine; + QStringList filepaths; + QList modules; + + void loadModules(QStringList moduleFiles); + void invokeCallback(CallbackType type, QJSValueList args); +}; + +#endif // SCRIPTING_H diff --git a/porymap.pro b/porymap.pro index ad12b9f0..1a58b420 100644 --- a/porymap.pro +++ b/porymap.pro @@ -4,7 +4,7 @@ # #------------------------------------------------- -QT += core gui +QT += core gui qml greaterThan(QT_MAJOR_VERSION, 4): QT += widgets @@ -70,6 +70,7 @@ SOURCES += src/core/block.cpp \ src/main.cpp \ src/mainwindow.cpp \ src/project.cpp \ + src/scripting.cpp \ src/settings.cpp \ src/log.cpp \ src/ui/newtilesetdialog.cpp @@ -94,7 +95,7 @@ HEADERS += include/core/block.h \ include/core/wildmoninfo.h \ include/lib/orderedmap.h \ include/lib/orderedjson.h \ - include/ui/aboutporymap.h \ + include/ui/aboutporymap.h \ include/ui/bordermetatilespixmapitem.h \ include/ui/collisionpixmapitem.h \ include/ui/connectionpixmapitem.h \ @@ -132,6 +133,7 @@ HEADERS += include/core/block.h \ include/editor.h \ include/mainwindow.h \ include/project.h \ + include/scripting.h \ include/settings.h \ include/log.h \ include/ui/newtilesetdialog.h diff --git a/src/core/block.cpp b/src/core/block.cpp index bc65c923..2a6b057d 100644 --- a/src/core/block.cpp +++ b/src/core/block.cpp @@ -3,6 +3,12 @@ Block::Block() : tile(0), collision(0), elevation(0) { } +Block::Block(uint16_t tile, uint16_t collision, uint16_t elevation) { + this->tile = tile; + this->collision = collision; + this->elevation = elevation; +} + Block::Block(uint16_t word) { tile = word & 0x3ff; diff --git a/src/core/map.cpp b/src/core/map.cpp index 667c151a..aada600b 100644 --- a/src/core/map.cpp +++ b/src/core/map.cpp @@ -2,6 +2,7 @@ #include "historyitem.h" #include "map.h" #include "imageproviders.h" +#include "scripting.h" #include #include @@ -363,10 +364,14 @@ Block* Map::getBlock(int x, int y) { return nullptr; } -void Map::_setBlock(int x, int y, Block block) { +void Map::setBlock(int x, int y, Block block, bool invokeCallback) { int i = y * getWidth() + x; - if (layout->blockdata && layout->blockdata->blocks) { + if (layout->blockdata && layout->blockdata->blocks && i < layout->blockdata->blocks->size()) { + Block prevBlock = layout->blockdata->blocks->value(i); layout->blockdata->blocks->replace(i, block); + if (invokeCallback) { + Scripting::cb_MetatileChanged(x, y, prevBlock, block); + } } } @@ -390,7 +395,7 @@ void Map::_floodFillCollisionElevation(int x, int y, uint16_t collision, uint16_ block->collision = collision; block->elevation = elevation; - _setBlock(x, y, *block); + setBlock(x, y, *block); if ((block = getBlock(x + 1, y)) && block->collision == old_coll && block->elevation == old_elev) { todo.append(QPoint(x + 1, y)); } @@ -495,14 +500,6 @@ void Map::commit() { } } -void Map::setBlock(int x, int y, Block block) { - Block *old_block = getBlock(x, y); - if (old_block && (*old_block) != block) { - _setBlock(x, y, block); - commit(); - } -} - void Map::floodFillCollisionElevation(int x, int y, uint16_t collision, uint16_t elevation) { Block *block = getBlock(x, y); if (block && (block->collision != collision || block->elevation != elevation)) { @@ -523,7 +520,7 @@ void Map::magicFillCollisionElevation(int initialX, int initialY, uint16_t colli if (block && block->collision == old_coll && block->elevation == old_elev) { block->collision = collision; block->elevation = elevation; - _setBlock(x, y, *block); + setBlock(x, y, *block); } } } diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 2a8cbd77..2e68858e 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -8,6 +8,8 @@ #include "ui_eventpropertiesframe.h" #include "bordermetatilespixmapitem.h" #include "currentselectedmetatilespixmapitem.h" +#include "customattributestable.h" +#include "scripting.h" #include #include @@ -44,6 +46,7 @@ MainWindow::MainWindow(QWidget *parent) : QCoreApplication::setApplicationName("porymap"); QApplication::setApplicationDisplayName("porymap"); QApplication::setWindowIcon(QIcon(":/icons/porymap-icon-2.ico")); + Scripting::init(this); ui->setupUi(this); this->initWindow(); @@ -2540,3 +2543,7 @@ void MainWindow::closeEvent(QCloseEvent *event) { QMainWindow::closeEvent(event); } + +void MainWindow::scriptapi_setBlock(int x, int y, int tile, int collision, int elevation) { + this->editor->map->setBlock(x, y, Block(tile, collision, elevation)); +} diff --git a/src/scripting.cpp b/src/scripting.cpp new file mode 100644 index 00000000..48ec12e3 --- /dev/null +++ b/src/scripting.cpp @@ -0,0 +1,69 @@ +#include "scripting.h" +#include "log.h" + +QMap callbackFunctions = { + {OnBlockChanged, "on_block_changed"}, +}; + +Scripting *instance = nullptr; + +void Scripting::init(MainWindow *mainWindow) { + instance = new Scripting(mainWindow); +} + +Scripting::Scripting(MainWindow *mainWindow) { + this->engine = new QJSEngine(mainWindow); + this->engine->installExtensions(QJSEngine::ConsoleExtension); + this->engine->globalObject().setProperty("api", this->engine->newQObject(mainWindow)); + this->filepaths.append("D:\\devkitProOld\\msys\\home\\huder\\pretmap\\test_script.js"); + this->loadModules(this->filepaths); +} + +void Scripting::loadModules(QStringList moduleFiles) { + for (QString filepath : moduleFiles) { + QJSValue module = this->engine->importModule(filepath); + if (module.isError()) { + logError(QString("Failed to load custom script file '%1'").arg(filepath)); + continue; + } + + this->modules.append(module); + } +} + +void Scripting::invokeCallback(CallbackType type, QJSValueList args) { + for (QJSValue module : this->modules) { + QString functionName = callbackFunctions[type]; + QJSValue callbackFunction = module.property(functionName); + if (callbackFunction.isError()) { + continue; + } + + QJSValue result = callbackFunction.call(args); + if (result.isError()) { + logError(QString("Module %1 encountered an error when calling '%2'").arg(module.toString()).arg(functionName)); + continue; + } + } +} + +void Scripting::cb_MetatileChanged(int x, int y, Block prevBlock, Block newBlock) { + if (!instance) return; + + QJSValueList args { + x, + y, + instance->newBlockObject(prevBlock), + instance->newBlockObject(newBlock), + }; + instance->invokeCallback(OnBlockChanged, args); +} + +QJSValue Scripting::newBlockObject(Block block) { + QJSValue obj = this->engine->newObject(); + obj.setProperty("tile", block.tile); + obj.setProperty("collision", block.collision); + obj.setProperty("elevation", block.elevation); + obj.setProperty("rawValue", block.rawValue()); + return obj; +} diff --git a/src/ui/collisionpixmapitem.cpp b/src/ui/collisionpixmapitem.cpp index 94589a8d..252b9f1a 100644 --- a/src/ui/collisionpixmapitem.cpp +++ b/src/ui/collisionpixmapitem.cpp @@ -39,7 +39,7 @@ void CollisionPixmapItem::paint(QGraphicsSceneMouseEvent *event) { if (block) { block->collision = this->movementPermissionsSelector->getSelectedCollision(); block->elevation = this->movementPermissionsSelector->getSelectedElevation(); - map->_setBlock(x, y, *block); + map->setBlock(x, y, *block); } if (event->type() == QEvent::GraphicsSceneMouseRelease) { map->commit(); diff --git a/src/ui/mappixmapitem.cpp b/src/ui/mappixmapitem.cpp index cf67e8c9..412892d2 100644 --- a/src/ui/mappixmapitem.cpp +++ b/src/ui/mappixmapitem.cpp @@ -56,7 +56,7 @@ void MapPixmapItem::shift(QGraphicsSceneMouseEvent *event) { int blockIndex = j * map->getWidth() + i; Block srcBlock = backupBlockdata->blocks->at(blockIndex); - map->_setBlock(destX, destY, srcBlock); + map->setBlock(destX, destY, srcBlock); } delete backupBlockdata; @@ -96,7 +96,7 @@ void MapPixmapItem::paintNormal(int x, int y) { block->collision = selectedCollisions->at(index).first; block->elevation = selectedCollisions->at(index).second; } - map->_setBlock(actualX, actualY, *block); + map->setBlock(actualX, actualY, *block, true); } } } @@ -159,7 +159,7 @@ void MapPixmapItem::paintSmartPath(int x, int y) { block->collision = openTileCollision; block->elevation = openTileElevation; } - map->_setBlock(actualX, actualY, *block); + map->setBlock(actualX, actualY, *block); } } @@ -203,7 +203,7 @@ void MapPixmapItem::paintSmartPath(int x, int y) { block->collision = selectedCollisions->at(smartPathTable[id]).first; block->elevation = selectedCollisions->at(smartPathTable[id]).second; } - map->_setBlock(actualX, actualY, *block); + map->setBlock(actualX, actualY, *block); } } @@ -312,7 +312,7 @@ void MapPixmapItem::magicFill(QGraphicsSceneMouseEvent *event) { block->collision = selectedCollisions->at(index).first; block->elevation = selectedCollisions->at(index).second; } - map->_setBlock(x, y, *block); + map->setBlock(x, y, *block); } } } @@ -362,7 +362,7 @@ void MapPixmapItem::_floodFill(int initialX, int initialY) { block->collision = selectedCollisions->at(index).first; block->elevation = selectedCollisions->at(index).second; } - map->_setBlock(x, y, *block); + map->setBlock(x, y, *block); } if (!visited[x + 1 + y * map->getWidth()] && (block = map->getBlock(x + 1, y)) && block->tile == old_tile) { todo.append(QPoint(x + 1, y)); @@ -427,7 +427,7 @@ void MapPixmapItem::_floodFillSmartPath(int initialX, int initialY) { block->collision = openTileCollision; block->elevation = openTileElevation; } - map->_setBlock(x, y, *block); + map->setBlock(x, y, *block); if ((block = map->getBlock(x + 1, y)) && block->tile == old_tile) { todo.append(QPoint(x + 1, y)); } @@ -482,7 +482,7 @@ void MapPixmapItem::_floodFillSmartPath(int initialX, int initialY) { block->collision = selectedCollisions->at(smartPathTable[id]).first; block->elevation = selectedCollisions->at(smartPathTable[id]).second; } - map->_setBlock(x, y, *block); + map->setBlock(x, y, *block); // Visit neighbors if they are smart-path tiles, and don't revisit any. if (!visited[x + 1 + y * map->getWidth()] && (block = map->getBlock(x + 1, y)) && IS_SMART_PATH_TILE(block)) { diff --git a/test_script.js b/test_script.js new file mode 100644 index 00000000..52f654fd --- /dev/null +++ b/test_script.js @@ -0,0 +1,15 @@ +function randInt(min, max) { + min = Math.ceil(min); + max = Math.floor(max); + return Math.floor(Math.random() * (max - min)) + min; +} + +const grassTiles = [0x8, 0x9, 0x10, 0x11]; + +// Porymap callback when a block is painted. +export function on_block_changed(x, y, prevBlock, newBlock) { + if (grassTiles.indexOf(newBlock.tile) != -1) { + const i = randInt(0, grassTiles.length); + api.scriptapi_setBlock(x, y, grassTiles[i], newBlock.collision, newBlock.elevation); + } +}