pynecraft: Write Your DataPacks in Python¶
Contents:
Introduction to pynecraft¶
pynecraft is a Python package that lets you create
a data pack and its functions using python, so you can take
advantage of python’s tools. It generates the files required
for a data pack from your python code.
Writing data packs requires handling a lot of details, from putting things in the right folders in the correct format, to how those things are expressed in the first place. All errors are discovered during reload, or possibly at run time, rather than when you’re editing the files in the first place.
In contrast, Python (and other actual programming languages) have syntax checkers, errors for misspelled function and field names, IDEs that do auto-completion and checks for improper data types. And of course you have basic programming language tools, like loops and functions with parameters that generate similar output for varying input.
pynecraft lets you use all that in creating your
data pack. It represents the data pack as an object that
understands much of the layout of the data pack, and provides
ways to generate commands inside functions from within Python,
as well as JSON files all around the data pack. This means you
have all the power of a real programming language to generate
your commands, including syntax and other error checks, python
functions that generate minecraft commands that can be used in
minecraft functions. And pynecraft also knows where to store
what files in a data pack, and how to generate some boilerplate,
like the pack.mcmeta.
As a simple example, the following is a “hello_world” data pack:
pack = DataPack('hello_world')
pack.function_set.add(Function('hello').add(say('Hello, World!')))
pack.save(minecraft_world)
We create a DataPack object, and add a Function to it. That function
has, as its sole command, the result of calling say, which is
a pynecraft function that returns a minecraft say command with
the provided string. We add it to the function hello_world:hello
inside the hello_world data pack. Any number of commands can
be added at once, or any other time before the save(), which
writes out the data pack files to a specified directory.
If ‘minecraft_world’ is a minecraft world save, the save()
method will create:
minecraft_world
|-- datapacks
|-- hello_world
|-- README # A generated warning not to edit this by hand
|-- pack.mcmeta
|-- data
|-- functions
|-- hello.mcfunction
"say Hello, World!"
Pynecraft also has simple calls for obscure or complex minecraft
mechanisms. For example, there is a Score class that holds the
definition of a score (name and objective). You can use it as a
parameter to commands like scoreboard, but more importantly,
it has methods to generate commands simply, such as score.add(15)
to generate scoreboard players add <i>player objective</i> 15.
Other examples of simplifications include:
Item.of(<i>block</i>), which returns the NBT required to store an item including its state and data as (say) an item you would summon into the world to be picked up by a playerThe
Signclass that generates all the messy NBT to set sign texts and commands, so:Sign((None, 'hi', 'there'), (tell(p(), 'Hello!'),)).place((0, 100, 0), WEST)
…which gives a list of three lines of text for the sign and one
tellcommand, generates the lovely:setblock 0 100 0 oak_sign[rotation=4]{front_text: {messages: ['{"text": "", "clickEvent": {"action": "run_command", "value": "/tell @p Hello!"}}', '{"text": "hi"}', '{"text": "there"}', '{"text": ""}']}}
…a command that places an oak sign facing west at 0 100 0 with three lines of text which, when touched by the player, tells them “Hello!” (in case you couldn’t decode all that NBT). (Actually it generates two commands, the first of which sets that block to
air, to work around the fact that setting a sign at a place sometimes will not overwrite an existing sign, which is another thing you’d probably prefer not to worry about, and pynecraft takes care of for you.)
Pynecraft is distributed under the standard MIT license.
Why?¶
Python has tools: editors, debugger, formatters, etc. It also checks syntax at compile time and has run-time type checking. These are really useful for eliminating errors from your commands quickly and easily.
It is a full language, so you can use it to generate your commands. You can have a check for every mob that starts with ‘a’, or every value of a score from one to ten. You can write a python function that generates complicated commands based on a few parameters.
There are macro languages that could help with this, but the quoting issues alone can be daunting, and they won’t do the syntax and run-time validation of commands.
I wrote pynecraft because I was working on my RestWorld resource pack test world. It makes extensive use of functions. After using a macro system to generate things for a while, it was clear that this was ugly and difficult. Pynecraft made a big difference. The “saying what I mean to say” stage of development nearly entirely vanished. I still had bugs and made mistakes, out they were almost never typos or command syntax problems. And with some of the simplification tools, I also stopped making mistakes about how relatively complicated things were done.
I hope it will help you, too!
(Currently pynecraft only supports Java Edition commands. A Bedrock version is certainly possible as a companion project; if someone wants to undertake it, please let me know.)
Getting Started¶
Installing pynecraft is easy, it’s a standard python package hosted at pypi.com, installed in the standard way:
pip install pynecraft
Pynecraft relies on Python 3.11 (although it probably could be used with somewhat earlier versions).
Resources¶
Here are some useful resources:
The “warning” example, a basic example included with the source.
The megavillage project, a simple project of mine (YouTube videos coming soon!)
The RestWorld project, a very complex use that is the original target, whose very own website is here.
Structure¶
Pynecraft has the following modules:
baseThe base-level data types, classes, and values
enumsEnums for large sets of values, like effects and particles.
commandsThe functions, classes, and constants for writing minecraft commands in python. These will generate strings that are the minecraft commands.
functionsSupport for grouping commands into functions, folders of functions, and the top-level data pack.
simplerSimplification classes and functions. Anything done here could be done directly with commands, but these provide simpler mechanisms.
infoSome useful standard information about minecraft that is useful in commands, such as the hex values of colors, the list of note block instruments, the kinds of fish, as well as a
Fishclass with helpful methods, etc. Oh, and lists of all blocks, items, and mobs.
Notes on Design¶
There are several choices to be made when deciding how to map the
minecraft commands into a python (and hopefully pythonic) functions
and types. For example, consider the fill command, which takes
two sets of (x, y, z) coordinates, a block to fill with and some
options, such as whether to replace only certain kinds of blocks.
Representing the coordinates as three-tuples is pretty normal in
python, but how to handle the block type? And the options?
For the block type, pynecraft lets you use several different styles. For simple blocks, you can just name the block:
fill((1, 2, 3), (4, 5, 6), 'air')
There is also a class simpler.Block that lets you specify both
block state and nbt:
fill((1, 2, 3), (4, 5, 6), Block('oak_sign', {'rotation', 5}, {'Text2': 'Howdy!'))
You can use a dict to specify block state and NBT (more on NBT
below). Or you can just give the specification as a tuple:
fill((1, 2, 3), (4, 5, 6), ('oak_sign', {'rotation', 5}, {'Text2': 'Howdy!'))
That pretty will handles blocks. You can do similarly with entities:
summon(('Zombie', {'IsBaby': True}), (1, 2, 3))
But how pynecraft should present the options for fill are less
obvious. There are several different ways, such as:
fill((1, 2, 3), (4, 5, 6), 'air', 'replace', 'stone')Just using strings as optional parameters to
fill(). This is pretty free form, and easily allows mistakes.fill((1, 2, 3), (4, 5, 6), 'air', FillsOptions.REPLACE, 'stone')Use an enum for the possible options, still as optional to parameters to
fill(). This is less prone to errors, but pretty verbose.fill((1, 2, 3), (4, 5, 6), 'air', REPLACE, 'stone')Provide pre-defined constants for the possible options, still as optional to parameters to
fill(). This is less verbose, although someone could still put in a string and mistype it, but that takes work to make the mistake.fill((1, 2, 3), (4, 5, 6), 'air').replace('stone')Provide chaining functions for further parameters. This prevents typos, and allows for more complex syntax. The
datacommand, for example, has far too many possible syntaxes to represent as just strings.
Pynecraft takes both the last two approaches, varying with the situation. In places where there are several choices that are syntactically identical, such as specifying direction of North, East, South, or West, it tends towards the pre-defined constants. In places where the choice affects syntax, there is a strong preference for the chaining approach.
There are other interesting places where choices can be made. For
example, in the data command, there are three kinds of targets:
blocks, entities, and storage. The command makes you specify which
one: data get **entity** ..., data get **block** ..., etc.
However block specification look different from entity specifications,
so that keyword is redundant, and actually complicates the syntax.
So the pynecraft commands like data().get(e().tag('foo')) to
get data from an entity, and data().get((1, 2, 3)) to get data
for a block at a given position. You can be specific if you want,
by using some predefined functions, such as
data().get(entity(e().tag('foo'))), but this is only required
if you are using macros in your command (more on macros later).
Finally, there is an overall preference to not be significantly
more verbose than the actual commands. This means that there are
several functions that could have longer names, but they don’t. For
example, as shown above, e() is the way pynecraft represents
@e, and relative coordinates -1 -2 -3 are represented as
r(1, 2, 3). Admittedly this takes up some of the single-character
identifier space, but it seems worth it.
There are a few large-scale collections of values that are expressed
in enums, like achievements and effects. These are in pynecraft.enums.
Error Checking¶
Several things are done to try to catch errors before you load the script into minecraft:
1. Some errors are simply illegal. You cannot misspell a command name, for example.
2. Some will be warned about by any competent IDE because of type hints in the method signatures.
3. Runtime type checking is used for many other things. For example,
a block or entity ID must be at least lexically legal: It must be
a sequence of letters, underscores, and digits, with an an optional
namespace (minecraft:...). If not, pynecraft will raise an
exception.
These will not prevent all errors, but it does mean errors are much more likely to be caught by language and runtime rules rather than by minecraft when the script is loaded.
Example¶
The example module contains a program that will generate an
example datapack, called warning. The primary function monitor
checks players who have opted in to see if they’re standing on top
of a bad block. If so, the first time it tells them to run, and
subsequent times it says “NOW!!!”
1"""
2Example for using pynecraft. This is a simple datapack that warns players to move if they are standing on top of a bad
3block, reminding them every second. It only warns players who have opted in, however.
4"""
5
6import sys
7
8from pynecraft.base import TimeSpec, r
9from pynecraft.commands import REPLACE, Score, a, comment, execute, return_, s, schedule, tell
10from pynecraft.function import BLOCK, DataPack, Function
11
12# Create the 'warning' datapack
13pack = DataPack('warning')
14
15# Set the 'bad_blocks' block tag
16pack.tags(BLOCK)['bad_blocks'] = ['magma_block', 'tnt']
17
18self_score = Score(s(), 'warning') # score that will be used for each player
19halt = Score('halt', 'warning') # whether to halt the process
20# The overall monitoring function
21monitor = Function('monitor').add(
22 comment('Stop if we have been told to'),
23 execute().if_().score(halt).matches(1).run(return_()),
24 comment('Find players with scores, and warn them if needed'),
25 execute().as_(a().scores({self_score.objective: (0, None)})).run(
26 execute().at(s()).if_().block(r(0, -1, 0), '#warning:bad_blocks').run(
27 execute().if_().score(self_score).matches(0).run(tell(s(), 'Run away!')),
28 execute().if_().score(self_score).matches((1, None)).run(tell(s(), 'NOW!!!!')),
29 self_score.add(1), # adds 1 to the score, generates 'scoreboard players add @s warning 1'
30 ),
31 execute().at(s()).unless().block(r(0, -1, 0), '#warning:bad_blocks').run(self_score.set(0))
32 )
33)
34# Add it to the pack. This sets the functions full name (which includes the pack name)
35pack.function_set.add(monitor)
36# This command schedules the next run of the monitor, which is used in a few places
37schedule = schedule().function(monitor, TimeSpec('1s'), REPLACE)
38# ... including at the end of the monitor function itself
39monitor.add(schedule)
40
41# Function to set the warning system in motion
42pack.function_set.add(Function('init').add(schedule, halt.init(0)))
43# Function to halt the warning system
44pack.function_set.add(Function('halt').add(halt.set(1)))
45
46# Function to start monitoring the invoking player
47pack.function_set.add(Function('start').add(self_score.set(0), schedule))
48# Function to stop monitoring the invoking player
49pack.function_set.add(Function('stop').add(self_score.reset()))
50
51# Write the datapack to the given directory / save
52pack.save(sys.argv[1])
It starts out by creating the pack. It then adds a block tag named
bad_blocks, listing the types of blocks considered bad.
It then creates two Score objects, one for each player who opts
in, the other is used to tell the system to stop.
Then comes the main function monitor function.
After that, monitor is added to the pack’s function_set.
Then we create a schedule command to invoke the function after
one second. That is used in several places, so we just create it
here once and reuse it.
Now we create all the other functions: init starts the system
running, while halt stops it; start opts the player in to
the system, and stop opts them out.
Finally, we save the pack to a directory.
To try this out, you can create a save. If you call it ‘PynecraftWorld’, then the path you give to this command is the path to that save dir. Then you can enter the world, run:
/function warning::init
/function warning::start
Now if you step on a bad block, you will be warned to move off it until you do.
You can see several of the basic features pynecraft at work here:
The datapack has a pretty natural Python structure.
You don’t have to remember (and correctly type) a bunch of files into a particular tree structure.
If you make typos or other errors, the compile will let you know before you get any farther.
Simplifications like
Scorelet you code more naturally, handling the actual expressions correctly for you. Similarly, setting a tag to a list of block names is simpler than a map with that list inside a"values"tag would be, and that’s nearly always what you want.The
executecommand’srunwill allow you to make several commands conditional on a single test in your code. This will mean multiple executions of the same test in the generated file, but typically these will be fast enough that the redunancy won’t matter. If there are more commands, you can split out all the work into a separate function and run it by the outerexecutecommand.
Usage Overview¶
Here is an expression that will print a minecraft give command
(of course, usually you won’t want to print commands, you want to
put them in functions, more on this below):
from pynecraft.commands import setblock
print(give(a(), 'iron_sword'))
The output will be:
give @a iron_sword
For almost every command (except a few specialized server-side
commands that seem unlikely to appear in functions or command
blocks), there is a function in pynecraft.commands. These may
return strings, or intermediate objects that support further chaining
calls. Here is how you could put together an experience command:
print(experience().add(s(), 3, LEVELS))
experience add 3 levels
In this case, experience() returns an object has the methods
add(), set(), and query(), the three subcommands of
experience.
You can remember this intermediate object and re-use it. One useful case for this is in target specifications, which can get complicated:
tgt = e().team('red').distance((None, 20))
who = give(tgt, 'redstone_dust', 10)
tag(tgt).add('redstoned')
This lets you say once what the constraint is and then use it across several commands, which is both briefer and easier to modify.
This remembers the prefix that says which entity to run the command
as, and the use it three times. (We use as_() because as
is a keyword in python, which is how all such conflicts are handled).
Each returned command object is immutable, so you can reuse them
without worry.
The execute command is another place where you could do this,
but you can also give run() multiple commands, and it
will generate a command for each one.:
- print(cmd) for cmd in execute().as_(e().tag(‘runner’)).run(
say(‘Ready to go!’), function(‘my_pack:go_to_it’) say(‘Done!’))
gives you:
execute as @e[tag=runner] run say Ready to go!
execute as @e[tag=runner] run function my_pack:go_to_it
execute as @e[tag=runner] run say Done!
Macro Commands¶
In Minecraft, macro commands are marked with a $, and substitute
incoming values using $(foo). Pynecraft just requires you to
mark where you are using incoming arguments, and prepends the $
if needed. So for example, you could use macro arguments like this:
execute().as_(e().tag(Arg('tag'))).run(say(Arg('msg')))
This would give you:
$execute as @e[tag=$(tag)] run say $(msg)
You could even just make the entire target a macro:
execute().as_(Arg('tgt')).run(say(Arg('msg')))
In most places you can also simply use a a string, which is most useful where the macro value represents part of a value. If you want:
tell @e[tag=xyz_$(foo)] Shh! There's a wumpus!!!
you can use:
tell(e().tag('xyz_$(foo)'), 'Shh! There's a wumpus!!!')
(Macros can be used even more wildly than this, such as to represent
the actual command or a part of it, such as $e$(cmd) to run any
command that starts with an ‘e’. In pynecraft you can only do this
literally by using a string as a command, or using the literal
function. This level of flexibility would severely limit the amount
of checking pynecraft could do, and is unlikely to be commonly used,
so it doesn’t provide for it any other way.)
Packs and Functions¶
Of course, usually you won’t want to print commands, you want to
put them in functions and put those functions in a data pack. The
pynecraft.functions module help you do this. You can start with
a top-level data pack:
pack = DataPack('my_pack', minecraft_saves / 'my_pack_world')
This creates a data pack named my_pack that will get saved in
the minecraft world my_pack_world. You can then go into that
world and use or test the pack. But you can use any directory, not
just a save.
Each pack has a top-level functions directory, which can have
one level of function directories beneath it (that’s the current
minecraft rule). If you add a function to the pack, it goes in the
top level directory:
func = Function('hello_world').add(say('Hello, world!'))
pack.functions.add(func)
pack.save()
Saving will first clear out the datapack directory, removing it entirely. This is important: The DatPack object owns the target directory, and you don’t want old files hanging around. If you rename a function, you don’t want the old version of the function to still exist with an older version of the code. If another function calls it, but you forget to change the name there, that would be confusing and possibly harmful.
Because of the way minecraft lays out its files, if you actually
give a path to the root of a save, DataPack will use the appropriate
subpath, rather than the save itself. And it will own that
directory, not the entire save. This means that “my_pack_world” is
a save, the directory it will own is my_pack_world/datapacks/my_pack.
It recognizes a save by the existence of the datapacks directory
inside it.
Otherwise you can point it at a directory that it will own, such as a staging area.
But whichever path it owns, remember: The DataPack owns the directory and will delete it!
And then it will write out the files. In this case, it will create
a structure like the following (assuming my_pack_world as the
world’s save path):
my_pack_save Top level of save
|-- datapack Where datapacks go
|-- my_pack Your specific pack
|-- README A warning about generated code
|-- pack.mcmeta The pack's metadata
|-- data The pack's data
|-- tags Any defined tags
|-- functions The pack's functions
|-- hellow_world.mcfunction The specific function
The DataPack field function_set is a FunctionSet object, and you
can add your own FunctionSet objects to it to create subdirectories.
Again, Minecraft limits you to one level of depth, pynecraft just
enforces it.
There is a special kind of function called a Loop, which is a
pynecraft utility that imitates having looping functionality. It
doesn’t actually run in a loop, but acts as a loop iteration each
time it is invoked. You tell it the items to loop over (say, the
various kinds of weather), and each time you run the loop’s minecraft
function it will increment a score and then work with the weather
that correlates to the score. The Loop documentation gives
more detail.
The Rest of the Pack¶
There are two other parts of a data pack: The pack.mcmeta file
and a slew of JSON files to configure block and entity tags, loot
tables, custom dimensions, world generation, and so on.
pack.mcmeta¶
The pack.mcmeta file lives at the top of the data pack and has
some simple configuration, including the pack format version and
filters for other packs. DataPack supports this, both in giving you
direct access to its dict that is serialized into the JSON in the
file, via the mcmeta property, and via particular methods to
set the description and filters.
JSON Files¶
DataPack organizes the JSON files as a top-level dict that contains the relevant directories and their contained JSON files. Under this dict, keys that end in ‘/’ are saved as directories. These keys have dict values whose keys are either subdirectories (they end in ‘/’ also) or files (they don’t). File keys have dict values that are saved as JSON files.
For example, the dict tree:
{
'advancements/': {
'story/': {
'battler': { 'criteria': { ... } },
}
'niceness': { 'criteria': { ... } },
}
}
translates into the following structure
my_pack_save
|-- datapack
|-- my_pack
|-- README
|-- pack.mcmeta
|-- data
|-- advancements
|-- story
|-- batter.json
|-- niceness.json
The standard members of a data pack’s JSON file set have defined
methods, such as advancements(), recipies(), and tags().
You can create other directories using the pack’s json_directory()
method.
Minecraft Versions¶
Mojang keeps producing new minecraft versions, and these have different commands, command syntaxes, restrictions, etc. How does pynecraft handle them?
Well, right now it doesn’t because it has been changed for each version. I could do this because it only had one user (me). But in the future, what will happen?
At one point I attempted to have pynecraft be for all versions. I started in 1.19, and when 1.20 came out, I added a way to specify the version, and did various runtime checks to make sure that incoming parameters or keywords were correct for each version, that 1.20 commands were not used in 1.19, etc. This proved so complicated I gave it up. And that was just one version change.
Currently pynecraft is built for the (as of this writing) upcoming
1.21 release. The plan is to keep it for 1.21, and produce a
separate, new (but derived) pynecraft for 1.22 when it arrives. You
will choose to install the version you want. The details of this
are to be worked out. Possibly the version gets encoded in the name
( pynecraft_1_21, pynecraft_1_22, etc.). Discussion will
be had; ideas are invited.