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.
Create statements are used for generating non-function files, mainly JSON. Currently,
they don't support interpolation in determining their names/type, which can be circumvented using the internal __other__
function.
See the Python output below to learn how.
Their values are allowed to be lists, dictionaries, strings, or bytes.
You can also specify a file extension in their name, if not included .json
will be appended.
Not specifying a namespace for the file name will use the same namespace as the source file.
Lists and dictionaries will be converted into JSON before being written to the file,
this is done with Python's builtin JSON library.
(None
-> null
, True
-> true
, False
-> false
)
a String or Bytes object will just be written raw to the file, but I do not know what this would be useful for.
=== data/main/source/main.dps ===
create tags/block ns:chests -> {
"values": [
"chest",
"trapped_chest",
"ender_chest"
]
}
outputs
=== data/ns/tags/block/chests.json ===
{
"values": [
"chest",
"trapped_chest",
"ender_chest"
]
}
Of course, you do not need to specify a literal dictionary right after.
def tag(*values):
return {"values": values}
create tags/block chests -> \
tag('chest', 'trapped_chest', 'ender_chest')
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])
__other__("tags/blocks")["example:chests"] = {
'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.
python3 packscript.py c
(you can also use compile or comp)python3 packscript.py init
More actions:
python3 packscript.py --help
list general helppython3 packscript.py --version
print the version of packscriptpython3 packscript.py c --help
print the help for compilingpython3 packscript.py init --help
print the help for initializing a datapack
-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: python3 packscript.py 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:
python3 packscript.py init
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
or any directory that contains .fps
files
The output will either be a directory or a zip file (generated by creating a temporary directory with the same name)
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 PackScript. You can find
packscript.py
here - Setup Environment. Place the
packscript.py
file into your working directory - Create A Datapack. Run
python3 packscript.py 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
python3 packscript.py compile -i <datapack directory> -o <output>