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")),