Submission Method: HTTP Server

work in progress

about

The http interface is a Minecraft Mod (based on the Forge Mod Framework), that opens an HTTP Server that can among other things read blocks and place blocks from within Minecraft through an API.

Getting started

To get started go over here and follow the install instructions. It is enough to install the jar file, there is no reason for you to build the mod from source.

After you are done you should have a working installation of the mod. Make sure to open a Minecraft world, or else the http server will not be running.

If you are already familiar with HTTP requests it's probably best to take a look at the different endpoints over here or to jump straight to the python example further down.

The basics

HTTP is a protocol that is entirely based around requests: You make a request to the server for some data, and the you get a response back which contains that data (or an error message).

To test if everything works we can just open a web browser and visit the following url http://localhost:9000/blocks?x=0&y=0&z=0. In this example localhost:9000 is the IP and port of the server, blocks is the endpoint for accessing blocks and ?x=0&y=0&z=0 are the query parameters further specifying the request.

You should see a webpage containing the text minecraft:bedrock. This is the block at position (0, 0, 0).

While this works for simple stuff, to be able to properly play around with the interface I would recommend using an application like Postman.

In Postman a request for a block would look like this.

postman_get_block.png

If you switch the request method to PUT, you can easily place a block in the world. You specify which block to set in the request body using the "namespaced id" of the block. (You can also omit the "minecraft:" part of the id)

postman_put_block.png

There are many ways to find out the id of a block. Pressing F3+H in Minecraft will toggle a setting that shows the block id when hovering over an item in your inventory. You can also press F3 to show the debug overlay, if you look at a block the block's id will also be displayed along with it's block states.

block_id_hover.png
block_id_f3.png

Small village example in python

While you can use any programming language to access the interface, a good way to get started is the example/template project based on python that you can find here

I'm going to show some of the features by using the simple village generator (example.py) as an example.

village_generator_result.jpg

running the example

I'm assuming some familiarity with python. You can install the dependencies for this project from the requirements.txt file. It's assumed that the python version is >=3.7. You can do this by running pip install -r requirements.txt, or just take a look into the file and install the requirements manually.

In order to tell the program where to build you can specify the build area within Minecraft: Simply write this command in the Minecraft chat: /setbuildarea ~-50 0 ~-50 ~50 255 ~50 This will set the build area to the 100x100 area around the player (buildarea endpoint). It won't have any effect on block placement, but we can query this data in our client script later, so we don't have to copy-paste coordinates.

You can now run the program with python example.py, or using an IDE of your choice, if you quickly switch to your Minecraft window you should see a village appear in front of your eyes.

If the generation was to fast for you you can set USE_BATCHING to False in line 11 of example.py.

How it works

This example can be thought of as a three step process:

Step 1: Loading the world data / getting a heightmap

There is currently no easy endpoint in the interface to get large amounts of blocks and other world data. Instead there is the chunks endpoint which returns the word data in a raw nbt format.

Luckily if you work of this example you can simply use the WorldSlice class implemented in worldLoader.py, which abstracts away the parsing of this nbt data, so you don't need to worry about it.

We can get a "world slice" like this:

area = (0, 0, 128, 128) # x, z, sizeX, sizeZ
worldSlice = WorldSlice(area)

You can get a heightmap by using this function in mapUtils. This function automatically ignores trees.

heightmap = mapUtils.calcGoodHeightmap(worldSlice)

Or you can one of these four heightmaps, which Minecraft includes in it's chunk data:

heightmap = worldSlice.heightmaps["MOTION_BLOCKING"]
heightmap = worldSlice.heightmaps["MOTION_BLOCKING_NO_LEAVES"]
heightmap = worldSlice.heightmaps["OCEAN_FLOOR"]
heightmap = worldSlice.heightmaps["WORLD_SURFACE"]

This heightmap is a numpy array, you can access values in it like this: heightmap[x,z]

You can also visualize a heightmap using this utility function:

mapUtils.visualize(heightmap, title="heightmap")

Step 2: Building a fence

In order to place blocks we define a little helper function.

# a wrapper function for setting blocks
def setBlock(x, y, z, block):
    if USE_BATCHING:
        interfaceUtils.placeBlockBatched(x, y, z, block, 100)
    else:
        interfaceUtils.setBlock(x, y, z, block)

Here we can see the two slightly different functions in interfaceUtils. setBlock just directly sends a http request to place a block. placeBlockBatched makes use of the "setting blocks in bulk" feature of the API. This will be a lot faster, instead of sending a request directly it will add the block and it's position to a buffer. And only when it has reached the specified number of block placements (100) it will send them all at once, speeding up the process significantly.

We also define a helper function to query the height at a x,z position, this function accounts for the offset of the build area, which is otherwise easy to forget:

# define a function for easier heightmap access
# heightmap coordinates are not equal to world coordinates!
def heightAt(x, z):
    return heightmap[(x - area[0], z - area[1])]

We can now use the setBlock function to build a fence in a big rectangle around the building area. For the sake of simplicity there is a separate for-loop for every edge of the rectangle, there are of course smarter ways to do this. But this should give you an idea of how to use this function.

You can also see how we use the heightAt function to adapt to the terrain.

# build a fence around the perimeter
for x in range(area[0], area[0] + area[2]):
    z = area[1]
    y = heightAt(x, z)
    setBlock(x, y-1, z, "cobblestone")
    setBlock(x, y,   z, "oak_fence")
for z in range(area[1], area[1] + area[3]):
    x = area[0]
    y = heightAt(x, z)
    setBlock(x, y-1, z, "cobblestone")
    setBlock(x, y  , z, "oak_fence")
for x in range(area[0], area[0] + area[2]):
    z = area[1] + area[3] - 1
    y = heightAt(x, z)
    setBlock(x, y-1, z, "cobblestone")
    setBlock(x, y,   z, "oak_fence")
for z in range(area[1], area[1] + area[3]):
    x = area[0] + area[2] - 1
    y = heightAt(x, z)
    setBlock(x, y-1, z, "cobblestone")
    setBlock(x, y  , z, "oak_fence")
fence_result.png

Step 3: Building a bunch of houses

I will skip over the details of how to build the houses since it's not really relevant to the http interface.

In summary we just place a bunch of houses at random positions with randomly sized rectangular footprints. If the rectangular footprint overlaps with an already placed house we don't place it.

to build a house we query the heightmap at the four corners and use the lowest point as our base y coordinate. Then with a bunch of clever for loops we place the floor, the four walls, a roof and fill the interior with air.

The finished result will look something like this:

result_houses.png

Restrictions and Pitfalls

It's important to remember that the http interface will never be as fast as other submission methods like MCEdit, since the networking and string parsing as well as placing the blocks into the minecraft world has a significant overhead.

Don't be afraid to send bulk block placement requests, even up to bigger numbers such as a 1000 blocks. Generally Minecraft will handle them fine.

Being a network interface it makes sense to program the code somewhat defensively, since things can always go wrong. If you look at the sendBlocks function in interfaceUtils, you will see that it will retry a few times when receiving a ConnectionError. Other functions deal with ConnectionError in different ways. As far as I'm aware ConnectionErrors can happen because of timeouts on a system level. My best guess is that these timeouts are caused by the Minecraft world saving at an un-opportune moment, delaying the request. So be aware of that.

If you have the minecraft world open your block placements will interact live with blocks in the world such as flowing water, lava and falling sand. This can slow down the game as well as mess up your generation.

I experienced that certain blocks can be overridden by outstanding block updates, such as new water sources spawning. So be careful around water.

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License