Skip to content

Instantly share code, notes, and snippets.

@Arcensoth
Last active July 7, 2018 22:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Arcensoth/8352a63ec32a334fecfad149693de2f5 to your computer and use it in GitHub Desktop.
Save Arcensoth/8352a63ec32a334fecfad149693de2f5 to your computer and use it in GitHub Desktop.

Minecraft command generation

This is a proposition on using Python to dynamically generate commands, which is useful for things like function trees, repetitive code, and/or any large-scale projects.

Preface

Most of the code blocks presented are just snippets that don't do anything useful on their own. The idea is that the commented line is the command you'd expect to see by printing the object that follows.

Here's a classic example:

# say hi
commands.say(message='hi')

This is what it might look like if you were doing something useful:

from ... import commands
mycommand = commands.say(message='hi')
print(mycommand)
# prints: say hi

So what's commands? For now I'll just say it's something that helps your IDE auto-complete all of the top-level commands. I'll explain exactly what it is later.

Case-by-case commands

We'll start with a top-level command that doesn't require any arguments (an executable literal):

# clear
commands.clear

A series of literals (subcommands):

# time query day
commands.time.query.day

(Keep in mind that all of these options may be auto-completed.)

As we've already seen, arguments are provided as parameters to the latest literal:

# clear @s minecraft:diamond
commands.clear(targets=selectors.self, item=items.diamond, max_count=64)

(Let's pretend that selectors contains some basic selector constants and items is just a big list of item ids. Hooray for auto-completion!)

This is true for any number of arguments following a literal, up until the next literal:

# setblock 0 64 0 minecraft:stone keep
commands.setblock(position=(0, 64, 0), block=blocks.stone).keep

(Let's pretend the same thing for blocks that we did for items.)

It's possible for literals and arguments to be siblings:

# time set noon
commands.time.set_.noon

# time set 9000
commands.time.set_(9000)

Here's a series of literals each with their own argument:

# tag @e add sometag
commands.tag(targets=selectors.entities).add(tag=tags.sometag)

(Here, tags might just be a list of custom-defined strings we use in our project that we can manage from one place.)

We can use a redirect in the form of an alias:

# tp 0 64 0
commands.tp(position=(0, 64, 0))

We can open another can of worms by using redirects in the form of subcommand chaining:

# execute at @a run setblock ~ ~ ~ minecraft:grass
commands.execute.at(
    target=selectors.all_players
).run.setblock(
    position=positions.relative,
    block=blocks.grass
)

(Let's further pretend that positions contains some basic position constants.)

Putting it all together:

# execute at @a if block ~ ~ ~ minecraft:dirt run setblock ~ ~ ~ minecraft:grass destroy
commands.execute.at(
    target=selectors.all_players
).if_.block(
    position=positions.relative,
    block=blocks.dirt
).run.setblock(
    position=positions.relative,
    block=blocks.grass
).destroy

How it works

This is where I attempt to explain the black-magic that makes all of this possible.

As promised: commands is just a singleton instance of RootCommand, which in turn has methods for all of the top-level commands. Each of these methods return a more specific instance of an object that extends the base Command class.

Note that we need to return a new instance every time in order to preserve a personalized history of the command.

For example:

commands.clear

Here, clear is a method of RootCommand that returns an instance of ClearCommand, but because Python's @property decorator has been applied to this "getter" method we don't explicitly call it.

But don't we use method syntax to supply parameters? Yes, we do:

commands.clear(targets=selectors.self, item=items.diamond, max_count=64)

So if RootCommand.clear returns an instance of ClearCommand, how are we calling the resulting instance like a method? There must be some black-magic going on here. Indeed, by defining the special __call__ method of ClearCommand Python allows us to call instances of ClearCommand as if they were methods.

This may appear even more confusing due to the presence of the property method which is not explicitly called but appears as such. Do not be fooled: the property method is returning an instance, and it is the instance that's being called.)

So we have ClearCommand.__call__ accept parameters targets, item, and max_count, returning an instance of ClearTargetsItemMaxCountCommand with the respective parameters filled-in.

As for multiple literals with their own arguments:

commands.tag(targets=selectors.entities).add(tag=tags.sometag)

It's as simple as TagCommand being callable with parameter targets, returning a TagTargetsCommand with an add property method that returns a TagTargetsAddCommand, which is in turn callable with parameter tag.

Now for this witchcraft:

commands.execute.at(
    target=selectors.all_players
).if_.block(
    position=positions.relative,
    block=blocks.dirt
).run.setblock(
    position=positions.relative,
    block=blocks.grass
).destroy

Actually, the only thing we need to add to our bag of tricks is how to handle redirects. This is achieved simply by returning some kind of Command from elsewhere down the hierarchy. For example, ExecuteAtCommand is callable with parameter targets and creates a cycle by returning an instance of ExecuteAtTargetsCommand which inherits all fields from ExecuteCommand.

Here's the full story:

  • RootCommand.execute returns an ExecuteCommand
  • ExecuteCommand.at returns an ExecuteAtCommand
  • ExecuteAtCommand is callable with parameter targets, returning an ExecuteAtTargetsCommand
    • ExecuteAtTargetsCommand inherits the same fields as ExecuteCommand
  • ExecuteCommand.if_ returns an ExecuteIfCommand
  • ExecuteIfCommand.block returns an ExecuteIfUnlessBlockCommand
  • ExecuteIfUnlessBlockCommand is callable with parameters position and block, returning an ExecuteIfUnlessBlockPositionBlockCommand
    • ExecuteIfUnlessBlockPositionBlockCommand inherits the same fileds as ExecuteCommand
  • ExecuteCommand.run returns an ExecuteRunCommand
    • ExecuteRunCommand inherits the same fields as RootCommand
  • RootCommand.setblock returns a SetblockCommand
  • SetblockCommand is callable with parameters position and block, returning a SetblockPositionBlockCommand
  • SetblockPositionBlockCommand.destroy returns a SetblockPositionBlockDestroyCommand

It's important to note that every instance of Command contains some data about how it was created. Every step we take, a new instance of Command is created with an additional piece of history.

Cavaets and considerations

Parameters may be provided out-of-order, however omitting an earlier argument will result in an error:

# clear ? minecraft:diamond
commands.clear(item=items.diamond)

This is preferred over defining arbitrary default values.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment