Played around with it a bit, and made one that just deletes the contents of the selected text file(s) for no reason.
Code
local M = {}
function M.get_commands()
return {
{
label = "Delete",
locations = {"Edit", "Assets", "Outline"},
query = {
selection = {type = "resource", cardinality = "many"}
},
active = function(opts)
return true
end,
run = function(opts)
local actions = {}
for _,id in ipairs(opts.selection) do
table.insert(actions, {action = "set", node_id = id, property = "text", value = ""})
end
return actions
end
}
}
end
return M
Splendid!
Pretty sad we cannot yet modify collection files. I tried to make an editor script that automatizes creating collection of levels, and catched this error:
ERROR:EXT: Pack levels's "run" in /main/collection_packer.editor_script failed:
ERROR:EXT: No method in multimethod 'transaction-action->txs' for dispatch value: ["set" :editor.collection/CollectionNode "text"]
The code:
local M = {}
M.target_file = nil
function M.get_commands()
return {
{
label = 'Pack levels',
locations = {'Assets'},
query = {
selection = { type = 'resource', cardinality = 'many' }
},
active = function(opts)
for _, node_id in ipairs(opts.selection) do
if not M.is_collection(node_id) then
return false
end
end
return true
end,
run = M.pack_collections,
},
{
label = 'Set as levels collection',
locations = {'Assets'},
query = {
selection = { type = 'resource', cardinality = 'one' }
},
active = function(opts)
return M.is_collection(opts.selection)
end,
run = function(opts)
local path = editor.get(opts.selection, 'path')
print('Pack target is set to '..path)
M.target_file = opts.selection
end,
}
}
end
function M.is_collection(node_id)
local path = editor.get(node_id, 'path')
return path:find('.collection', nil, true)
end
local function make_embedded_factory(path, exclude)
return [[embedded_instances {
id: "]]..path..[["
data: "embedded_components {\n"
" id: \"collectionproxy\"\n"
" type: \"collectionproxy\"\n"
" data: \"collection: \\\"]]..path..[[\\\"\\n"
"exclude: ]]..tostring(exclude)..[[\\n"
"\"\n"
" position {\n"
" x: 0.0\n"
" y: 0.0\n"
" z: 0.0\n"
" }\n"
" rotation {\n"
" x: 0.0\n"
" y: 0.0\n"
" z: 0.0\n"
" w: 1.0\n"
" }\n"
"}\n"
""
position {
x: 0.0
y: 0.0
z: 0.0
}
rotation {
x: 0.0
y: 0.0
z: 0.0
w: 1.0
}
scale3 {
x: 1.0
y: 1.0
z: 1.0
}
}
]]
end
function M.pack_collections(opts)
assert(M.target_file, 'No target file specified. Use "Set as levels collection" first')
local content = [[name: "pack"
scale_along_z: 0
]]
for _, node_id in ipairs(opts.selection) do
filename = editor.get(node_id, 'path')
content = content .. make_embedded_factory(filename, false)
end
return {
{
action = 'set',
node_id = M.target_file,
property = 'text',
value = content,
},
}
end
return M
Hi! Error happens because from the point of view of an editor, collections are not text files: they are structured data structures. This allows editor to automatically fix references to collections from other collections while renaming them. We want to expand over time on what properties are available for different nodes, but for now you can just write to a collection file with io.open(M.target_file, "w"):write(...).
Thanks for explanations!
It’s cool that different file types have different scripting interfaces, shooting yourself in the foot became less handy (sigh). Editor scripting feels easy and handy unless you hit limitations. I already can make most things I planned, though it can take much more effort than I planned.
I’ll improve error message for invalid input to editor.get.
Project → Reload Editor Scripts (it’s mentioned in Editor Script Runtime section of a manual).
You can just write a function that receives node id and expected file extension and checks if path of this node has this extension, is it not enough? What else is cumbersome here?
Yes, that’s what I did, however almost every editor script I can imagine will contain this function, so maybe it could be part of editor module or query.selection?
OK, I’m just messing around learning how this works, so I’m probably doing something wrong, but it seems like a command may not be run depending on what you have selected?
My Test Code...
local M = {}
local runIdx = 1
local commands = {
{
label = "Test Command",
locations = {"Edit", "Assets", "Outline"},
query = {
selection = { type = "resource", cardinality = "many" }
},
run = function(opts)
print("action run", runIdx)
runIdx = runIdx + 1
return {}
end
},
}
function M.get_commands()
print("Test Editor Extension Loaded.")
return commands
end
return M
With that, if I have an embedded (not from another file) game object or component selected, the run function doesn’t get called. If I have the root of the .go or .collection file selected, or anything instanced from another file, it does run.
Add new collection --> right-click base node and click custom command --> it runs.
…in new collection --> press A to add new game object --> right click that go and click custom command --> nothing.
@ross.grams That’s because currently selection query supports only resources, which are files on a file system. Root of a .go or .collection file has corresponding file, but embedded components do not have their own files, that’s why you can’t target them.
Got it, thanks. Makes sense. And uhh, I guess I should have read the manual more closely!
…currently only "resource" is allowed. In Assets and Outline, resource is selected item. In menu bar (Edit or View), resource is a currently open file;
But now I’ve discovered if I add an “active” callback and always return true, then it does run on embedded game objects and I get an ID back (but of course there’s not much I can do with it yet).
I guess that’s my first feature request then: access to embedded game object and component properties. My main use for editor extensions would be to speed up and expand on the workflow for editing scenes. I guess you could maaaybe do this right now by parsing the collection file and editing it, but I’m not sure how you could specify which things to edit. I think it would be quite hard to make something convenient to use (which is the whole point).
For example consider a simple “Align Vertically” command. You select multiple objects, right click in the Outline, click “Align Vertically”, and boom, the selected objects have their positions changed so they line up. Being able to do things like that with editor extensions would be pretty amazing.
Some things I wish for down the road…
Input - There’s only so much you can do with menu items. If extensions could work with keyboard input then you could really do cool stuff (at the press of a button…).
…and probably make a big mess with multiple extensions and conflicting, hard-coded keybindings, but that’s ok.
Drawing stuff in the viewport - Lower priority, but if you really want to make custom editing tools I think you need some way to visually indicate what’s going on. Just something like the render script “draw_line” message, would do it.
Viewport camera transforms - OK now this is really wishful thinking, but now that I am thinking about it, to make a custom editing gizmo you would need 1) The viewport camera matrices for collision checking, and 2) Be able to hijack mouse input before the editor uses it.
Anyway! The fact that editor extensions already work, are doable very easily with lua, can be reloaded instantly, and are so far bug-free, is really awesome!!!
Thank you very much for feedback! I agree, getting and setting transforms looks like a good next step for editor scripts, as well as keyboard shortcuts.
Another question: How do I require lua modules from an editor_script? The usual require path doesn’t seem to work. The manual specifically mentions that it’s possible.