Submission Method: Amulet

Amulet is an interface able to read Minecraft saves files, and rewrite them if needed. Based on the data in the save file, Amulet will render the loaded world. Any modification made to the rendered world will be saved in the Minecraft saves. Therefore, you are then able to reopen the same save with the game later on, and see your changes in game.

If you want to submit via this method, your submission should contain the python script you wrote for amulet which should include a single operation that should be named like your generator.

Installing Amulet

If you are on Windows, you can simply download the latest build fom this link : https://www.amuletmc.com/
Once the download is complete, extract the folder and open it. You can run the Amulet Editor simply by launching the `amulet_app` exe file.

If you are using Linux or macOS, you will have to install the software by using the source code: https://www.amuletmc.com/installing-from-source
Note that you can reuse any existing Python 3.7 (or above) virtual environement that you have already set up. Make sure to use the latest full release.

Once Amulet is successfully installed on you computer, you can now launch it and I will walk through the interface in the next section.

Using Amulet

On the opening screen, click on "Open World".

Once the map is opened, you can click on "3D Editor" on the left side of Amulet. You should have a short loading time, and then the world should be visible.

You can use the WASD keys to move around. With the right click of your mouse, you can change the direction in which you look. By pressing Tab, you can look at the world from above. Press Tab a second time to go back to the first person view.

By now you may have noticed that there is a white boxe in place of your cursor. It's actually your selection tool. By left clicking and dragging your cursor, you can select a range of block where you want your modifications to happens. You can select blocks in both 1st person and top view, up to your preference. If you are unhappy with your current selection, simply create another one, or press Ctrl+D to unselect all blocks.
You can also manually change your selection by using the toolbox on the right side.

Amulet Operations

Now that we are familiar on how the Amulet's interface work, we can start diving into how do we write scripts for modiying the terrain.

Amulet let you run scripts on your selection, which are called "Operations". The framework comes in with a couple of those. You can view them by making a selection, and in the lower part of your screen, click on "Operation". This will change the toolbox on the left, that let you pick the Operation you want to run.

What is really nice about Operations, is that they are very simple to create and import. All you have to do is write a Python script implementing the Operation interface.
Operations are created using Python, and can be easily added to your Amulet client by adding the python file in the `plugin/operations` folders.
In order for your script to be recognized, you simply have to follow a few steps that we are detailing in the exemple bellow. You can reload Operations after modifying them using the sync buton.

You can find more documentation on the operation's API here : https://amulet-core.readthedocs.io/en/latest/

As an example, here is a simple operation creating a fence surrounding the selection.

We first need to do various includes, in order to connect to the Amulet API

from amulet.api.selection import SelectionGroup
from amulet.api.level import BaseLevel
from amulet.api.data_types import Dimension
from amulet.api.block import Block

We then export our Python script for Amulet.
The name field is the name rendered in the Amulet's interface.
The operation field is the name of the python's fuction that is launched when we run the operation.

export = {  
    "name": "Build Fence",
    "operation": operation,
}

Note that there are a range of options you can add to your operation, including custom inputs fields. These will not be used in this assignement, but you can have a lookt at this example:https://github.com/Amulet-Team/Amulet-Map-Editor/blob/master/amulet_map_editor/programs/edit/plugins/operations/examples/3_fixed_function_pipeline.py

Since Amulet supports all Minecraft's versions, we are storing the game version we are interested in, which will be required by some of the framework's functions.

game_version = ("java", (1, 19, 1))

We start we 3 simple helper functions, in order to get the block at a given set of coordinates, check its type, and set it to a specific type.

def getBlockAt(x,y,z, level):
    block, block_entity = level.get_version_block(
    x,  # x location
    y,  # y location
    z,  # z location
    "minecraft:overworld",  # dimension
    game_version,
    )
    return block
 
def isBlockType(block, blocktype):
    if not isinstance(block, Block):
        print("Object is not a block")
        return False
    return block.base_name == blocktype
 
def isBlockAir(block):
    return isBlockType(block, "air")
 
