-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add script API / make it scriptable #949
Comments
Did you have any idea as to how the feature would be implemented? Were you thinking a JS console? Macros and custom key binds? I'd be interested in helping add this feature. |
@kkworden Great to hear you'd like to help out with this! All your suggestions would be nice things to have eventually. My approach would be the following:
Since I'd like the scripting support to extend to a QtQuick based version of Tiled later, I think it may be good to do this in the |
Sounds interesting and something i would need at the moment. So i have to stick to exporting the map, modifying it with scripting and then reopening it. I would suggest the PostgreSQL approach of developing such a feature: Start with an easy implementation and make it more powerful over time. The first iteration could simply be the automappig feature implemented for scripting. So whenever the map is modified the script will be called with the changed element and the script can modify the map. This would enable:
|
I wanted to suggest the using Lua is the easiest way to implement this after just replacing a huge mess of AI code in a game to Lua. It's was really easy and had a very small learning curve. Here's a possible implementation idea:
function brush(x, y, palette)
if get_tile(x, y) == 1 then -- if the tile under the brush is id 1
set_tile(x, y, palette[1]) -- set the tile under the brush to tile #1 from the palette
end
end
Here's the basic outline of how the C bindings work but my pseudo code is Pascal: // create a new lua instance
// you'll want to keep track of this unless the user changes scripts on the brush
l := luaL_newstate();
luaL_openlibs(l);
// bind 2 c functions (LuaGetTile, LuaSetTile) to Lua
lua_pushcfunction(l, @LuaGetTile);
lua_setglobal(l, 'get_tile');
lua_pushcfunction(l, @LuaSetTile);
lua_setglobal(l, 'set_tile');
// run the lua script from a string you loaded early
luaL_dostring(l, scriptText);
// when the user uses the brush tool retrieve the lua instance you created early and call:
lua_getglobal(l, 'brush');
lua_pushnumber(L, 1);
// ??? we need an array of tile IDs passed in! I'll provide that code also since passing arrays to lua is confusing imo.
lua_pcall(L, 1, 0, 0);
// when get_tile() is called from lua it will run this C function (mine being Pascal, sorry)
function LuaGetTile (l: Plua_State): integer; cdecl;
var
x, y: integer;
tileID: string;
begin
// get the x and y coords that were passed from lua
x := luaL_checkinteger(L, 1);
y := luaL_checkinteger(L, 2);
// find the tile ID at x,y and return the value back to lua
tileID := FindTileID(x, y)
lua_pushstring(L, tileID);
result := 1;
end; This is trivially easy to implement and would mean the user could have precise control over what tile the brush will insert and what tiles are around the cursor. I'm happy to help answering any questions. |
@genericptr Thanks for the suggestion. Actually I know Lua inside out and have used it in several C++ hobby projects as well as at work, where I essentially code Lua full time. Yet, I'm not convinced Lua is the best choice here because Qt ships with a JavaScript engine and provides an easy way of exposing existing C++ objects to it. Also, when I finally get time to work on a "Tiled 2", then probably all the high-level stuff will be written with JavaScript (as part of Qt Quick). I think as languages go, Lua and JavaScript are very similar, though granted Lua is a lot less cryptic in its syntax. On the other hand, JavaScript is much more widely used. In the end though, it's more about what Qt supports and where I want to go with Tiled in the future. The danger with starting with two simple API functions like in your example, is that there will be no end to requests for more functionality to be exposed. Maybe the tool wants access to tile properties, maybe it wants to base its decisions on data from other layers, etc. So in the end, we need a solution that can provide a full API and does not require extra maintenance. |
If it's just as easy with JS than go that route for sure. I'd assume add a larger API eventually but you could implement something really powerful even with those 2 functions and build from there. Thanks. |
and there is a Lua module for QtScript: http://www.nongnu.org/libqtlua/ you can use this directly to get same ability as JavaScript in QtScript. |
@starwing I'm aware of QtLua. It would indeed help creating Lua APIs, but I'd still rather go with JavaScript since it's shipping with Qt and would anyway be the basis of a possible Qt Quick based Tiled. |
Combined with #1665 I think the following would be useful:
|
Currently the actions you can perform here are still very limited. There's the "tiled" module, which provides some simple properties, access to the document manager (which isn't very usable yet) and a way to trigger registered actions (and most global actions are now registered). Issue #949
Alright, I've finally pushed the initial change that adds some script evaluation capabilities to Tiled! It is already possible to automate some basic things, like applying automapping to all open maps. (That the console mentions the Python script path is a little confusing, but those are messages from the Python plugin whereas here you can (at least currently) only execute JavaScript) It is also possible to connect to signals, for example you can execute the following snippet to have Tiled write the file name of the current document every time you switch maps to the terminal: tiled.documentManager.currentDocumentChanged.connect(function() {
print(tiled.documentManager.currentDocument.fileName);
}); What is of course missing is most of the API that would actually be useful, along with ways to add custom actions or tools or some way to execute a script from a file. I'll be working on all of that in the coming weeks! Also, I need to adjust the autobuilds to ship the additional Qt module required for running JavaScript code, before I can push this in a snapshot. @tomcashman Thanks for listing some additional use-cases that could be covered by scripting! It's very helpful to have all these examples to see where the script API is still incomplete. |
Is it possible to see the whole API somewhere, so that I can see what is currently already possible? I tried to type a few commands to see whether it is possible to inspect the objects in the console: Another thing I noticed was that every object has a empty string 'objectName', and that sizes always have two properties. For example: EDIT: Never mind, I can find out by using Object.keys(tiled). |
@EJlol Yep, you can use Note that you can use |
@Ktar5 I've doing some other things like the 1.2.4 patch release, attending the GitHub Satellite event and recently been replacing the "auto-updater" with a simpler and cross-platform new version check and re-activated the snap builds. I did also upgrade the builds to Qt 5.12, but apart from that my previous comment is still relevant regarding the current state. As for the API, I'm making an effort to keep the reference updated continuously, it's available here. |
Tiled now searches a system-dependent "extensions" path, where each child folder is considered an extension. The extensions path can be easily opened from the Preferences > Plugins page. An extension can contain multiple .js files at its root, all of which will get evaluated. Extensions can also include icon files, which can then be used on scripted actions and tools. Issue #949
To enable this, the last relevant change signal, MapDocument::objectGroupChanged, was changed to the ObjectGroupChangeEvent, allowing an EditableObjectGroup and the EditableMapObject to be changed so that they only need to refer to an EditableAsset rather than an EditableMap. This means EditableTile::objectGroup can now return an EditableObjectGroup, and the Tile Collision Editor can use the change events to make sure its copy of the tile's object group is re-created when necessary. What doesn't work, is the "selected" property of map objects that are part of the tile's object group, since object selection still requires a MapDocument, and there is no way to access to the dummy document used by the Tile Collision Editor, which anyway uses a copy of the object group. To solve this, ideally there would not be a copy, but to avoid that would require way too many other changes at this point. Issue #949
I forgot to associate some commits with this issue. Here's a bit of progress:
I did not expect this task to be so immense. It seems like each property that I want to make accessible through the script API comes with its own challenges and open questions. There's a lot of boxes in my comment above still unchecked, but progress is being made! |
tiled.open(fileName) tiled.close(asset) tiled.reload(asset) Issue #949
The current tile stamp can now be accessed via the MapEditor.currentBrush property. If the tile stamp has multiple variations, this property returns the first one. To change the tile stamp, a map can be assigned to this property. Support for stamps with multiple variations may be added later. A scripted tool can now set a tile edit preview via the ScriptedTool.preview property. Soon it will also be possible to conveniently merge the preview to actually apply the change. Issue #949
This function makes it possible to prepare a TileMap() instance in advance and then to apply changes to multiple layers at once to the target map. It is mainly useful to apply the changes that have already been made visible through the ScriptedTool.preview. Issue #949
The 'toString' member for scripted map formats has now been renamed to 'write' and may return either a string or an ArrayBuffer object. When an error happens during export a backtrace is now reported in the Console and Issues views. Also, no longer fall back to "Export As" when triggering "Export" runs into an error. Turns out opening the file dialog after the error dialog is just annoying. Issue #949
Either binary or text files are supported. However, the scripting API is still severely lacking, so actually setting up the map in the read function is not possible yet. Problems due to missing API include: * Can't set the size of the map * Can't set up any tilesets, which means we can't assign any tiles unless we have the tileset already open in Tiled. Issue #949
Also refactored the EditableAsset and EditableMap a little, moving the undo stack back into Document and the 'resize' method back into MapDocument. This is to keep the "Editable" wrappers as small as possible since they are only meant to be used from the script. Issue #949
Added the following functions and property: * Tileset.addTile() * Tileset.removeTiles(tiles) * Tileset.setTileSize(width, height) * Tileset.image * Map.setTileSize(width, height) Made the following properties writable: * Tileset.tileWidth * Tileset.tileHeight Also made attempted Layer and Tileset modifications report read-only errors instead of silently doing nothing (in case of assets not loaded from a file). The API now finally enables implementing a reader for certain custom map formats. Issue #949
I've missed to refer to this issue, but today I made it possible to script custom tileset formats (in addition to the already supported custom map formats). However I did not have time to test this feature yet. A few days ago I announced the Tiled 1.3 Beta and today I have also announced the string freeze. The aim is to release Tiled 1.3 in one week. I realize there's still some scripting API gaps, like support for templates and Wang tiles, but I may choose to fill these only if there is demand because there are many more interesting improvements to be made. |
I've been trying to port my generic C export python script to the new javascript export but sadly there's a few missing features that makes it impossible to fully port it. It is possible to implement generic file I/O? Maybe similar to node.js fs? It's possible to silently abort export, either by Also want to let you know that throwing exceptions using |
Right, if you just throw a string then this is what will be returned on the C++ side apparently. For the C++ side to realize you've actually thrown an error you need to But throwing an error will actually get a report with full stack trace, which may not be what you want. I'll look into changing the API so that you can control the error shown in the dialog without throwing it.
I didn't immediately add it due to the security concerns with running potentially other people's scripts with full file system access. That said, there should definitely be a way to read other files. Maybe it's better to just put a security warning in the documentation for now.
Once there is support for reading other files, it should be possible to load files from within the extensions path using the Thanks for trying it out and the feedback! |
Based on the same API found in Qbs for reading and writing files in either text or binary mode. Issue #949
@justburn I've added API for reading and writing arbitrary files in either binary of text mode. Also, for reading text files you could alternatively use Unfortunately I'll need to delay the release, probably by another week. I'll consider pushing out another release candidate soon. I'll still try to find a solution to the error case as well. |
Thank you for the file API! Can't wait for the next release :) |
Rather than handling the return value as either a string or an ArrayBuffer to be written to a file, the script can use the TextFile or BinaryFile API to write to the file by itself. This also enables export formats that write several files, so the 'outputFiles' function is now available for custom map formats. When the 'write' function returns a non-empty string it is now shown as error message. Also made TextFile and BinaryFile use QSaveFile when writing, which adds the need to call commit() when done but avoids loss of data in case of disk errors or code errors. Also fixed BinaryFile.OpenMode enum to be actually exposed to the script. Issue #949
Alright, I've now changed the writing function signature, such that if it returns a string it is considered the error string, and for writing to the file it is expected to use the That concludes the support for scripting in Tiled 1.3! I will open new issues for the remaining gaps in the scripting API. |
I've opened #2662, #2663 and #2664 for the features that remained unchecked in my comment listing the things still to do. I've also opened #2665 for adding an API to work with XML files. |
Most people are not into C++ / Qt and compiling Tiled themselves, and as such small additions that would benefit their productivity are often not even started on. There should be an easy way to write scripts to perform actions in Tiled. Examples of things that should be possible:
At the moment I think JavaScript would be the best choice of scripting language, because a JavaScript engine is provided as part of Qt and because having a JavaScript Tiled API should help with the development of a QtQuick based version of Tiled.
Examples of where scripting would have been useful (also see issues referencing this issue below):
The text was updated successfully, but these errors were encountered: