Editor Scripts 🔥: Alpha Release

We’ve been working on adding scripting support to the editor, and finally an alpha version is ready for testing.

Editor scripts allow you to use Lua to add menu items to the editor, as well as run build and bundle hooks.

Check for updates, and you will be able to create Editor Script files. Take a look at editor script manual to see how you can use editor scripts.

Please tell us if you discover any issues, and share your ideas how to improve it :heart:

37 Likes

Nose in the manual now (I’ll probably get fired!!!)

6 Likes

Congrats on alpha for this feature it is highly anticipated! :heart_eyes:

4 Likes

Is there a way to download this version other than through the in-editor updater? My internet is pretty bad, so updating with the editor rarely works.

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
4 Likes

Oooh, I’m excited to see that your guys are starting to use it already. @vlaaad will take a look at your specific issue @azotkirill

2 Likes

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(...).

1 Like

What are the chance of us getting our hands on the protobuf definitions? That could make creating files a bit neater :smiley:

4 Likes

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.

Some issues I faced this far:

  • editor.get(nil, 'text') throws java.NullPointerException.
  • Is there any way to reload editor scripts without reloading editor?
  • Filtering resources of specific type is cumbersome, and it will be used in most editor scripts.
2 Likes

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?

3 Likes

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?

2 Likes

Just pushed a couple of improvements (and updated manual to reflect the changes):

  • better error reporting when invalid arguments are passed to editor.get()
  • added os.remove
  • allow resource paths as a node_id argument to editor.get(), like that: editor.get("/main/game.script", "text")
5 Likes

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.

2 Likes

@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.

3 Likes

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. :slight_smile:
  • 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!!!

3 Likes

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.

3 Likes

Also I want to point out that active callback is optional, and it will be more performant if you omit it.

2 Likes

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.

1 Like