def setBlockAt(x, y, z, block, level):
    level.set_version_block(
    x,  # x location
    y,  # y location
    z,  # z location
    "minecraft:overworld",  # dimension
    game_version,
    block
    )

We can now declare an entry point for our operation:

def operation(
    world: BaseLevel, dimension: Dimension, selection: SelectionGroup, options: dict
):

World is the object containing the level, dimension is simply refering to the Minecraft's dimension being used and we won't use it, selections is an array containing all the selection box, and finally options is a dictionnarie containing all our options, here it should be empty.

First we retrieve the first selection box available (Amulet supports multiple selections).
`box = selection[0]`

We now can retrieve our 4 corners out of the selection box.

xmin = box.min_x
xmax = box.max_x -1
zmin = box.min_z
zmax = box.max_z -1

We also declare a block object of type "fence", that we will use in order to set the block type.

fence = Block("minecraft", "oak_fence")

All we need to do is to loop over our coordinates and put fences on top of the ground.

for x in range(xmin, xmax):
        for y in range(255, - 1, -1):
                block = getBlockAt(x, y, zmin, world)
                if isBlockAir(block) == False:
                    print(x)
                    setBlockAt(x, y+1, zmin, fence, world)
                    break
 
        for y in range(255, - 1, -1):
                block = getBlockAt(x, y, zmax, world)
                if isBlockAir(block) == False:
                    setBlockAt(x, y+1, zmax, fence, world)
                    break
 
    for z in range(zmin, zmax):
        for y in range(255, - 1, -1):
                block = getBlockAt(xmin, y, z, world)
                if isBlockAir(block) == False:
                    setBlockAt(xmin, y+1, z, fence, world)
                    break
 
        for y in range(255, - 1, -1):
                block = getBlockAt(xmax, y, z, world)
                if isBlockAir(block) == False:
                    setBlockAt(xmax, y+1, z, fence, world)
                    break

Here is the full code.

from amulet.api.selection import SelectionGroup
from amulet.api.level import BaseLevel
from amulet.api.data_types import Dimension
from amulet.api.block import Block
 
game_version = ("java", (1, 19, 1))
 
def getBlockAt(x,y,z, level):
    block, block_entity = level.get_version_block(
    x,  # x location
    y,  # y location
    z,  # z location
    "minecraft:overworld",  # dimension
    game_version,
    )
    return block
 
def isBlockType(block, blocktype):
    if not isinstance(block, Block):
        print("Object is not a block")
        return False
    return block.base_name == blocktype
 
def isBlockAir(block):
    return isBlockType(block, "air")
 
def setBlockAt(x, y, z, block, level):
    level.set_version_block(
    x,  # x location
    y,  # y location
    z,  # z location
    "minecraft:overworld",  # dimension
    game_version,
    block
    )
 
def operation(
    world: BaseLevel, dimension: Dimension, selection: SelectionGroup, options: dict
): 
 
    box = selection[0]
 
    xmin = box.min_x
    xmax = box.max_x -1
    zmin = box.min_z
    zmax = box.max_z -1
    fence = Block("minecraft", "oak_fence")
 
    for x in range(xmin, xmax):
        for y in range(255, - 1, -1):
                block = getBlockAt(x, y, zmin, world)
                if isBlockAir(block) == False:
                    print(x)
                    setBlockAt(x, y+1, zmin, fence, world)
                    break
 
        for y in range(255, - 1, -1):
                block = getBlockAt(x, y, zmax, world)
                if isBlockAir(block) == False:
                    setBlockAt(x, y+1, zmax, fence, world)
                    break
 
    for z in range(zmin, zmax):
        for y in range(255, - 1, -1):
                block = getBlockAt(xmin, y, z, world)
                if isBlockAir(block) == False:
                    setBlockAt(xmin, y+1, z, fence, world)
                    break
 
        for y in range(255, - 1, -1):
                block = getBlockAt(xmax, y, z, world)
                if isBlockAir(block) == False:
                    setBlockAt(xmax, y+1, z, fence, world)
                    break
 
export = {  
    "name": "Build Fence",
    "operation": operation,
}
Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License