diff --git a/NEWS.md b/NEWS.md index 168618e165..45ac6f586d 100644 --- a/NEWS.md +++ b/NEWS.md @@ -17,6 +17,7 @@ * Godot 4 plugin: Use Godot 4.2 tile transformation flags (by Rick Yorgason, #3895) * Godot 4 plugin: Fixed positioning of tile collision shapes (by Ryan Petrie, #3862) * GameMaker 2 plugin: Fixed positioning of objects on isometric maps +* Python plugin: Added support for implementing tileset formats (with Pablo Duboue, #3857) * Python plugin: Raised minimum Python version to 3.8 * tmxrasterizer: Added --hide-object and --show-object arguments (by Lars Luz, #3819) * tmxrasterizer: Added --frames and --frame-duration arguments to export animated maps as multiple images (#3868) diff --git a/docs/manual/python.rst b/docs/manual/python.rst index 8021a5fb23..4c0c786ea2 100644 --- a/docs/manual/python.rst +++ b/docs/manual/python.rst @@ -118,6 +118,16 @@ above script. This example does not support the use of group layers. +.. raw:: html + +
New in Tiled 1.11
+ +Tileset Plugins +--------------- + +To write tileset plugins, extend your class from ``tiled.TilesetPlugin`` +instead of ``tiled.Plugin``. + Debugging Your Script --------------------- diff --git a/src/plugins/python/python.qbs b/src/plugins/python/python.qbs index 99d23411b0..f1c959d3ed 100644 --- a/src/plugins/python/python.qbs +++ b/src/plugins/python/python.qbs @@ -34,8 +34,10 @@ TiledPlugin { Properties { condition: pkgConfigPython3.found - cpp.cxxFlags: outer.concat(pkgConfigPython3.cflags) + cpp.cxxFlags: outer.concat(pkgConfigPython3.compilerFlags) + cpp.defines: pkgConfigPython3.defines cpp.dynamicLibraries: pkgConfigPython3.libraries + cpp.includePaths: pkgConfigPython3.includePaths cpp.libraryPaths: pkgConfigPython3.libraryPaths cpp.linkerFlags: pkgConfigPython3.linkerFlags } diff --git a/src/plugins/python/pythonbind.cpp b/src/plugins/python/pythonbind.cpp index e8475f27b8..18d5f5cb3d 100644 --- a/src/plugins/python/pythonbind.cpp +++ b/src/plugins/python/pythonbind.cpp @@ -72,6 +72,17 @@ typedef struct { extern PyTypeObject PyPythonPythonScript_Type; + +typedef struct { + PyObject_HEAD + Python::PythonTilesetScript *obj; + PyObject *inst_dict; + PyBindGenWrapperFlags flags:8; +} PyPythonPythonTilesetScript; + + +extern PyTypeObject PyPythonPythonTilesetScript_Type; + /* --- forward declarations --- */ @@ -7996,6 +8007,102 @@ PyTypeObject PyPythonPythonScript_Type = { }; + + +static int +_wrap_PyPythonPythonTilesetScript__tp_init(void) +{ + PyErr_SetString(PyExc_TypeError, "class 'PythonTilesetScript' cannot be constructed ()"); + return -1; +} + +static PyMethodDef PyPythonPythonTilesetScript_methods[] = { + {NULL, NULL, 0, NULL} +}; + +static void +PyPythonPythonTilesetScript__tp_clear(PyPythonPythonTilesetScript *self) +{ + Py_CLEAR(self->inst_dict); + Python::PythonTilesetScript *tmp = self->obj; + self->obj = NULL; + if (!(self->flags&PYBINDGEN_WRAPPER_FLAG_OBJECT_NOT_OWNED)) { + delete tmp; + } +} + + +static int +PyPythonPythonTilesetScript__tp_traverse(PyPythonPythonTilesetScript *self, visitproc visit, void *arg) +{ + Py_VISIT(self->inst_dict); + + return 0; +} + + +static void +_wrap_PyPythonPythonTilesetScript__tp_dealloc(PyPythonPythonTilesetScript *self) +{ + PyPythonPythonTilesetScript__tp_clear(self); + Py_TYPE(self)->tp_free((PyObject*)self); +} + +PyTypeObject PyPythonPythonTilesetScript_Type = { + PyVarObject_HEAD_INIT(NULL, 0) + (char *) "tiled.PythonTilesetScript", /* tp_name */ + sizeof(PyPythonPythonTilesetScript), /* tp_basicsize */ + 0, /* tp_itemsize */ + /* methods */ + (destructor)_wrap_PyPythonPythonTilesetScript__tp_dealloc, /* tp_dealloc */ + (printfunc)0, /* tp_print */ + (getattrfunc)NULL, /* tp_getattr */ + (setattrfunc)NULL, /* tp_setattr */ +#if PY_MAJOR_VERSION >= 3 + NULL, +#else + (cmpfunc)NULL, /* tp_compare */ +#endif + (reprfunc)NULL, /* tp_repr */ + (PyNumberMethods*)NULL, /* tp_as_number */ + (PySequenceMethods*)NULL, /* tp_as_sequence */ + (PyMappingMethods*)NULL, /* tp_as_mapping */ + (hashfunc)NULL, /* tp_hash */ + (ternaryfunc)NULL, /* tp_call */ + (reprfunc)NULL, /* tp_str */ + (getattrofunc)NULL, /* tp_getattro */ + (setattrofunc)NULL, /* tp_setattro */ + (PyBufferProcs*)NULL, /* tp_as_buffer */ + Py_TPFLAGS_BASETYPE|Py_TPFLAGS_DEFAULT|Py_TPFLAGS_HAVE_GC, /* tp_flags */ + "", /* Documentation string */ + (traverseproc)PyPythonPythonTilesetScript__tp_traverse, /* tp_traverse */ + (inquiry)PyPythonPythonTilesetScript__tp_clear, /* tp_clear */ + (richcmpfunc)NULL, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + (getiterfunc)NULL, /* tp_iter */ + (iternextfunc)NULL, /* tp_iternext */ + (struct PyMethodDef*)PyPythonPythonTilesetScript_methods, /* tp_methods */ + (struct PyMemberDef*)0, /* tp_members */ + 0, /* tp_getset */ + NULL, /* tp_base */ + NULL, /* tp_dict */ + (descrgetfunc)NULL, /* tp_descr_get */ + (descrsetfunc)NULL, /* tp_descr_set */ + offsetof(PyPythonPythonTilesetScript, inst_dict), /* tp_dictoffset */ + (initproc)_wrap_PyPythonPythonTilesetScript__tp_init, /* tp_init */ + (allocfunc)PyType_GenericAlloc, /* tp_alloc */ + (newfunc)PyType_GenericNew, /* tp_new */ + (freefunc)0, /* tp_free */ + (inquiry)NULL, /* tp_is_gc */ + NULL, /* tp_bases */ + NULL, /* tp_mro */ + NULL, /* tp_cache */ + NULL, /* tp_subclasses */ + NULL, /* tp_weaklist */ + (destructor) NULL /* tp_del */ +}; + + #if PY_VERSION_HEX >= 0x03000000 static struct PyModuleDef tiled_moduledef = { PyModuleDef_HEAD_INIT, @@ -8041,6 +8148,11 @@ MOD_INIT(tiled) return MOD_ERROR; } PyModule_AddObject(m, (char *) "Plugin", (PyObject *) &PyPythonPythonScript_Type); + /* Register the 'Python::PythonTilesetScript' class */ + if (PyType_Ready(&PyPythonPythonTilesetScript_Type)) { + return MOD_ERROR; + } + PyModule_AddObject(m, (char *) "TilesetPlugin", (PyObject *) &PyPythonPythonTilesetScript_Type); submodule = inittiled_qt(); if (submodule == NULL) { return MOD_ERROR; @@ -8083,6 +8195,37 @@ int _wrap_convert_py2c__Tiled__Map___star__(PyObject *value, Tiled::Map * *addre return 1; } +int _wrap_convert_py2c__Tiled__SharedTileset___star__(PyObject *value, Tiled::SharedTileset * *address) +{ + PyObject *py_retval; + PyTiledSharedTileset *tmp_SharedTileset; + + py_retval = Py_BuildValue((char *) "(O)", value); + if (!PyArg_ParseTuple(py_retval, (char *) "O!", &PyTiledSharedTileset_Type, &tmp_SharedTileset)) { + Py_DECREF(py_retval); + return 0; + } + *address = new Tiled::SharedTileset(*tmp_SharedTileset->obj); + Py_DECREF(py_retval); + return 1; +} + +PyObject* _wrap_convert_c2py__Tiled__Tileset_const(Tiled::Tileset const *cvalue) +{ + PyObject *py_retval; + PyTiledTileset *py_Tileset; + + if (!cvalue) { + Py_INCREF(Py_None); + return Py_None; + } + py_Tileset = PyObject_New(PyTiledTileset, &PyTiledTileset_Type); + py_Tileset->obj = (Tiled::Tileset *) cvalue; + py_Tileset->flags = PYBINDGEN_WRAPPER_FLAG_OBJECT_NOT_OWNED; + py_retval = Py_BuildValue((char *) "N", py_Tileset); + return py_retval; +} + PyObject* _wrap_convert_c2py__Tiled__Map_const___star__(Tiled::Map const * *cvalue) { diff --git a/src/plugins/python/pythonplugin.cpp b/src/plugins/python/pythonplugin.cpp index de137f7c0f..7d29178f8e 100644 --- a/src/plugins/python/pythonplugin.cpp +++ b/src/plugins/python/pythonplugin.cpp @@ -21,7 +21,6 @@ #include "pythonplugin.h" #include "logginginterface.h" -#include "map.h" #include #include @@ -29,8 +28,10 @@ PyMODINIT_FUNC PyInit_tiled(void); extern int _wrap_convert_py2c__Tiled__Map___star__(PyObject *obj, Tiled::Map * *address); +extern int _wrap_convert_py2c__Tiled__SharedTileset___star__(PyObject *obj, Tiled::SharedTileset * *address); extern PyObject* _wrap_convert_c2py__Tiled__Map_const___star__(Tiled::Map const * *cvalue); extern PyObject* _wrap_convert_c2py__Tiled__LoggingInterface(Tiled::LoggingInterface *cvalue); +extern PyObject* _wrap_convert_c2py__Tiled__Tileset_const(Tiled::Tileset const *cvalue); namespace Python { @@ -48,6 +49,7 @@ static void handleError() PythonPlugin::PythonPlugin() : mScriptDir(QDir::homePath() + "/.tiled") , mPluginClass(nullptr) + , mTilesetPluginClass(nullptr) { mReloadTimer.setSingleShot(true); mReloadTimer.setInterval(1000); @@ -63,12 +65,18 @@ PythonPlugin::PythonPlugin() PythonPlugin::~PythonPlugin() { - for (const ScriptEntry &script : mScripts) { + for (const ScriptEntry &script : std::as_const(mScripts)) { Py_DECREF(script.module); - Py_DECREF(script.mapFormat->pythonClass()); + + if (script.mapFormat) + Py_DECREF(script.mapFormat->pythonClass()); + + if (script.tilesetFormat) + Py_DECREF(script.tilesetFormat->pythonClass()); } Py_XDECREF(mPluginClass); + Py_XDECREF(mTilesetPluginClass); Py_Finalize(); } @@ -101,6 +109,7 @@ void PythonPlugin::initialize() if (pmod) { PyObject *tiledPlugin = PyObject_GetAttrString(pmod, "Plugin"); + PyObject *tiledTilesetPlugin = PyObject_GetAttrString(pmod, "TilesetPlugin"); Py_DECREF(pmod); if (tiledPlugin) { @@ -110,6 +119,13 @@ void PythonPlugin::initialize() Py_DECREF(tiledPlugin); } } + if (tiledTilesetPlugin) { + if (PyCallable_Check(tiledTilesetPlugin)) { + mTilesetPluginClass = tiledTilesetPlugin; + } else { + Py_DECREF(tiledTilesetPlugin); + } + } } if (!mPluginClass) { @@ -118,6 +134,12 @@ void PythonPlugin::initialize() return; } + if (!mTilesetPluginClass) { + Tiled::ERROR("Can't find tiled.TilesetPlugin baseclass"); + handleError(); + return; + } + // w/o differentiating error messages could just rename "log" // to "write" in the binding and assign plugin directly to stdout/stderr PySys_SetObject((char *)"_tiledplugin", @@ -181,18 +203,17 @@ void PythonPlugin::reloadModules() Py_DECREF(pluginClass); } + if (script.tilesetFormat) { + PyObject *pluginClass = script.tilesetFormat->pythonClass(); + Py_DECREF(pluginClass); + } + if (loadOrReloadModule(script)) { mScripts.insert(name, script); } else { if (!script.module) { PySys_WriteStderr("** Parse exception **\n"); PyErr_Print(); - PyErr_Clear(); - } - - if (script.mapFormat) { - removeObject(script.mapFormat); - delete script.mapFormat; } } } @@ -202,27 +223,36 @@ void PythonPlugin::reloadModules() } /** - * Finds the first Python class that extends tiled.Plugin + * Finds the first Python class that extends the given \a pluginClass. */ -PyObject *PythonPlugin::findPluginSubclass(PyObject *module) +PyObject *PythonPlugin::findPluginSubclass(PyObject *module, PyObject *pluginClass) { - PyObject *dir = PyObject_Dir(module); PyObject *result = nullptr; - for (int i = 0; i < PyList_Size(dir); i++) { - PyObject *value = PyObject_GetAttr(module, PyList_GetItem(dir, i)); + PyObject *dir = PyObject_Dir(module); + if (!dir) { + handleError(); + return result; + } + const int dirSize = PyList_Size(dir); + for (int i = 0; i < dirSize; i++) { + PyObject *value = PyObject_GetAttr(module, PyList_GetItem(dir, i)); if (!value) { handleError(); break; } - if (value != mPluginClass && - PyCallable_Check(value) && - PyObject_IsSubclass(value, mPluginClass) == 1) { - result = value; - handleError(); - break; + if (value != pluginClass && PyCallable_Check(value)) { + const int isSubclass = PyObject_IsSubclass(value, pluginClass); + + if (isSubclass == -1) { + // usually "TypeError: issubclass() arg 1 must be a class" + PyErr_Clear(); + } else if (isSubclass == 1) { + result = value; + break; + } } Py_DECREF(value); @@ -247,98 +277,58 @@ bool PythonPlugin::loadOrReloadModule(ScriptEntry &script) script.module = PyImport_ImportModule(name.constData()); } - if (!script.module) - return false; + PyObject *pluginClass = nullptr; + PyObject *tilesetPluginClass = nullptr; - PyObject *pluginClass = findPluginSubclass(script.module); + if (script.module) { + pluginClass = findPluginSubclass(script.module, mPluginClass); + tilesetPluginClass = findPluginSubclass(script.module, mTilesetPluginClass); + } - if (!pluginClass) { - PySys_WriteStderr("Extension of tiled.Plugin not defined in " - "script: %s\n", name.constData()); - return false; + if (pluginClass) { + if (script.mapFormat) { + script.mapFormat->setPythonClass(pluginClass); + } else { + PySys_WriteStdout("---- Map plugin\n"); + script.mapFormat = new PythonMapFormat(name, pluginClass, this); + addObject(script.mapFormat); + } + } else if (script.mapFormat) { + removeObject(script.mapFormat); + delete script.mapFormat; } - if (script.mapFormat) { - script.mapFormat->setPythonClass(pluginClass); - } else { - script.mapFormat = new PythonMapFormat(name, pluginClass, this); - addObject(script.mapFormat); + if (tilesetPluginClass) { + if (script.tilesetFormat) { + script.tilesetFormat->setPythonClass(tilesetPluginClass); + } else { + PySys_WriteStdout("---- Tileset plugin\n"); + script.tilesetFormat = new PythonTilesetFormat(name, tilesetPluginClass, this); + addObject(script.tilesetFormat); + } + } else if (script.tilesetFormat) { + removeObject(script.tilesetFormat); + delete script.tilesetFormat; + } + + if (!pluginClass && !tilesetPluginClass) { + PySys_WriteStderr("No extension of tiled.Plugin or tiled.TilesetPlugin defined in " + "script: %s\n", name.constData()); + return false; } return true; } -PythonMapFormat::PythonMapFormat(const QString &scriptFile, - PyObject *class_, - QObject *parent) - : MapFormat(parent) - , mClass(nullptr) +PythonFormat::PythonFormat(const QString &scriptFile, PyObject *class_) + : mClass(nullptr) , mScriptFile(scriptFile) { setPythonClass(class_); } -std::unique_ptr PythonMapFormat::read(const QString &fileName) -{ - mError = QString(); - - Tiled::INFO(tr("-- Using script %1 to read %2").arg(mScriptFile, fileName)); - - if (!PyObject_HasAttrString(mClass, "read")) { - mError = "Please define class that extends tiled.Plugin and " - "has @classmethod read(cls, filename)"; - return nullptr; - } - PyObject *pinst = PyObject_CallMethod(mClass, (char *)"read", - (char *)"(s)", fileName.toUtf8().constData()); - - Tiled::Map *ret = nullptr; - if (!pinst) { - PySys_WriteStderr("** Uncaught exception in script **\n"); - } else { - _wrap_convert_py2c__Tiled__Map___star__(pinst, &ret); - Py_DECREF(pinst); - } - handleError(); - - if (ret) - ret->setProperty("__script__", mScriptFile); - return std::unique_ptr(ret); -} - -bool PythonMapFormat::write(const Tiled::Map *map, const QString &fileName, Options options) -{ - Q_UNUSED(options) - - mError = QString(); - - Tiled::INFO(tr("-- Using script %1 to write %2").arg(mScriptFile, fileName)); - - PyObject *pmap = _wrap_convert_c2py__Tiled__Map_const___star__(&map); - if (!pmap) - return false; - PyObject *pinst = PyObject_CallMethod(mClass, - (char *)"write", (char *)"(Ns)", - pmap, - fileName.toUtf8().constData()); - - if (!pinst) { - PySys_WriteStderr("** Uncaught exception in script **\n"); - mError = tr("Uncaught exception in script. Please check console."); - } else { - bool ret = PyObject_IsTrue(pinst); - Py_DECREF(pinst); - if (!ret) - mError = tr("Script returned false. Please check console."); - return ret; - } - - handleError(); - return false; -} - -bool PythonMapFormat::supportsFile(const QString &fileName) const +bool PythonFormat::_supportsFile(const QString &fileName) const { if (!PyObject_HasAttrString(mClass, "supportsFile")) return false; @@ -357,7 +347,7 @@ bool PythonMapFormat::supportsFile(const QString &fileName) const return ret; } -QString PythonMapFormat::nameFilter() const +QString PythonFormat::_nameFilter() const { QString ret; @@ -385,7 +375,7 @@ QString PythonMapFormat::nameFilter() const return ret; } -QString PythonMapFormat::shortName() const +QString PythonFormat::_shortName() const { QString ret; @@ -393,7 +383,7 @@ QString PythonMapFormat::shortName() const PyObject *pfun = PyObject_GetAttrString(mClass, "shortName"); if (!pfun || !PyCallable_Check(pfun)) { PySys_WriteStderr("Plugin extension doesn't define \"shortName\". Falling back to \"nameFilter\"\n"); - return nameFilter(); + return _nameFilter(); } // have fun @@ -413,16 +403,16 @@ QString PythonMapFormat::shortName() const return ret; } -QString PythonMapFormat::errorString() const +QString PythonFormat::_errorString() const { return mError; } -void PythonMapFormat::setPythonClass(PyObject *class_) +void PythonFormat::setPythonClass(PyObject *class_) { mClass = class_; - mCapabilities = NoCapability; + mCapabilities = Tiled::FileFormat::NoCapability; // @classmethod nameFilter(cls) if (PyObject_HasAttrString(mClass, "nameFilter")) { // @classmethod write(cls, map, filename) @@ -439,4 +429,138 @@ void PythonMapFormat::setPythonClass(PyObject *class_) } } + +PythonMapFormat::PythonMapFormat(const QString &scriptFile, + PyObject *class_, + QObject *parent) + : MapFormat(parent) + , PythonFormat(scriptFile, class_) +{ +} + +std::unique_ptr PythonMapFormat::read(const QString &fileName) +{ + mError = QString(); + + Tiled::INFO(tr("-- Using script %1 to read %2").arg(mScriptFile, fileName)); + + if (!PyObject_HasAttrString(mClass, "read")) { + mError = "Please define class that extends tiled.Plugin and " + "has @classmethod read(cls, filename)"; + return nullptr; + } + PyObject *pinst = PyObject_CallMethod(mClass, (char *)"read", + (char *)"(s)", fileName.toUtf8().constData()); + + Tiled::Map *ret = nullptr; + if (!pinst) { + PySys_WriteStderr("** Uncaught exception in script **\n"); + } else { + _wrap_convert_py2c__Tiled__Map___star__(pinst, &ret); + Py_DECREF(pinst); + } + handleError(); + + if (ret) + ret->setProperty("__script__", mScriptFile); + return std::unique_ptr(ret); +} + +bool PythonMapFormat::write(const Tiled::Map *map, const QString &fileName, Options options) +{ + Q_UNUSED(options) + + mError = QString(); + + Tiled::INFO(tr("-- Using script %1 to write %2").arg(mScriptFile, fileName)); + + PyObject *pmap = _wrap_convert_c2py__Tiled__Map_const___star__(&map); + if (!pmap) + return false; + PyObject *pinst = PyObject_CallMethod(mClass, + (char *)"write", (char *)"(Ns)", + pmap, + fileName.toUtf8().constData()); + + if (!pinst) { + PySys_WriteStderr("** Uncaught exception in script **\n"); + mError = tr("Uncaught exception in script. Please check console."); + } else { + bool ret = PyObject_IsTrue(pinst); + Py_DECREF(pinst); + if (!ret) + mError = tr("Script returned false. Please check console."); + return ret; + } + + handleError(); + return false; +} + + +PythonTilesetFormat::PythonTilesetFormat(const QString &scriptFile, + PyObject *class_, + QObject *parent) + : TilesetFormat(parent) + , PythonFormat(scriptFile, class_) +{ +} + +Tiled::SharedTileset PythonTilesetFormat::read(const QString &fileName) +{ + mError = QString(); + + Tiled::INFO(tr("-- Using script %1 to read %2").arg(mScriptFile, fileName)); + + if (!PyObject_HasAttrString(mClass, "read")) { + mError = "Please define class that extends tiled.TilesetPlugin and " + "has @classmethod read(cls, filename)"; + return nullptr; + } + PyObject *pinst = PyObject_CallMethod(mClass, (char *)"read", + (char *)"(s)", fileName.toUtf8().constData()); + + Tiled::SharedTileset *ret = nullptr; + if (!pinst) { + PySys_WriteStderr("** Uncaught exception in script **\n"); + } else { + _wrap_convert_py2c__Tiled__SharedTileset___star__(pinst, &ret); + Py_DECREF(pinst); + } + handleError(); + + return *ret; +} + +bool PythonTilesetFormat::write(const Tiled::Tileset &tileset, const QString &fileName, Options options) +{ + Q_UNUSED(options) + + mError = QString(); + + Tiled::INFO(tr("-- Using script %1 to write %2").arg(mScriptFile, fileName)); + + PyObject *ptileset = _wrap_convert_c2py__Tiled__Tileset_const(&tileset); + if (!ptileset) + return false; + PyObject *pinst = PyObject_CallMethod(mClass, + (char *)"write", (char *)"(Ns)", + ptileset, + fileName.toUtf8().constData()); + + if (!pinst) { + PySys_WriteStderr("** Uncaught exception in script **\n"); + mError = tr("Uncaught exception in script. Please check console."); + } else { + bool ret = PyObject_IsTrue(pinst); + Py_DECREF(pinst); + if (!ret) + mError = tr("Script returned false. Please check console."); + return ret; + } + + handleError(); + return false; +} + } // namespace Python diff --git a/src/plugins/python/pythonplugin.h b/src/plugins/python/pythonplugin.h index 9485fb252f..03cbf046de 100644 --- a/src/plugins/python/pythonplugin.h +++ b/src/plugins/python/pythonplugin.h @@ -28,6 +28,7 @@ #include "mapformat.h" #include "plugin.h" +#include "tilesetformat.h" #include #include @@ -41,17 +42,14 @@ class Map; namespace Python { class PythonMapFormat; +class PythonTilesetFormat; struct ScriptEntry { - ScriptEntry() - : module(nullptr) - , mapFormat(nullptr) - {} - QString name; - PyObject *module; - PythonMapFormat *mapFormat; + PyObject *module = nullptr; + PythonMapFormat *mapFormat = nullptr; + PythonTilesetFormat *tilesetFormat = nullptr; }; class Q_DECL_EXPORT PythonPlugin : public Tiled::Plugin @@ -70,11 +68,12 @@ class Q_DECL_EXPORT PythonPlugin : public Tiled::Plugin void reloadModules(); bool loadOrReloadModule(ScriptEntry &script); - PyObject *findPluginSubclass(PyObject *module); + PyObject *findPluginSubclass(PyObject *module, PyObject *pluginClass); QString mScriptDir; QMap mScripts; PyObject *mPluginClass; + PyObject *mTilesetPluginClass; QFileSystemWatcher mFileSystemWatcher; QTimer mReloadTimer; @@ -91,8 +90,38 @@ class PythonScript { QString nameFilter() const; }; +// Class exposed for Python scripts to extend +class PythonTilesetScript { +public: + // perhaps provide default that throws NotImplementedError + Tiled::SharedTileset *read(const QString &fileName); + bool supportsFile(const QString &fileName) const; + bool write(const Tiled::Tileset &tileset, const QString &fileName); + QString nameFilter() const; +}; + +class PythonFormat +{ +public: + PyObject *pythonClass() const { return mClass; } + void setPythonClass(PyObject *class_); + +protected: + PythonFormat(const QString &scriptFile, PyObject *class_); + + bool _supportsFile(const QString &fileName) const; + + QString _nameFilter() const; + QString _shortName() const; + QString _errorString() const; + + PyObject *mClass; + QString mScriptFile; + QString mError; + Tiled::FileFormat::Capabilities mCapabilities; +}; -class PythonMapFormat : public Tiled::MapFormat +class PythonMapFormat : public Tiled::MapFormat, public PythonFormat { Q_OBJECT Q_INTERFACES(Tiled::MapFormat) @@ -102,25 +131,38 @@ class PythonMapFormat : public Tiled::MapFormat PyObject *class_, QObject *parent = nullptr); - Capabilities capabilities() const override { return mCapabilities; } + Capabilities capabilities() const override { return mCapabilities; }; std::unique_ptr read(const QString &fileName) override; - bool supportsFile(const QString &fileName) const override; + bool supportsFile(const QString &fileName) const override { return _supportsFile(fileName); } bool write(const Tiled::Map *map, const QString &fileName, Options options) override; - QString nameFilter() const override; - QString shortName() const override; - QString errorString() const override; + QString nameFilter() const override { return _nameFilter(); } + QString shortName() const override { return _shortName(); } + QString errorString() const override { return _errorString(); } +}; - PyObject *pythonClass() const { return mClass; } - void setPythonClass(PyObject *class_); +class PythonTilesetFormat : public Tiled::TilesetFormat, public PythonFormat +{ + Q_OBJECT + Q_INTERFACES(Tiled::TilesetFormat) -private: - PyObject *mClass; - QString mScriptFile; - QString mError; - Capabilities mCapabilities; +public: + PythonTilesetFormat(const QString &scriptFile, + PyObject *class_, + QObject *parent = nullptr); + + Capabilities capabilities() const override { return mCapabilities; }; + + Tiled::SharedTileset read(const QString &fileName) override; + bool supportsFile(const QString &fileName) const override { return _supportsFile(fileName); } + + bool write(const Tiled::Tileset &tileset, const QString &fileName, Options options) override; + + QString nameFilter() const override { return _nameFilter(); } + QString shortName() const override { return _shortName(); } + QString errorString() const override { return _errorString(); } }; } // namespace Python diff --git a/src/plugins/python/scripts/tileset.py b/src/plugins/python/scripts/tileset.py new file mode 100644 index 0000000000..e2a66d0239 --- /dev/null +++ b/src/plugins/python/scripts/tileset.py @@ -0,0 +1,24 @@ +""" +Trivial example of a Tileset export plugin. Place it under ~/.tiled. +2024, +""" + +from tiled import * + +class TExample(TilesetPlugin): + @classmethod + def nameFilter(cls): + return "TExample files (*.texample)" + + @classmethod + def shortName(cls): + return "texample" + + @classmethod + def write(cls, tileset, fileName): + with open(fileName, 'w') as f: + f.write("{}\n".format(tileset.tileCount())) + for idx in range(tileset.tileCount()): + tile = tileset.tileAt(idx) + f.write("\t{}. {}: {} {}x{}\n".format(idx, tile.id(), tile.type(), tile.width(), tile.height())) + return True diff --git a/src/plugins/python/tiledbinding.py b/src/plugins/python/tiledbinding.py index 3895b70960..2901f270aa 100755 --- a/src/plugins/python/tiledbinding.py +++ b/src/plugins/python/tiledbinding.py @@ -398,7 +398,7 @@ def _decorate(obj, *args, **kwargs): """) """ - C++ class PythonScript is seen as Tiled.Plugin from Python script + C++ class PythonScript is seen as tiled.Plugin from Python script (naming describes the opposite side from either perspective) """ cls_pp = mod.add_class('PythonScript', @@ -406,6 +406,15 @@ def _decorate(obj, *args, **kwargs): foreign_cpp_namespace='Python', custom_name='Plugin') +""" + C++ class PythonTilesetScript is seen as tiled.TilesetPlugin from + Python script (naming describes the opposite side from either perspective) +""" +cls_ptp = mod.add_class('PythonTilesetScript', + allow_subclassing=True, + foreign_cpp_namespace='Python', + custom_name='TilesetPlugin') + """ PythonPlugin implements LoggingInterface for messaging to Tiled """ @@ -453,6 +462,37 @@ def _decorate(obj, *args, **kwargs): Py_DECREF(py_retval); return 1; } + +int _wrap_convert_py2c__Tiled__SharedTileset___star__(PyObject *value, Tiled::SharedTileset * *address) +{ + PyObject *py_retval; + PyTiledSharedTileset *tmp_SharedTileset; + + py_retval = Py_BuildValue((char *) "(O)", value); + if (!PyArg_ParseTuple(py_retval, (char *) "O!", &PyTiledSharedTileset_Type, &tmp_SharedTileset)) { + Py_DECREF(py_retval); + return 0; + } + *address = new Tiled::SharedTileset(*tmp_SharedTileset->obj); + Py_DECREF(py_retval); + return 1; +} + +PyObject* _wrap_convert_c2py__Tiled__Tileset_const(Tiled::Tileset const *cvalue) +{ + PyObject *py_retval; + PyTiledTileset *py_Tileset; + + if (!cvalue) { + Py_INCREF(Py_None); + return Py_None; + } + py_Tileset = PyObject_New(PyTiledTileset, &PyTiledTileset_Type); + py_Tileset->obj = (Tiled::Tileset *) cvalue; + py_Tileset->flags = PYBINDGEN_WRAPPER_FLAG_OBJECT_NOT_OWNED; + py_retval = Py_BuildValue((char *) "N", py_Tileset); + return py_retval; +} """, file=fh) #mod.generate_c_to_python_type_converter( # utils.eval_retval(retval("Tiled::LoggingInterface")),