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.
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.
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
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 anExecuteCommand
ExecuteCommand.at
returns anExecuteAtCommand
ExecuteAtCommand
is callable with parametertargets
, returning anExecuteAtTargetsCommand
ExecuteAtTargetsCommand
inherits the same fields asExecuteCommand
ExecuteCommand.if_
returns anExecuteIfCommand
ExecuteIfCommand.block
returns anExecuteIfUnlessBlockCommand
ExecuteIfUnlessBlockCommand
is callable with parametersposition
andblock
, returning anExecuteIfUnlessBlockPositionBlockCommand
ExecuteIfUnlessBlockPositionBlockCommand
inherits the same fileds asExecuteCommand
ExecuteCommand.run
returns anExecuteRunCommand
ExecuteRunCommand
inherits the same fields asRootCommand
RootCommand.setblock
returns aSetblockCommand
SetblockCommand
is callable with parametersposition
andblock
, returning aSetblockPositionBlockCommand
SetblockPositionBlockCommand.destroy
returns aSetblockPositionBlockDestroyCommand
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.
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.