PackScript, provided as a CLI (Command Line Interface), is designed to generate files for Minecraft datapacks. Datapacks can often have repetitive files or files that really should be together, that are not. There are many tools that already that solve these problems but this tool has some advantages:
This tool has no knowledge of the syntax of any individual Minecraft command, this makes it version agnostic and means there is no need to learn a new way to write commands.
If you know Python you can leverage that knowledge to use this tool, since by default, PackScript is Python.
The entirety of PackScript is defined in the packscript.py
file. This makes it
easy to add to your path or relocate.
The entire language adds very few constructs (see below) that are relatively simple to understand. These constructs give you the power to add commands to functions, and generate any other kind of file (especially JSON) easily.
There is an associated syntax highlighter for VSCode with this project. Get it here.
This is the main feature behind PackScript:
Command lines start with a /
and are treated as commands instead of Python.
When not inside a function, command lines do nothing.
Command lines may also contain interpolation and function definitions, which will be explained below.
Command lines can also be extended to further lines by ending them with \
the subsequent line will have its leading
whitespace removed
/execute as @e \
at @s run ...
# is the same as
/execute as @e at @s run ...
On command lines, the user may use ${python_expression}
or $python_var
to refer to previously defined things in the
file. A $
that is not followed by an identifier or {
is interpreted as a literal dollar sign.
To always introduce a literal dollar sign, use ${'$'}
This is ugly, but rarely needed.
If a command line ends with a :
and contains the word function
, it is interpreted as a function definition.
This is the feature in PackScript that allows you to combine multiple files into one and have arbitrary levels of scope within your functions.
There are three main components of function definitions, they may all be omitted.
/function main:func [tag1, tag2] with storage args:
- Name:
main:func
- Tags:
tag1, tag2
- Extra:
with storage args
-
Name: Placed right after
function
. Specifies the function's location within the datapack. Defaults to the namespace of the source file if omitted. -
Tags: Placed within square brackets. Automatically generates function tags. Tags default to the
minecraft
namespace and are commonly used to mark functions astick
orload
. They can also be empty. -
Extra: Placed after square brackets. Reserved for additional command text. To use "extra" without tags, specify as
/function [] with storage args:
. Note the significant leading space in" with storage args."
.
Any text before the function definition will appear in the final line but is not considered part of the definition itself.
/function a:
/schedule function [] 5t:
/function send_message [] {message:"Vital Message"}:
/function minecraft:func []:
/execute as @e at @s run function:
You may use interpolation within the function definition, this is useful for creating functions in loops or shortening the "extra" part to a constant (like in the example in the syntax highlighting section)
If the name of a function is omitted, (/function:
) then the name of the function is set to anon/function
(within the source namespace)
multiple anon/function
's take up names like anon/function_1
, anon/function_2
etc.
ex:
=== data/main/source/main.dps ===
/function tick [tick]:
/data modify storage args msg set value "example"
/execute as @a run function main:func [] with storage args:
/$say $(msg)
outputs
=== data/main/function/tick.mcfunction ===
# Generated by PackScript 0.1.4
data modify storage args msg set value "example"
execute as @a run function main:func with storage args
=== data/main/function/func.mcfunction ===
# Generated by PackScript 0.1.4
$say $(msg)
=== data/minecraft/tags/function/tick.json ===
{
"values": [
"main:tick"
]
}
You can also create a line like:
/#function example:
in order to define a function within another function without having an actual line. It's unlikely for this to be useful though, as such a function could be pulled to the top level.
The dp
object provides a flexible way to create, manipulate, and delete datapack files directly from your code.
You can use dp
with either dot notation or dictionary-style access:
# Dot notation
dp.tags.block.chests = {
"values": [
"minecraft:chest",
"minecraft:trapped_chest",
"minecraft:ender_chest"
]
}
# Dictionary-style access
dp["tags/block", "example:metals"] = {
"values": ["iron_block", "gold_block"]
}
Files created with dp
can be accessed and modified later:
# Add a value to existing tag
dp.tags.block.chests["values"].append("minecraft:barrel")
# Delete a file or property
del dp.tags.block.outdated_tag
The dp
object fully supports dynamic paths for programmatic file generation:
# Generate multiple related files
for material in ["iron", "gold", "diamond"]:
dp.recipe[f"{material}_upgrade"] = {
"type": "minecraft:crafting_shapeless",
"ingredients": [{"item": f"minecraft:{material}_ingot"}],
"result": {"item": f"minecraft:{material}_block"}
}
The dp
object works with all datapack file types and automatically handles namespacing based on the source file's namespace, making it a powerful tool for organizing and manipulating your datapack structure.
The capture_lines() function diverts command lines to a list of strings instead of directly writing to a function file. This is niche, but useful for constructing 'Only One Command's.
=== data/ooc/source/main.dps ===
def only_one_command(definer):
""" Generates "Only One Command"s given a function that defines commands """
with capture_lines() as lines:
/gamerule commandBlockOutput false
definer()
/setblock ~ ~1 ~ command_block{auto:1b,Command:"fill ~ ~ ~ ~ ~-3 ~ air"}
/kill @e[type=command_block_minecart,distance=..1]
def escape(ln):
return ln.replace("\\", "\\\\").replace("'", "\\'")
main = ','.join(f"{{id:command_block_minecart,Command:'{escape(ln)}'}}" for ln in lines)
/summon falling_block ~ ~1 ~ {BlockState:{Name:redstone_block},Passengers:[{id:falling_block,BlockState:{Name:activator_rail}},$main]}
/function ooc:
def say(): # Commands For Only One Command!
/say as second lame command
/say third lamer command lol
/tellraw @a "look ma I'm using quotes \\/!"
/tellraw @a[name=!"Slackow"] "say \"lol\""
only_one_command(say)
outputs
=== data/ooc/function/ooc.mcfunction ===
# Generated by PackScript 0.1.4
summon falling_block ~ ~1 ~ {BlockState:{Name:redstone_block},Passengers:[{id:falling_block,BlockState:{Name:activator_rail}},{id:command_block_minecart,Command:'gamerule commandBlockOutput false'},{id:command_block_minecart,Command:'say as second lame command'},{id:command_block_minecart,Command:'say third lamer command lol'},{id:command_block_minecart,Command:'tellraw @a "look ma I\'m using quotes \\\\/!"'},{id:command_block_minecart,Command:'tellraw @a[name=!"Slackow"] "say \\"lol\\""'},{id:command_block_minecart,Command:'setblock ~ ~1 ~ command_block{auto:1b,Command:"fill ~ ~ ~ ~ ~-3 ~ air"}'},{id:command_block_minecart,Command:'kill @e[type=command_block_minecart,distance=..1]'}]}
The ns
global variable lets you access the namespace of the dps file you are in as a string. note that ns
is the namespace of the source file, not the function you are in.
=== data/example_pack/source/example.dps ===
/function say_something:
/say something!
/function example:
# Regular functions don't default to the namespace, so this is needed
/function $ns:say_something
outputs
=== data/example_pack/function/say_something.mcfunction ===
# Generated by PackScript 0.1.4
say something!
=== data/example_pack/function/example.mcfunction ===
# Generated by PackScript 0.1.4
function example_pack:say_something
This was used with the associated packscript_reloader fabric mod.
This mod works on snapshots, and will automatically compile packs under the dev
directory in the world into the
datapacks
folder of that world.
Well it's basically just doing a bunch of very fancy find and replaces to turn your invalid Python code into valid Python code, and then executing it. You can see this when you compile in verbose mode:
turns into
__f, __extra = __function_name__(f"tick [tick]")
__line__(rf""" function {__f}{__extra} """[1:-1])
with __function__(__f):
__f, __extra = __function_name__(f"")
__line__(rf""" execute as @a at @s run function {__f}{__extra} """[1:-1])
with __function__(__f):
__line__(rf""" title @s actionbar "Hey" """[1:-1])
# remove short grass near the player
__line__(rf""" fill ~10 ~10 ~10 ~-10 ~-10 ~-10 air replace short_grass """[1:-1])
__f, __extra = __function_name__(f"say_stuff")
__line__(rf""" function {__f}{__extra} """[1:-1])
with __function__(__f):
__line__(rf""" $say packscript> $(message) < actual working macro parameter """[1:-1])
__line__(rf""" say packscript> $(message) < that is just text """[1:-1])
__line__(rf""" say packscript> 2 + 2 = {2 + 2} < this works because it is compile time """[1:-1])
dp.tags.block.chest = {
'values': [
'chest',
'trapped_chest'
]
}
dp.tags.block.chest['values'].append('ender_chest')
del dp.tags.block.chest
dp['tags/block', 'chest'] = {
'values': ['chest', 'trapped_chest', 'ender_chest']
}
All the functions being called here are provided by PackScript and are just routes through which the program can add lines or create new files.
Most files shown so far have been .dps
files, standing for DataPackScript,
but there's also FunctionPackScript with .fps
files. These are contained in the root of the input directory instead
of under a proper datapack with a namespace underneath source
[1], these are meant for generating independent
function files easily, usually those with repetitive lines. In these files you cannot use create statements, but you can generate
additional functions. All the generated function files
will have their namespace ignored and be generated in the same directory
as the main generated function.
[1]: In older versions of minecraft (pre 1.21) this folder is sources
instead. PackScript will automatically figure out which folder name to use based on your pack_format value in your pack.mcmeta
.
This tool has two main actions it can perform: compiling packs and initializing templates.
packscript c
(you can also use compile or comp)packscript init
More actions:
packscript --help
list general helppackscript --version
print the version of packscriptpackscript c --help
print the help for compilingpackscript init --help
print the help for initializing a datapackpackscript update
update the tool to the latest version (if installed via pip, use that instead)packscript pf
edit and view the pack_format(s) supported by your datapack, you can use recognized version numbers directly
-i/--input <dir>
specify the directory of the pack you are compiling defaults to current dir.-o/--output <dir/zip>
specify the output of the pack (can output zip too) defaults tooutput
-s/--source
output the source files into the resulting pack, by default they get deleted-v/--verbose
print out all the generated Python code with line numbers. Very good for debugging.
When init is called missing any options, it will prompt you to interactively fill them, this is the recommended way of using this action.
You can also specify options using flags
ex: packscript init --output "Datapack" --name "Datapack" --namespace "main" --description "Datapack for version 1.20.4" --pack-format 26
it's preferable to do just call the following instead:
packscript init
There is also the packscript init --modded
command, it lets you add config files for compiling to fabric, forge, and neoforge.
It is recommended that you take a look at the files it generates to add any additional metadata you may want.
When a PackScript file is being compiled it will first print its location to the console, this is so if an error is encountered
you can easily tell which file was last being run, and ensure that your .dps
files are being executed.
The input directory should be a directory that contains a data
and pack.mcmeta
and optional pack.png
and overlays
or any directory that contains .fps
files
The output will either be a directory, a zip file, or a jar file (jar file will make a mod that applies your pack)
When compiling to a jar, an assets
folder can also be included to include a resourcepack.
ex: packscript c -o output
, packscript c -o datapack.zip
, packscript c -o mod.jar
When there is an error in your PackScript file, it will print out the Python version of your PackScript code, this lets you pinpoint exactly where your error is. You should see a line that looks like:
File "<string>", line 41, in <module>
This is what an error in the generated Python code looks like.
This line number will not line up with your .dps
file, but it will line up with the generated Python code.
The line with the error will get printed like this:
38: 'ender_chest'
39: ]
40: }
41: a/say some text
In this case it's because there's a letter before the command line, making it get interpreted as regular Python.
- Install Python. Get Python3 and
make sure it's in your path. (You can check by running
python3 -V
in your terminal) - Download/Install PackScript. run
pip3 install packscript
, or directly use thepackscript.py
file in releases, and usepython3 packscript.py
instead ofpackscript
- Create a Datapack. Run
packscript init
in order to create a datapack with PackScript. You'll be prompted for information about the datapack. You should have a new datapack, you can put files in there as usual for them to be outputted, files in<pack>/data/*/source/*.dps
and<pack>/*.fps
will be interpreted as PackScript on compilation. By default, you will find a main.dps file there. - Compile Datapack. To compile, run
packscript compile -i <datapack directory> -o <output>