/PackScript

Primary LanguagePythonMIT LicenseMIT

What is PackScript?

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:

Direct use of Commands

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.

Extension of Python

If you know Python you can leverage that knowledge to use this tool, since by default, PackScript is Python.

Small Footprint

The entirety of PackScript is defined in the packscript.py file. This makes it easy to add to your path or relocate.

Simplicity

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.

Syntax Highlighting

There is an associated syntax highlighter for VSCode with this project. Get it here.

packscript_ex

Constructs

Command Lines

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 ...

Interpolation

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.

Function Definitions

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:

  1. Name: main:func
  2. Tags: tag1, tag2
  3. 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 as tick or load. 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.

More Valid Examples

/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

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')

capture_lines() Function

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]'}]}

ns String

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

Example Usage + PackScript Reloader

packscript_ex1

minecraft_output

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.

How does it work internally?

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:

packscript_ex2

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.

FunctionPackScript

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.

The CLI

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 help
  • python3 packscript.py --version print the version of packscript
  • python3 packscript.py c --help print the help for compiling
  • python3 packscript.py init --help print the help for initializing a datapack

Compile Options

  • -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 to output
  • -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.

Init Options

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

Compile Action

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)

Debugging

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.

Get Started

  1. Install Python. Get Python3 and make sure it's in your path. (You can check by running python3 -V in your terminal)
  2. Download PackScript. You can find packscript.py here
  3. Setup Environment. Place the packscript.py file into your working directory
  4. 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.
  5. Compile Datapack. To compile, run python3 packscript.py compile -i <datapack directory> -o <output>