Repository: avdstaaij/gdpc (language: python)

Issue:
Importing the library without a world open should behave more clearly
Moved from https://github.com/nilsgawlik/gdmc_http_client_python/issues/69, by @caranha:

---

If you import sub-modules from the library (for example, GEO) without a minecraft world open, a large scary error message shows.

Either you should be amble to import sub-modules without a world running, or if that is impossible, a better error message (i.e "Don't forget to open the minecraft world before loading this module!") should be displayed (or maybe an appropriate error should be thrown?)
Geometric fill/outline tools
Moved issue from https://github.com/nilsgawlik/gdmc_http_client_python/issues/39, by @Flashing-Blinkenlights:

---

It would be nice to have a selection of functions to draw a variety of geometric shapes efficiently, both filled and hollow.
Some ideas for useful shapes:

- [x] Cuboids
- [x] Point-to-point shapes / polygons
- [x] Circles, ellipses and cylinders
- [x] Ellipsoids
- [ ] Arches
- [x] Repeated shapes
- [x] Transformed shapes
- [x] Rotated shapes

### Suggested implementation:

Using various combinations of Bresenham's algorithms
Integrate enhancements
This issue tracks the integration of the enhancements from https://github.com/avdstaaij/gdpc-enhancements.

- [x] Vectors
- [x] Transformations
- [x] NBT data
- [ ] Models
  - [x] Model class
  - [ ] Model scanning script
  - [ ] Model building script
- [x] Asynchronous requests
- [x] Miscellaneous additions
- ~~Examples~~
Building in other dimensions
Moved from https://github.com/nilsgawlik/gdmc_http_client_python/issues/65, by @Flashing-Blinkenlights (edited):
See https://github.com/nilsgawlik/gdmc_http_client_python/issues/65 for a suggested implementation.

---

Since ruined portals are fairly common, enabling the user to build inside the nether (and potentially other dimensions) would greatly expand the potential for infrastructure.
Design of interface.py
# Design of interface.py

I am currently significantly refactoring GDPC (on the `refactor` branch). Since
the refactor will break compatibility all over the place anyway, I would like to
take the opportunity to make the design of GDPC as good as possible.
However, there are a few design aspects related to `interface.py` for which I
find it difficult to make a decision. I would like to hear the opinion of others
on these aspects.

First, I will give some background, and then, I will explain the design
questions.

## The current "entities"

On `master`, there are currently four "entities" that directly interact with
the HTTP interface:
- `direct_interface` (free functions)
- `interface` (free functions)
- `interface.Interface`
- `WorldSlice`

On `refactor`, `interface.Interface` is renamed to `interface.Editor`, but
otherwise the entities are the same.

The responsibilities of the "entities" are as follows:
- `direct_interface` contains only _direct_ Python bindings for the HTTP
  interface, and has minimal dependencies. It is not intended to be used
  by developers, but may be useful if they want to create their own higher-level
  classes.

- The free functions in `interface` perform stateless Minecraft operations, such
  as executing commands or getting the build area

- `Editor` contains functions to place and get blocks, contains lots of state
  to make this easier or more efficient, and contains other functions that also
  require this state. It has a lot of responsibilities, but it cannot be broken
  up any further due to the shared state (for example, both `getBlock` and
  `setBlock` need to access the block cache if caching is enabled).

- `worldSlice` contains logic to call the `GET /bytes` endpoint and parse the
  result into a world slice datastructure.

Currently, the URL of the HTTP interface is hardcoded to
`http://localhost:9000`. I would like to make this configurable, ideally in the
next update to avoid future compatibility issues. I would rather avoid requiring
developers to pass a URL parameter to every interface-related function if they
use a non-default url, so I would prefer if the URL was stored as state.

I would also like to make the interface-related error reporting of GDPC
configurable. Currently, GDPC prints errors to stderr, but users may
not want this. This setting should also be stored as state.

Note that *all* interface-related entities would depend on these two bits of
state.

My first thought is to add an `Interface` class (do not confuse with the old
`Interface` class on `master`, which is now called `Editor`), and to put this
state (url, error logging setting) into it along with the free functions from
`interface.py`. Both `Editor` and the `WorldSlice` constructor would require
`Interface` to get this url and error logging info.

So, assuming the plan described above, the entities and their state are as follows:
- `direct_interface` has no state
- `Interface` state:
  - URL
  - Error logging setting
- `Editor` state:
  - `Interface`
  - Local coordinate transform (translation, rotation, flip)
  - Block buffer
  - Block cache
  - Block placement settings
  - etc.
- `WorldSlice` constructor has no state

## Design questions

There are two related design questions for which I find it difficult to make a
decision:
1. To what degree should `Interface` and `Editor` be global?
2. Does splitting `Interface` and `Editor` make sense? Should they be merged?

I will go in more detail for both.

### Question 1 - To what degree should Interface and Editor be global?

`Interface` and `Editor` are both "entities" with state. There are (at least)
four ways to implement an entity with state, each with advantages and
disadvantages:

1. Put the state in global variables and implement the entity with free
   functions. Equivalent to a singleton class.
   
   *Advantages:*
   - Easiest to use, developers need not even know about the existence of state
     unless they want to change it.
   - No classes to pass around.
   
   *Disadvantages:*
   - It becomes *impossible* to have multiple instances of the entity. This
     is the only option that makes something impossible.
   - It is "magic"-y: it may be hard to developers to understand which function
     calls affect others, since state is hidden away.

2. Implement the entity as a class.

   *Advantages:*
   - Multiple instances are possible.
   - Simplest and cleanest design from the perspective of GDPC.
   - Good discoverability via code completion on a class instance.
   
   *Disadvantages:*
   - Developers need to instantiate the classes, making GDPC slightly more
     difficult to use.
   - Class instances need to be passed around everywhere. Developers are forced
     to add parameters for the classes in nearly all of their functions, again
     making GDPC slightly more difficult to use.

3. Combination of option 1 and 2: implement the entity as a class, but create
   a default instance of it and add free functions that work with this default
   instance. This is how `Editor` (previously called `Interface`) is implemented
   on `master`.
   
   *Advantages:*
   - Both the advantages of options 1 and 2: developers can choose.
   
   *Disadvantages:*
   - All functions of the entity need to be duplicated (methods and free
     functions). Perhaps multiple duplications are even needed, for example if
     `Interface` has a default `Editor` and there is also a default `Interface`.
     This is somewhat painful to maintain, and may even cause name collision
     issues.
   - Difficult to understand for both GDPC contributors and generator
     developers.

4. Implement the entity as free functions and require that all state is
   managed by the caller and is passed to each function as parameters.
   `direct_interface` and the `worldSlice` constructor are implemented in this
   way, but I find it unacceptable for `Interface` and `Editor`.

This is all quite abstract, so I'll give some representative syntax examples of
option 1 and option 2 (for option 3, developers can choose either syntax).

`Interface` global (option 1), `Editor` global (option 1):
```python
def buildHouse(position):
	runCommand("say 'building house!'")
	placeCuboid(position, position+10, hollow=True)
	...

setURL("http://example.com:9000")
setBuffering(True)

worldSlice = WorldSlice(Rect((0,0), (64,255,64)))
placeBlock((0,64,0), Block("stone"))
buildHouse((10,64,0))
```

`Interface` global (option 1), `Editor` as class (option 2):
```python
def buildHouse(editor, position):
	runCommand("say 'building house!'")
	placeCuboid(editor, position, position+10, hollow=True)
	...

setURL("http://example.com:9000")

editor = Editor(buffering=True)

worldSlice = WorldSlice(Rect((0,0), (64,255,64)))
editor.placeBlock((0,64,0), Block("stone"))
buildHouse(editor, (10,64,0))
```

`Interface` as class (option 2), `Editor` global (option 1) (this one makes
little sense, but also see design question 2):
```python
def buildHouse(interface, position):
	interface.runCommand("say 'building house!'")
	placeCuboid(position, position+10, hollow=True)
	...

interface = Interface(url="http://example.com:9000")

setInterface(interface)
setBuffering(True)

worldSlice = WorldSlice(interface, Rect((0,0), (64,255,64)))
placeBlock((0,64,0), Block("stone"))
buildHouse(interface, (10,64,0))
```

`Interface` as class (option 2), `Editor` as class (option 2):
```python
def buildHouse(interface, editor, position):
	interface.runCommand("say 'building house!'")
	placeCuboid(editor, position, position+10, hollow=True)
	...

interface = Interface(url="http://example.com:9000")

editor = Editor(interface)
editor.setBuffering(True)

worldSlice = WorldSlice(interface, Rect((0,0), (64,255,64)))
editor.placeBlock((0,64,0), Block("stone"))
buildHouse(interface, editor, (10,64,0))
```

### Question 2 - Should Interface and Editor be merged?

I'm also not sure if it even makes sense to split `Interface` and `Editor`.
The reasons for splitting them is that the combined entity would have a *lot* of
state and responsibilities, which is not desirable from a software design
perspective, and that there might be cases where it is useful to have multiple
Editors for a single Interface. However, having them as one class might be much
easier for developers.

`Editor` can be merged into `Interface` by simply flattening it: all state and
methods of `Editor` become state and methods of `Interface`.

If `Interface` and `Editor` are merged, two syntaxes remain for design
question 1:

`Interface` global:
```python
def buildHouse(position):
	runCommand("say 'building house!'")
	placeCuboid(position, position+10, hollow=True)
	...

setURL("http://example.com/9000")
setBuffering(True)

worldSlice = WorldSlice(Rect((0,0), (64,255,64)))
placeBlock((0,64,0), Block("stone"))
buildHouse((10,64,0))
```

`Interface` as class:
```python
def buildHouse(interface, position):
	interface.runCommand("say 'building house!'")
	placeCuboid(interface, position, position+10, hollow=True)
	...

interface = Interface(url="http://example.com/9000")
interface.setBuffering(True)

worldSlice = WorldSlice(interface, Rect((0,0), (64,255,64)))
interface.placeBlock((0,64,0), Block("stone"))
buildHouse(interface, (10,64,0))
```

## Feedback

Which options do you think are best for both questions? Do you have an entirely
different idea? I would like to hear!

I am currently slightly leaning towards merging `Interface` and `Editor` into
just `Interface`, and implementing `Interface` as option 3 (as class, but
with a default instance and free functions that work with the default instance).



Discussion / hints:

Fix patch:
diff --git a/LICENSE b/LICENSE
old mode 100755
new mode 100644
index 91decee..4e59077
--- a/LICENSE
+++ b/LICENSE
@@ -2,7 +2,7 @@ MIT License
 
 Copyright (c) 2020-2021 Nils Gawlik
 Copyright (c) 2021-2022 Blinkenlights
-Copyright (c) 2022 Arthur van der Staaij
+Copyright (c) 2022-2023 Arthur van der Staaij
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
old mode 100755
new mode 100644
index b512eb1..4cd002e
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
 # GDPC 5.0 (Manicule)
 
-GDPC (Generative Design Python Client) is a framework for use in conjunction with the [Minecraft HTTP Interface Mod](https://github.com/Niels-NTG/gdmc_http_interface) (version >= 0.7.1, < 0.8.0), built for the [GDMC competition](https://gendesignmc.engineering.nyu.edu).
+GDPC (Generative Design Python Client) is a framework for use in conjunction with the [Minecraft HTTP Interface Mod](https://github.com/Niels-NTG/gdmc_http_interface) (version >= 0.7.4, < 0.8.0), built for the [GDMC competition](https://gendesignmc.engineering.nyu.edu).
 
 You need to be playing in a Minecraft world with the mod installed to use the framework.
 
diff --git a/examples/Start_Here.py b/examples/Start_Here.py
index e031e1c..0455327 100755
--- a/examples/Start_Here.py
+++ b/examples/Start_Here.py
@@ -35,13 +35,17 @@
 # === STRUCTURE #1
 # These are the modules (libraries) we will use in this code
 # We are giving these modules shorter, but distinct, names for convenience
+import logging
+from termcolor import colored
 from random import randint
 
-from gdpc import geometry as GEO
-from gdpc import interface as INTF
-from gdpc import toolbox as TB
-from gdpc import interface_toolbox as ITB
-from gdpc import worldLoader as WL
+from glm import ivec3
+
+from gdpc import Block, Editor
+from gdpc import geometry as geometry
+from gdpc import minecraft_tools as minecraft_tools
+from gdpc import editor_tools as editor_tools
+
 
 # === STRUCTURE #2
 # These variables are global and can be read from anywhere in the code
@@ -49,8 +53,15 @@
 # NOTE: if you want to change this value inside one of your functions,
 #   you'll have to add a line of code. For an example search 'GLOBAL'
 
+logging.basicConfig(format=colored("%(name)s - %(levelname)s - %(message)s", color="yellow"))
+
+# Here we construct an Editor object
+ED = Editor()
+
 # Here we read start and end coordinates of our build area
-STARTX, STARTY, STARTZ, ENDX, ENDY, ENDZ = INTF.requestBuildArea()  # BUILDAREA
+BUILD_AREA = ED.getBuildArea()  # BUILDAREA
+STARTX, STARTY, STARTZ = BUILD_AREA.begin
+LASTX, LASTY, LASTZ = BUILD_AREA.end - 1
 
 # WORLDSLICE
 # Using the start and end coordinates we are generating a world slice
@@ -60,8 +71,7 @@
 #
 # IMPORTANT: Keep in mind that a wold slice is a 'snapshot' of the world,
 #   and any changes you make later on will not be reflected in the world slice
-WORLDSLICE = WL.WorldSlice(STARTX, STARTZ,
-                           ENDX + 1, ENDZ + 1)  # this takes a while
+WORLDSLICE = ED.loadWorldSlice(BUILD_AREA.toRect(), cache=True)  # this takes a while
 
 ROADHEIGHT = 0
 
@@ -91,42 +101,42 @@ def buildPerimeter():
     print("Building east-west walls...")
     # building the east-west walls
 
-    for x in range(STARTX, ENDX + 1):
+    for x in range(STARTX, LASTX + 1):
         # the northern wall
         y = heights[(x - STARTX, 0)]
-        GEO.placeCuboid(x, y - 2, STARTZ, x, y, STARTZ, "granite")
-        GEO.placeCuboid(x, y + 1, STARTZ, x, y + 4, STARTZ, "granite_wall")
+        geometry.placeCuboid(ED, ivec3(x, y - 2, STARTZ), ivec3(x, y, STARTZ), Block("granite"))
+        geometry.placeCuboid(ED, ivec3(x, y + 1, STARTZ), ivec3( x, y + 4, STARTZ), Block("granite_wall"))
         # the southern wall
-        y = heights[(x - STARTX, ENDZ - STARTZ)]
-        GEO.placeCuboid(x, y - 2, ENDZ, x, y, ENDZ, "red_sandstone")
-        GEO.placeCuboid(x, y + 1, ENDZ, x, y + 4, ENDZ, "red_sandstone_wall")
+        y = heights[(x - STARTX, LASTZ - STARTZ)]
+        geometry.placeCuboid(ED, ivec3(x, y - 2, LASTZ), ivec3( x, y, LASTZ), Block("red_sandstone"))
+        geometry.placeCuboid(ED, ivec3(x, y + 1, LASTZ), ivec3( x, y + 4, LASTZ), Block("red_sandstone_wall"))
 
     print("Building north-south walls...")
     # building the north-south walls
-    for z in range(STARTZ, ENDZ + 1):
+    for z in range(STARTZ, LASTZ + 1):
         # the western wall
         y = heights[(0, z - STARTZ)]
-        GEO.placeCuboid(STARTX, y - 2, z, STARTX, y, z, "sandstone")
-        GEO.placeCuboid(STARTX, y + 1, z, STARTX, y + 4, z, "sandstone_wall")
+        geometry.placeCuboid(ED, ivec3(STARTX, y - 2, z), ivec3( STARTX, y, z), Block("sandstone"))
+        geometry.placeCuboid(ED, ivec3(STARTX, y + 1, z), ivec3( STARTX, y + 4, z), Block("sandstone_wall"))
         # the eastern wall
-        y = heights[(ENDX - STARTX, z - STARTZ)]
-        GEO.placeCuboid(ENDX, y - 2, z, ENDX, y, z, "prismarine")
-        GEO.placeCuboid(ENDX, y + 1, z, ENDX, y + 4, z, "prismarine_wall")
+        y = heights[(LASTX - STARTX, z - STARTZ)]
+        geometry.placeCuboid(ED, ivec3(LASTX, y - 2, z), ivec3( LASTX, y, z), Block("prismarine"))
+        geometry.placeCuboid(ED, ivec3(LASTX, y + 1, z), ivec3( LASTX, y + 4, z), Block("prismarine_wall"))
 
 
 def buildRoads():
     """Build a road from north to south and east to west."""
-    xaxis = STARTX + (ENDX - STARTX) // 2  # getting start + half the length
-    zaxis = STARTZ + (ENDZ - STARTZ) // 2
+    xaxis = STARTX + (LASTX - STARTX) // 2  # getting start + half the length
+    zaxis = STARTZ + (LASTZ - STARTZ) // 2
     heights = WORLDSLICE.heightmaps["MOTION_BLOCKING_NO_LEAVES"]
 
     print("Calculating road height...")
     # caclulating the average height along where we want to build our road
     y = heights[(xaxis - STARTX, zaxis - STARTZ)]
-    for x in range(STARTX, ENDX + 1):
+    for x in range(STARTX, LASTX + 1):
         newy = heights[(x - STARTX, zaxis - STARTZ)]
         y = (y + newy) // 2
-    for z in range(STARTZ, ENDZ + 1):
+    for z in range(STARTZ, LASTZ + 1):
         newy = heights[(xaxis - STARTX, z - STARTZ)]
         y = (y + newy) // 2
 
@@ -138,121 +148,98 @@ def buildRoads():
 
     print("Building east-west road...")
     # building the east-west road
-    GEO.placeCuboid(xaxis - 2, y, STARTZ,
-                    xaxis - 2, y, ENDZ, "end_stone_bricks")
-    GEO.placeCuboid(xaxis - 1, y, STARTZ,
-                    xaxis + 1, y, ENDZ, "gold_block")
-    GEO.placeCuboid(xaxis + 2, y, STARTZ,
-                    xaxis + 2, y, ENDZ, "end_stone_bricks")
-    GEO.placeCuboid(xaxis - 1, y + 1, STARTZ,
-                    xaxis + 1, y + 3, ENDZ, "air")
+    geometry.placeCuboid(ED, ivec3(xaxis - 2, y, STARTZ), ivec3(xaxis - 2, y, LASTZ), Block("end_stone_bricks"))
+    geometry.placeCuboid(ED, ivec3(xaxis - 1, y, STARTZ), ivec3(xaxis + 1, y, LASTZ), Block("gold_block"))
+    geometry.placeCuboid(ED, ivec3(xaxis + 2, y, STARTZ), ivec3(xaxis + 2, y, LASTZ), Block("end_stone_bricks"))
+    geometry.placeCuboid(ED, ivec3(xaxis - 1, y + 1, STARTZ), ivec3(xaxis + 1, y + 3, LASTZ), Block("air"))
 
     print("Building north-south road...")
     # building the north-south road
-    GEO.placeCuboid(STARTX, y, zaxis - 2,
-                    ENDX, y, zaxis - 2, "end_stone_bricks")
-    GEO.placeCuboid(STARTX, y, zaxis - 1,
-                    ENDX, y, zaxis + 1, "gold_block")
-    GEO.placeCuboid(STARTX, y, zaxis + 2,
-                    ENDX, y, zaxis + 2, "end_stone_bricks")
-    GEO.placeCuboid(STARTX, y + 1, zaxis - 1,
-                    ENDX, y + 3, zaxis + 1, "air")
+    geometry.placeCuboid(ED, ivec3(STARTX, y, zaxis - 2), ivec3(LASTX, y, zaxis - 2), Block("end_stone_bricks"))
+    geometry.placeCuboid(ED, ivec3(STARTX, y, zaxis - 1), ivec3(LASTX, y, zaxis + 1), Block("gold_block"))
+    geometry.placeCuboid(ED, ivec3(STARTX, y, zaxis + 2), ivec3(LASTX, y, zaxis + 2), Block("end_stone_bricks"))
+    geometry.placeCuboid(ED, ivec3(STARTX, y + 1, zaxis - 1), ivec3(LASTX, y + 3, zaxis + 1), Block("air"))
 
 
 def buildCity():
-    xaxis = STARTX + (ENDX - STARTX) // 2  # getting center
-    zaxis = STARTZ + (ENDZ - STARTZ) // 2
+    xaxis = STARTX + (LASTX - STARTX) // 2  # getting center
+    zaxis = STARTZ + (LASTZ - STARTZ) // 2
     y = ROADHEIGHT
 
     print("Building city platform...")
     # Building a platform and clearing a dome for the city to sit in
-    GEO.placeCenteredCylinder(xaxis, y, zaxis, 1, 21, "end_stone_bricks")
-    GEO.placeCenteredCylinder(xaxis, y, zaxis, 1, 20, "gold_block")
-    GEO.placeCenteredCylinder(xaxis, y + 1, zaxis, 3, 20, "air")
-    GEO.placeCenteredCylinder(xaxis, y + 4, zaxis, 2, 19, "air")
-    GEO.placeCenteredCylinder(xaxis, y + 6, zaxis, 1, 18, "air")
-    GEO.placeCenteredCylinder(xaxis, y + 7, zaxis, 1, 17, "air")
-    GEO.placeCenteredCylinder(xaxis, y + 8, zaxis, 1, 15, "air")
-    GEO.placeCenteredCylinder(xaxis, y + 9, zaxis, 1, 12, "air")
-    GEO.placeCenteredCylinder(xaxis, y + 10, zaxis, 1, 8, "air")
-    GEO.placeCenteredCylinder(xaxis, y + 11, zaxis, 1, 3, "air")
-
-    for i in range(50):
+    geometry.placeCylinder(ED, ivec3(xaxis, y,      zaxis), 39, 1, Block("end_stone_bricks"))
+    geometry.placeCylinder(ED, ivec3(xaxis, y,      zaxis), 37, 1, Block("gold_block"))
+    geometry.placeCylinder(ED, ivec3(xaxis, y +  1, zaxis), 37, 3, Block("air"))
+    geometry.placeCylinder(ED, ivec3(xaxis, y +  4, zaxis), 35, 2, Block("air"))
+    geometry.placeCylinder(ED, ivec3(xaxis, y +  6, zaxis), 33, 1, Block("air"))
+    geometry.placeCylinder(ED, ivec3(xaxis, y +  7, zaxis), 32, 1, Block("air"))
+    geometry.placeCylinder(ED, ivec3(xaxis, y +  8, zaxis), 27, 1, Block("air"))
+    geometry.placeCylinder(ED, ivec3(xaxis, y +  9, zaxis), 21, 1, Block("air"))
+    geometry.placeCylinder(ED, ivec3(xaxis, y + 10, zaxis), 13, 1, Block("air"))
+    geometry.placeCylinder(ED, ivec3(xaxis, y + 11, zaxis),  3, 1, Block("air"))
+
+    for _ in range(50):
         buildTower(randint(xaxis - 20, xaxis + 20),
                    randint(zaxis - 20, zaxis + 20))
 
     # Place a book on a Lectern
     # See the wiki for book formatting codes
-    INTF.placeBlock(xaxis, y, zaxis, "emerald_block")
-    bookData = TB.writeBook("This book has a page!")
-    ITB.placeLectern(xaxis, y + 1, zaxis, bookData)
+    ED.placeBlock(ivec3(xaxis, y, zaxis), Block("emerald_block"))
+    bookData = minecraft_tools.bookData("This book has a page!")
+    editor_tools.placeLectern(ED, ivec3(xaxis, y + 1, zaxis), bookData=bookData)
 
 
 def buildTower(x, z):
     radius = 3
+    diameter = 2*radius + 1
     y = ROADHEIGHT
 
     print(f"Building tower at {x}, {z}...")
     # if the blocks to the north, south, east and west aren't all gold
-    if not (INTF.getBlock(x - radius, y, z) == "minecraft:gold_block"
-            and INTF.getBlock(x + radius, y, z) == "minecraft:gold_block"
-            and INTF.getBlock(x, y, z - radius) == "minecraft:gold_block"
-            and INTF.getBlock(x, y, z + radius) == "minecraft:gold_block"):
+    if not (
+            ED.getBlock(ivec3(x - radius, y, z)).id == "minecraft:gold_block"
+        and ED.getBlock(ivec3(x + radius, y, z)).id == "minecraft:gold_block"
+        and ED.getBlock(ivec3(x, y, z - radius)).id == "minecraft:gold_block"
+        and ED.getBlock(ivec3(x, y, z + radius)).id == "minecraft:gold_block"
+    ):
         return  # return without building anything
 
     # lay the foundation
-    GEO.placeCenteredCylinder(x, y, z, 1, radius, "emerald_block")
+    geometry.placeCylinder(ED, ivec3(x, y, z), diameter, 1, Block("emerald_block"))
 
     # build ground floor
-    GEO.placeCenteredCylinder(x, y + 1, z, 3, radius,
-                              "lime_concrete", tube=True)
+    geometry.placeCylinder(ED, ivec3(x, y + 1, z), diameter, 3, Block("lime_concrete"), tube=True)
 
     # extend height
     height = randint(5, 20)
-    GEO.placeCenteredCylinder(
-        x, y + 4, z, height, radius, "lime_concrete", tube=True)
+    geometry.placeCylinder(ED, ivec3(x, y + 4, z), diameter, height, Block("lime_concrete"), tube=True)
     height += 4
 
     # build roof
-    GEO.placeCenteredCylinder(x, y + height, z, 1, radius, "emerald_block")
-    GEO.placeCenteredCylinder(x, y + height + 1, z, 1,
-                              radius - 1, "emerald_block")
-    GEO.placeCenteredCylinder(x, y + height + 2, z, 1,
-                              radius - 2, "emerald_block")
-    GEO.placeCuboid(x, y + height, z, x, y + height
-                    + 2, z, "lime_stained_glass")
-    INTF.placeBlock(x, y + 1, z, "beacon")
+    geometry.placeCylinder(ED, ivec3(x, y + height, z), diameter, 1, Block("emerald_block"))
+    geometry.placeCylinder(ED, ivec3(x, y + height + 1, z), diameter-2, 2, Block("emerald_block"))
+    geometry.placeCuboid(ED, ivec3(x, y + height, z), ivec3(x, y + height + 2, z), Block("lime_stained_glass"))
+    ED.placeBlock(ivec3(x, y + 1, z), Block("beacon"))
 
     # trim sides and add windows and doors
-    # GEO.placeCuboid(x + radius, y + 1, z, x + radius, y + height + 2, z, "air")
-    GEO.placeCuboid(x + radius - 1, y + 1, z,
-                    x + radius - 1, y + height + 2, z, "lime_stained_glass")
-    # NOTE: When placing doors you need to place two blocks,
-    #   the upper block defines the direction
-    INTF.placeBlock(x + radius - 1, y + 1, z, "warped_door")
-    INTF.placeBlock(x + radius - 1, y + 2, z,
-                    "warped_door[facing=west, half=upper]")
-
-    GEO.placeCuboid(x - radius, y + 1, z, x - radius, y + height + 2, z, "air")
-    GEO.placeCuboid(x - radius + 1, y + 1, z,
-                    x - radius + 1, y + height + 2, z, "lime_stained_glass")
-    INTF.placeBlock(x - radius + 1, y + 1, z, "warped_door")
-    INTF.placeBlock(x - radius + 1, y + 2, z,
-                    "warped_door[facing=east, half=upper]")
-
-    GEO.placeCuboid(x, y + 1, z + radius, x, y + height + 2, z + radius, "air")
-    GEO.placeCuboid(x, y + 1, z + radius - 1,
-                    x, y + height + 2, z + radius - 1, "lime_stained_glass")
-    INTF.placeBlock(x, y + 1, z + radius - 1, "warped_door")
-    INTF.placeBlock(x, y + 2, z + radius - 1,
-                    "warped_door[facing=south, half=upper]")
-
-    GEO.placeCuboid(x, y + 1, z - radius, x, y + height + 2, z - radius, "air")
-    GEO.placeCuboid(x, y + 1, z - radius + 1,
-                    x, y + height + 2, z - radius + 1, "lime_stained_glass")
-    INTF.placeBlock(x, y + 1, z - radius + 1, "warped_door")
-    INTF.placeBlock(x, y + 2, z - radius + 1,
-                    "warped_door[facing=north, half=upper]")
+    # NOTE: When placing doors, you only need to place the bottom block.
+    # the upper block defines the direction
+    geometry.placeCuboid(ED, ivec3(x + radius, y + 1, z), ivec3( x + radius, y + height + 2, z), Block("air"))
+    geometry.placeCuboid(ED, ivec3(x + radius - 1, y + 1, z), ivec3(x + radius - 1, y + height + 2, z), Block("lime_stained_glass"))
+    ED.placeBlock(ivec3(x + radius - 1, y + 1, z), Block("warped_door", {"facing": "west"}))
+
+    geometry.placeCuboid(ED, ivec3(x - radius, y + 1, z), ivec3( x - radius, y + height + 2, z), Block("air"))
+    geometry.placeCuboid(ED, ivec3(x - radius + 1, y + 1, z), ivec3(x - radius + 1, y + height + 2, z), Block("lime_stained_glass"))
+    ED.placeBlock(ivec3(x - radius + 1, y + 1, z), Block("warped_door", {"facing": "east"}))
+
+    geometry.placeCuboid(ED, ivec3(x, y + 1, z + radius), ivec3( x, y + height + 2, z + radius), Block("air"))
+    geometry.placeCuboid(ED, ivec3(x, y + 1, z + radius - 1), ivec3(x, y + height + 2, z + radius - 1), Block("lime_stained_glass"))
+    ED.placeBlock(ivec3(x, y + 1, z + radius - 1), Block("warped_door", {"facing": "north"}))
+
+    geometry.placeCuboid(ED, ivec3(x, y + 1, z - radius), ivec3( x, y + height + 2, z - radius), Block("air"))
+    geometry.placeCuboid(ED, ivec3(x, y + 1, z - radius + 1), ivec3(x, y + height + 2, z - radius + 1), Block("lime_stained_glass"))
+    ED.placeBlock(ivec3(x, y + 1, z - radius + 1), Block("warped_door", {"facing": "south"}))
 
 
 # === STRUCTURE #4
@@ -265,8 +252,8 @@ def buildTower(x, z):
     try:
         # height = WORLDSLICE.heightmaps["MOTION_BLOCKING"][(STARTX, STARTY)]
         height = WORLDSLICE.heightmaps["MOTION_BLOCKING"][(0, 0)]
-        INTF.runCommand(f"tp @a {STARTX} {height} {STARTZ}")
-        print(f"/tp @a {STARTX} {height} {STARTZ}")
+        # INTF.runCommand(f"tp @a {STARTX} {height} {STARTZ}")
+        # print(f"/tp @a {STARTX} {height} {STARTZ}")
         buildPerimeter()
         buildRoads()
         buildCity()
diff --git a/examples/visualizeMap.py b/examples/visualizeMap.py
index 917b8b9..1914d6e 100755
--- a/examples/visualizeMap.py
+++ b/examples/visualizeMap.py
@@ -2,21 +2,27 @@
 
 """### Displays a map of the build area."""
 
+from glm import ivec3
 import cv2
 import matplotlib.pyplot as plt
 import numpy as np
-from gdpc import interface, lookup
-from gdpc.toolbox import loop2d
-from gdpc.worldLoader import WorldSlice
+
+from gdpc import Editor, lookup
+
 
 if __name__ == '__main__':
+    editor = Editor()
+
     # see if a different build area was defined ingame
-    x1, _, z1, x2, _, z2 = interface.requestBuildArea()
+    buildArea = editor.getBuildArea()
+    buildRect = buildArea.toRect()
+    x1, z1 = buildRect.begin
+    x2, z2 = buildRect.end
 
     # load the world data and extract the heightmap(s)
-    slice = WorldSlice(x1, z1, x2, z2)
+    worldSlice = editor.loadWorldSlice(buildRect)
 
-    heightmap = np.array(slice.heightmaps["OCEAN_FLOOR"], dtype=int)
+    heightmap = np.array(worldSlice.heightmaps["OCEAN_FLOOR"], dtype=int)
 
     # calculate the gradient (steepness)
     decrementor = np.vectorize(lambda a: a - 1)
@@ -25,29 +31,29 @@
     gradientY = cv2.Scharr(cvheightmap, cv2.CV_16S, 0, 1)
 
     # create a dictionary mapping block ids ("minecraft:...") to colors
-    palette = lookup.PALETTELOOKUP
+    palette = lookup.BLOCK_TO_COLOR
 
     # create a 2d map containing the surface block colors
-    topcolor = np.zeros((x2 - x1 + 1, z2 - z1 + 1), dtype='int')
+    topcolor = np.zeros(buildRect.size, dtype='int')
     unknownBlocks = set()
 
-    for x, z in loop2d(x1, z1, x2, z2):
+    for x, z in buildRect.inner:
         # check up to 5 blocks below the heightmap
         for dy in range(5):
             # calculate absolute coordinates
             y = int(heightmap[(x - x1, z - z1)]) - dy
 
-            blockID = slice.getBlockAt(x, y, z)
-            if blockID in lookup.MAPTRANSPARENT:
+            block = worldSlice.getBlockGlobal(ivec3(x,y,z))
+            if block.id in lookup.MAP_TRANSPARENT:
                 # transparent blocks are ignored
                 continue
+
+            if block.id not in palette:
+                # unknown blocks remembered for debug purposes
+                unknownBlocks.add(block.id)
             else:
-                if blockID not in palette:
-                    # unknown blocks remembered for debug purposes
-                    unknownBlocks.add(blockID)
-                else:
-                    topcolor[(x - x1, z - z1)] = palette[blockID]
-                break
+                topcolor[(x - x1, z - z1)] = palette[block.id]
+            break
 
     if len(unknownBlocks) > 0:
         print("Unknown blocks: " + str(unknownBlocks))
diff --git a/gdpc/__init__.py b/gdpc/__init__.py
index d375852..a855f99 100644
--- a/gdpc/__init__.py
+++ b/gdpc/__init__.py
@@ -6,5 +6,12 @@
 __maintainer__       = "Arthur van der Staaij"
 __maintainer_email__ = "arthurvanderstaaij@gmail.com"
 __license__          = "MIT"
-__copyright__        = "Copyright 2022 Arthur van der Staaij, Copyright 2021-2022 Blinkenlights, Copyright 2020-2021 Nils Gawlik"
+__copyright__        = "Copyright 2022-2023 Arthur van der Staaij, Copyright 2021-2022 Blinkenlights, Copyright 2020-2021 Nils Gawlik"
 __version__          = "5.0.2"
+
+
+from .vector_tools import Rect, Box
+from .transform import Transform
+from .block import Block
+from .world_slice import WorldSlice
+from .editor import Editor
diff --git a/gdpc/bitarray.py b/gdpc/bitarray.py
deleted file mode 100644
index 5a86a9d..0000000
--- a/gdpc/bitarray.py
+++ /dev/null
@@ -1,63 +0,0 @@
-"""Read the bitarray format used by Minecraft."""
-
-from math import floor
-
-
-def inclusiveBetween(start, end, value):
-    """Raise an exception when the value is out of bounds."""
-    if not (start <= value <= end):
-        raise ValueError(
-            f"The value {value} is not in the inclusive range "
-            f"of {start} to {end}")
-
-
-class BitArray:
-    """Store an array of binary values and its metrics.
-
-    Minecraft stores block and heightmap data in compacted arrays of longs.
-    This class performs index mapping and bit shifting to access the data.
-    """
-
-    def __init__(self, bitsPerEntryIn, arraySizeIn, data):
-        """Initialise a BitArray."""
-        inclusiveBetween(1, 32, bitsPerEntryIn)
-        self.arraySize = arraySizeIn
-        self.bitsPerEntry = bitsPerEntryIn
-        self.maxEntryValue = (1 << bitsPerEntryIn) - 1
-        self.entriesPerLong = floor(64 / bitsPerEntryIn)
-        j = floor((arraySizeIn + self.entriesPerLong - 1)
-                  / self.entriesPerLong)
-        if data is not None:
-            if len(data) != j:
-                raise Exception(
-                    "Invalid length given for storage, "
-                    f"got: {len(data)} but expected: {j}")
-
-            self.longArray = data
-        else:
-            self.longArray = []  # length j
-
-    # __repr__ displays the class well enough so __str__ is omitted
-    def __repr__(self):
-        """Represent the BitArray as a constructor."""
-        return f"BitArray{(self.bitsPerEntry, self.arraySize, self.longArray)}"
-
-    def getPosOfLong(self, index):
-        """Return the position of the long that contains index."""
-        return index // self.entriesPerLong
-
-    def getAt(self, index):
-        """Return the binary value stored at index."""
-        # If longArray size is 0, this is because the corresponding palette
-        # only contains a single value.
-        if len(self.longArray) == 0:
-            return 0
-        inclusiveBetween(0, (self.arraySize - 1), index)
-        i = self.getPosOfLong(index)
-        j = self.longArray[i]
-        k = (index - i * self.entriesPerLong) * self.bitsPerEntry
-        return j >> k & self.maxEntryValue
-
-    def size(self):
-        """Return self.arraySize."""
-        return self.arraySize
diff --git a/gdpc/block.py b/gdpc/block.py
new file mode 100644
index 0000000..57a282e
--- /dev/null
+++ b/gdpc/block.py
@@ -0,0 +1,125 @@
+"""Provides the Block class"""
+
+
+from typing import Any, Union, Optional, Dict, Sequence
+from dataclasses import dataclass, field
+from copy import copy, deepcopy
+import random
+
+from glm import bvec3
+from nbt import nbt
+
+from .nbt_tools import nbtToPythonObject, pythonObjectToSnbt
+from .block_state_tools import transformAxis, transformFacing, transformRotation
+
+
+@dataclass
+class Block:
+    """A Minecraft block.
+
+    Block states can be stored in .states, and block entity NBT data can be stored in .data.
+
+    Block entity NBT data should be specified as a JSON-like structure of built-in Python objects
+    (dict, list, int, etc.). For example: `{"Key1": [1, 2, 3], "Key2": "foobar"}`
+
+    If .id is a sequence, the instance represents a palette of blocks that share the same
+    block states and block entity NBT data.
+
+    If (an element of) .id is an empty string or None, that element represents "no placement":
+    placing such a block has no effect.
+
+    The transform methods modify a number of orientation-related block states. These are:
+    - axis
+    - facing
+    - rotation
+
+    Other orientation-related block states are currently not supported by the transformation
+    system, but support may be added in a future version.
+    """
+
+    # TODO: Known orientation-related block states that are currently not supported:
+    # - type="bottom"/"top" (e.g. slabs)  (note that slabs can also have type="double"!)
+    # - half="bottom"/"top" (e.g. stairs) ("half" is also used for other purposes, see e.g. doors)
+
+    id:     Union[str, Sequence[Optional[str]]] = "minecraft:stone"
+    states: Dict[str, str]                      = field(default_factory=dict)
+    data:   Any                                 = None
+
+
+    def chooseId(self):
+        """Returns a copy of this block with a single ID.\n
+        If .id is a sequence, one ID is chosen at random."""
+        result = copy(self)
+        if not isinstance(self.id, str):
+            self.id = random.choice(self.id)
+        return result
+
+
+    def transform(self, rotation: int = 0, flip: bvec3 = bvec3()):
+        """Transforms this block.\n
+        Flips first, rotates second."""
+        axisState     = self.states.get("axis")
+        facingState   = self.states.get("facing")
+        rotationState = self.states.get("rotation")
+        if axisState     is not None: self.states["axis"]     = transformAxis    (axisState,     rotation)
+        if facingState   is not None: self.states["facing"]   = transformFacing  (facingState,   rotation, flip)
+        if rotationState is not None: self.states["rotation"] = transformRotation(rotationState, rotation, flip)
+
+
+    def transformed(self, rotation: int = 0, flip: bvec3 = bvec3()):
+        """Returns a transformed copy of this block.\n
+        Flips first, rotates second."""
+        block = deepcopy(self)
+        block.transform(rotation, flip)
+        return block
+
+
+    def stateString(self):
+        """Returns a string containing the block states of this block, including the outer brackets."""
+        stateString = ",".join([f"{key}={value}" for key, value in self.states.items()])
+        return "" if stateString == "" else f"[{stateString}]"
+
+
+    def dataString(self):
+        """Returns this block's block entity NBT data as an SNBT string."""
+        return "" if self.data is None else pythonObjectToSnbt(self.data)
+
+
+    def __str__(self):
+        statesAndData = self.stateString() + self.dataString()
+        if isinstance(self.id, str):
+            return self.id + statesAndData
+        return ",".join(id + statesAndData for id in self.id if id is not None)
+
+
+    def __repr__(self):
+        # This is used for model dumping; it needs to return a string that eval()'s to this Block.
+        # The default repr includes unnecessary default values, which make model dumps way larger
+        # than they need to be.
+        return (
+            f"Block({repr(self.id)}"
+            + (f",states={repr(self.states)}" if self.states else "")
+            + (f",data={repr(self.data)}" if self.data is not None else "")
+            + ")"
+        )
+
+
+    @staticmethod
+    def fromBlockStateTag(blockStateTag: nbt.TAG_Compound, blockEntityTag: Optional[nbt.TAG_Compound] = None):
+        """Parses a block state compound tag (as found in chunk palettes) into a Block.\n
+        If <blockEntityTag> is provided, it is parsed into the Block's .data attribute."""
+        block = Block(str(blockStateTag["Name"]))
+
+        if "Properties" in blockStateTag:
+            for tag in blockStateTag["Properties"].tags:
+                block.states[str(tag.name)] = str(tag.value)
+
+        if blockEntityTag is not None:
+            block.data = nbtToPythonObject(blockEntityTag)
+            del block.data['x']
+            del block.data['y']
+            del block.data['z']
+            del block.data['id']
+            del block.data['keepPacked']
+
+        return block
diff --git a/gdpc/block_state_tools.py b/gdpc/block_state_tools.py
new file mode 100644
index 0000000..23f4ed2
--- /dev/null
+++ b/gdpc/block_state_tools.py
@@ -0,0 +1,238 @@
+"""Utilities for working with orientation-related block states"""
+
+
+from glm import ivec3, bvec3
+
+
+# ==================================================================================================
+# Listings
+# ==================================================================================================
+
+
+AXIS_VALUES     = ("x", "y", "z")
+FACING_VALUES   = ("up", "down", "north", "east", "south", "west")
+ROTATION_VALUES = ("0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15")
+
+
+# ==================================================================================================
+# Conversions between different block states
+# ==================================================================================================
+
+
+__FACING_TO_ROTATION = {
+    "south": "0",
+    "west":  "4",
+    "north": "8",
+    "east":  "12",
+}
+def facingToRotation(facing: str):
+    """Converts <facing> to the corresponding "rotation" block state string"""
+    return __FACING_TO_ROTATION[facing]
+
+
+__ROTATION_TO_FACING = {
+    "0":  "south",
+    "1":  "south",
+    "2":  "south",
+    "3":  "west",
+    "4":  "west",
+    "5":  "west",
+    "6":  "west",
+    "7":  "north",
+    "8":  "north",
+    "9":  "north",
+    "10": "north",
+    "11": "east",
+    "12": "east",
+    "13": "east",
+    "14": "east",
+    "15": "south",
+}
+def rotationToFacing(rotation: str):
+    """Converts <rotation> to the nearest corresponding "facing" block state string"""
+    return __ROTATION_TO_FACING[rotation]
+
+
+# ==================================================================================================
+# String <-> vector conversion functions
+# ==================================================================================================
+
+
+# --------------------------------------------------------------------------------------------------
+# Axis
+
+
+__VECTOR_TO_AXIS = {
+    (1,0,0): "x",
+    (0,1,0): "y",
+    (0,0,1): "z",
+}
+def vectorToAxis(vec: ivec3):
+    """Returns the "axis" block state string corresponding to the direction vector [vec]"""
+    v = (
+        vec.x != 0,
+        vec.y != 0,
+        vec.z != 0,
+    )
+    try:
+        return __VECTOR_TO_AXIS[v]
+    except KeyError as e:
+        raise ValueError("Exactly one vector component must be non-zero") from e
+
+
+__AXIS_TO_VECTOR = {
+    "x": ivec3(1,0,0),
+    "y": ivec3(0,1,0),
+    "z": ivec3(0,0,1),
+}
+def axisToVector(axis: str):
+    """Returns the direction vector corresponding to the "axis" block state string [axis]"""
+    return __AXIS_TO_VECTOR[axis]
+
+
+# --------------------------------------------------------------------------------------------------
+# Facing
+
+
+__VECTOR_TO_FACING = {
+    ( 0, 0,-1): "north",
+    ( 0, 0, 1): "south",
+    ( 0,-1, 0): "down",
+    ( 0, 1, 0): "up",
+    (-1, 0, 0): "west",
+    ( 1, 0, 0): "east",
+}
+def vectorToFacing(vec: ivec3):
+    """Returns the "facing" block state string corresponding to the direction vector [vec]"""
+    v = (
+        -1 if vec[0] < 0 else 1 if vec[0] > 0 else 0,
+        -1 if vec[1] < 0 else 1 if vec[1] > 0 else 0,
+        -1 if vec[2] < 0 else 1 if vec[2] > 0 else 0,
+    )
+    try:
+        return __VECTOR_TO_FACING[v]
+    except KeyError as e:
+        raise ValueError("Exactly one vector component must be non-zero") from e
+
+
+__FACING_TO_VECTOR = {
+    "north": ivec3( 0, 0,-1),
+    "south": ivec3( 0, 0, 1),
+    "down":  ivec3( 0,-1, 0),
+    "up":    ivec3( 0, 1, 0),
+    "west":  ivec3(-1, 0, 0),
+    "east":  ivec3( 1, 0, 0),
+}
+def facingToVector(facing: str):
+    """Returns the direction vector corresponding to the "facing" block state string [facing]"""
+    return __FACING_TO_VECTOR[facing]
+
+
+# --------------------------------------------------------------------------------------------------
+# Rotation
+
+
+# TODO: vectorToRotation
+
+
+def rotationToVector(rotation: str):
+    """Returns the axis-aligned direction vector corresponding to <rotation>"""
+    return facingToVector(rotationToFacing(rotation))
+
+
+# ==================================================================================================
+# String transformation functions
+# ==================================================================================================
+
+
+# --------------------------------------------------------------------------------------------------
+# Axis
+
+
+def rotateAxis(axis: str, rotation: int):
+    """Returns the rotated "axis" block state string"""
+    strings = ["x", "z"]
+    try:
+        return strings[(strings.index(axis) + rotation) % 2]
+    except ValueError: # if axis == "y"
+        return axis
+
+
+def transformAxis(axis: str, rotation: int = 0):
+    """Returns the transformed "axis" block state string"""
+    # Flipping is a no-op for axis strings
+    return rotateAxis(axis, rotation)
+
+
+# --------------------------------------------------------------------------------------------------
+# Facing
+
+
+def rotateFacing(facing: str, rotation: int):
+    """Returns the rotated "facing" block state string"""
+    strings = ["north", "east", "south", "west"]
+    try:
+        return strings[(strings.index(facing) + rotation) % 4]
+    except ValueError:
+        return facing # up, down
+
+
+def flipFacing(facing: str, flip: bvec3):
+    """Returns the flipped "facing" block state string"""
+    if flip.x:
+        if facing == "east":  return "west"
+        if facing == "west":  return "east"
+    if flip.y:
+        if facing == "down":  return "up"
+        if facing == "up":    return "down"
+    if flip.z:
+        if facing == "north": return "south"
+        if facing == "south": return "north"
+    return facing
+
+
+def transformFacing(facing: str, rotation: int = 0, flip: bvec3 = bvec3()):
+    """Returns the transformed "facing" block state string.\n
+    Flips first, rotates second."""
+    return rotateFacing(flipFacing(facing, flip), rotation)
+
+
+__INVERT_FACING = {
+    "north": "south",
+    "south": "north",
+    "down":  "up",
+    "up":    "down",
+    "west":  "east",
+    "east":  "west",
+}
+def invertFacing(facing: str):
+    """Returns the inverted "facing" block state string"""
+    return __INVERT_FACING[facing]
+
+
+# --------------------------------------------------------------------------------------------------
+# Rotation
+
+
+def rotateRotation(blockStateRotation: str, rotation: int):
+    """Returns the rotated "rotation" block state string.\n
+    Yes, this is confusing. <blockStateRotation> denotes a value of the "rotation" block state, as
+    used by e.g. signs. <rotation> denotes a rotation as used by GDPC's transformation system, so one
+    of {0,1,2,3}. This function name is consistent with the other block state rotation functions."""
+    return str((int(blockStateRotation) + 4*rotation) % 16)
+
+
+def flipRotation(rotation: str, flip: bvec3):
+    """Returns the flipped "rotation" block state string"""
+    rotationInt = int(rotation)
+    if flip.x:
+        rotationInt = (16 - rotationInt) % 16
+    if flip.z:
+        rotationInt = (8 - rotationInt) % 16
+    return str(rotationInt)
+
+
+def transformRotation(blockStateRotation: str, rotation: int = 0, flip: bvec3 = bvec3()):
+    """Returns the transformed "rotation" block state string.\n
+    Flips first, rotates second."""
+    return rotateRotation(flipRotation(blockStateRotation, flip), rotation)
diff --git a/gdpc/direct_interface.py b/gdpc/direct_interface.py
deleted file mode 100644
index 5b3aceb..0000000
--- a/gdpc/direct_interface.py
+++ /dev/null
@@ -1,147 +0,0 @@
-"""Provide access to the HTTP interface of the Minecraft HTTP server.
-
-This file contains various functions that map directly onto the HTTP interface.
-It is recommended to use `interface.py` instead.
-"""
-
-import requests
-from requests.exceptions import ConnectionError as RequestConnectionError
-
-
-def get(*args):
-    try:
-        return requests.get(*args)
-    except RequestConnectionError as e:
-        raise RequestConnectionError(
-            "Connection could not be established! (is Minecraft running?)"
-        ) from e
-
-
-def post(*args):
-    try:
-        return requests.post(*args)
-    except RequestConnectionError as e:
-        raise RequestConnectionError(
-            "Connection could not be established! (is Minecraft running?)"
-        ) from e
-
-
-def getBlock(x, y, z, dx=None, dy=None, dz=None, includeState=None, includeData=None, dimension=None, asJsonResponse=True):
-    """Return block material, position and other attributes on one or multiple positions in the world."""
-    url = 'http://localhost:9000/blocks'
-    parameters = {
-        'x': x,
-        'y': y,
-        'z': z,
-        'dx': dx,
-        'dy': dy,
-        'dz': dz,
-        'includeState': includeState,
-        'includeData': includeData,
-        'dimension': dimension,
-    }
-    try:
-        if asJsonResponse:
-            response = requests.get(url, params=parameters, headers={'Accept': 'application/json'}).json()
-            if len(response) == 1:
-                response = response[0]
-        else:
-            response = requests.get(url, params=parameters).text
-    except RequestConnectionError:
-        if asJsonResponse:
-            return {
-                'id': 'minecraft:void_air',
-                'x': x,
-                'y': y,
-                'z': z,
-            }
-        return f'{x} {y} {z} minecraft:void_air'
-    return response
-
-
-def placeBlock(x, y, z, blockStr, doBlockUpdates=True, customFlags=None, dimension=None, asJsonResponse=False):
-    """Place one or multiple blocks in the world."""
-    url = 'http://localhost:9000/blocks'
-    parameters = {
-        'x': x,
-        'y': y,
-        'z': z,
-        'doBlockUpdates': doBlockUpdates,
-        'customFlags': customFlags,
-        'dimension': dimension,
-    }
-    headers = {'Accept': 'application/json'} if asJsonResponse else {}
-    try:
-        response = requests.put(url, data=bytes(blockStr, "utf-8"), params=parameters, headers=headers)
-    except RequestConnectionError:
-        return "0"
-    if asJsonResponse:
-        return response.json()
-    return response.text
-
-
-def sendBlocks(blockList, x=0, y=0, z=0, retries=5,
-               doBlockUpdates=True, customFlags=None, dimension=None):
-    """Take a list of blocks and place them into the world in one go."""
-    body = str.join("\n", ['~{} ~{} ~{} {}'.format(*bp) for bp in blockList])
-    try:
-        response = placeBlock(x, y, z, body, doBlockUpdates, customFlags, dimension)
-        return response
-    except RequestConnectionError as e:
-        print("Request failed: {} Retrying ({} left)".format(e, retries))
-        if retries > 0:
-            return sendBlocks(blockList, x, y, z, retries - 1, doBlockUpdates, customFlags, dimension)
-    return False
-
-
-def runCommand(command, dimension=None):
-    """Run a Minecraft command in the world."""
-    url = 'http://localhost:9000/command'
-    try:
-        response = requests.post(url, bytes(command, "utf-8"), params={'dimension': dimension})
-    except RequestConnectionError:
-        return "connection error"
-    return response.text
-
-
-def requestBuildArea():
-    """Return the building area."""
-    area = 0, 0, 0, 128, 256, 128   # default area for beginners
-    response = requests.get('http://localhost:9000/buildarea')
-    if response.ok:
-        buildArea = response.json()
-        if buildArea != -1:
-            x1 = buildArea["xFrom"]
-            y1 = buildArea["yFrom"]
-            z1 = buildArea["zFrom"]
-            x2 = buildArea["xTo"]
-            y2 = buildArea["yTo"]
-            z2 = buildArea["zTo"]
-            area = x1, y1, z1, x2, y2, z2
-    else:
-        print(response.text)
-        print("Using default build area (0, 0, 0, 128, 256, 128).")
-    return area
-
-
-def getChunks(x, z, dx, dz, dimension=None, rtype='text'):
-    """Get raw chunk data."""
-    url = 'http://localhost:9000/chunks'
-    parameters = {
-        'x': x,
-        'z': z,
-        'dx': dx,
-        'dz': dz,
-        'dimension': dimension,
-    }
-    acceptType = 'application/octet-stream' if rtype == 'bytes' else 'text/plain'
-    response = requests.get(url, params=parameters, headers={"Accept": acceptType})
-    if response.status_code >= 400:
-        print(f"Error: {response.text}")
-
-    if rtype == 'text':
-        return response.text
-    elif rtype == 'bytes':
-        return response.content
-    else:
-        raise Exception(f"{rtype} is not a valid return type.")
diff --git a/gdpc/editor.py b/gdpc/editor.py
new file mode 100644
index 0000000..491f1e4
--- /dev/null
+++ b/gdpc/editor.py
@@ -0,0 +1,523 @@
+"""Provides the `Editor` class, which provides high-level functions to interact with the Minecraft
+world through the GDMC HTTP interface"""
+
+
+from typing import Dict, Sequence, Union, Optional, List, Iterable
+from contextlib import contextmanager
+from copy import copy, deepcopy
+import random
+from concurrent import futures
+import logging
+
+import numpy as np
+from glm import ivec3
+
+from .utils import eagerAll, OrderedByLookupDict
+from .vector_tools import Rect, Box, addY, dropY
+from .transform import Transform, TransformLike, toTransform
+from .block import Block
+from . import lookup
+from . import interface
+from .world_slice import WorldSlice
+
+
+logger = logging.getLogger(__name__)
+
+
+class Editor:
+    """Provides a high-level functions to interact with the Minecraft world through the GDMC HTTP
+    interface.
+
+    Stores various settings, resources, buffers and caches related to block placement, and a
+    transform that defines a local coordinate system.
+    """
+
+    def __init__(
+        self,
+        transformLike: Optional[TransformLike] = None,
+        buffering             = False,
+        bufferLimit           = 1024,
+        caching               = False,
+        cacheLimit            = 8192,
+        multithreading        = False,
+        multithreadingWorkers = 1,
+        retries               = 4,
+        timeout               = None,
+        host                  = interface.DEFAULT_HOST,
+    ):
+        """Constructs an Editor instance with the specified transform and settings"""
+        self._retries = retries
+        self._timeout = timeout
+        self._host    = host
+
+        self._transform = Transform() if transformLike is None else toTransform(transformLike)
+
+        self._buffering = buffering
+        self._bufferLimit = bufferLimit
+        self._buffer: Dict[ivec3,Block] = {}
+        self._commandBuffer: List[str] = []
+
+        self._caching = caching
+        self._cache = OrderedByLookupDict[ivec3,Block](cacheLimit)
+
+        self._multithreading = False
+        self._multithreadingWorkers = multithreadingWorkers
+        self.multithreading = multithreading # The property setter initializes the multithreading system.
+        self._bufferFlushFutures: List[futures.Future] = []
+
+        self._doBlockUpdates = True
+        self._spawnDrops     = False
+        self._bufferDoBlockUpdates = self._doBlockUpdates
+        self._bufferSpawnDrops     = self._spawnDrops
+
+        self._worldSlice: Optional[WorldSlice] = None
+        self._worldSliceDecay: Optional[np.ndarray] = None
+
+
+    def __del__(self):
+        """Cleans up this Editor instance"""
+        # awaits any pending buffer flush futures and shuts down the buffer flush executor
+        self.multithreading = False # awaits any pending buffer flush futures and shuts down the buffer flush executor
+        # Flush any remaining blocks in the buffer.
+        # This is purposefully done *after* disabling multithreading! This __del__ may be called at
+        # interpreter shutdown, and it appears that scheduling a new future at that point fails with
+        # "RuntimeError: cannot schedule new futures after shutdown" even if the executor has not
+        # actually shut down yet. For safety, the last buffer flush must be done on the main thread.
+        self.flushBuffer()
+
+
+    @property
+    def transform(self):
+        """This editor's local coordinate transform (used for block placement and retrieval)"""
+        return self._transform
+
+    @transform.setter
+    def transform(self, value: Union[Transform, ivec3]):
+        self._transform = toTransform(value)
+
+    @property
+    def buffering(self) -> bool:
+        """Whether block placement buffering is enabled"""
+        return self._buffering
+
+    @buffering.setter
+    def buffering(self, value: bool):
+        if self.buffering and not value:
+            self.flushBuffer()
+        self._buffering = value
+
+    @property
+    def bufferLimit(self) -> int:
+        """Size of the block buffer"""
+        return self._bufferLimit
+
+    @bufferLimit.setter
+    def bufferLimit(self, value: int):
+        self._bufferLimit = value
+        if len(self._buffer) >= self.bufferLimit:
+            self.flushBuffer()
+
+    @property
+    def caching(self):
+        """Whether caching placed and retrieved blocks is enabled"""
+        return self._caching
+
+    @caching.setter
+    def caching(self, value: bool):
+        self._caching = value
+
+    @property
+    def cacheLimit(self):
+        """Size of the block cache"""
+        return self._cache.maxSize
+
+    @cacheLimit.setter
+    def cacheLimit(self, value: int):
+        self._cache.maxSize = value
+
+    @property
+    def multithreading(self):
+        """Whether multithreaded buffer flushing is enabled"""
+        return self._multithreading
+
+    @multithreading.setter
+    def multithreading(self, value: bool):
+        if not self._multithreading and value:
+            self._bufferFlushExecutor = futures.ThreadPoolExecutor(self._multithreadingWorkers)
+            if self._multithreadingWorkers > 1:
+                logger.warning(
+                    "Multithreading with more than one worker thread.\n"
+                    "An editor has been set to use multithreaded buffer flushing with more than one\n"
+                    "worker thread.\n"
+                    "The editor can no longer guarantee that blocks will be placed in the same order\n"
+                    "as they were sent. If buffering or caching is used, this can also cause the\n"
+                    "caches to become inconsistent with the actual world.\n"
+                    "Multithreading with more than one worker thread can speed up block placement on\n"
+                    "some machines, which can be nice during development, but it is NOT RECOMMENDED\n"
+                    "for production code."
+                )
+        elif self._multithreading and not value:
+            self._bufferFlushExecutor.shutdown(wait=True)
+            del self._bufferFlushExecutor
+        self._multithreading = value
+
+    @property
+    def multithreadingWorkers(self):
+        """The amount of buffer flush worker threads."""
+        return self._multithreadingWorkers
+
+    @multithreadingWorkers.setter
+    def multithreadingWorkers(self, value: int):
+        restartExecutor = self.multithreading and self._multithreadingWorkers != value
+        self._multithreadingWorkers = value
+        if restartExecutor:
+            self.multithreading = False
+            self.multithreading = True
+
+    @property
+    def doBlockUpdates(self):
+        """Whether placed blocks should receive a block update"""
+        return self._doBlockUpdates
+
+    @doBlockUpdates.setter
+    def doBlockUpdates(self, value: bool):
+        if self.buffering and value != self._doBlockUpdates:
+            self.flushBuffer()
+        self._doBlockUpdates = value
+
+    @property
+    def spawnDrops(self):
+        """Whether overwritten blocks should drop items"""
+        return self._spawnDrops
+
+    @spawnDrops.setter
+    def spawnDrops(self, value: bool):
+        if self.buffering and value != self._spawnDrops:
+            self.flushBuffer()
+        self._spawnDrops = value
+
+    @property
+    def retries(self):
+        """The amount of retries for requests to the GDMC HTTP interface"""
+        return self._retries
+
+    @retries.setter
+    def retries(self, value: int):
+        self._retries = value
+
+    @property
+    def timeout(self):
+        """The timeout for requests to the GDMC HTTP interface (as described by the `requests` package)"""
+        return self._timeout
+
+    @timeout.setter
+    def timeout(self, value):
+        self._timeout = value
+
+    @property
+    def host(self):
+        """The address (hostname+port) of the GDMC HTTP interface to use"""
+        return self._host
+
+    @host.setter
+    def host(self, value: str):
+        self.flushBuffer()
+        self.awaitBufferFlushes()
+        self._host = value
+
+    @property
+    def worldSlice(self):
+        """The cached WorldSlice"""
+        return self._worldSlice
+
+    @property
+    def worldSliceDecay(self):
+        """3D boolean array indicating whether the block at the specified position in the cached
+        worldSlice is still valid.\n
+        Do not edit the returned array.\n
+        Note that the lowest Y-layer is at [:,0,:], despite Minecraft's negative Y coordinates."""
+        return self._worldSliceDecay
+
+
+    def runCommand(self, command: str, syncWithBuffer=False):
+        """Executes one or multiple Minecraft commands (separated by newlines).\n
+        The leading "/" must be omitted.\n
+        If buffering is enabled and <syncWithBuffer>=True, the command is deferred until after the
+        next buffer flush."""
+        if self.buffering and syncWithBuffer:
+            self._commandBuffer.append(command)
+            return
+        result = interface.runCommand(command, retries=self.retries, timeout=self.timeout, host=self.host)
+        if not result[0].isnumeric():
+            logger.error("Server returned error upon running command:\n  %s", result[0])
+
+
+    def getBuildArea(self) -> Box:
+        """Returns the build area that was specified by /setbuildarea in-game.\n
+        The build area is always in **global coordinates**; self.transform is ignored."""
+        return interface.getBuildArea(retries=self.retries, timeout=self.timeout, host=self.host)
+
+
+    def setBuildArea(self, buildArea: Box):
+        """Sets the build area to [box], and returns it.\n
+        The build area must be given in **global coordinates**; self.transform is ignored."""
+        self.runCommand(f"setbuildarea {buildArea.begin.x} {buildArea.begin.y} {buildArea.begin.z} {buildArea.end.x} {buildArea.end.y} {buildArea.end.z}")
+        return self.getBuildArea()
+
+
+    def getBlock(self, position: ivec3):
+        """Returns the block at [position].\n
+        <position> is interpreted as local to the coordinate system defined by self.transform.
+        The returned block's orientation is also from the perspective of self.transform.\n
+        If the given coordinates are invalid, returns Block("minecraft:void_air")."""
+        block = self.getBlockGlobal(self.transform * position)
+        invTransform = ~self.transform
+        block.transform(invTransform.rotation, invTransform.flip)
+        return block
+
+
+    def getBlockGlobal(self, position: ivec3):
+        """Returns the block at [position], ignoring self.transform.\n
+        If the given coordinates are invalid, returns Block("minecraft:void_air")."""
+        if self.caching:
+            block = self._cache.get(position)
+            if block is not None:
+                return copy(block)
+
+        if self.buffering:
+            block = self._buffer.get(position)
+            if block is not None:
+                return copy(block)
+
+        if (
+            self._worldSlice is not None and
+            self._worldSlice.rect.contains(dropY(position)) and
+            not self._worldSliceDecay[tuple(position - addY(self._worldSlice.rect.offset, lookup.BUILD_Y_MIN))]
+        ):
+            block = self._worldSlice.getBlockGlobal(position)
+        else:
+            block = interface.getBlocks(position, includeState=True, includeData=True, retries=self.retries, timeout=self.timeout, host=self.host)[0][1]
+
+        if self.caching:
+            self._cache[position] = copy(block)
+
+        return block
+
+
+    def placeBlock(
+        self,
+        position:       Union[ivec3, Iterable[ivec3]],
+        block:          Union[Block, Sequence[Optional[Block]]],
+        replace:        Optional[Union[str, List[str]]] = None
+    ):
+        """Places <block> at <position>.\n
+        <position> is interpreted as local to the coordinate system defined by self.transform.\n
+        If <position> is iterable (e.g. a list), <block> is placed at all positions.
+        This is slightly more efficient than calling this method in a loop.\n
+        If <block> is a sequence (e.g. a list), blocks are sampled randomly. If a block's .id is
+        iterable, block IDs are sampled randomly for that block.\n
+        Returns whether the placement succeeded fully."""
+        return self.placeBlockGlobal(
+            self.transform * position if isinstance(position, ivec3) else (self.transform * pos for pos in position),
+            block.transformed(self.transform.rotation, self.transform.flip) if isinstance(block, Block) else (block.transformed(self.transform.rotation, self.transform.flip) for block in block),
+            replace
+        )
+
+
+    def placeBlockGlobal(
+        self,
+        position:       Union[ivec3, Iterable[ivec3]],
+        block:          Union[Block, Sequence[Optional[Block]]],
+        replace:        Optional[Union[str, Iterable[str]]] = None
+    ):
+        """Places <block> at <position>, ignoring self.transform.\n
+        If <position> is iterable (e.g. a list), <block> is placed at all positions.
+        In this case, buffering is temporarily enabled for better performance.\n
+        If <block> is a sequence (e.g. a list), blocks are sampled randomly. If a block's .id is
+        iterable, block IDs are sampled randomly for that block.\n
+        Returns whether the placement succeeded fully."""
+
+        if isinstance(position, ivec3):
+            return self._placeSingleBlockGlobal(position, block, replace)
+
+        oldBuffering = self.buffering
+        self.buffering = True
+        success = eagerAll(self._placeSingleBlockGlobal(pos, block, replace) for pos in position)
+        self.buffering = oldBuffering
+        return success
+
+
+    def _placeSingleBlockGlobal(
+        self,
+        position:       ivec3,
+        block:          Union[Block, Sequence[Optional[Block]]],
+        replace:        Optional[Union[str, Iterable[str]]] = None
+    ):
+        """Places <block> at <position>, ignoring self.transform.\n
+        If <block> is a sequence (e.g. a list), blocks are sampled randomly. If a block's .id is
+        iterable, block IDs are sampled randomly for that block.\n
+        Returns whether the placement succeeded fully."""
+
+        # Check replace condition
+        if replace is not None:
+            if isinstance(replace, str):
+                replace = [replace]
+            if self.getBlockGlobal(position) not in replace:
+                return True
+
+        # Select block from palette
+        if not isinstance(block, Block):
+            block = random.choice(block)
+            if block is None:
+                return True
+        block = block.chooseId()
+        if not block.id:
+            return True
+
+        if self._buffering:
+            success = self._placeSingleBlockGlobalBuffered(position, block)
+        else:
+            success = self._placeSingleBlockGlobalDirect(position, block)
+
+        if not success:
+            return False
+
+        if self.caching:
+            self._cache[position] = block
+
+        if self._worldSlice is not None and self._worldSlice.rect.contains(dropY(position)):
+            self._worldSliceDecay[tuple(position - addY(self._worldSlice.rect.offset, lookup.BUILD_Y_MIN))] = True
+
+        return True
+
+
+    def _placeSingleBlockGlobalDirect(self, position: ivec3, block: Block):
+        """Place a single block in the world directly.\n
+        Returns whether the placement succeeded."""
+        result = interface.placeBlocks([(position, block)], doBlockUpdates=self.doBlockUpdates, spawnDrops=self.spawnDrops, retries=self.retries, timeout=self.timeout, host=self.host)
+        if not result[0].isnumeric():
+            logger.error("Server returned error upon placing block:\n  %s", result[0])
+            return False
+        return True
+
+
+    def _placeSingleBlockGlobalBuffered(self, position: ivec3, block: Block):
+        """Place a block in the buffer and send once limit is exceeded.\n
+        Returns whether placement succeeded."""
+        if len(self._buffer) >= self.bufferLimit:
+            self.flushBuffer()
+        self._buffer[position] = block
+        return True
+
+
+    def flushBuffer(self):
+        """Flushes the block placement buffer.\n
+        If multithreaded buffer flushing is enabled, the worker threads can be awaited with
+        awaitBufferFlushes()."""
+
+        def flush(blockBuffer: Dict[ivec3, Block], commandBuffer: List[str]):
+            # Flush block buffer
+            if blockBuffer:
+                response = interface.placeBlocks(blockBuffer.items(), doBlockUpdates=self._bufferDoBlockUpdates, spawnDrops=self.spawnDrops, retries=self.retries, timeout=self.timeout, host=self.host)
+                blockBuffer.clear()
+
+                for line in response:
+                    if not line.isnumeric():
+                        logger.error("Server returned error upon placing buffered block:\n  %s", line)
+
+            # Flush command buffer
+            if commandBuffer:
+                response = interface.runCommand("\n".join(commandBuffer), retries=self.retries, timeout=self.timeout, host=self.host)
+                commandBuffer.clear()
+
+                for line in response:
+                    if not line.isnumeric():
+                        logger.error("Server returned error upon running buffered command:\n  %s", line)
+
+        if self._multithreading:
+            # Clean up finished buffer flush futures
+            self._bufferFlushFutures = [
+                future for future in self._bufferFlushFutures if not future.done()
+            ]
+
+            # Shallow copies are good enough here
+            blockBufferCopy   = self._buffer
+            commandBufferCopy = self._commandBuffer
+            def task():
+                flush(blockBufferCopy, commandBufferCopy)
+
+            # Submit the task
+            future = self._bufferFlushExecutor.submit(task)
+            self._bufferFlushFutures.append(future)
+
+            # Empty the buffers (the thread has copies of the references)
+            self._buffer = {}
+            self._commandBuffer = []
+
+        else: # No multithreading
+            flush(self._buffer, self._commandBuffer)
+
+
+    def awaitBufferFlushes(self, timeout: Optional[float] = None):
+        """Awaits all pending buffer flushes.\n
+        If [timeout] is not None, waits for at most [timeout] seconds.\n
+        Does nothing if no buffer flushes have occured while multithreaded buffer flushing was
+        enabled."""
+        self._bufferFlushFutures = futures.wait(self._bufferFlushFutures, timeout).not_done
+
+
+    def loadWorldSlice(self, rect: Optional[Rect]=None, heightmapTypes: Optional[Iterable[str]] = None, cache=False):
+        """Loads the world slice for the given XZ-rectangle.\n
+        The rectangle must be given in **global coordinates**; self.transform is ignored.\n
+        If <rect> is None, the world slice of the current build area is loaded.\n
+        If <cache>=True, the loaded worldSlice is cached in this editor. It can then be accessed
+        through .worldSlice.
+        If a world slice was already cached, it is replaced.
+        The cached world slice is used for faster block retrieval. Note that the editor assumes
+        that nothing besides itself changes the given area of the world. If the given world area is
+        changed other than through this editor, call .updateWorldSlice() to update the cached world
+        slice."""
+        if rect is None:
+            rect = self.getBuildArea()
+        worldSlice = WorldSlice(rect, heightmapTypes, retries=self.retries, timeout=self.timeout, host=self.host)
+        if cache:
+            self._worldSlice      = worldSlice
+            self._worldSliceDecay = np.zeros(addY(self._worldSlice.rect.size, lookup.BUILD_HEIGHT), dtype=np.bool)
+        return worldSlice
+
+
+    def updateWorldSlice(self):
+        """Updates the cached world slice."""
+        if self._worldSlice is None:
+            raise RuntimeError("No world slice is cached. Call .loadWorldSlice() with cache=True first.")
+        return self.loadWorldSlice(self._worldSlice.rect, self._worldSlice.heightmaps.keys(), cache=True)
+
+
+    def getMinecraftVersion(self):
+        """Returns the Minecraft version as a string."""
+        return interface.getVersion(retries=self.retries, timeout=self.timeout, host=self.host)
+
+
+    def checkConnection(self):
+        """Raises an InterfaceConnectionError if the GDMC HTTP interface cannot be reached.\n
+        Does not perform any retries."""
+        interface.getVersion(retries=0, timeout=self.timeout, host=self.host)
+
+
+    @contextmanager
+    def pushTransform(self, transformLike: Optional[TransformLike] = None):
+        """Creates a context that reverts all changes to self.transform on exit.
+        If <transformLine> is not None, it is pushed to self.transform on enter.
+
+        Can be used to create a local coordinate system on top of the current local coordinate
+        system.
+
+        Not to be confused with Transform.push()!"""
+
+        originalTransform = deepcopy(self.transform)
+        if transformLike is not None:
+            self.transform @= toTransform(transformLike)
+        try:
+            yield
+        finally:
+            self.transform = originalTransform
diff --git a/gdpc/editor_tools.py b/gdpc/editor_tools.py
new file mode 100644
index 0000000..86bdd3d
--- /dev/null
+++ b/gdpc/editor_tools.py
@@ -0,0 +1,138 @@
+"""Provides various utilities that require an Editor."""
+
+
+from typing import Optional, Iterable, Set, Tuple, Union
+import random
+
+import numpy as np
+from glm import ivec2, ivec3
+
+from .vector_tools import Box, neighbors3D
+from .block import Block
+from .block_state_tools import facingToRotation, facingToVector
+from .minecraft_tools import getObtrusiveness, lecternBlock, positionToInventoryIndex, signBlock
+from . import lookup
+from .editor import Editor
+
+
+def centerBuildAreaOnPlayer(editor: Editor, size: ivec3):
+    """Sets <editor>'s build area to a box of <size> centered on the player, and returns it.\n
+    The build area is always in **global coordinates**; <editor>.transform is ignored."""
+    # -1 to correct for offset from player position
+    radius = (size - 1) // 2
+    editor.runCommand(
+        "execute at @p run setbuildarea "
+        f"~{-radius.x} ~{-radius.y} ~{-radius.z} ~{radius.x} ~{radius.y} ~{radius.z}")
+    return editor.getBuildArea()
+
+
+def flood_search_3D(
+    editor: Editor,
+    origin: ivec3,
+    boundingBox: Box,
+    search_block_ids: Iterable[str],
+    diagonal=False,
+    depth=256
+):
+    """Return a list of coordinates with blocks that fulfill the search.\n
+    Activating caching is *highly* recommended."""
+    def flood_search_3D_recursive(point: ivec3, result: Set[ivec3], visited: Set[ivec3], depth_: int):
+        if point in visited:
+            return
+
+        visited.add(point)
+
+        if editor.getBlock(point) not in search_block_ids:
+            return
+
+        result.add(point)
+
+        for neighbor in neighbors3D(point, boundingBox, diagonal):
+            flood_search_3D_recursive(neighbor, result, visited, depth_ - 1)
+
+    result:  Set[ivec3] = set()
+    visited: Set[ivec3] = set()
+    flood_search_3D_recursive(origin, result, visited, depth)
+    return result
+
+
+def placeSign(
+    editor: Editor,
+    position: ivec3,
+    wood="oak", wall=False,
+    facing: Optional[str] = None, rotation: Optional[Union[str, int]] = None,
+    text1="", text2="", text3="", text4="", color="", isGlowing=False
+):
+    """Places a sign with the specified properties.\n
+    If <wall> is True, <facing> is used. Otherwise, <rotation> is used.
+    If the used property is None, a least obstructed direction will be used."""
+    if wall and facing is None:
+        facing = random.choice(getOptimalFacingDirection(editor, position))
+    elif not wall and rotation is None:
+        rotation = facingToRotation(random.choice(getOptimalFacingDirection(editor, position)))
+    editor.placeBlock(position, signBlock(wood, wall, facing, rotation, text1, text2, text3, text4, color, isGlowing))
+
+
+def placeLectern(editor: Editor, position: ivec3, facing: Optional[str] = None, bookData: Optional[str] = None, page: int = 0):
+    """Place a lectern with the specified properties.\n
+    If <facing> is None, a least obstructed facing direction will be used."""
+    if facing is None:
+        facing = random.choice(getOptimalFacingDirection(editor, position))
+    editor.placeBlock(position, lecternBlock(facing, bookData, page))
+
+
+def placeContainerBlock(
+    editor: Editor,
+    position: ivec3,
+    block: Block = Block("minecraft:chest"),
+    items: Optional[Iterable[Union[Tuple[ivec2, str], Tuple[ivec2, str, int]]]] = None,
+    replace=True
+):
+    """Place a container block with the specified items in the world.\n
+    <items> should be a sequence of (position, item, [amount,])-tuples."""
+    inventorySize = lookup.CONTAINER_BLOCK_TO_INVENTORY_SIZE.get(block.id)
+    if inventorySize is None:
+        raise ValueError(f'"{block}" is not a known container block. Make sure you are using its namespaced ID.')
+
+    if not replace and editor.getBlock(position).id != block.id:
+        return
+
+    editor.placeBlock(position, block)
+
+    if items is None:
+        return
+
+    for item in items:
+        index = positionToInventoryIndex(item[0], inventorySize)
+        if len(item) == 3:
+            item = list(item)
+            item.append(1)
+        globalPosition = editor.transform * position
+        editor.runCommand(f"replaceitem block {' '.join(globalPosition)} container.{index} {item[2]} {item[3]}", syncWithBuffer=True)
+
+
+def setContainerItem(editor: Editor, position: ivec3, itemPosition: ivec2, item: str, amount: int = 1):
+    """Sets the item at <itemPosition> in the container block at <position> to the item with id <item>."""
+    globalPosition = editor.transform * position
+
+    block = editor.getBlockGlobal(globalPosition)
+    inventorySize = lookup.CONTAINER_BLOCK_TO_INVENTORY_SIZE.get(block.id)
+    if inventorySize is None:
+        raise ValueError(f'The block at ({",".join(position)}) is "{block}", which is not a known container block.')
+
+    index = positionToInventoryIndex(itemPosition, inventorySize)
+    editor.runCommand(f"replaceitem block {' '.join(globalPosition)} container.{index} {item} {amount}", syncWithBuffer=True)
+
+
+def getOptimalFacingDirection(editor: Editor, pos: ivec3):
+    """Returns the least obstructed directions to have something facing (a "facing" block state value).\n
+    Ranks directions by obtrusiveness first, and by obtrusiveness of the opposite direction second."""
+    directions = ["north", "east", "south", "west"]
+    obtrusivenesses = np.array([
+        getObtrusiveness(editor.getBlock(pos + facingToVector(direction)))
+        for direction in directions
+    ])
+    candidates              = np.nonzero(obtrusivenesses == np.min(obtrusivenesses))[0]
+    oppositeObtrusivenesses = obtrusivenesses[(candidates + 2) % 4]
+    winners                 = candidates[oppositeObtrusivenesses == np.max(oppositeObtrusivenesses)]
+    return [directions[winner] for winner in winners]
diff --git a/gdpc/exceptions.py b/gdpc/exceptions.py
new file mode 100644
index 0000000..c691be9
--- /dev/null
+++ b/gdpc/exceptions.py
@@ -0,0 +1,17 @@
+"""Exceptions used by GDPC"""
+
+
+class InterfaceError(RuntimeError):
+    """An error occured when communicating with the GDMC HTTP interface"""
+
+
+class InterfaceConnectionError(InterfaceError):
+    """An error occured when trying to connect to the GDMC HTTP interface"""
+
+
+class InterfaceInternalError(InterfaceError):
+    """The GDMC HTTP interface reported an internal server error (500)"""
+
+
+class BuildAreaNotSetError(InterfaceError):
+    """Attempted to retieve the build area while it was not set"""
diff --git a/gdpc/geometry.py b/gdpc/geometry.py
index d46dac5..50b60fb 100644
--- a/gdpc/geometry.py
+++ b/gdpc/geometry.py
@@ -1,574 +1,131 @@
-"""Provides tools for creating multidimensional shapes."""
-
-import numpy as np
-
-from . import lookup, toolbox
-from .interface import globalinterface as gi
-
-
-def placeLine(x1, y1, z1, x2, y2, z2, blocks, replace=None, interface=gi):
-    """Draw a line from point to point efficiently."""
-    settings = blocks, replace, interface
-    dimension, _ = getDimension(x1, y1, z1, x2, y2, z2)
-    if dimension == 0:
-        return interface.placeBlock(x1, y1, z1, blocks, replace)
-    elif dimension == 1:
-        return placeVolume(x1, y1, z1, x2, y2, z2, *settings)
-    else: # dimension == 2 or dimension == 3
-        return placeFromList(line3d(x1, y1, z1, x2, y2, z2), *settings)
-
-
-def placeJointedLine(points, blocks, replace=None, interface=gi):
-    """Place a line that runs from point to point."""
-    return placeFromList(lineSequence(points), blocks, replace, interface)
-
-
-def placePolygon(points, blocks, replace=None, filled=False, interface=gi):
-    """Place a polygon that runs from line to line and may be filled."""
-    polygon = set()
-    polygon.update(lineSequence(points))
-    polygon.update(line3d(*points[0], *points[-1]))
-    dimension, _ = getDimension(*getShapeBoundaries(polygon))
-    if filled and dimension == 2:
-        polygon.update(fill2d(polygon))
-    elif filled and dimension == 3:
-        raise ValueError(f'{lookup.TCOLORS["red"]}Cannot fill 3D polygons!')
-    placeFromList(polygon, blocks, replace, interface)
-
-
-def placeVolume(x1, y1, z1, x2, y2, z2, blocks, replace=None, interface=gi):
-    """Fill volume with blocks.
-
-    Args:
-        x1 (int): starting X
-        y1 (int): starting Y
-        z1 (int): starting Z
-        x2 (int): ending X
-        y2 (int): ending Y
-        z2 (int): ending Z
-        blocks (Union(list, str)): A block or list of blocks to be used.
-        replace (Union(list, str), optional): Only replace these blocks.
-                                                Defaults to None.
-        interface (Interface, optional): Interface to be used. Defaults to gi.
-
-    Returns:
-        Any: Result of block placement.
-    """
-    buffering = interface.isBuffering()
-    if not buffering:
-        interface.setBuffering(True, False)
-        result = placeFromList(toolbox.loop3d(x1, y1, z1, x2, y2, z2),
-                               blocks, replace, interface)
-        interface.setBuffering(False, False)
-        return result
-    return placeFromList(toolbox.loop3d(x1, y1, z1, x2, y2, z2),
-                         blocks, replace, interface)
-
-
-def placeCuboid(x1, y1, z1, x2, y2, z2, blocks, replace=None,
-                hollow=False, interface=gi):
-    """Place a cubic shape that fills the entire region and may be hollow.
-
-    Adjusts function call based on dimensionality.
-    """
-    settings = blocks, replace, interface
-    dimension, _ = getDimension(x1, y1, z1, x2, y2, z2)
-
-    if dimension == 0:                       # single block
-        return interface.placeBlock(x1, y1, z1, blocks, replace)
-
-    elif dimension in (1, 2) or not hollow:  # line, rectangle or solid cuboid
-        return placeVolume(x1, y1, z1, x2, y2, z2,  *settings)
-
-    elif dimension == 3 and hollow:          # hollow cuboid
-        bottom = placeVolume(x1, y1, z1, x2, y1, z2, *settings)      # bottom
-        top = placeVolume(x1, y2, z1, x2, y2, z2, *settings)         # top
-        north = placeVolume(x1, y1, z1, x1, y2, z2, *settings)       # north
-        south = placeVolume(x2, y1, z1, x2, y2, z2, *settings)       # south
-        west = placeVolume(x1, y1, z1, x2, y2, z1, *settings)        # west
-        east = placeVolume(x1, y1, z2, x2, y2, z2, *settings)        # east
-        try:
-            return bottom + top + north + south + west + east
-        except TypeError:
-            return (f'{lookup.TCOLORS["orange"]}Errors while drawing hollow '
-                    f'cuboid:\n\tTop {top}\n\tBottom {bottom}\n\t'
-                    f'North {north}\n\tSouth {south}\n\t'
-                    f'West {west}\n\tEast {east}')
-
-
-def placeCenteredCylinder(x, y, z, h, r, blocks, replace=None,
-                          axis='y', tube=False, hollow=False, interface=gi):
+"""Provides tools for placing geometrical shapes of blocks."""
+
+
+from typing import Optional, Union, List, Iterable
+
+from glm import ivec2, ivec3
+
+from .vector_tools import Rect, Box, cylinder, fittingCylinder, line3D, lineSequence3D
+from .block import Block
+from .editor import Editor
+
+
+def placeCuboid(editor: Editor, first: ivec3, last: ivec3, block: Block, replace: Optional[Union[str, List[str]]] = None):
+    """Places a box of [block] blocks from [first] to [last] (inclusive)."""
+    # Transform only the key points instead of all points
+    first = editor.transform * first
+    last = editor.transform * last
+    block = block.transformed(editor.transform.rotation, editor.transform.flip)
+    editor.placeBlockGlobal(Box.between(first, last).inner, block, replace)
+
+
+def placeCuboidHollow(editor: Editor, first: ivec3, last: ivec3, block: Block, replace: Optional[Union[str, List[str]]] = None):
+    """Places a hollow box of [block] blocks from [first] to [last] (inclusive)."""
+    # Transform only the key points instead of all points
+    first = editor.transform * first
+    last = editor.transform * last
+    block = block.transformed(editor.transform.rotation, editor.transform.flip)
+    editor.placeBlockGlobal(Box.between(first, last).shell, block, replace)
+
+
+def placeCuboidWireframe(editor: Editor, first: ivec3, last: ivec3, block: Block, replace: Optional[Union[str, List[str]]] = None):
+    """Places a wireframe of [block] blocks from [first] to [last] (inclusive)."""
+    # Transform only the key points instead of all points
+    first = editor.transform * first
+    last = editor.transform * last
+    block = block.transformed(editor.transform.rotation, editor.transform.flip)
+    editor.placeBlockGlobal(Box.between(first, last).wireframe, block, replace)
+
+
+def placeBox(editor: Editor, box: Box, block: Block, replace: Optional[Union[str, List[str]]] = None):
+    """Places a box of [block] blocks."""
+    if (box.size.x == 0 or box.size.y == 0 or box.size.z == 0): return
+    placeCuboid(editor, box.begin, box.end - 1, block, replace)
+
+
+def placeBoxHollow(editor: Editor, box: Box, block: Block, replace: Optional[Union[str, List[str]]] = None):
+    """Places a hollow box of [block] blocks."""
+    if (box.size.x == 0 or box.size.y == 0 or box.size.z == 0): return
+    placeCuboidHollow(editor, box.begin, box.end - 1, block, replace)
+
+
+def placeBoxWireframe(editor: Editor, box: Box, block: Block, replace: Optional[Union[str, List[str]]] = None):
+    """Places a wireframe of [block] blocks."""
+    if (box.size.x == 0 or box.size.y == 0 or box.size.z == 0): return
+    placeCuboidWireframe(editor, box.begin, box.end - 1, block, replace)
+
+
+def placeRect(editor: Editor, rect: Rect, y: int, block: Block, replace: Optional[Union[str, List[str]]] = None):
+    """Places a rectangle of blocks in the XY-plane, at height [y]"""
+    placeBox(editor, rect.toBox(y, 1), block, replace)
+
+
+def placeRectOutline(editor: Editor, rect: Rect, y: int, block: Block, replace: Optional[Union[str, List[str]]] = None):
+    """Places the outline of a rectangle of blocks in the XY-plane, at height [y]"""
+    placeBoxWireframe(editor, rect.toBox(y, 1), block, replace)
+
+
+def placeCheckeredCuboid(editor: Editor, first: ivec3, last: ivec3, block1: Block, block2: Block = Block(""), replace: Optional[Union[str, List[str]]] = None):
+    """Places a checker pattern of [block1] and [block2] in the box between [first] and [last] (inclusive)"""
+    placeCheckeredBox(editor, Box.between(first, last), block1, block2, replace)
+
+
+def placeCheckeredBox(editor: Editor, box: Box, block1: Block, block2: Block = Block(""), replace: Optional[Union[str, List[str]]] = None):
+    """Places a checker pattern of [block1] and [block2] in [box]"""
+    # We loop through [box]-local positions so that the pattern start is independent of [box].offset
+    for pos in Box(size=box.size).inner:
+        editor.placeBlock(box.offset + pos, block1 if sum(pos) % 2 == 0 else block2, replace)
+
+
+def placeStripedCuboid(editor: Editor, first: ivec3, last: ivec3, stripeAxis: int, block1: Block, block2: Block = Block(""), replace: Optional[Union[str, List[str]]] = None):
+    """Places a stripe pattern of [block1] and [block2] along [stripeAxis] (0, 1 or 2) in the box
+    between [first] and [last] (inclusive)"""
+    placeStripedBox(editor, Box.between(first, last), stripeAxis, block1, block2, replace)
+
+
+def placeStripedBox(editor: Editor, box: Box, stripeAxis: int, block1: Block, block2: Block = Block(""), replace: Optional[Union[str, List[str]]] = None):
+    """Places a stripe pattern of [block1] and [block2] along [stripeAxis] (0, 1 or 2) in [box]"""
+    # We loop through [box]-local positions so that the pattern start is independent of [box].offset
+    for pos in Box(size=box.size).inner:
+        editor.placeBlock(box.offset + pos, block1 if pos[stripeAxis] % 2 == 0 else block2, replace)
+
+
+def placeLine(editor: Editor, first: ivec3, last: ivec3, block: Block, width=1, replace: Optional[Union[str, List[str]]] = None):
+    """Places a line of [block] blocks from [first] to [last] (inclusive).\n
+    When placing axis-aligned lines, placeCuboid and placeBox are more efficient."""
+    # Transform only the key points instead of all points
+    first = editor.transform * first
+    last = editor.transform * last
+    block = block.transformed(editor.transform.rotation, editor.transform.flip)
+    editor.placeBlockGlobal(line3D(first, last, width), block, replace)
+
+
+def placeLineSequence(editor: Editor, points: Iterable[ivec3], block: Block, closed=False, replace: Optional[Union[str, List[str]]] = None):
+    """Place lines that run from point to point."""
+    editor.placeBlock(lineSequence3D(points, closed=closed), block, replace)
+
+
+def placeCylinder(
+    editor: Editor,
+    baseCenter: ivec3, diameters: Union[ivec2, int], length: int,
+    block: Block,
+    axis=1, tube=False, hollow=False,
+    replace: Optional[Union[str, List[str]]] = None
+):
     """Place a cylindric shape centered on xyz with height and radius."""
-    if axis == 'x':
-        placeCylinder(x, y - r, z - r, x + h - 1, y + r, z + r,
-                      blocks, replace, 'x', tube, hollow, interface)
-    elif axis == 'y':
-        placeCylinder(x - r, y, z - r, x + r, y + h - 1, z + r,
-                      blocks, replace, 'y', tube, hollow, interface)
-    elif axis == 'z':
-        placeCylinder(x - r, y - r, z, x + r, y + r, z + h - 1,
-                      blocks, replace, 'z', tube, hollow, interface)
-    else:
-        raise ValueError(f'{lookup.TCOLORS["red"]}{axis} is not a valid axis!')
-
-
-def placeCylinder(x1, y1, z1, x2, y2, z2, blocks, replace=None,
-                  axis='y', tube=False, hollow=False, interface=gi):
+    editor.placeBlock(cylinder(baseCenter, diameters, length, axis, tube, hollow), block, replace)
+
+
+def placeFittingCylinder(
+    editor: Editor,
+    corner1: ivec3, corner2: ivec3,
+    block: Block,
+    axis=1, tube=False, hollow=False,
+    replace: Optional[Union[str, List[str]]] = None
+):
     """Place a cylindric shape that fills the entire region."""
-    settings = blocks, replace, interface
-    dimension, flatSides = getDimension(x1, y1, z1, x2, y2, z2)
-
-    def placeCylinderBody(a1, b1, a2, b2, h0, hn):
-        """Build a cylinder."""
-        tubePoints, basePoints = ellipse(a1, b1, a2, b2, filled=True)
-        basePoints = padDimension(basePoints, h0, axis)
-        tubePoints = padDimension(tubePoints, h0, axis)
-        if tube:
-            basePoints = tubePoints
-        elif not hollow:
-            tubePoints = basePoints
-
-        bottom = placeFromList(basePoints,  *settings)
-        top = 0
-        body = 0
-        if h0 != hn:
-            top = placeFromList(
-                translate(basePoints, hn - h0, axis),  *settings)
-            body = placeFromList(
-                repeat(tubePoints, hn - h0 - 2, axis), *settings)
-
-        try:
-            return bottom + top + body
-        except TypeError:
-            return (f'{lookup.TCOLORS["orange"]}Errors while drawing '
-                    f'cylinder:\n\tBase {bottom}\n\tTop {top}\n\tBody {body}')
-
-    if dimension == 0:
-        return interface.placeBlock(x1, y1, z1, blocks, replace)
-    elif (dimension == 1
-          or (len(flatSides) > 0 and flatSides[0] != axis)):
-        return placeVolume(x1, y1, z1, x2, y2, z2, *settings)
-    elif dimension == 2:
-        if flatSides == ['x']:
-            return placeCylinderBody(y1, z1, y2, z2, x1, x2)
-        elif flatSides == ['y']:
-            return placeCylinderBody(x1, z1, x2, z2, y1, y2)
-        elif flatSides == ['z']:
-            return placeCylinderBody(x1, y1, x2, y2, z1, z2)
-        raise ValueError(f'{lookup.TCOLORS["red"]}Unexpected shape!')
-    elif dimension == 3:
-        if axis == 'x':
-            return placeCylinderBody(y1, z1, y2, z2, x1, x2)
-        elif axis == 'y':
-            return placeCylinderBody(x1, z1, x2, z2, y1, y2)
-        elif axis == 'z':
-            return placeCylinderBody(x1, y1, x2, y2, z1, z2)
-        raise ValueError(f'{lookup.TCOLORS["red"]}{axis} is not a valid axis!')
-
-
-def placeFromList(pos_list, blocks, replace=None, interface=gi):
-    """Replace all blocks at coordinates in list with blocks."""
-    result = 0
-    ERRORMESSAGE = (f"\n{lookup.TCOLORS['orange']}"
-                    f"Fails:\n{lookup.TCOLORS['gray']}")
-    errors = ERRORMESSAGE
-    for x, y, z in pos_list:
-        response = interface.placeBlock(x, y, z, blocks, replace)
-        if response.isnumeric():
-            result += int(response)
-        else:
-            errors += response + '\n'
-    if errors is not ERRORMESSAGE:
-        result = str(result) + errors
-    return result
-
-# ========================================================= calculations
-
-
-def getShapeBoundaries(points):
-    """Return the smallest and largest values used in a shape."""
-    points = np.array(points)
-    dimension = len(points[0])
-    if dimension == 2:
-        minx, miny = points.min(axis=0)
-        maxx, maxy = points.max(axis=0)
-        return minx, miny, maxx, maxy
-    elif dimension == 3:
-        minx, miny, minz = points.min(axis=0)
-        maxx, maxy, maxz = points.max(axis=0)
-        return minx, miny, minz, maxx, maxy, maxz
-    raise ValueError(f'{lookup.TCOLORS["red"]}{dimension}D '
-                     'shapes are not supported!')
-
-
-def getDimension(x1, y1, z1, x2, y2, z2):
-    """Determine the number of dimensions the input uses."""
-    if (x1, y1, z1) == (x2, y2, z2):
-        return 0, list(lookup.AXES)
-    # NOTE: if dimension needs to be known, return isdifferent
-    isflat = np.subtract((x1, y1, z1), (x2, y2, z2)) == 0
-    flatSides = []
-    flatSides += ['x'] if isflat[0] else []
-    flatSides += ['y'] if isflat[1] else []
-    flatSides += ['z'] if isflat[2] else []
-    return 3 - isflat.sum(), flatSides
-
-
-def padDimension(points, value=0, axis='z'):
-    """Pad a list of 2D points with a value in the appropriate position.
-
-    May also be used to replace all values in an axis.
-    """
-    if axis == 'x':
-        return [(value, i[-2], i[-1]) for i in points]
-    elif axis == 'y':
-        return [(i[0], value, i[-1]) for i in points]
-    elif axis == 'z':
-        return [(i[0], i[1], value) for i in points]
-    raise ValueError(f'{lookup.TCOLORS["red"]}{axis} is not a valid axis '
-                     'to pad with!')
-
-
-def cutDimension(points, axis='z'):
-    """Cut the appropriate axis from a list of points."""
-    try:
-        if axis == 'x':
-            return [(i[0:]) for i in points]
-        elif axis == 'y':
-            dimension = len(points[0])
-            if dimension == 2:
-                return [(i[:-1]) for i in points]
-            elif dimension == 3:
-                return [(i[0], i[-1]) for i in points]
-        elif axis == 'z':
-            return [(i[:-1]) for i in points]
-    except IndexError:
-        pass
-    raise ValueError(f'{lookup.TCOLORS["red"]}{axis} is not a valid axis '
-                     f'to cut from this set!\n{points}')
-
-
-def translate(points, amount, axis='y'):
-    """Return a clone of the points translateed by amount in axis."""
-    points = set(points)
-    vx, vy, vz = lookup.AXIS2VECTOR[axis]
-    clone = [(x + amount * vx, y + amount * vy, z + amount * vz)
-             for x, y, z in points]
-    return clone
-
-
-def repeat(points, times, axis='y', step=1):
-    """Return points with duplicates shifted along the appropriate axis."""
-    clone = set(points)
-    for n in range(1, times + 2):
-        clone.update(translate(points, step * n, axis))
-    return clone
-
-
-def fill2d(points):
-    """Return all filling within the shape of points."""
-    points = list(points)
-    filling = []
-    minx, miny = np.array(points).min(axis=0)
-    maxx, maxy = np.array(points).max(axis=0)
-    cx, cy = minx + (maxx - minx) // 2, miny + (maxy - miny) // 2
-
-    def fill(x, y):
-        if (x, y) in points:
-            return
-        elif not (minx <= x <= maxx and miny <= y <= maxy):
-            raise ValueError(f'{lookup.TCOLORS["red"]}Aborted filling '
-                             'open-sided shape!')
-        points.append((x, y))
-        filling.append((x, y))
-        fill(x + 1, y)
-        fill(x - 1, y)
-        fill(x, y + 1)
-        fill(x, y - 1)
-
-    fill(cx, cy)
-    return filling
-
-
-def fill3d(points):
-    """Return all filling within the shape of points."""
-    points = list(points)
-    filling = []
-    minx, miny, minz = np.array(points).min(axis=0)
-    maxx, maxy, maxz = np.array(points).max(axis=0)
-    cx, cy, cz = (minx + (maxx - minx) // 2,
-                  miny + (maxy - miny) // 2,
-                  minz + (maxz - minz) // 2)
-
-    def fill(x, y, z):
-        if (x, y, z) in points:
-            return
-        elif not (minx <= x <= maxx
-                  and miny <= y <= maxy
-                  and minz <= z <= maxz):
-            raise ValueError(f'{lookup.TCOLORS["red"]}Aborted filling '
-                             'open-sided 3D shape!')
-        points.append((x, y, z))
-        filling.append((x, y, z))
-        fill(x + 1, y, z)
-        fill(x - 1, y, z)
-        fill(x, y + 1, z)
-        fill(x, y - 1, z)
-        fill(x, y, z + 1)
-        fill(x, y, z - 1)
-
-    fill(cx, cy, cz)
-    return filling
-
-
-def line2d(x1, y1, x2, y2):
-    """Return coordinates for a 2D line from point to point.
-
-    From https://www.codegrepper.com/code-examples/python/line+algorithm+in+python
-    """
-    dx = x2 - x1
-    dy = y2 - y1
-    is_steep = abs(dy) > abs(dx)
-    if abs(dy) > abs(dx):
-        x1, y1 = y1, x1
-        x2, y2 = y2, x2
-
-    # Swap start and end points if necessary
-    if x1 > x2:
-        x1, x2 = x2, x1
-        y1, y2 = y2, y1
-
-    dx = x2 - x1
-    dy = y2 - y1
-
-    # Calculate error
-    error = int(dx / 2.0)
-    ystep = 1 if y1 < y2 else -1
-
-    # Iterate over bounding box generating points between start and end
-    y = y1
-    points = set()
-    for x in range(x1, x2 + 1):
-        coord = (y, x) if is_steep else (x, y)
-        points.add(coord)
-        error -= abs(dy)
-        if error < 0:
-            y += ystep
-            error += dx
-
-    return points
-
-
-def line3d(x1, y1, z1, x2, y2, z2):
-    """Return coordinates for a 3D line from point to point.
-
-    With 'inspiration' from
-    https://www.geeksforgeeks.org/bresenhams-algorithm-for-3-d-line-drawing/
-    """
-
-    def if_greater_pos_else_neg(a, b):
-        return 1 if a > b else -1
-
-    def bresenham_line_next_point(x, y, z, xs, ys, zs, dx, dy, dz, p1, p2):
-        x += xs
-        if p1 >= 0:
-            y += ys
-            p1 -= 2 * dx
-        if p2 >= 0:
-            z += zs
-            p2 -= 2 * dx
-        p1 += 2 * dy
-        p2 += 2 * dz
-        return x, y, z, p1, p2
-
-    points = set()
-    points.add((x1, y1, z1))
-    dx = abs(x2 - x1)
-    dy = abs(y2 - y1)
-    dz = abs(z2 - z1)
-    xs = if_greater_pos_else_neg(x2, x1)
-    ys = if_greater_pos_else_neg(y2, y1)
-    zs = if_greater_pos_else_neg(z2, z1)
-
-    # Driving axis is X-axis"
-    if dx >= dy and dx >= dz:
-        p1 = 2 * dy - dx
-        p2 = 2 * dz - dx
-        while x1 != x2:
-            x1, y1, z1, p1, p2 = bresenham_line_next_point(x1, y1, z1,
-                                                           xs, ys, zs,
-                                                           dx, dy, dz,
-                                                           p1, p2)
-            points.add((x1, y1, z1))
-
-    # Driving axis is Y-axis"
-    elif dy >= dx and dy >= dz:
-        p1 = 2 * dx - dy
-        p2 = 2 * dz - dy
-        while y1 != y2:
-            y1, x1, z1, p1, p2 = bresenham_line_next_point(y1, x1, z1,
-                                                           ys, xs, zs,
-                                                           dy, dx, dz,
-                                                           p1, p2)
-            points.add((x1, y1, z1))
-
-    # Driving axis is Z-axis"
-    else:
-        p1 = 2 * dy - dz
-        p2 = 2 * dx - dz
-        while z1 != z2:
-            z1, x1, y1, p1, p2 = bresenham_line_next_point(z1, x1, y1,
-                                                           zs, xs, ys,
-                                                           dz, dx, dy,
-                                                           p1, p2)
-            points.add((x1, y1, z1))
-
-    return points
-
-
-def lineSequence(points):
-    """Return all points connecting points in sequence."""
-    last = points[0]
-    dimension = len(last)
-    toPlace = set()
-    for point in points[0:]:
-        if dimension == 2:
-            toPlace.update(line2d(*last, *point))
-        elif dimension == 3:
-            toPlace.update(line3d(*last, *point))
-        else:
-            raise ValueError(f'{lookup.TCOLORS["red"]}{dimension}D '
-                             'lineSequence not supported!')
-        last = point
-    return toPlace
-
-
-def circle(x1, y1, x2, y2, filled=False):
-    """Return the points of a circle with a given centre and diameter.
-
-    With 'inspiration' from:
-    https://www.geeksforgeeks.org/bresenhams-circle-drawing-algorithm/
-    """
-    toolbox.normalizeCoordinates(x1, y1, x2, y2)
-
-    diameter = min(x2 - x1, y2 - y1)
-    e = diameter % 2    # for even centers
-    cx, cy = x1 + diameter // 2, y1 + diameter // 2
-    points = set()
-
-    def eightPoints(x, y):
-        points.add((cx + e + x, cy + e + y))
-        points.add((cx - 0 - x, cy + e + y))
-        points.add((cx + e + x, cy - 0 - y))
-        points.add((cx - 0 - x, cy - 0 - y))
-        points.add((cx + e + y, cy + e + x))
-        points.add((cx - 0 - y, cy + e + x))
-        points.add((cx + e + y, cy - 0 - x))
-        points.add((cx - 0 - y, cy - 0 - x))
-
-    r = diameter // 2
-    x, y = 0, r
-    d = 3 - 2 * r
-    eightPoints(x, y)
-    while y >= x:
-        # for each pixel we will
-        # draw all eight pixels
-
-        x += 1
-
-        # check for decision parameter
-        # and correspondingly
-        # update d, x, y
-        if d > 0:
-            y -= 1
-            d = d + 4 * (x - y) + 10
-        else:
-            d = d + 4 * x + 6
-        eightPoints(x, y)
-
-    if filled:
-        return points, fill2d(points)
-    return points
-
-
-def ellipse(x1, y1, x2, y2, filled=False):
-    """Return the points of an ellipse with a given centre and size.
-
-    Modified version 'inspired' by chandan_jnu from
-    https://www.geeksforgeeks.org/midpoint-ellipse-drawing-algorithm/
-    """
-    toolbox.normalizeCoordinates(x1, y1, x2, y2)
-
-    dx, dy = x2 - x1, y2 - y1
-    ex, ey = dx % 2, dy % 2
-    if dx == dy:
-        return circle(x1, y1, x2, y2, filled)
-    cx, cy = x1 + dx / 2, y1 + dy / 2
-    points = set()
-    filledpoints = set()
-
-    def fourpoints(x, y):
-        points.add((cx + x, cy + y))
-        points.add((cx - x, cy + y))
-        points.add((cx + x, cy - y))
-        points.add((cx - x, cy - y))
-
-        if filled:
-            filledpoints.update(line2d(cx + ex, cy + ey, cx + x, cy + y))
-            filledpoints.update(line2d(cx, cy + ey, cx - x, cy + y))
-            filledpoints.update(line2d(cx + ex, cy, cx + x, cy - y))
-            filledpoints.update(line2d(cx, cy, cx - x, cy - y))
-
-    rx, ry = dx / 2, dy / 2
-
-    x = 0
-    y = ry
-
-    # Initial decision parameter of region 1
-    d1 = ((ry * ry) - (rx * rx * ry) + (0.25 * rx * rx))
-    dx = 2 * ry * ry * x
-    dy = 2 * rx * rx * y
-
-    # For region 1
-    while dx < dy:
-
-        fourpoints(x, y)
-
-        # Checking and updating value of
-        # decision parameter based on algorithm
-        if d1 < 0:
-            x += 1
-            dx = dx + (2 * ry * ry)
-            d1 = d1 + dx + (ry * ry)
-        else:
-            x += 1
-            y -= 1
-            dx = dx + (2 * ry * ry)
-            dy = dy - (2 * rx * rx)
-            d1 = d1 + dx - dy + (ry * ry)
-
-    # Decision parameter of region 2
-    d2 = (((ry * ry) * ((x + 0.5) * (x + 0.5)))
-          + ((rx * rx) * ((y - 1) * (y - 1))) - (rx * rx * ry * ry))
-
-    # Plotting points of region 2
-    while y >= 0:
-
-        fourpoints(x, y)
-
-        # Checking and updating parameter
-        # value based on algorithm
-        if d2 > 0:
-            y -= 1
-            dy = dy - (2 * rx * rx)
-            d2 = d2 + (rx * rx) - dy
-        else:
-            y -= 1
-            x += 1
-            dx = dx + (2 * ry * ry)
-            dy = dy - (2 * rx * rx)
-            d2 = d2 + dx - dy + (rx * rx)
-
-    if filled:
-        return points, fill2d(points)
-    return points
+    # Transform only the key points instead of all points
+    corner1 = editor.transform * corner1
+    corner2 = editor.transform * corner2
+    block = block.transformed(editor.transform.rotation, editor.transform.flip)
+    editor.placeBlockGlobal(fittingCylinder(corner1, corner2, axis, tube, hollow), block, replace)
diff --git a/gdpc/interface.py b/gdpc/interface.py
old mode 100755
new mode 100644
index b0c2e64..c044fa8
--- a/gdpc/interface.py
+++ b/gdpc/interface.py
@@ -1,438 +1,202 @@
-"""Provide tools for placing and getting blocks and more.
+"""Provides wrappers for the endpoints of the GDMC HTTP interface.
 
-This module contains functions to:
-- Request the build area as defined in-world
-- Run Minecraft commands
-- Get the block ID at a particular coordinate
-- Place blocks in the world
+It is recommended to use the higher-level `editor.Editor` class instead.
 """
 
-from collections import OrderedDict
-from random import choice
 
-import numpy as np
+from typing import Sequence, Tuple, Optional, List, Dict, Any
+from functools import partial
+import time
+from urllib.parse import urlparse
+import logging
+import json
 
-from . import direct_interface as di
-from .lookup import TCOLORS
-from .toolbox import is_sequence, normalizeCoordinates
-from .worldLoader import WorldSlice
+from glm import ivec2, ivec3
+import requests
+from requests.exceptions import ConnectionError as RequestConnectionError
 
+from . import __url__
+from .utils import withRetries
+from .vector_tools import Box
+from .block import Block
+from . import exceptions
 
-class OrderedByLookupDict(OrderedDict):
-    """Limit size, evicting the least recently looked-up key when full.
-
-    Taken from
-    https://docs.python.org/3/library/collections.html?highlight=ordereddict#collections.OrderedDict
-    """
-
-    def __init__(self, maxsize, *args, **kwds):
-        self.maxsize = maxsize
-        super().__init__(*args, **kwds)
-
-    # inherited __repr__ from OrderedDict is sufficient
-
-    def __getitem__(self, key):
-        value = super().__getitem__(key)
-        self.move_to_end(key)
-        return value
-
-    def __setitem__(self, key, value):
-        if key in self:
-            self.move_to_end(key)
-        super().__setitem__(key, value)
-        if self.maxsize > 0 and len(self) > self.maxsize:
-            oldest = next(iter(self))
-            del self[oldest]
-
-
-class Interface():
-    """Provides tools for interacting with the HTML interface.
-
-    All function parameters and returns are in local coordinates.
-    """
-
-    def __init__(self, x=0, y=0, z=0,
-                 buffering=False, bufferlimit=1024,
-                 caching=False, cachelimit=8192):
-        """Initialise an interface with offset and buffering."""
-        self.offset = x, y, z
-        self.__buffering = buffering
-        self.bufferlimit = bufferlimit
-        self.buffer = []    # buffer is in global coordinates
-        self.placeBlockflags = (True, None)   # (doBlockUpdates, CustomFlags)
-        self.bufferblockflags = (True, None)   # (doBlockUpdates, CustomFlags)
-        self.caching = caching
-        # cache is in global coordinates
-        self.cache = OrderedByLookupDict(cachelimit)
-        # Interface.cache.maxsize to change size
-
-    def __del__(self):
-        """Clean up before destruction."""
-        self.sendBlocks()
-
-    # __repr__ displays the class well enough so __str__ is omitted
-    def __repr__(self):
-        """Represent the Interface as a constructor."""
-        return "Interface(" \
-            f"{self.offset[0]}, {self.offset[1]}, {self.offset[2]}, " \
-            f"{self.__buffering}, {self.bufferlimit}, " \
-            f"{self.caching}, {self.cache.maxsize})"
-
-    def getBlock(self, x, y, z):
-        """Return the block ID in the world.
-
-        Takes local coordinates, works with global coordinates
-        """
-        x, y, z = self.local2global(x, y, z)
-
-        if self.caching and (x, y, z) in self.cache.keys():
-            return self.cache[(x, y, z)]
-
-        if self.caching and globalWorldSlice is not None:
-            dx, dy, dz = global2buildlocal(x, y, z)  # convert for decay index
-            if not checkOutOfBounds(x, y, z) and not globalDecay[dx][dy][dz]:
-                block = globalWorldSlice.getBlockAt(x, y, z)
-                if block == 'minecraft:void_air':
-                    block = di.getBlock(x, y, z).get('id')
-                self.cache[(x, y, z)] = block
-                return block
-
-        response = di.getBlock(x, y, z).get('id')
-        if self.caching:
-            self.cache[(x, y, z)] = response
-
-        return response
-
-    def placeBlock(self, x, y, z, block, replace=None,
-                   doBlockUpdates=-1, customFlags=-1):
-        """Place a block in the world depending on buffer activation.
-
-        Takes local coordinates, works with local and global coordinates
-        """
-        flags = doBlockUpdates, customFlags
-        if isinstance(replace, str):
-            if self.getBlock(x, y, z) != replace:
-                return '0'
-        elif is_sequence(replace) and self.getBlock(x, y, z) not in replace:
-            return '0'
-        elif (self.caching
-              and isinstance(block, str) and block in self.getBlock(x, y, z)):
-            return '0'
-
-        if not isinstance(block, str) and is_sequence(block):
-            block = choice(block)
-
-        if self.__buffering:
-            response = self.placeBlockBuffered(x, y, z, block,
-                                               self.bufferlimit, flags)
-        else:
-            response = self.placeBlockDirect(x, y, z, block, flags)
-
-        # switch to global coordinates
-        x, y, z = self.local2global(x, y, z)
-        if self.caching:
-            self.cache[(x, y, z)] = block
-        # mark block as decayed
-        if not checkOutOfBounds(x, y, z) and globalDecay is not None:
-            x, y, z = global2buildlocal(x, y, z)
-            globalDecay[x][y][z] = True
-
-        return response
-
-    def placeBlockDirect(self, x, y, z, blockStr,
-                         doBlockUpdates=-1, customFlags=-1):
-        """Place a single block in the world directly.
-
-        Takes local coordinates, works with global coordinates
-        """
-        if doBlockUpdates == -1:
-            doBlockUpdates = self.placeBlockflags[0]
-        if customFlags == -1:
-            customFlags = self.placeBlockflags[1]
-
-        x, y, z = self.local2global(x, y, z)
-        result = di.placeBlock(x, y, z, blockStr, doBlockUpdates, customFlags)
-        if not result.isnumeric():
-            print(f"{TCOLORS['orange']}Warning: Server returned error "
-                  f"upon placing block:\n\t{TCOLORS['CLR']}{result}")
-        return result
-
-    def getBlockFlags(self):
-        """Get default block placement flags."""
-        return self.placeBlockflags
-
-    def placeBlockFlags(self, doBlockUpdates=True, customFlags=None):
-        """Set default block placement flags."""
-        self.placeBlockflags = doBlockUpdates, customFlags
-
-    # ----------------------------------------------------- block buffers
-
-    def isBuffering(self):
-        """Get self.__buffering."""
-        return self.__buffering
-
-    def setBuffering(self, value, notify=True):
-        """Set self.__buffering."""
-        self.__buffering = value
-        if self.__buffering and notify:
-            print("Buffering has been activated.")
-        elif notify:
-            self.sendBlocks()
-            print("Buffering has been deactivated.")
-
-    def getBufferLimit(self):
-        """Get self.bufferlimit."""
-        return self.bufferlimit
-
-    def setBufferLimit(self, value):
-        """Set self.bufferlimit."""
-        self.bufferlimit = value
-
-    def isCaching(self):
-        """Get self.caching."""
-        return self.caching
-
-    def setCaching(self, value=False):
-        """Set self.caching."""
-        globalinterface.caching = value
-        if self == globalinterface and not value:
-            global globalDecay
-            globalDecay = None
-
-    def getCacheLimit(self):
-        """Get maximum cache size."""
-        return self.cache.maxsize
-
-    def setCacheLimit(self, value=8192):
-        """Set maximum cache size."""
-        self.cache.maxsize = value
-
-    def placeBlockBuffered(self, x, y, z, blockStr, limit=50,
-                           doBlockUpdates=-1, customFlags=-1):
-        """Place a block in the buffer and send once limit is exceeded.
-
-        Takes local coordinates and works with global coordinates
-        """
-        if doBlockUpdates == -1:
-            doBlockUpdates = self.placeBlockflags[0]
-        if customFlags == -1:
-            customFlags = self.placeBlockflags[1]
-
-        if (doBlockUpdates, customFlags) != self.bufferblockflags:
-            self.sendBlocks()
-            self.bufferblockflags = doBlockUpdates, customFlags
-
-        x, y, z = self.local2global(x, y, z)
-
-        self.buffer.append((x, y, z, blockStr))
-        if len(self.buffer) >= limit:
-            return self.sendBlocks()
-        else:
-            return '0'
-
-    def sendBlocks(self, x=0, y=0, z=0, retries=5):
-        """Send the buffer to the server and clear it.
-
-        Since the buffer contains global coordinates,
-        no conversion takes place in this function.
-        """
-        if self.buffer == []:
-            return '0'
-        response = di.sendBlocks(self.buffer, x, y, z,
-                                 retries, *self.bufferblockflags).split('\n')
-        if all(map(lambda val: val.isnumeric(), response)):  # no errors
-            self.buffer = []
-            return str(sum(map(int, response)))
-
-        print(f"{TCOLORS['orange']}Warning: Server returned error upon "
-              f"sending block buffer:\n\t{TCOLORS['CLR']}{repr(response)}")
-        return repr(response)
-
-    # ----------------------------------------------------- utility functions
-
-    def local2global(self, x, y, z):
-        """Translate local to global coordinates."""
-        result = []
-        if x is not None:
-            result.append(x + self.offset[0])
-        if y is not None:
-            result.append(y + self.offset[1])
-        if z is not None:
-            result.append(z + self.offset[2])
-        return result
-
-    def global2local(self, x, y, z):
-        """Translate global to local coordinates."""
-        result = []
-        if x is not None:
-            result.append(x - self.offset[0])
-        if y is not None:
-            result.append(y - self.offset[1])
-        if z is not None:
-            result.append(z - self.offset[2])
-        return result
-
-
-def runCommand(command):
-    """Run a Minecraft command in the world."""
-    if command[0] == '/':
-        command = command[1:]
-    return di.runCommand(command)
-
-
-def setBuildArea(x1, y1, z1, x2, y2, z2):
-    """Set and return the build area."""
-    runCommand(f"setbuildarea {x1} {y1} {z1} {x2} {y2} {z2}")
-    return requestBuildArea()
-
-
-def requestBuildArea():
-    """Return the current building area.
-
-    Will reset anything dependant on the build area.
-    """
-    global globalBuildArea
-
-    globalBuildArea = normalizeCoordinates(*di.requestBuildArea())
-
-    if globalWorldSlice is not None:
-        resetGlobalDecay()
-
-    return globalBuildArea
-
-
-def requestPlayerArea(dx=128, dz=128):
-    """Return the building area surrounding the player."""
-    # Correcting for offset from player position
-    dx -= 1
-    dz -= 1
-    runCommand("execute at @p run setbuildarea "
-               f"~{-dx//2} 0 ~{-dz//2} ~{dx//2} 255 ~{dz//2}")
-    return requestBuildArea()
-
-
-# ========================================================= global interface
 
+DEFAULT_HOST = "http://localhost:9000"
 
-globalWorldSlice = None
-globalDecay = None
-globalBuildArea = requestBuildArea()
 
-globalinterface = Interface()
+logger = logging.getLogger(__name__)
 
 
-def makeGlobalSlice():
-    """Instantiate a global WorldSlice and refresh building area."""
-    global globalWorldSlice
-    x1, _, z1, x2, _, z2 = requestBuildArea()
-    globalWorldSlice = WorldSlice(x1, z1, x2, z2)
-    resetGlobalDecay()
-    return globalWorldSlice
+def _onRequestRetry(e: Exception, retriesLeft: int):
+    logger.warning(
+        "HTTP request failed!\n"
+        "Request exception:\n"
+        "%s\n"
+        "I'll retry in a bit (%i retries left).",
+        e, retriesLeft
+    )
+    time.sleep(3)
 
 
-def getBlock(x, y, z):
-    """Global getBlock."""
-    return globalinterface.getBlock(x, y, z)
+def _request(method: str, url: str, *args, retries: int, **kwargs):
+    try:
+        response = withRetries(partial(requests.request, method, url, *args, **kwargs), retries=retries, onRetry=_onRequestRetry)
+    except RequestConnectionError as e:
+        u = urlparse(url)
+        raise exceptions.InterfaceConnectionError(
+            f"Could not connect to the GDMC HTTP interface at {u.scheme}://{u.netloc}.\n"
+             "To use GDPC, you need to use a \"backend\" that provides the GDMC HTTP interface.\n"
+             "For example, by running Minecraft with the GDMC HTTP mod installed.\n"
+            f"See {__url__}/README.md for more information."
+        ) from e
 
+    if response.status_code == 500:
+        raise exceptions.InterfaceInternalError("The GDMC HTTP interface reported an internal server error (500)")
 
-def placeBlock(x, y, z, blocks, replace=None):
-    """Global placeBlock."""
-    return globalinterface.placeBlock(x, y, z, blocks, replace)
+    return response
 
 
-def getBlockFlags():
-    """Global getBlockFlags."""
-    return globalinterface.getBlockFlags()
+def getBlocks(position: ivec3, size: Optional[ivec3] = None, dimension: Optional[str] = None, includeState=True, includeData=True, retries=0, timeout=None, host=DEFAULT_HOST):
+    """Returns the blocks in the specified region.
 
+    <dimension> can be one of {"overworld", "the_nether", "the_end"} (default "overworld").
 
-def placeBlockFlags(doBlockUpdates=True, customFlags=None):
-    """Global placeBlockFlags."""
-    globalinterface.placeBlockFlags(doBlockUpdates, customFlags)
+    Returns a list of (position, block)-tuples.
 
-# ----------------------------------------------------- block buffers
-
-
-def isCaching():
-    """Global isCaching."""
-    return globalinterface.isCaching()
-
-
-def setCaching(value=False):
-    """Global setCaching."""
-    globalinterface.setCaching(value)
-
-
-def getCacheLimit():
-    """Global getCacheLimit."""
-    return globalinterface.getCacheLimit()
-
-
-def setCacheLimit(value=8192):
-    """Global setCacheLimit."""
-    globalinterface.setCacheLimit(value)
-
-
-def isBuffering():
-    """Global isBuffering."""
-    return globalinterface.isBuffering()
-
-
-def setBuffering(val):
-    """Global setBuffering."""
-    globalinterface.setBuffering(val)
-
-
-def getBufferLimit():
-    """Global getBufferLimit."""
-    return globalinterface.getBufferLimit()
+    If a set of coordinates is invalid, the returned block ID will be "minecraft:void_air".
+    """
+    url = f"{host}/blocks"
+    dx, dy, dz = (None, None, None) if size is None else size
+    parameters = {
+        'x': position.x,
+        'y': position.y,
+        'z': position.z,
+        'dx': dx,
+        'dy': dy,
+        'dz': dz,
+        'includeState': True if includeState else None,
+        'includeData':  True if includeData  else None,
+        'dimension': dimension
+    }
+    response = _request("GET", url, params=parameters, headers={"accept": "application/json"}, retries=retries, timeout=timeout)
+    blockDicts: List[Dict[str, Any]] = response.json()
+    return [(ivec3(b["x"], b["y"], b["z"]), Block(b["id"], b.get("state", {}), b.get("data"))) for b in blockDicts]
+
+
+def placeBlocks(blocks: Sequence[Tuple[ivec3, Block]], dimension: Optional[str] = None, doBlockUpdates=True, spawnDrops=False, customFlags: str = "", retries=0, timeout=None, host=DEFAULT_HOST):
+    """Places blocks in the world.
+
+    Each element of <blocks> should be a tuple (position, block). The blocks must each describe
+    exactly one block: palettes or "no placement" blocks are not allowed.
+
+    <dimension> can be one of {"overworld", "the_nether", "the_end"} (default "overworld").
+
+    The <doBlockUpdates>, <spawnDrops> and <customFlags> parameters control block update
+    behavior. See the GDMC HTTP API documentation for more info.
+
+    Returns a list with one string for each block placement. If the block placement was
+    successful, the string is "1" if the block changed, or "0" otherwise. If the placement
+    failed, it is the error message.
+    """
+    url = f"{host}/blocks"
 
+    if customFlags != "":
+        blockUpdateParams = {"customFlags": customFlags}
+    else:
+        blockUpdateParams = {"doBlockUpdates": doBlockUpdates, "spawnDrops": spawnDrops}
 
-def setBufferLimit(val):
-    """Global setBufferLimit."""
-    globalinterface.setBufferLimit(val)
+    parameters = {"dimension": dimension}
+    parameters.update(blockUpdateParams)
 
+    body = (
+        "[" +
+        ",".join(
+            '{' +
+            f'"x":{pos.x},"y":{pos.y},"z":{pos.z},"id":"{block.id}"' +
+            (f',"state":{json.dumps(block.states)}' if block.states else '') +
+            (f',"data":{json.dumps(block.data)}' if block.data is not None else '') +
+            '}'
+            for pos, block in blocks
+        ) +
+        "]"
+    )
 
-def sendBlocks(x=0, y=0, z=0, retries=5):
-    """Global sendBlocks."""
-    return globalinterface.sendBlocks(x, y, z, retries)
+    return _request("PUT", url, data=bytes(body, "utf-8"), params=parameters, headers={"Content-Type": "application/json"}, retries=retries, timeout=timeout).text.split("\n")
 
-# ----------------------------------------------------- utility functions
 
+def runCommand(command: str, dimension: Optional[str] = None, retries=0, timeout=None, host=DEFAULT_HOST):
+    """Executes one or multiple Minecraft commands (separated by newlines).
 
-def checkOutOfBounds(x, y, z,
-                     x1=None, y1=None, z1=None,
-                     x2=None, y2=None, z2=None,
-                     warn=True):
-    """Check whether a given coordinate is outside the build area."""
-    x1 = globalBuildArea[0] if x1 is None else x1
-    y1 = globalBuildArea[1] if y1 is None else y1
-    z1 = globalBuildArea[2] if z1 is None else z1
-    x2 = globalBuildArea[3] if x2 is None else x2
-    y2 = globalBuildArea[4] if y2 is None else y2
-    z2 = globalBuildArea[5] if z2 is None else z2
-    if not (x1 <= x <= x2 and y1 <= y <= y2 and z1 <= z <= z2):
-        if warn:
-            # building outside the build area can be less efficient
-            print(f"{TCOLORS['orange']}WARNING: Block at {x, y, z} is outside "
-                  f"the build area!{TCOLORS['CLR']}")
-        return True
-    return False
+    The leading "/" must be omitted.
 
+    <dimension> can be one of {"overworld", "the_nether", "the_end"} (default "overworld").
 
-def global2buildlocal(x, y, z):
-    """Convert global coordinates to ones relative to the build area."""
-    x0, y0, z0, _, _, _ = globalBuildArea
-    return x - x0, y - y0, z - z0
+    Returns a list with one string for each command. If the command was successful, the string
+    is its return value. Otherwise, it is the error message.
+    """
+    url = f"{host}/command"
+    return _request("POST", url, bytes(command, "utf-8"), params={'dimension': dimension}, retries=retries, timeout=timeout).text.split("\n")
 
 
-def buildlocal2global(x, y, z):
-    """Convert global coordinates to ones relative to the build area."""
-    x0, y0, z0, _, _, _ = globalBuildArea
-    return x + x0, y + y0, z + z0
+def getBuildArea(retries=0, timeout=None, host=DEFAULT_HOST):
+    """Retrieves the build area that was specified with /setbuildarea in-game.
 
+    Fails if the build area was not specified yet.
 
-def resetGlobalDecay():
-    """Reset the global decay marker."""
-    global globalDecay
-    x1, _, z1, x2, _, z2 = globalBuildArea
-    globalDecay = np.zeros((x2 - x1, 256, z2 - z1), dtype=bool)
+    Returns (success, result).
+    If a build area was specified, result is the box describing the build area.
+    Otherwise, result is the error message string.
+    """
+    response = _request("GET", f"{host}/buildarea", retries=retries, timeout=timeout)
+
+    if not response.ok or response.json() == -1:
+        raise exceptions.BuildAreaNotSetError(
+            "Failed to get the build area.\n"
+            "Make sure to set the build area with /setbuildarea in-game.\n"
+            "For example: /setbuildarea ~0 0 ~0 ~128 255 ~128"
+        )
+
+    buildAreaJson = response.json()
+    fromPoint = ivec3(
+        buildAreaJson["xFrom"],
+        buildAreaJson["yFrom"],
+        buildAreaJson["zFrom"]
+    )
+    toPoint = ivec3(
+        buildAreaJson["xTo"],
+        buildAreaJson["yTo"],
+        buildAreaJson["zTo"]
+    )
+    return Box.between(fromPoint, toPoint)
+
+
+def getChunks(position: ivec2, size: Optional[ivec2] = None, dimension: Optional[str] = None, asBytes=False, retries=0, timeout=None, host=DEFAULT_HOST):
+    """Returns raw chunk data.
+
+    <position> specifies the position in chunk coordinates, and <size> specifies how many chunks
+    to get in each axis (default 1).
+    <dimension> can be one of {"overworld", "the_nether", "the_end"} (default "overworld").
+
+    If <asBytes> is True, returns raw binary data. Otherwise, returns a human-readable
+    representation.
+
+    On error, returns the error message instead.
+    """
+    url = f"{host}/chunks"
+    dx, dz = (None, None) if size is None else size
+    parameters = {
+        "x": position.x,
+        "z": position.y,
+        "dx": dx,
+        "dz": dz,
+        "dimension": dimension,
+    }
+    acceptType = "application/octet-stream" if asBytes else "text/plain"
+    response = _request("GET", url, params=parameters, headers={"Accept": acceptType}, retries=retries, timeout=timeout)
+    return response.content if asBytes else response.text
+
+
+def getVersion(retries=0, timeout=None, host=DEFAULT_HOST):
+    """Returns the Minecraft version as a string."""
+    return _request("GET", f"{host}/version", retries=retries, timeout=timeout).text
diff --git a/gdpc/interface_toolbox.py b/gdpc/interface_toolbox.py
deleted file mode 100644
index d005397..0000000
--- a/gdpc/interface_toolbox.py
+++ /dev/null
@@ -1,189 +0,0 @@
-"""Provides various small functions for the interface."""
-
-from random import choice
-
-from . import lookup
-from .interface import checkOutOfBounds, getBlock
-from .interface import globalinterface as gi
-from .interface import runCommand
-from .toolbox import direction2rotation, identifyObtrusiveness, index2slot
-
-
-def flood_search_3D(x, y, z, x1, y1, z1, x2, y2, z2, search_blocks,
-                    result=None, observed=None, diagonal=False,
-                    vectors=None, depth=256):
-    """Return a list of coordinates with blocks that fulfil the search.
-
-    Activating caching is *highly* recommended.
-    """
-    result = set() if result is None else result
-    observed = set() if observed is None else observed
-    result.add((x, y, z))
-    observed.add((x, y, z))
-    if vectors is None:
-        vectors = lookup.VECTORS
-        if diagonal:
-            vectors += lookup.DIAGONALVECTORS
-
-    # prevent RecursionError by limiting recursion depth
-    if depth > 0:
-        for dx, dy, dz in vectors:
-            if ((x + dx, y + dy, z + dz) not in observed
-                and not checkOutOfBounds(x + dx, y + dy, z + dz, x1, y1, z1,
-                                         x2, y2, z2, warn=False)):
-                if getBlock(x + dx, y + dy, z + dz) in search_blocks:
-                    result, observed = flood_search_3D(x + dx, y + dy, z + dz,
-                                                       x1, y1, z1, x2, y2, z2,
-                                                       search_blocks,
-                                                       result, observed,
-                                                       diagonal, vectors,
-                                                       depth - 1)
-                else:
-                    observed.add((x + dx, y + dy, z + dz))
-    return result, observed
-
-
-def placeLectern(x, y, z, bookData, facing=None, interface=gi):
-    """Place a lectern with a book in the world."""
-    if facing is None:
-        facing = choice(getOptimalDirection(x, y, z))
-    response = interface.placeBlock(x, y, z, f"lectern[facing={facing}, has_book=true]" + \
-               f'{{Book: {{id: "minecraft:written_book", '
-               f'Count: 1b, tag: {bookData}'
-               '}, Page: 0}')
-    if not response.isnumeric():
-        print(f"{lookup.TCOLORS['orange']}Warning: Server returned error "
-              f"upon placing book in lectern:\n\t{lookup.TCOLORS['CLR']}"
-              f"{response}")
-
-
-def placeInventoryBlock(x, y, z, block='minecraft:chest', facing=None,
-                        items=None, replace=True, interface=gi):
-    """Place an inventorised block with any number of items in the world.
-
-    Items is expected to be a sequence of (x, y, item[, amount])
-    or a sequence of such sequences e.g. ((x, y, item), (x, y, item), ...)
-    """
-    items = [] if items is None else items
-    if block not in lookup.INVENTORYLOOKUP:
-        raise ValueError(f"The inventory for {block} is not available.\n"
-                         "Make sure you are using the namespaced ID.")
-    dx, dy = lookup.INVENTORYLOOKUP[block]
-    if replace:
-        if facing is None:
-            facing = choice(getOptimalDirection(x, y, z))
-        interface.placeBlock(x, y, z, f"{block}[facing={facing}]")
-    else:
-        if block not in gi.getBlock(x, y, z):
-            print(f"{lookup.TCOLORS['orange']}Warning: Block at {x} {y} {z} "
-                  f"is not of specified type {block}!\n"
-                  f"\t{lookup.TCOLORS['CLR']}This may result in "
-                  f"incorrectly placed items.")
-
-    # we received a single item
-    if 3 <= len(items) <= 4 and isinstance(items[0], int):
-        items = [items, ]
-
-    response = '0'
-    for item in items:
-        slot = index2slot(item[0], item[1], dx, dy)
-        if len(item) == 3:
-            item = list(item)
-            item.append(1)
-        response = runCommand(f"replaceitem block {x} {y} {z} "
-                              f"container.{slot} {item[2]} {item[3]}")
-
-    if not response.isnumeric():
-        print(f"{lookup.TCOLORS['orange']}Warning: Server returned error "
-              f"upon placing items:\n\t{lookup.TCOLORS['CLR']}{response}")
-
-
-def placeSign(x, y, z, facing=None, rotation=None,
-              text1="", text2="", text3="", text4="",
-              wood='oak', wall=False, interface=gi):
-    """Place a written sign in the world.
-
-    Facing is for wall placement, rotation for ground placement
-    If there is no supporting wall the sign will revert to ground placement
-    By default the sign will attempt to orient itself to be most legible
-
-    Note: If you are experiencing performance issues provide your own facing
-    and rotation values to reduce the required calculations
-    """
-    if wood not in lookup.WOODS:
-        raise ValueError(f"{wood} is not a valid wood type!")
-
-    if facing is not None and facing not in lookup.DIRECTIONS:
-        print(f"{facing} is not a valid direction.\n"
-              "Working with default behaviour.")
-        facing = None
-    try:
-        if not 0 <= int(rotation) <= 15:
-            raise TypeError
-    except TypeError:
-        if rotation is not None:
-            print(f"{rotation} is not a valid rotation.\n"
-                  "Working with default behaviour.")
-        rotation = None
-
-    if facing is None and rotation is None:
-        facing = getOptimalDirection(x, y, z)
-
-    if wall:
-        wall = False
-        for direction in facing:
-            inversion = lookup.INVERTDIRECTION[direction]
-            dx, dz = lookup.DIRECTION2VECTOR[inversion]
-            if getBlock(x + dx, y, z + dz) in lookup.TRANSPARENT:
-                break
-            wall = True
-            interface.placeBlock(
-                x, y, z, f"{wood}_wall_sign[facing={choice(facing)}]")
-
-    if not wall:
-        if rotation is None:
-            rotation = direction2rotation(facing)
-        interface.placeBlock(x, y, z, f"{wood}_sign[rotation={rotation}]")
-
-    data = "{" + f'Text1:\'{{"text":"{text1}"}}\','
-    data += f'Text2:\'{{"text":"{text2}"}}\','
-    data += f'Text3:\'{{"text":"{text3}"}}\','
-    data += f'Text4:\'{{"text":"{text4}"}}\'' + "}"
-    runCommand(f"data merge block {x} {y} {z} {data}")
-
-
-def getOptimalDirection(x, y, z):
-    """Return the least obstructed direction to have something facing."""
-    north = (identifyObtrusiveness(getBlock(x, y, z - 1)), 'north')
-    east = (identifyObtrusiveness(getBlock(x + 1, y, z)), 'east')
-    south = (identifyObtrusiveness(getBlock(x, y, z + 1)), 'south')
-    west = (identifyObtrusiveness(getBlock(x - 1, y, z)), 'west')
-
-    min_obstruction = min(north[0], east[0], south[0], west[0])
-    max_obstruction = max(north[0], east[0], south[0], west[0])
-
-    surrounding = [north, east, south, west]
-
-    while surrounding[0][0] != max_obstruction:
-        surrounding.append(surrounding.pop(0))
-
-    directions = []
-    while len(directions) == 0:
-        if min_obstruction == max_obstruction:
-            return lookup.DIRECTIONS[2:]
-
-        if surrounding[2][0] == min_obstruction:
-            directions.append(surrounding[2][1])
-        if (surrounding[1][0] == min_obstruction
-                and surrounding[3][0] != min_obstruction):
-            directions.append(surrounding[1][1])
-        elif (surrounding[3][0] == min_obstruction
-                and surrounding[1][0] != min_obstruction):
-            directions.append(surrounding[3][1])
-        elif len(directions) == 0:
-            directions.append(surrounding[1][1])
-            directions.append(surrounding[3][1])
-
-        min_obstruction += 1
-
-    return directions
diff --git a/gdpc/lookup.py b/gdpc/lookup.py
index c9de45c..42eb2f0 100644
--- a/gdpc/lookup.py
+++ b/gdpc/lookup.py
@@ -1,155 +1,46 @@
 """Store lists of various information on blocks, biomes and more."""
 
-import os
-import sys
-from typing import Set
-
-
-# to translate a string of regular names
-# into the appropriate list of minecraft block IDs
-# >>> def f(string):
-# >>>     return ["minecraft:" + i.strip().lower().replace(" ", "_")
-# >>>         for i in string.split(", ")]
-
-# to translate a 255 RGB to hex RGB value
-# >>> def f(r, g, b): return "0x"+(hex(r)+hex(g)+hex(b)).replace("0x", "")
-
-# See https://minecraft.fandom.com/wiki/Data_version#List_of_data_versions
-SUPPORTS = 3120  # Supported Minecraft version code
-
-# all major Minecraft version codes
-VERSIONS = {
-    3105: "1.19",
-    3117: "1.19.1",
-    3120: "1.19.2",
-    3218: "1.19.3",
-    2860: "1.18",
-    2865: "1.18.1",
-    2975: "1.18.2",
-    2724: "1.17",
-    2730: "1.17.1",
-    2566: "1.16",
-    2567: "1.16.1",
-    2578: "1.16.2",
-    2580: "1.16.3",
-    2584: "1.16.4",
-    2586: "1.16.5",
-    2225: "1.15",
-    2227: "1.15.1",
-    2230: "1.15.2",
-    1952: "1.14",
-    1957: "1.14.1",
-    1963: "1.14.2",
-    1968: "1.14.3",
-    1976: "1.14.4",
-    1519: "1.13",
-    1628: "1.13.1",
-    1631: "1.13.2",
-    1139: "1.12",
-    1241: "1.12.1",
-    1343: "1.12.2",
-    819:  "1.11",
-    921:  "1.11.1",
-    922:  "1.11.2",
-    510:  "1.10",
-    511:  "1.10.1",
-    512:  "1.10.2",
-    169:  "1.9",
-    175:  "1.9.1",
-    176:  "1.9.2",
-    183:  "1.9.3",
-    184:  "1.9.4",
-    0:    "Pre-1.9",
-}
-VERSIONIDS = dict([(value, key) for key, value in VERSIONS.items()])
-
-# ========================================================= custom values
-
-
-AXES = ("x", "y", "z")
-DIRECTIONS = ("top", "bottom", "north", "east", "south", "west")
-VECTORS = ((0, 1, 0), (0, -1, 0), (0, 0, -1), (1, 0, 0), (0, 0, 1), (-1, 0, 0))
-DIAGONALVECTORS = (
-    (1, 1, 0),
-    (1, 0, 1),
-    (0, 1, 1),
-    (1, -1, 0),
-    (1, 0, -1),
-    (0, 1, -1),
-    (-1, 1, 0),
-    (-1, 0, 1),
-    (0, -1, 1),
-    (-1, -1, 0),
-    (-1, 0, -1),
-    (0, -1, -1),
-    (1, 1, 1),
-    (1, 1, -1),
-    (1, -1, 1),
-    (-1, 1, 1),
-    (1, -1, -1),
-    (-1, -1, 1),
-    (-1, 1, -1),
-    (-1, -1, -1),
-)
-INVERTDIRECTION = {
-    "top":    "bottom",
-    "bottom": "top",
-    "north":  "south",
-    "east":   "west",
-    "south":  "north",
-    "west":   "east",
-}
-DIRECTION2VECTOR = {
-    "top":    (0, 1, 0),
-    "bottom": (0, -1, 0),
-    "north":  (0, 0, -1),
-    "east":   (1, 0, 0),
-    "south":  (0, 0, 1),
-    "west":   (-1, 0, 0),
-}
-VECTOR2DIRECTION = dict([(val, key) for key, val in DIRECTION2VECTOR.items()])
-AXIS2VECTOR = {"x": (1, 0, 0), "y": (0, 1, 0), "z": (0, 0, 1)}
-VECTOR2AXIS = dict([(val, key) for key, val in AXIS2VECTOR.items()])
 
+from typing import Dict, Iterable, Optional, Set, Union
+
+from glm import ivec2
+
+from .utils import isIterable
 
-# ========================================================= materials
 
+# ==================================================================================================
+# Helper functions
+# ==================================================================================================
 
-def variate(variations, extensions=None, isprefix=False,
-            namespace="minecraft", separator="_", ns_separator=":") -> set:
-    """Generate block variations.
 
-    TODO: documentation
-    TODO: refactor to take optional suffix and prefix, replace named types
+def variate(
+    variations: Iterable[str],
+    extensions: Optional[Union[str, Iterable[Optional[str]]]] = None,
+    isPrefix:   bool          = False,
+    separator:  str           = "_",
+    namespace:  Optional[str] = "minecraft",
+):
+    """Generates block variations.
+
+    Returns a set of strings. For each variation, each extension is either appended or prepended
+    depending on <isPrefix>, using <separator>.
+
+    If <namespace> is not None, each string is additionally prefixed with "<namespace>:".
     """
 
-    def is_iterable(iterable):
-        """Determine whether iterable is an iterable."""
-        try:
-            _ = iter(iterable)
-            return True
-        except TypeError:
-            return False
-
-    if not is_iterable(variations):
-        # TODO: improve error message
-        raise ValueError()
     joined = None
     combinations = set()
+
     if extensions is None:
         joined = variations
     elif isinstance(extensions, str):
         combinations = {(v, extensions) for v in variations}
-    elif is_iterable(extensions):
+    elif isIterable(extensions):
         combinations = {(v, e) for v in variations for e in extensions}
-    if isprefix:
+    if isPrefix:
         combinations = {(e, v) for v, e in combinations}
-    if namespace is None:
-        namespace, ns_separator = "", ""
-    elif ns_separator is None:
-        ns_separator = ""
-    if separator is None:
-        separator = ""
+
+    namespacePrefix = f"{namespace}:" if namespace is not None else ""
 
     if joined is None:
         joined = set()
@@ -159,7 +50,25 @@ def is_iterable(iterable):
                 temp.remove(None)
                 c = tuple(temp)
             joined.add(separator.join(c))
-    return {f"{namespace}{ns_separator}{j}" for j in joined}
+    return {f"{namespacePrefix}{j}" for j in joined}
+
+
+# ==================================================================================================
+# Data
+# ==================================================================================================
+
+
+SUPPORTED_MINECRAFT_VERSIONS = [
+    "1.19.2",
+]
+
+
+BUILD_Y_MIN = -64
+BUILD_Y_MAX = 319
+BUILD_HEIGHT = BUILD_Y_MAX - BUILD_Y_MIN + 1
+
+
+# ========================================================= materials
 
 
 # COLOURS
@@ -301,7 +210,7 @@ def is_iterable(iterable):
 
 LIVE_CORAL_TYPES = set(CORAL_SHADES) - {"dead"}
 DEAD_CORAL_TYPES = variate(LIVE_CORAL_TYPES, "dead",
-                           isprefix=True, namespace=None)
+                           isPrefix=True, namespace=None)
 CORAL_TYPES = LIVE_CORAL_TYPES | DEAD_CORAL_TYPES
 
 WOODY_TYPES = WOOD_TYPES | FUNGUS_TYPES
@@ -316,9 +225,9 @@ def is_iterable(iterable):
 POLISHED_BLACKSTONE_TYPES = {None, "brick", }
 POLISHED_BLACKSTONE_BRICK_TYPES = {None, "cracked", }
 SMOOTH_SANDSTONE_TYPES = variate(SAND_TYPES, "smooth",
-                                 isprefix=True, namespace=None)
+                                 isPrefix=True, namespace=None)
 CUT_SANDSTONE_TYPES = variate(SAND_TYPES, "cut",
-                              isprefix=True, namespace=None)
+                              isPrefix=True, namespace=None)
 PRISMARINE_TYPES = {None, "dark", }
 LIMITED_NETHER_BRICK_TYPES = {None, "red", }
 NETHER_BRICK_TYPES = {"cracked", "chiseled", } | LIMITED_NETHER_BRICK_TYPES
@@ -355,9 +264,9 @@ def is_iterable(iterable):
 
 NAMED_POLISHED_BLACKSTONE_TYPES = \
     variate(POLISHED_BLACKSTONE_TYPES, "polished_blackstone",
-            isprefix=True, namespace=None)
+            isPrefix=True, namespace=None)
 NAMED_POLISHED_IGNEOUS_TYPES = variate(IGNEOUS_TYPES, "polished",
-                                       isprefix=True, namespace=None)
+                                       isPrefix=True, namespace=None)
 NAMED_PRISMARINE_TYPES = variate(PRISMARINE_TYPES, "prismarine",
                                  namespace=None)
 
@@ -402,8 +311,8 @@ def is_iterable(iterable):
 OBSIDIAN_BLOCKS = variate(OBSIDIAN_TYPES, "obsidian")
 COBBLESTONES = variate(COBBLESTONE_TYPES, "cobblestone")
 INFESTED_STONE_BRICKS = variate(
-    NAMED_STONE_BRICK_TYPES, "infested", isprefix=True)
-INFESTED = variate(STONE_TYPES, "infested", isprefix=True) \
+    NAMED_STONE_BRICK_TYPES, "infested", isPrefix=True)
+INFESTED = variate(STONE_TYPES, "infested", isPrefix=True) \
            | INFESTED_STONE_BRICKS
 RAW_SANDSTONES = variate(SAND_TYPES, "sandstone")
 TERRACOTTAS = variate({None, } | set(DYE_COLORS), "terracotta")
@@ -454,8 +363,8 @@ def is_iterable(iterable):
 BARKED_FUNGUS_STEMS = variate(FUNGUS_TYPES, "stem")
 BARKED_FUNGUS_HYPHAE = variate(FUNGUS_TYPES, "hyphae")
 BARKED_FUNGUS_STALKS = BARKED_FUNGUS_STEMS | BARKED_FUNGUS_HYPHAE
-STRIPPED_FUNGUS_STEMS = variate(NAMED_STEM_TYPES, "stripped", isprefix=True)
-STRIPPED_FUNGUS_HYPHAE = variate(NAMED_HYPHAE_TYPES, "stripped", isprefix=True)
+STRIPPED_FUNGUS_STEMS = variate(NAMED_STEM_TYPES, "stripped", isPrefix=True)
+STRIPPED_FUNGUS_HYPHAE = variate(NAMED_HYPHAE_TYPES, "stripped", isPrefix=True)
 STRIPPED_FUNGUS_STALKS = STRIPPED_FUNGUS_STEMS | STRIPPED_FUNGUS_HYPHAE
 FUNGUS_STEMS = BARKED_FUNGUS_STEMS | STRIPPED_FUNGUS_STEMS
 FUNGUS_HYPHAE = BARKED_FUNGUS_HYPHAE | STRIPPED_FUNGUS_HYPHAE
@@ -480,8 +389,8 @@ def is_iterable(iterable):
 BARKED_LOGS = variate(WOOD_TYPES, "log")
 BARKED_WOODS = variate(WOOD_TYPES, "wood")
 BARKED_TRUNKS = BARKED_LOGS | BARKED_WOODS
-STRIPPED_LOGS = variate(NAMED_LOG_TYPES, "stripped", isprefix=True)
-STRIPPED_WOODS = variate(NAMED_WOOD_TYPES, "stripped", isprefix=True)
+STRIPPED_LOGS = variate(NAMED_LOG_TYPES, "stripped", isPrefix=True)
+STRIPPED_WOODS = variate(NAMED_WOOD_TYPES, "stripped", isPrefix=True)
 STRIPPED_TRUNKS = STRIPPED_LOGS | STRIPPED_WOODS
 WOODS = BARKED_TRUNKS | STRIPPED_WOODS
 LOGS = BARKED_LOGS | STRIPPED_LOGS
@@ -734,7 +643,7 @@ def is_iterable(iterable):
 FUNGUS_PLANKS = variate(FUNGUS_TYPES, "planks")
 PLANKS = WOOD_PLANKS | FUNGUS_PLANKS
 
-POLISHED_IGNEOUS_BLOCKS = variate(IGNEOUS_TYPES, "polished", isprefix=True)
+POLISHED_IGNEOUS_BLOCKS = variate(IGNEOUS_TYPES, "polished", isPrefix=True)
 
 STONE_BRICKS = {"minecraft:smooth_stone", } \
                | variate(STONE_BRICK_TYPES, "stone_bricks")
@@ -764,7 +673,7 @@ def is_iterable(iterable):
 QUARTZES = {"minecraft:smooth_quartz", "minecraft:chiseled_quartz_block",
             "minecraft:quartz_block", "minecraft:quartz_bricks",
             "minecraft:quartz_pillar", }
-PURPUR_BLOCKS = variate(PURPUR_TYPES, "purpur", isprefix=True)
+PURPUR_BLOCKS = variate(PURPUR_TYPES, "purpur", isPrefix=True)
 
 SHULKER_BOXES = variate({None, } | set(DYE_COLORS), "shulker_box")
 DYEABLE_BLOCKS = WOOLS | CARPETS | BEDS | BANNERS | STAINED_GLASSES \
@@ -843,7 +752,7 @@ def is_iterable(iterable):
 
 # interaction has an immediate effect (no UI)
 FLOWER_POTS = {"minecraft:flower_pot", } \
-              | variate(POTTED_PLANT_TYPES, "potted", isprefix=True)
+              | variate(POTTED_PLANT_TYPES, "potted", isPrefix=True)
 USABLE_BLOCKS = {"minecraft:bell", "minecraft:cake", "minecraft:conduit",
                  "minecraft:jukebox", "minecraft:lodestone",
                  "minecraft:respawn_anchor", "minecraft:spawner",
@@ -1461,7 +1370,7 @@ def is_iterable(iterable):
 # liberty was taken to move stained glass panes and various flowers
 # into the appropriate colour category
 
-MAPTRANSPARENT = {"minecraft:redstone_lamp", "minecraft:cake",
+MAP_TRANSPARENT = {"minecraft:redstone_lamp", "minecraft:cake",
                   "minecraft:ladder",
                   "minecraft:tripwire_hook", "minecraft:tripwire",
                   "minecraft:end_rod",
@@ -1472,7 +1381,7 @@ def is_iterable(iterable):
 
 # base map colours
 # WARNING: all non-transparent blocks are listed individually here again
-PALETTE = {
+COLOR_TO_BLOCKS: Dict[int, Set[str]] = {
     0x7FB238: {"minecraft:grass_block", "minecraft:slime_block", },
     0xF7E9A3: {
                   "minecraft:sand",
@@ -2108,10 +2017,10 @@ def is_iterable(iterable):
     0x562C3E: ("minecraft:warped_hyphae", "minecraft:stripped_warped_hyphae"),
     0x14B485: ("minecraft:warped_wart_block",),
 }
-PALETTELOOKUP = {}
-for hexval, blocks in PALETTE.items():
-    for block in blocks:
-        PALETTELOOKUP[block] = hexval
+BLOCK_TO_COLOR: Dict[str, int] = {}
+for hexval, ids in COLOR_TO_BLOCKS.items():
+    for bid in ids:
+        BLOCK_TO_COLOR[bid] = hexval
 
 # ========================================================= biome-related
 
@@ -2202,7 +2111,7 @@ def is_iterable(iterable):
 # the width of ASCII characters in pixels
 # space between characters is 1
 # the widest supported Unicode character is 9 wide
-ASCIIPIXELS = {
+ASCII_CHAR_TO_WIDTH = {
     "A":  5,
     "a":  5,
     "B":  5,
@@ -2302,64 +2211,19 @@ def is_iterable(iterable):
 }
 
 
-# terminal colour codes
-
-
-def supports_color():
-    """Return True if the running system's terminal supports color."""
-    plat = sys.platform
-    supported_platform = plat != "Pocket PC" and (
-            plat != "win32" or "ANSICON" in os.environ
-    )
-    # isatty is not always implemented, #6223.
-    is_a_tty = hasattr(sys.stdout, "isatty") and sys.stdout.isatty()
-    return supported_platform and is_a_tty
-
-
-if supports_color():
-    TCOLORS = {
-        "black":     "\033[38;2;000;000;000m",
-        "gray":      "\033[38;2;128;128;128m",
-        "white":     "\033[38;2;255;255;255m",
-        "pink":      "\033[38;2;255;192;203m",
-        "red":       "\033[38;2;255;000;000m",
-        "orange":    "\033[38;2;255;165;000m",
-        "yellow":    "\033[38;2;255;255;000m",
-        "darkgreen": "\033[38;2;000;128;000m",
-        "green":     "\033[38;2;000;255;000m",
-        "blue":      "\033[38;2;135;206;235m",
-        "darkblue":  "\033[38;2;000;000;255m",
-        "magenta":   "\033[38;2;255;000;255m",
-        "brown":     "\033[38;2;139;069;019m",
-        "CLR":       "\033[0m",
-    }  # 38 is replaced by 48 for background
-else:
-    TCOLORS = {
-        "black":     "",
-        "gray":      "",
-        "white":     "",
-        "pink":      "",
-        "red":       "",
-        "orange":    "",
-        "yellow":    "",
-        "darkgreen": "",
-        "green":     "",
-        "blue":      "",
-        "darkblue":  "",
-        "magenta":   "",
-        "brown":     "",
-        "CLR":       "",
-    }  # colour codes not printed
-
-INVENTORYDIMENSIONS = {
-    (9, 3): {"minecraft:barrel", } | CHESTS | SHULKER_BOXES,
-    (3, 3): {"minecraft:dispenser", "minecraft:dropper", },
-    (5, 1): {"minecraft:hopper", "minecraft:brewing_stand", },
-    (3, 1): FURNACES,
-}
-INVENTORYLOOKUP = {}
-for dimensions, blocks in INVENTORYDIMENSIONS.items():
-    for block in blocks:
-        INVENTORYLOOKUP[block] = dimensions
+BOOK_PAGES_PER_BOOK      = 100
+BOOK_CHARACTERS_PER_PAGE = 255
+BOOK_LINES_PER_PAGE      = 14
+BOOK_PIXELS_PER_LINE     = 114
 
-# version checking
+
+INVENTORY_SIZE_TO_CONTAINER_BLOCKS = {
+    ivec2(9,3): {"minecraft:barrel", } | CHESTS | SHULKER_BOXES,
+    ivec2(3,3): {"minecraft:dispenser", "minecraft:dropper", },
+    ivec2(5,1): {"minecraft:hopper", "minecraft:brewing_stand", },
+    ivec2(3,1): FURNACES,
+}
+CONTAINER_BLOCK_TO_INVENTORY_SIZE = {}
+for size, ids in INVENTORY_SIZE_TO_CONTAINER_BLOCKS.items():
+    for bid in ids:
+        CONTAINER_BLOCK_TO_INVENTORY_SIZE[bid] = size
diff --git a/gdpc/minecraft_tools.py b/gdpc/minecraft_tools.py
new file mode 100644
index 0000000..83d2eed
--- /dev/null
+++ b/gdpc/minecraft_tools.py
@@ -0,0 +1,243 @@
+"""Provides various Minecraft-related utility functions."""
+
+
+from typing import Any, Optional, Union, List
+from functools import lru_cache
+
+from glm import ivec2
+
+from .vector_tools import Rect
+from . import lookup
+from .block import Block
+
+
+# ==================================================================================================
+# NBT generation utilities
+# ==================================================================================================
+
+
+def signData(
+    line1: str = "",
+    line2: str = "",
+    line3: str = "",
+    line4: str = "",
+    color: str = "",
+    isGlowing: bool = False,
+):
+    """Returns a JSON-like structure of Python objects with sign data"""
+    data = {}
+
+    for i, line in enumerate([line1, line2, line3, line4]):
+        if line:
+            data[f"Text{i+1}"] = f'{{"text":"{line}"}}'
+
+    if color:
+        data["Color"] = color
+
+    if isGlowing:
+        data["GlowingText"] = "true"
+
+    return data
+
+
+def lecternData(bookData: Any, page: int = 0):
+    """Returns a JSON-like structure of Python objects with lectern data"""
+    if bookData is None:
+        return None
+    return {"Book": {"id": "minecraft:written_book", "Count": 1, "tag": bookData}, "Page": page}
+
+
+def bookData(
+    text: str,
+    title       = "Chronicle",
+    author      = "Anonymous",
+    description = "I wonder what\\'s inside?",
+    desccolor   = "gold"
+):
+    r"""Returns a JSON-like structure of Python objects with written book data
+
+    The following special characters are used for formatting the book:
+    - `\n`: New line
+    - `\f`: Form/page break
+
+    - `§0`: Black text
+    - '§1': Dark blue text
+    - '§2': Dark green text
+    - '§3': Dark aqua text
+    - '§4': Dark red text
+    - '§5': Dark purple text
+    - '§6': Gold text
+    - '§7': Gray text
+    - '§8': Dark gray text
+    - '§9': Blue text
+    - `§a`: Green text
+    - `§b`: Aqua text
+    - `§c`: Red text
+    - `§d`: Light purple text
+    - `§e`: Yellow text
+    - `§f`: White text
+
+    - `§k`: Obfuscated text
+    - `§l`: **Bold** text
+    - `§m`: ~~Strikethrough~~ text
+    - `§n`: __Underline__ text
+    - `§o`: *Italic* text
+    - `§r`: Reset text formatting
+
+    - `\\\\s`: When at start of page, print page as string directly
+    - `\\c`: When at start of line, align text to center
+    - `\\r`: When at start of line, align text to right side
+
+    NOTE: For supported special characters see
+    https://minecraft.fandom.com/wiki/Language#Font
+
+    IMPORTANT: When using `\\s` text is directly interpreted by Minecraft,
+    so all line breaks must be `\\\\n` to function
+    """
+    pages_left      = lookup.BOOK_PAGES_PER_BOOK
+    characters_left = lookup.BOOK_CHARACTERS_PER_PAGE
+    lines_left      = lookup.BOOK_LINES_PER_PAGE
+    pixels_left     = lookup.BOOK_PIXELS_PER_LINE
+    toprint = ''
+
+    @lru_cache()
+    def fontwidth(word):
+        """Return the length of a word based on character width.
+
+        If a letter is not found, a width of 9 is assumed
+        A character spacing of 1 is automatically integrated
+        """
+        return sum([lookup.ASCII_CHAR_TO_WIDTH.get(letter, 9) + 1 for letter in word]) - 1
+
+    def printline():
+        nonlocal pageData, toprint
+        formatting = toprint[:2]
+        spaces_left = pixels_left // 4 + 3
+        if formatting == '\\c':      # centered text
+            pageData[-1] += spaces_left // 2 * ' ' + toprint[2:-1] + spaces_left // 2 * ' '
+        elif formatting == '\\r':    # right-aligned text
+            pageData[-1] += spaces_left * ' ' + toprint[2:-1]
+        else:
+            pageData[-1] += toprint
+        toprint = ''
+
+    def newline():
+        nonlocal characters_left, lines_left, pixels_left, pageData
+        printline()
+        if characters_left < 2 or lines_left < 1:
+            newpage()
+            return
+        characters_left -= 2
+        lines_left -= 1
+        pixels_left = lookup.BOOK_PIXELS_PER_LINE
+        pageData[-1] += "\\n"
+
+    def newpage():
+        nonlocal characters_left, lines_left, pixels_left, pageData
+        printline()
+        characters_left = lookup.BOOK_CHARACTERS_PER_PAGE
+        lines_left      = lookup.BOOK_LINES_PER_PAGE
+        pixels_left     = lookup.BOOK_PIXELS_PER_LINE
+        pageData.append("") # end page and start new page
+
+    pages = [page for page in text.split('\f')]
+
+    pageData: List[str] = [""] # start first page
+
+    for page in pages:
+        if pages_left < 1:
+            break
+        if page[:3] == '\\\\s':
+            pageData[-1] += page[3:]
+            newpage()
+            continue
+        else:
+            page = [[word for word in line.split()] for line in page.split('\n')]
+        for line in page:
+            toprint = ""
+            for word in line:
+                width = fontwidth(word + ' ')
+                if width > pixels_left:
+                    if width > lookup.BOOK_PIXELS_PER_LINE:  # cut word to fit
+                        original = word
+                        for letter in original:
+                            charwidth = fontwidth(letter) + 1
+                            if charwidth > pixels_left:
+                                newline()
+                            toprint += letter
+                            width -= charwidth
+                            word = word[1:]
+                            characters_left -= 1
+                            pixels_left -= charwidth
+                            if not width > pixels_left:
+                                break
+                    else:
+                        newline()
+                if len(word) > characters_left:
+                    newpage()
+                toprint += word + ' '
+                characters_left -= len(word) + 1
+                pixels_left -= width
+            newline()           # finish line
+        newpage()               # finish page
+    del pageData[-1] # end last page (book is complete)
+
+    return {
+        "title": title,
+        "author": author,
+        "display": {"Lore": [f'[{{"text":"{description}","color":"{desccolor}"}}]']},
+        "pages": [f'{{"text":"{page}"}}' for page in pageData]
+    }
+
+
+# ==================================================================================================
+# Block generating utilities
+# ==================================================================================================
+
+
+def signBlock(
+    wood="oak", wall=False,
+    facing: str = "north", rotation: Union[str,int] = "0",
+    text1="", text2="", text3="", text4="", color="", isGlowing=False
+):
+    """Returns a sign Block with the specified properties."""
+    blockId = f"minecraft:{wood}_{'wall_' if wall else ''}sign"
+    states = {"facing": facing} if wall else {"rotation": str(rotation)}
+    return Block(blockId, states, data=signData(text1, text2, text3, text4, color, isGlowing))
+
+
+def lecternBlock(facing: str = "north", bookData: Optional[str] = None, page: int = 0):
+    """Returns a lectern Block with the specified properties."""
+    return Block(
+        "minecraft:lectern",
+        {"facing": facing, "has_book": ("false" if bookData is None else "true")},
+        data=lecternData(bookData, page)
+    )
+
+
+# ==================================================================================================
+# Misc. utilities
+# ==================================================================================================
+
+
+def positionToInventoryIndex(position: ivec2, inventorySize: ivec2):
+    """Returns the flat index of the slot at <position> in an inventory of size <inventorySize>."""
+    if not Rect(size=inventorySize).contains(position):
+        raise ValueError(f"{position} is not between (0, 0) and {inventorySize}!")
+    return position.x + position.y * inventorySize.x
+
+
+def getObtrusiveness(block: Block):
+    """Returns the percieved obtrusiveness of the given <block>.\n
+    Returns a numeric weight from 0 (invisible) to 4 (opaque)."""
+    if not isinstance(block.id, str):
+        raise ValueError("Cannot determine the obtrusiveness of a block palette!")
+    if block.id in lookup.INVISIBLE:
+        return 0
+    if block.id in lookup.FILTERING:
+        return 1
+    if block.id in lookup.UNOBTRUSIVE:
+        return 2
+    if block.id in lookup.OBTRUSIVE:
+        return 3
+    return 4
diff --git a/gdpc/model.py b/gdpc/model.py
new file mode 100644
index 0000000..92d5d24
--- /dev/null
+++ b/gdpc/model.py
@@ -0,0 +1,74 @@
+"""Provides the Model class"""
+
+
+from typing import Union, Optional, List, Dict
+from copy import copy
+from glm import ivec3
+
+from .vector_tools import Box
+from .transform import TransformLike
+from .editor import Editor
+from .block import Block
+
+
+class Model:
+    """A 3D model of Minecraft blocks.
+
+    Can be used to store a structure in memory, allowing it to be built under different
+    transformations.
+    """
+
+    def __init__(self, size: ivec3, blocks: Optional[List[Optional[Block]]] = None):
+        """Constructs a Model of size [size], optionally filled with [blocks]."""
+        self._size = copy(size)
+        if blocks is not None:
+            if len(blocks) != size.x * size.y * size.z:
+                raise ValueError("The number of blocks should be equal to size.x * size.y * size.z")
+            self._blocks = copy(blocks)
+        else:
+            self._blocks = [None] * (size.x * size.y * size.z)
+
+
+    @property
+    def size(self):
+        """This Model's size"""
+        return copy(self._size)
+
+    @property
+    def blocks(self) -> List[Optional[Block]]:
+        """This Model's block list"""
+        return copy(self._blocks) # Allows block modification, but not resizing
+
+
+    def getBlock(self, position: ivec3):
+        """Returns the block at [vec]"""
+        return self._blocks[(position.x * self._size.y + position.y) * self._size.z + position.z]
+
+    def setBlock(self, position: ivec3, block: Optional[Block]):
+        """Sets the block at [vec] to [block]"""
+        self._blocks[(position.x * self._size.y + position.y) * self._size.z + position.z] = block
+
+
+    def build(
+        self,
+        editor:         Editor,
+        transformLike:  Optional[TransformLike]         = None,
+        substitutions:  Optional[Dict[str, str]]        = None,
+        replace:        Optional[Union[str, List[str]]] = None
+    ):
+        """Builds the model.
+
+        Use [substitutions] to build the model with certain blocks types replaced by others.
+        """
+        if substitutions is None: substitutions = {}
+
+        with editor.pushTransform(transformLike):
+            for vec in Box(size=self._size).inner:
+                block = self.getBlock(vec)
+                if block is not None:
+                    blockToPlace = copy(block)
+                    blockToPlace.id = substitutions.get(block.id, block.id)
+                    editor.placeBlock(vec, blockToPlace, replace)
+
+    def __repr__(self):
+        return f"Model(size={repr(self.size)}, blocks={repr(self.blocks)})"
diff --git a/gdpc/nbt_tools.py b/gdpc/nbt_tools.py
new file mode 100644
index 0000000..75027b5
--- /dev/null
+++ b/gdpc/nbt_tools.py
@@ -0,0 +1,32 @@
+"""Utilities for working with Minecraft's NBT format"""
+
+
+from nbt import nbt
+
+
+def nbtToPythonObject(tag: nbt.TAG):
+    """Recursively converts an NBT tag to a JSON-like structure of built-in Python objects"""
+    if isinstance(tag, nbt.TAG_List):
+        return [nbtToPythonObject(t) for t in tag]
+    if isinstance(tag, nbt.TAG_Compound):
+        return {k: nbtToPythonObject(v) for k, v in tag.items()}
+    return tag.value
+
+
+# TODO: pythonObjectToNbt
+
+
+def pythonObjectToSnbt(obj) -> str:
+    """Recursively converts a JSON-like structure of built-in Python objects to an SNBT string"""
+    if isinstance(obj, (list, tuple)):
+        return f"[{','.join(pythonObjectToSnbt(o) for o in obj)}]"
+    if isinstance(obj, dict):
+        return f"{{{','.join(f'{k}:{pythonObjectToSnbt(v)}' for k, v in obj.items())}}}"
+    if isinstance(obj, bool):
+        return "true" if obj else "false"
+    if isinstance(obj, (str, int, float)):
+        return repr(obj)
+    raise TypeError(f'Cannot convert type "{type(obj)}" to SNBT')
+
+
+# TODO: SnbtToPythonObject
diff --git a/gdpc/toolbox.py b/gdpc/toolbox.py
deleted file mode 100644
index 4297bf7..0000000
--- a/gdpc/toolbox.py
+++ /dev/null
@@ -1,377 +0,0 @@
-"""Provides various small functions for the average workflow."""
-
-from functools import lru_cache
-from itertools import product
-
-import cv2
-import numpy as np
-from matplotlib import pyplot as plt
-
-from . import lookup
-from .lookup import SUPPORTS, TCOLORS, VERSIONS
-from .worldLoader import WorldSlice
-
-
-def is_sequence(sequence):
-    """Determine whether sequence is a sequence."""
-    try:
-        _ = sequence[0:-1]
-        return True
-    except TypeError:
-        return False
-
-
-def closest_version(version):
-    """Retrieve next-best version code to given version code."""
-    if version in VERSIONS:
-        return version
-    for val in sorted(VERSIONS.keys(), reverse=True):
-        if version - val >= 0:
-            return val
-    return 0
-
-
-def check_version():
-    """Retrieve Minecraft version and check compatibility."""
-    wslice = WorldSlice(0, 0, 1, 1)  # single-chunk slice
-    current = int(wslice.nbtfile["Chunks"][0]["DataVersion"].value)
-    closestname = "Unknown"
-    # check compatibility
-    if current not in VERSIONS or VERSIONS[SUPPORTS] not in VERSIONS[current]:
-        closest = closest_version(current)
-        closestname = VERSIONS[closest]
-        closestname += " snapshot" if current > closest else ""
-        if closest > SUPPORTS:
-            print(
-                f"{TCOLORS['orange']}WARNING: You are using a newer "
-                "version of Minecraft then GDPC supports!\n"
-                f"\tSupports: {VERSIONS[SUPPORTS]} "
-                f"Detected: {closestname}{TCOLORS['CLR']}"
-            )
-        elif closest < SUPPORTS:
-            print(
-                f"{TCOLORS['orange']}WARNING: You are using an older "
-                "version of Minecraft then GDPC supports!\n"
-                f"\tSupports: {VERSIONS[SUPPORTS]} "
-                f"Detected: {closestname}{TCOLORS['CLR']}"
-            )
-        else:
-            raise ValueError(
-                f"{TCOLORS['red']}Invalid supported version: "
-                f"SUPPORTS = {current}!{TCOLORS['CLR']}"
-            )
-    else:
-        closestname = VERSIONS[current]
-
-    return (current, closestname)
-
-
-def normalizeCoordinates(x1, y1, z1, x2, y2=None, z2=None):
-    """Return set of coordinates where (x1, y1, z1) <= (x2, y2, z2)."""
-    # if 2D coords are provided reshape to 3D coords
-    if y2 is None or z2 is None:
-        y1, z1, x2, y2, z2 = 0, y1, z1, 255, x2
-    if x1 > x2:
-        x1, x2 = x2, x1
-    if y1 > y2:
-        y1, y2 = y2, y1
-    if z1 > z2:
-        z1, z2 = z2, z1
-    return x1, y1, z1, x2, y2, z2
-
-
-def loop2d(a1, b1, a2=None, b2=None):
-    """Return all coordinates in a 2D region.
-
-    If only one pair is provided, the loop will yield a1*b1 values.
-
-    If two pairs are provided the loop will yield all results between them inclusively.
-    """
-    if a2 is None or b2 is None:
-        a1, b1, a2, b2 = 0, 0, a1 - 1, b1 - 1
-
-    a1, b1, _, a2, b2, _ = normalizeCoordinates(a1, b1, 0, a2, b2, 0)
-
-    return product(range(a1, a2 + 1), range(b1, b2 + 1))
-
-
-def loop3d(x1, y1, z1, x2=None, y2=None, z2=None):
-    """Return all coordinates in a region of size dx, dy, dz.
-
-    Behaves like loop2d
-    """
-    if x2 is None or y2 is None or z2 is None:
-        x1, y1, z1, x2, y2, z2 = 0, 0, 0, x1 - 1, y1 - 1, z1 - 1
-
-    x1, y1, z1, x2, y2, z2 = normalizeCoordinates(x1, y1, z1, x2, y2, z2)
-
-    return product(range(x1, x2 + 1), range(y1, y2 + 1), range(z1, z2 + 1))
-
-
-def index2slot(sx, sy, ox, oy):
-    """Return slot number of an inventory correlating to a 2d index."""
-    if not (0 <= sx < ox and 0 <= sy < oy):
-        raise ValueError(f"{sx, sy} is not within (0, 0) and {ox, oy}!")
-    return sx + sy * ox
-
-
-def writeBook(text, title="Chronicle", author="Anonymous",
-              description="I wonder what\\'s inside?", desccolor='gold'):
-    r"""Return NBT data for a correctly formatted book.
-
-    The following special characters are used for formatting the book:
-    - `\n`: New line
-    - `\f`: Form/page break
-
-    - `§0`: Black text
-    - '§1': Dark blue text
-    - '§2': Dark_green text
-    - '§3': Dark_aqua text
-    - '§4': Dark_red text
-    - '§5': Dark_purple text
-    - '§6': Gold text
-    - '§7': Gray text
-    - '§8': Dark_gray text
-    - '§9': Blue text
-    - `§a`: Green text
-    - `§b`: Aqua text
-    - `§c`: Red text
-    - `§d`: Light_purple text
-    - `§e`: Yellow text
-    - `§f`: White text
-
-    - `§k`: Obfuscated text
-    - `§l`: **Bold** text
-    - `§m`: ~~Strikethrough~~ text
-    - `§n`: __Underline__ text
-    - `§o`: *Italic* text
-    - `§r`: Reset text formatting
-
-    - `\\\\s`: When at start of page, print page as string directly
-    - `\\c`: When at start of line, align text to center
-    - `\\r`: When at start of line, align text to right side
-
-    NOTE: For supported special characters see
-    https://minecraft.fandom.com/wiki/Language#Font
-
-    IMPORTANT: When using `\\s` text is directly interpreted by Minecraft,
-    so all line breaks must be `\\\\n` to function
-    """
-    pages_left = 97                     # per book
-    characters_left = CHARACTERS = 255  # per page
-    lines_left = LINES = 14             # per page
-    pixels_left = PIXELS = 113          # per line
-    toprint = ''
-
-    @lru_cache()
-    def fontwidth(word):
-        """Return the length of a word based on character width.
-
-        If a letter is not found, a width of 9 is assumed
-        A character spacing of 1 is automatically integrated
-        """
-        return sum([lookup.ASCIIPIXELS[letter] + 1
-                    if letter in lookup.ASCIIPIXELS
-                    else 10
-                    for letter in word]) - 1
-
-    def printline():
-        nonlocal bookData, toprint
-        formatting = toprint[:2]
-        spaces_left = pixels_left // 4 + 3
-        if formatting == '\\c':      # centered text
-            bookData += spaces_left // 2 * ' ' \
-                + toprint[2:-1] + spaces_left // 2 * ' '
-        elif formatting == '\\r':    # right-aligned text
-            bookData += spaces_left * ' ' + toprint[2:-1]
-        else:
-            bookData += toprint
-        toprint = ''
-
-    def newline():
-        nonlocal characters_left, lines_left, pixels_left, bookData
-        printline()
-        if characters_left < 2 or lines_left < 1:
-            return newpage()
-        characters_left -= 2
-        lines_left -= 1
-        pixels_left = PIXELS
-        bookData += "\\\\n"
-
-    def newpage():
-        nonlocal characters_left, lines_left, pixels_left, bookData
-        printline()
-        characters_left = CHARACTERS
-        lines_left = LINES
-        pixels_left = PIXELS
-        bookData += '"}\',\'{"text":"'    # end page and start new page
-
-    def jokepage():
-        nonlocal bookData
-        bookData += ('"}\',\'{"text":"'
-                     '…and there was more\\\\n'
-                     'to say, but the paper\\\\n'
-                     '        ran out…\\\\n'
-                     '\\\\n'
-                     '\\\\n'
-                     '        ⌠       ⌠\\\\n'
-                     '        `|  THE  |\\\\n'
-                     '        `|  END  |\\\\n'
-                     '        ⌡`       ⌡\\\\n'
-                     '\\\\n'
-                     '\\\\n'
-                     '\\\\n'
-                     '§7§o…and frankly it was\\\\n'
-                     'getting boring…§r')
-        newpage()
-
-    def finalpage():
-        nonlocal bookData
-        bookData += ('§8╔══════════╗\\\\n'
-                     '║                      `║\\\\n'
-                     '║                      `║\\\\n'
-                     '║      ᴘᴜʙʟiꜱʜᴇᴅ   .`║\\\\n'
-                     '║          ʙʏ         `║\\\\n'
-                     '║     §2Ⓟ§8ᴇɴᴅᴇʀᴍᴀɴ  .`║\\\\n'
-                     '║                      `║\\\\n'
-                     '║           ⁂         `║\\\\n'
-                     '║                      `║\\\\n'
-                     '║         GDMC       `║\\\\n'
-                     '║                      `║\\\\n'
-                     '║                      `║\\\\n'
-                     '║                      `║\\\\n'
-                     '╚══════════╝\\\\n'
-                     '"}\']}')
-
-    pages = [page for page in text.split('\f')]
-    text = [[[word for word in line.split()] for line in page.split('\n')]
-            for page in text.split('\f')]   # convert string to 3D list
-
-    bookData = ("{"
-                f'title: "{title}", author: "{author}", '
-                f'display:{{Lore:[\'[{{"text":"{description}",'
-                f'"color":"{desccolor}"}}]\']}}, pages:[')
-
-    bookData += '\'{"text":"'   # start first page
-    for page in pages:
-        if pages_left < 1:
-            jokepage()
-            break
-        if page[:3] == '\\\\s':
-            bookData += page[3:]
-            newpage()
-            continue
-        else:
-            page = [[word for word in line.split()]
-                    for line in page.split('\n')]
-        for line in page:
-            toprint = ""
-            for word in line:
-                width = fontwidth(word + ' ')
-                if width > pixels_left:
-                    if width > PIXELS:  # cut word to fit
-                        original = word
-                        for letter in original:
-                            charwidth = fontwidth(letter) + 1
-                            if charwidth > pixels_left:
-                                newline()
-                            toprint += letter
-                            width -= charwidth
-                            word = word[1:]
-                            characters_left -= 1
-                            pixels_left -= charwidth
-                            if not width > pixels_left:
-                                break
-                    else:
-                        newline()
-                if len(word) > characters_left:
-                    newpage()
-                toprint += word + ' '
-                characters_left -= len(word) + 1
-                pixels_left -= width
-            newline()           # finish line
-        newpage()               # finish page
-    finalpage()        # end last page (book is complete)
-    return bookData
-
-
-def identifyObtrusiveness(blockStr):
-    """Return the percieved obtrusiveness of a given block.
-
-    Returns a numeric weight from 0 (invisible) to 4 (opaque)
-    """
-    if blockStr in lookup.INVISIBLE:
-        return 0
-    if blockStr in lookup.FILTERING:
-        return 1
-    if blockStr in lookup.UNOBTRUSIVE:
-        return 2
-    if blockStr in lookup.OBTRUSIVE:
-        return 3
-    return 4
-
-
-def visualizeHeightmap(*arrays, title=None, autonormalize=True):
-    """Visualizes one or multiple numpy arrays."""
-    def normalize(array):
-        """Normalize the array to contain values from 0 to 1."""
-        return (array - array.min()) / (array.max() - array.min())
-
-    for array in arrays:
-        if autonormalize:
-            array = (normalize(array) * 255).astype(np.uint8)
-
-        plt.figure()
-        if title:
-            plt.title(title)
-        plt_image = cv2.cvtColor(array, cv2.COLOR_BGR2RGB)
-        plt.imshow(plt_image)
-    plt.show()
-
-
-# ========================================================= converters
-# The 'data types' commonly used in this package are:
-#
-# axis: 'x', 'y', 'z'
-# direction: 'up', 'down', 'north', 'south', 'east', 'west'
-# rotation: 0 - 15 (22.5° clockwise turns starting at north)
-# vector: any multiple of (1, 1, 1) e.g. (0, 1, -4)
-# =========================================================
-
-
-def invertDirection(direction):
-    """Return the inverted direction of direcion."""
-    if is_sequence(direction):
-        return [lookup.INVERTDIRECTION[n] for n in direction]
-    return lookup.INVERTDIRECTION[direction]
-
-
-def direction2rotation(direction):
-    """Convert a direction to a rotation.
-
-    If a sequence is provided, the average is returned.
-    """
-    reference = {'north': 0, 'east': 4, 'south': 8, 'west': 12}
-    if len(direction) == 1:
-        rotation = reference[lookup.INVERTDIRECTION[direction[0]]]
-    else:
-        rotation = 0
-        for direction in direction:
-            rotation += reference[lookup.INVERTDIRECTION[direction]]
-        rotation //= 2
-
-        if rotation == 6 and 'north' not in direction:
-            rotation = 14
-        if rotation % 4 != 2:
-            rotation = reference[direction[0]]
-    return rotation
-
-
-def direction2vector(direction):
-    """Convert a direction to a vector."""
-    return lookup.DIRECTION2VECTOR[direction]
-
-
-def axis2vector(axis):
-    """Convert an axis to a vector."""
-    return lookup.AXIS2VECTOR[axis]
diff --git a/gdpc/transform.py b/gdpc/transform.py
new file mode 100644
index 0000000..b0a5050
--- /dev/null
+++ b/gdpc/transform.py
@@ -0,0 +1,166 @@
+"""Provides the Transform class and related functions"""
+
+
+from abc import ABC
+from dataclasses import dataclass, field
+
+from glm import ivec3, bvec3
+
+from .vector_tools import rotateXZ, flipRotationXZ, flipToScale3D, rotateSizeXZ, Box
+
+
+# ==================================================================================================
+# Transform class
+# ==================================================================================================
+
+
+@dataclass
+class Transform:
+    """Represents a transformation of space.
+
+    When applied to a vector, [flip] is applied first, [rotation] second, and [translation] third.
+
+    Note that only the four 90-degree rotations in the XZ-plane are supported. Hence, [rotation]
+    should be 0, 1, 2 or 3.
+    """
+
+    translation: ivec3 = field(default_factory=ivec3)
+    rotation:    int   = 0
+    flip:        bvec3 = field(default_factory=bvec3)
+
+    def apply(self, vec: ivec3):
+        """Applies this transform to [vec].\n
+        Equivalent to [self] * [vec]. """
+        return rotateXZ(vec * flipToScale3D(self.flip), self.rotation) + self.translation
+
+    def invApply(self, vec: ivec3):
+        """Applies the inverse of this transform to [vec].\n
+        Faster version of ~[self] * [vec]."""
+        return rotateXZ(vec - self.translation, (-self.rotation + 4) % 4) * flipToScale3D(self.flip)
+
+    def compose(self, other: 'Transform'):
+        """Returns a transform that applies [self] after [other].\n
+        Equivalent to [self] @ [other]. """
+        return Transform(
+            translation = self.apply(other.translation),
+            rotation    = (self.rotation + flipRotationXZ(other.rotation, self.flip)) % 4,
+            flip        = self.flip ^ other.flip
+        )
+
+    def invCompose(self, other: 'Transform'):
+        """Returns a transform that applies [self]^-1 after [other].\n
+        Faster version of ~[self] @ [other]."""
+        return Transform(
+            translation = self.invApply(other.translation),
+            rotation    = flipRotationXZ((other.rotation - self.rotation + 4) % 4, self.flip),
+            flip        = self.flip ^ other.flip
+        )
+
+    def composeInv(self, other: 'Transform'):
+        """Returns a transform that applies [self] after [other]^-1.\n
+        Faster version of [self] @ ~[other]."""
+        flip = self.flip ^ other.flip
+        rotation = (self.rotation - flipRotationXZ(other.rotation, flip) + 4) % 4
+        return Transform(
+            translation = self.translation - rotateXZ(other.translation * flipToScale3D(flip), rotation),
+            rotation    = rotation,
+            flip        = flip
+        )
+
+    def push(self, other: 'Transform'):
+        """Adds the effect of [other] to this transform.\n
+        Equivalent to [self] @= [other]."""
+        self.translation += rotateXZ(other.translation * flipToScale3D(self.flip), self.rotation)
+        self.rotation     = (self.rotation + flipRotationXZ(other.rotation, self.flip)) % 4
+        self.flip         = self.flip ^ other.flip
+
+    def pop(self, other: 'Transform'):
+        """The inverse of push. Removes the effect of [other] from this transform.\n
+        Faster version of [self] @= ~[other]."""
+        self.flip         = self.flip ^ other.flip
+        self.rotation     = (self.rotation - flipRotationXZ(other.rotation, self.flip) + 4) % 4
+        self.translation -= rotateXZ(other.translation * flipToScale3D(self.flip), self.rotation)
+
+    def inverted(self):
+        """Equivalent to ~[self]."""
+        flip = self.flip # Flip stays unchanged
+        rotation = flipRotationXZ((-self.rotation + 4) % 4, flip)
+        return Transform(
+            translation = - rotateXZ(self.translation * flipToScale3D(self.flip), self.rotation),
+            rotation    = rotation,
+            flip        = flip
+        )
+
+    def invert(self):
+        """Faster version of [self] = ~[self]."""
+        # Flip stays unchanged
+        self.rotation    = flipRotationXZ((-self.rotation + 4) % 4, self.flip)
+        self.translation = - rotateXZ(self.translation * flipToScale3D(self.flip), self.rotation)
+
+    def __matmul__(self, other: 'Transform') -> 'Transform':
+        return self.compose(other)
+
+    def __mul__(self, vec: ivec3) -> ivec3:
+        return self.apply(vec)
+
+    def __imatmul__(self, other: 'Transform'):
+        self.push(other)
+        return self
+
+    def __invert__(self):
+        return self.inverted()
+
+
+# ==================================================================================================
+# TransformLike ABC
+# ==================================================================================================
+
+
+class TransformLike(ABC):
+    """An abstract base class. A class is a TransformLike if it is a Transform or an ivec3, the
+    latter being interpreted as a translation."""
+TransformLike.register(Transform)
+TransformLike.register(ivec3)
+
+
+def toTransform(transformLike: TransformLike) -> Transform:
+    """Converts <transformLike> to a Transform, interpreting a vector as a translation.
+
+    Functions that take a Transform parameter are very often called with just a translation.
+    By taking a transformLike pararameter instead and using this converter, calling such a
+    function with just a translation becomes slightly easier. This does however cost a bit
+    of performance (for an isinstance call).
+    """
+    return transformLike if isinstance(transformLike, Transform) else Transform(transformLike)
+
+
+# ==================================================================================================
+# Transform utilities
+# ==================================================================================================
+
+
+def rotatedBoxTransform(box: Box, rotation: int):
+    """Returns a transform that maps the box ((0,0,0), size) to [box] under [rotation], where
+    size == vector_util.rotateXZScale([box].size, [rotation])."""
+    return Transform(
+        translation = box.offset + ivec3(
+            box.size.x - 1 if rotation in [1, 2] else 0,
+            0,
+            box.size.z - 1 if rotation in [2, 3] else 0,
+        ),
+        rotation = rotation
+    )
+
+
+def rotatedBoxTransformAndSize(box: Box, rotation: int):
+    """Returns (transform, size) such that [transform] maps the box ((0,0,0), [size]) to [box],
+    under [rotation]."""
+    return rotatedBoxTransform(box, rotation), rotateSizeXZ(box.size, rotation)
+
+
+def flippedBoxTransform(box: Box, flip: bvec3):
+    """Returns a transform that maps the box ((0,0,0), [box].size) to [box] under [flip]."""
+    return Transform(
+        translation = box.offset + (box.size - 1) * ivec3(flip),
+        flip = flip
+    )
diff --git a/gdpc/utils.py b/gdpc/utils.py
new file mode 100644
index 0000000..154590b
--- /dev/null
+++ b/gdpc/utils.py
@@ -0,0 +1,148 @@
+"""Various utilities that are not specific to GDPC"""
+
+
+from typing import TypeVar, Generic, Callable, Iterable, OrderedDict
+import time
+import sys
+
+import numpy as np
+import cv2
+from matplotlib import pyplot as plt
+
+
+T = TypeVar("T")
+KT = TypeVar("KT")
+VT = TypeVar("VT")
+
+
+def sign(x) -> int:
+    """Returns the sign of [x]"""
+    return (x > 0) - (x < 0)
+
+
+def nonZeroSign(x) -> int:
+    """Returns the sign of [x], except that non_zero_sign(0) == 1"""
+    return 1 if x >= 0 else -1
+
+
+def clamp(x: T, minimum: T, maximum: T) -> T:
+    """Clamps [x] to the range [minimum, maximum]"""
+    return max(minimum, min(maximum, x))
+
+
+def eprint(*args, **kwargs):
+    """print(), but to stderr"""
+    print(*args, file=sys.stderr, **kwargs)
+
+
+def eagerAll(iterable: Iterable):
+    """Like all(), but always evaluates every element"""
+    results = [result for result in iterable]
+    return all(results)
+
+def eagerAny(iterable: Iterable):
+    """Like any(), but always evaluates every element"""
+    results = [result for result in iterable]
+    return any(results)
+
+
+# Based on https://stackoverflow.com/a/21032099
+def normalized(a, order=2, axis=-1):
+    """Normalizes [a] using the L[order] norm.\n
+    If [axis] is specified, normalizes along that axis."""
+    norm = np.atleast_1d(np.linalg.norm(a, order, axis))
+    norm[norm==0] = 1
+    return a / np.expand_dims(norm, axis)
+
+
+def withRetries(
+    function: Callable[[], T],
+    retries:  int                              = 1,
+    onRetry:  Callable[[Exception, int], None] = lambda *_: time.sleep(1)
+):
+    """Retries <function> up to <retries> times if an exception occurs.\n
+    Before retrying, calls <onRetry>(last exception, remaining retries).
+    The default callback sleeps for one second.\n
+    If the retries have ran out, re-raises the last exception."""
+    retriesLeft = retries
+    while True:
+        try:
+            return function()
+        except Exception as e: # pylint: disable=broad-except
+            if retriesLeft == 0:
+                raise e
+            onRetry(e, retriesLeft)
+            retriesLeft -= 1
+
+
+def isIterable(value):
+    """Determine whether <value> is iterable."""
+    try:
+        _ = iter(value)
+        return True
+    except TypeError:
+        return False
+
+
+def isSequence(value):
+    """Determine whether <value> is a sequence."""
+    try:
+        _ = value[0]
+        return True
+    except TypeError:
+        return False
+
+
+class OrderedByLookupDict(OrderedDict[KT, VT], Generic[KT, VT]):
+    """Dict ordered from least to most recently looked-up key\n
+
+    Unless maxSize is 0, the dict size is limited to maxSize by evicting the least recently
+    looked-up key when full.
+    """
+    # Based on
+    # https://docs.python.org/3/library/collections.html?highlight=ordereddict#collections.OrderedDict
+
+    def __init__(self, maxSize: int, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self._maxSize = maxSize
+
+    # inherited __repr__ from OrderedDict is sufficient
+
+    @property
+    def maxSize(self):
+        return self._maxSize
+
+    @maxSize.setter
+    def maxSize(self, value: int):
+        self._maxSize = value
+        if self._maxSize > 0:
+            while len(self) > self.maxSize:
+                oldest = next(iter(self))
+                del self[oldest]
+
+    def __getitem__(self, key: KT):
+        value = super().__getitem__(key)
+        self.move_to_end(key)
+        return value
+
+    def __setitem__(self, key: KT, value: VT):
+        if key in self:
+            self.move_to_end(key)
+        super().__setitem__(key, value)
+        if self._maxSize > 0 and len(self) > self._maxSize:
+            oldest = next(iter(self))
+            del self[oldest]
+
+
+def visualizeMaps(*arrays, title="", normalize=True):
+    """Visualizes one or multiple 2D numpy arrays."""
+    for array in arrays:
+        if normalize:
+            array = ((array - array.min()) / (array.max() - array.min()) * 255).astype(np.uint8)
+
+        plt.figure()
+        if title:
+            plt.title(title)
+        plt_image = cv2.cvtColor(array, cv2.COLOR_BGR2RGB)
+        plt.imshow(plt_image)
+    plt.show()
diff --git a/gdpc/vector_tools.py b/gdpc/vector_tools.py
new file mode 100644
index 0000000..b776fd2
--- /dev/null
+++ b/gdpc/vector_tools.py
@@ -0,0 +1,1032 @@
+"""Various vector utilities"""
+
+
+from typing import Any, Iterable, List, Optional, Set, Tuple, Union, overload
+from dataclasses import dataclass, field
+import math
+
+from more_itertools import powerset
+import numpy as np
+from scipy import ndimage
+import skimage.segmentation
+import glm
+from glm import ivec2, ivec3, vec2, vec3, bvec2, bvec3
+
+from .utils import nonZeroSign
+
+
+# ==================================================================================================
+# General
+# ==================================================================================================
+
+
+UP    = ivec3( 0, 1, 0)
+DOWN  = ivec3( 0,-1, 0)
+EAST  = ivec3( 1, 0, 0)
+WEST  = ivec3(-1, 0, 0)
+NORTH = ivec3( 0, 0,-1)
+SOUTH = ivec3( 0, 0, 1)
+X     = ivec3( 1, 0, 0)
+Y     = ivec3( 0, 1, 0)
+Z     = ivec3( 0, 0, 1)
+XY    = ivec3( 1, 1, 0)
+XZ    = ivec3( 1, 0, 1)
+YZ    = ivec3( 0, 1, 1)
+XYZ   = ivec3( 1, 1, 1)
+
+DIAGONALS_2D = (
+    ivec2( 1,  1),
+    ivec2( 1, -1),
+    ivec2(-1,  1),
+    ivec2(-1, -1),
+)
+
+DIAGONALS_3D = (
+    ivec3( 1,  1,  0),
+    ivec3( 1,  0,  1),
+    ivec3( 0,  1,  1),
+    ivec3( 1, -1,  0),
+    ivec3( 1,  0, -1),
+    ivec3( 0,  1, -1),
+    ivec3(-1,  1,  0),
+    ivec3(-1,  0,  1),
+    ivec3( 0, -1,  1),
+    ivec3(-1, -1,  0),
+    ivec3(-1,  0, -1),
+    ivec3( 0, -1, -1),
+    ivec3( 1,  1,  1),
+    ivec3( 1,  1, -1),
+    ivec3( 1, -1,  1),
+    ivec3(-1,  1,  1),
+    ivec3( 1, -1, -1),
+    ivec3(-1, -1,  1),
+    ivec3(-1,  1, -1),
+    ivec3(-1, -1, -1),
+)
+
+
+def dropDimension(vec: ivec3, dimension) -> ivec2:
+    """Returns <vec> without its <dimension>-th component"""
+    if dimension == 0: return vec.yz
+    if dimension == 1: return vec.xz
+    if dimension == 2: return vec.xy
+    raise ValueError(f'Invalid dimension "{dimension}"')
+
+
+def addDimension(vec: ivec2, dimension, value=0):
+    """Inserts <value> into <vec> at <dimension> and returns the resulting 3D vector"""
+    l = list(vec)
+    return ivec3(*l[:dimension], value, *l[dimension:])
+
+
+def dropY(vec: ivec3) -> ivec2:
+    """Returns [vec] without its y-component (i.e., projected on the XZ-plane)"""
+    return vec.xz
+
+
+def addY(vec: ivec2, y=0):
+    """Returns a 3D vector (vec[0], y, vec[1])"""
+    return ivec3(vec.x, y, vec.y)
+
+
+def setY(vec: ivec3, y=0):
+    """Returns [vec] with its y-component set to [y]"""
+    return ivec3(vec.x, y, vec.z)
+
+
+def trueMod2D(vec: ivec2, modulus: int):
+    """Returns <v> modulo <modulus>.\n
+    Negative numbers are handled just like Python's built-in integer modulo."""
+    return ivec2(vec.x % modulus, vec.y % modulus)
+
+def trueMod3D(vec: ivec3, modulus: int):
+    """Returns <v> modulo <modulus>.\n
+    Negative numbers are handled just like Python's built-in integer modulo."""
+    return ivec3(vec.x % modulus, vec.y % modulus, vec.z % modulus)
+
+
+@overload
+def perpendicular(vec: vec2) -> vec2: ...
+@overload
+def perpendicular(vec: ivec2) -> ivec2: ...
+
+def perpendicular(vec):
+    """Returns the vector perpendicular to [vec] that points to the right of [vec] and has the same
+    length as [vec]."""
+    if isinstance(vec[0], float): return  vec2(vec.y, -vec.x)
+    if isinstance(vec[0], int  ): return ivec2(vec.y, -vec.x)
+    raise ValueError()
+
+
+def rotate(vec: ivec2, rotation: int):
+    """Returns [vec], rotated by [rotation]"""
+    if rotation == 0: return vec
+    if rotation == 1: return ivec2(-vec.y,  vec.x)
+    if rotation == 2: return ivec2(-vec.x, -vec.y)
+    if rotation == 3: return ivec2( vec.y, -vec.x)
+    raise ValueError("Rotation must be in {0,1,2,3}")
+
+
+def rotateXZ(vec: ivec3, rotation: int):
+    """Returns [vec], rotated in the XZ-plane by [rotation]"""
+    return addY(rotate(dropY(vec), rotation), vec.y)
+
+
+def flipRotation(rotation: int, flip: bvec2):
+    """Returns rotation such that applying rotation after <flip> is equivalent to applying <flip>
+    after <rotation>."""
+    scale = flipToScale2D(flip)
+    return (rotation * scale.x * scale.y + 4) % 4
+
+def flipRotationXZ(rotation: int, flip: bvec3):
+    """Returns rotation such that applying rotation after <flip> is equivalent to applying <flip>
+    after <rotation>"""
+    return flipRotation(rotation, dropY(flip))
+
+
+def rotateSize(size: ivec2, rotation: int):
+    """Returns the effective size of a rect of size [size] that has been rotated by [rotation]."""
+    return ivec2(size.y, size.x) if rotation in [1, 3] else size
+
+
+def rotateSizeXZ(size: ivec3, rotation: int):
+    """Returns the effective size of a box of size [size] that has been rotated by [rotation]."""
+    return addY(rotateSize(dropY(size), rotation), size.y)
+
+
+def flipToScale2D(flip: bvec2):
+    """Returns a vector with a 1 where <flip> is false, and -1 where <flip> is true"""
+    return 1 - 2*ivec2(flip)
+
+def flipToScale3D(flip: bvec3):
+    """Returns a vector with a 1 where <flip> is false, and -1 where <flip> is true"""
+    return 1 - 2*ivec3(flip)
+
+
+def scaleToFlip2D(scale: ivec2):
+    """Returns whether [scale] flips space in each axis"""
+    return bvec2(scale.x < 0, scale.y < 0)
+
+def scaleToFlip3D(scale: ivec3):
+    """Returns whether [scale] flips space in each axis"""
+    return bvec3(scale.x < 0, scale.y < 0, scale.z < 0)
+
+
+def toAxisVector(vec: ivec2):
+    """Returns the axis-aligned unit vector closest to [vec]"""
+    if abs(vec.x) > abs(vec.y): # pylint: disable=no-else-return
+        return ivec2(nonZeroSign(vec.x), 0)
+    else:
+        return ivec2(0, nonZeroSign(vec.y))
+
+
+def directionToRotation(direction: ivec2):
+    """Returns the rotation that rotates (0,-1) closest to [direction]"""
+    vec = toAxisVector(direction)
+    if vec.y < 0: return 0
+    if vec.x > 0: return 1
+    if vec.y > 0: return 2
+    if vec.x < 0: return 3
+    raise ValueError()
+
+
+# For some reason, glm's length, length2, distance, distance2 and l1Norm refuse to work with integer
+# vectors. We provide some wrappers.
+
+def length(vec: Union[ivec2, ivec3]):
+    """Returns the length of [vec]"""
+    if isinstance(vec, ivec2): return glm.length(vec2(vec))
+    if isinstance(vec, ivec3): return glm.length(vec3(vec))
+    raise ValueError()
+
+def length2(vec: Union[ivec2, ivec3]):
+    """Returns the squared length of [vec]"""
+    if isinstance(vec, ivec2): return int(glm.length2(vec2(vec)))
+    if isinstance(vec, ivec3): return int(glm.length2(vec3(vec)))
+    raise ValueError()
+
+def distance(vecA: Union[ivec2, ivec3], vecB: Union[ivec2, ivec3]) -> float:
+    """Returns the distance between [vecA] and [vecB]"""
+    if isinstance(vecA, ivec2) and isinstance(vecB, ivec2): return glm.distance(vec2(vecA), vec2(vecB))
+    if isinstance(vecA, ivec3) and isinstance(vecB, ivec2): return glm.distance(vec3(vecA), vec3(vecB))
+    raise ValueError()
+
+def distance2(vecA: Union[ivec2, ivec3], vecB: Union[ivec2, ivec3]):
+    """Returns the squared distance between [vecA] and [vecB]"""
+    if isinstance(vecA, ivec2) and isinstance(vecB, ivec2): return int(glm.distance2(vec2(vecA), vec2(vecB)))
+    if isinstance(vecA, ivec3) and isinstance(vecB, ivec2): return int(glm.distance2(vec3(vecA), vec3(vecB)))
+    raise ValueError()
+
+def l1Norm(vec: Union[ivec2, ivec3]):
+    """Returns the L1 norm of [vec]"""
+    if isinstance(vec, ivec2): return abs(vec.x) + abs(vec.y)
+    if isinstance(vec, ivec3): return abs(vec.x) + abs(vec.y) + abs(vec.z)
+    raise ValueError()
+
+def l1Distance(vecA: Union[ivec2, ivec3], vecB: Union[ivec2, ivec3]):
+    """Returns the L1 norm distance between [vecA] and [vecB]"""
+    return l1Norm(vecA - vecB)
+
+
+def orderedCorners2D(corner1: ivec2, corner2: ivec2):
+    """Returns two corners of the rectangle defined by <corner1> and <corner2>, such that the first
+    corner is smaller than the second corner in each axis"""
+    return (
+        ivec2(
+            corner1.x if corner1.x <= corner2.x else corner2.x,
+            corner1.y if corner1.y <= corner2.y else corner2.y,
+        ),
+        ivec2(
+            corner1.x if corner1.x > corner2.x else corner2.x,
+            corner1.y if corner1.y > corner2.y else corner2.y,
+        )
+    )
+
+
+def orderedCorners3D(corner1: ivec3, corner2: ivec3):
+    """Returns two corners of the box defined by <corner1> and <corner2>, such that the first
+    corner is smaller than the second corner in each axis"""
+    return (
+        ivec3(
+            corner1.x if corner1.x <= corner2.x else corner2.x,
+            corner1.y if corner1.y <= corner2.y else corner2.y,
+            corner1.z if corner1.z <= corner2.z else corner2.z,
+        ),
+        ivec3(
+            corner1.x if corner1.x > corner2.x else corner2.x,
+            corner1.y if corner1.y > corner2.y else corner2.y,
+            corner1.z if corner1.z > corner2.z else corner2.z,
+        )
+    )
+
+
+def getDimensionality(corner1: Union[ivec2, ivec3], corner2: Union[ivec2, ivec3]) -> Tuple[int, List[str]]:
+    """Determines the number of dimensions for which <corner1> and <corner2> are in general
+    position, i.e. the number of dimensions for which the volume they define is not flat.\n
+    Returns (dimensionality, list of indices of dimensions for which the volume is flat).
+    For example: (2, [0,2]) means that the volume is flat in the x and z axes."""
+    difference = np.array(corner1) - np.array(corner2)
+    flatSides = np.argwhere(difference == 0).flatten()
+    return len(corner1) - np.sum(flatSides), list(flatSides)
+
+
+# ==================================================================================================
+# Rect and Box
+# ==================================================================================================
+
+
+# TODO: If someone knows how to fix the duplication in Rect and Box, please do tell.
+
+
+@dataclass
+class Rect:
+    """A rectangle, defined by an offset and a size"""
+
+    offset: ivec2 = field(default_factory=ivec2)
+    size:   ivec2 = field(default_factory=ivec2)
+
+    @property
+    def begin(self):
+        """Equivalent to self.offset. Setting will modify self.offset."""
+        return self.offset
+
+    @begin.setter
+    def begin(self, value: ivec2):
+        self.offset = value
+
+    @property
+    def end(self):
+        """Equivalent to self.offset + self.size. Setting will modify self.size."""
+        return self.begin + self.size
+
+    @end.setter
+    def end(self, value: ivec2):
+        self.size = value - self.begin
+
+    @property
+    def last(self):
+        """Equivalent to self.offset + self.size - 1. Setting will modify self.size."""
+        return self.offset + self.size - 1
+
+    @last.setter
+    def last(self, value: ivec2):
+        self.size = value - self.offset + 1
+
+    @property
+    def middle(self):
+        """This Rect's middle point, rounded down"""
+        return (self.begin + self.size) // 2
+
+    @property
+    def inner(self):
+        """Yields all points contained in this Rect"""
+        return (
+            ivec2(x, y)
+            for x in range(self.begin.x, self.end.x)
+            for y in range(self.begin.y, self.end.y)
+        )
+
+    @property
+    def area(self):
+        """This Rect's surface area"""
+        return self.size.x*self.size.y
+
+    @property
+    def corners(self):
+        """Yields this Rect's corner points"""
+        return (
+            self.offset + sum(subset)
+            for subset in powerset([ivec2(self.size.x, 0), ivec2(0, self.size.y)])
+        )
+
+    def contains(self, vec: ivec2):
+        """Returns whether this Rect contains [vec]"""
+        return (
+            self.begin.x <= vec.x < self.end.x and
+            self.begin.y <= vec.y < self.end.y
+        )
+
+    def collides(self, other: 'Rect'):
+        """Returns whether this Rect and [other] have any overlap"""
+        return (
+            self.begin.x <= other.end  .x and
+            self.end  .x >= other.begin.x and
+            self.begin.y <= other.end  .y and
+            self.end  .y >= other.begin.y
+        )
+
+    def squaredDistanceToVec(self, vec: ivec2):
+        """Returns the squared distance between this Rect and [vec]"""
+        dx = max(self.begin.x - vec.x, 0, vec.x - (self.end.x - 1))
+        dy = max(self.begin.y - vec.y, 0, vec.y - (self.end.y - 1))
+        return dx*dx + dy*dy
+
+    def distanceToVec(self, vec: ivec2):
+        """Returns the distance between this Rect and [vec]"""
+        return math.sqrt(self.squaredDistanceToVec(vec))
+
+    def translated(self, translation: Union[ivec2, int]):
+        """Returns a copy of this Rect, translated by [translation]"""
+        return Rect(self.offset + translation, self.size)
+
+    def dilate(self, dilation: int = 1):
+        """Morphologically dilates this rect by [dilation]"""
+        self.offset -= dilation
+        self.size   += dilation*2
+
+    def dilated(self, dilation: int = 1):
+        """Returns a copy of this Rect, morphologically dilated by [dilation]"""
+        return Rect(self.offset - dilation, self.size + dilation*2)
+
+    def erode(self, erosion: int = 1):
+        """Morphologically erodes this rect by [erosion]"""
+        self.dilate(-erosion)
+
+    def eroded(self, erosion: int = 1):
+        """Returns a copy of this Rect, morphologically eroded by [erosion]"""
+        return self.dilated(-erosion)
+
+    def centeredSubRectOffset(self, size: ivec2):
+        """Returns an offset such that Rect(offset, [size]).middle == self.middle"""
+        difference = self.size - size
+        return self.offset + difference/2
+
+    def centeredSubRect(self, size: ivec2):
+        """Returns a rect of size [size] with the same middle as this rect"""
+        return Rect(self.centeredSubRectOffset(size), size)
+
+    @staticmethod
+    def between(cornerA: ivec2, cornerB: ivec2):
+        """Returns the Rect between [cornerA] and [cornerB] (inclusive),
+        which may be any opposing corners."""
+        first, last = orderedCorners2D(cornerA, cornerB)
+        return Rect(first, (last - first) + 1)
+
+    @staticmethod
+    def bounding(points: Iterable[ivec2]):
+        """Returns the smallest Rect containing all [points]"""
+        pointArray = np.array(points)
+        minPoint = np.min(pointArray, axis=0)
+        maxPoint = np.max(pointArray, axis=0)
+        return Rect(minPoint, maxPoint - minPoint + 1)
+
+    def toBox(self, offsetY = 0, sizeY = 0):
+        """Returns a corresponding Box"""
+        return Box(addY(self.offset, offsetY), addY(self.size, sizeY))
+
+    @property
+    def outline(self):
+        """Yields this Rect's outline points"""
+        # It's surprisingly difficult to get this right without duplicates. (Think of the corners!)
+        first = self.begin
+        last  = self.end - 1
+        yield from loop2D(ivec2(first.x, first.y), ivec2(last.x  -1, first.y   ) + 1)
+        yield from loop2D(ivec2(last.x,  first.y), ivec2(last.x,     last.y  -1) + 1)
+        yield from loop2D(ivec2(last.x,  last.y),  ivec2(first.x +1, last.y    ) - 1)
+        yield from loop2D(ivec2(first.x, last.y),  ivec2(first.x,    first.y +1) - 1)
+
+
+@dataclass()
+class Box:
+    """A box, defined by an offset and a size"""
+
+    offset: ivec3 = field(default_factory=ivec3)
+    size:   ivec3 = field(default_factory=ivec3)
+
+    @property
+    def begin(self):
+        """Equivalent to self.offset. Setting will modify self.offset."""
+        return self.offset
+
+    @begin.setter
+    def begin(self, value: ivec3):
+        self.offset = value
+
+    @property
+    def end(self):
+        """Equivalent to self.offset + self.size. Setting will modify self.size."""
+        return self.begin + self.size
+
+    @end.setter
+    def end(self, value: ivec3):
+        self.size = value - self.begin
+
+    @property
+    def last(self):
+        """Equivalent to self.offset + self.size - 1. Setting will modify self.size."""
+        return self.offset + self.size - 1
+
+    @last.setter
+    def last(self, value: ivec3):
+        self.size = value - self.offset + 1
+
+    @property
+    def middle(self):
+        """This Box's middle point, rounded down"""
+        return (self.begin + self.size) // 2
+
+    @property
+    def inner(self):
+        """Yields all points contained in this Box"""
+        return (
+            ivec3(x, y, z)
+            for x in range(self.begin.x, self.end.x)
+            for y in range(self.begin.y, self.end.y)
+            for z in range(self.begin.z, self.end.z)
+        )
+
+    @property
+    def volume(self):
+        """This Box's volume"""
+        return self.size.x*self.size.y*self.size.z
+
+    @property
+    def corners(self):
+        """Yields this Box's corner points"""
+        return [
+            self.offset + sum(subset)
+            for subset in powerset([ivec3(self.size.x, 0, 0), ivec3(0, self.size.y, 0), ivec3(0, 0, self.size.z)])
+        ]
+
+    def contains(self, vec: ivec3):
+        """Returns whether this Box contains [vec]"""
+        return (
+            self.begin.x <= vec.x < self.end.x and
+            self.begin.y <= vec.y < self.end.y and
+            self.begin.z <= vec.z < self.end.z
+        )
+
+    def collides(self, other: 'Box'):
+        """Returns whether this Box and [other] have any overlap"""
+        return (
+            self.begin.x <= other.end  .x and
+            self.end  .x >= other.begin.x and
+            self.begin.y <= other.end  .y and
+            self.end  .y >= other.begin.y and
+            self.begin.z <= other.end  .z and
+            self.end  .z >= other.begin.z
+        )
+
+    def squaredDistanceToVec(self, vec: ivec3):
+        """Returns the squared distance between this Box and [vec]"""
+        dx = max(self.begin.x - vec.x, 0, vec.x - (self.end.x - 1))
+        dy = max(self.begin.y - vec.y, 0, vec.y - (self.end.y - 1))
+        dz = max(self.begin.z - vec.z, 0, vec.z - (self.end.z - 1))
+        return dx*dx + dy*dy + dz*dz
+
+    def distanceToVec(self, vec: ivec3):
+        """Returns the distance between this Box and [vec]"""
+        return math.sqrt(self.squaredDistanceToVec(vec))
+
+    def translated(self, translation: Union[ivec3, int]):
+        """Returns a copy of this Box, translated by [translation]"""
+        return Box(self.offset + translation, self.size)
+
+    def dilate(self, dilation: int = 1):
+        """Morphologically dilates this box by [dilation]"""
+        self.offset -= dilation
+        self.size   += dilation*2
+
+    def dilated(self, dilation: int = 1):
+        """Returns a copy of this Box, morphologically dilated by [dilation]"""
+        return Rect(self.offset - dilation, self.size + dilation*2)
+
+    def erode(self, erosion: int = 1):
+        """Morphologically erodes this box by [erosion]"""
+        self.dilate(-erosion)
+
+    def eroded(self, erosion: int = 1):
+        """Returns a copy of this Box, morphologically eroded by [erosion]"""
+        return self.dilated(-erosion)
+
+    def centeredSubBoxOffset(self, size: ivec3):
+        """Returns an offset such that Box(offset, [size]).middle == self.middle"""
+        difference = self.size - size
+        return self.offset + difference/2
+
+    def centeredSubBox(self, size: ivec3):
+        """Returns an box of size [size] with the same middle as this box"""
+        return Box(self.centeredSubBoxOffset(size), size)
+
+    @staticmethod
+    def between(cornerA: ivec3, cornerB: ivec3):
+        """Returns the Box between [cornerA] and [cornerB] (both inclusive),
+        which may be any opposing corners"""
+        first, last = orderedCorners3D(cornerA, cornerB)
+        return Box(first, last - first + 1)
+
+    @staticmethod
+    def bounding(points: Iterable[ivec3]):
+        """Returns the smallest Box containing all [points]"""
+        pointArray = np.array(points)
+        minPoint = np.min(pointArray, axis=0)
+        maxPoint = np.max(pointArray, axis=0)
+        return Rect(minPoint, maxPoint - minPoint + 1)
+
+    def toRect(self):
+        """Returns this Box's XZ-plane as a Rect"""
+        return Rect(dropY(self.offset), dropY(self.size))
+
+    @property
+    def shell(self):
+        """Yields all points on this Box's surface"""
+        # It's surprisingly difficult to get this right without duplicates. (Think of the corners!)
+        first = self.begin
+        last  = self.end - 1
+        # Bottom face
+        yield from loop3D(ivec3(first.x, first.y, first.z), ivec3(last.x, first.y, last.z) + 1)
+        # Top face
+        yield from loop3D(ivec3(first.x, last.y, first.z), ivec3(last.x, last.y, last.z) + 1)
+        # Sides
+        yield from loop3D(ivec3(first.x, first.y+1, first.z), ivec3(last.x -1,  last.y-1, first.z   ) + 1)
+        yield from loop3D(ivec3(last.x,  first.y+1, first.z), ivec3(last.x,     last.y-1, last.z  -1) + 1)
+        yield from loop3D(ivec3(last.x,  first.y+1, last.z ), ivec3(first.x +1, last.y+1, last.z    ) - 1)
+        yield from loop3D(ivec3(first.x, first.y+1, last.z ), ivec3(first.x,    last.y+1, first.z +1) - 1)
+
+    @property
+    def wireframe(self):
+        """Yields all points on this Box's edges"""
+        # It's surprisingly difficult to get this right without duplicates. (Think of the corners!)
+        first = self.begin
+        last  = self.end - 1
+        # Bottom face
+        yield from loop3D(ivec3(first.x, first.y, first.z), ivec3(last.x -1,  first.y, first.z   ) + 1)
+        yield from loop3D(ivec3(last.x,  first.y, first.z), ivec3(last.x,     first.y, last.z  -1) + 1)
+        yield from loop3D(ivec3(last.x,  first.y, last.z ), ivec3(first.x +1, first.y, last.z    ) - 1)
+        yield from loop3D(ivec3(first.x, first.y, last.z ), ivec3(first.x,    first.y, first.z +1) - 1)
+        # top face
+        yield from loop3D(ivec3(first.x, last.y,  first.z), ivec3(last.x -1,  last.y,  first.z   ) + 1)
+        yield from loop3D(ivec3(last.x,  last.y,  first.z), ivec3(last.x,     last.y,  last.z  -1) + 1)
+        yield from loop3D(ivec3(last.x,  last.y,  last.z ), ivec3(first.x +1, last.y,  last.z    ) - 1)
+        yield from loop3D(ivec3(first.x, last.y,  last.z ), ivec3(first.x,    last.y,  first.z +1) - 1)
+        # sides
+        yield from loop3D(ivec3(first.x, first.y+1, first.z), ivec3(first.x, last.y-1, first.z) + 1)
+        yield from loop3D(ivec3(last.x,  first.y+1, first.z), ivec3(last.x,  last.y-1, first.z) + 1)
+        yield from loop3D(ivec3(last.x,  first.y+1, last.z ), ivec3(last.x,  last.y-1, last.z ) + 1)
+        yield from loop3D(ivec3(first.x, first.y+1, last.z ), ivec3(first.x, last.y-1, last.z ) + 1)
+
+
+def rectSlice(array: np.ndarray, rect: Rect):
+    """Returns the slice from [array] defined by [rect]"""
+    return array[rect.begin.x:rect.end.x, rect.begin.y:rect.end.y]
+
+
+def setRectSlice(array: np.ndarray, rect: Rect, value: Any):
+    """Sets the slice from [array] defined by [rect] to [value]"""
+    array[rect.begin.x:rect.end.x, rect.begin.y:rect.end.y] = value
+
+
+def boxSlice(array: np.ndarray, box: Box):
+    """Returns the slice from [array] defined by [box]"""
+    return array[box.begin.x:box.end.x, box.begin.y:box.end.y, box.begin.z:box.end.z]
+
+
+def setBoxSlice(array: np.ndarray, box: Box, value: Any):
+    """Sets the slice from [array] defined by [box] to [value]"""
+    array[box.begin.x:box.end.x, box.begin.y:box.end.y, box.begin.z:box.end.z] = value
+
+
+# ==================================================================================================
+# Point generation
+# ==================================================================================================
+
+
+def loop2D(begin: ivec2, end: Optional[ivec2] = None):
+    """Yields all points between <begin> and <end> (end-exclusive).\n
+    If <end> is not given, yields all points between (0,0) and <begin>."""
+    if end is None:
+        begin, end = ivec2(0, 0), begin
+
+    for x in range(begin.x, end.x, nonZeroSign(end.x - begin.x)):
+        for y in range(begin.y, end.y, nonZeroSign(end.y - begin.y)):
+            yield ivec2(x, y)
+
+
+def loop3D(begin: ivec3, end: Optional[ivec3] = None):
+    """Yields all points between <begin> and <end> (end-exclusive).\n
+    If <end> is not given, yields all points between (0,0,0) and <begin>."""
+    if end is None:
+        begin, end = ivec3(0, 0, 0), begin
+
+    for x in range(begin.x, end.x, nonZeroSign(end.x - begin.x)):
+        for y in range(begin.y, end.y, nonZeroSign(end.y - begin.y)):
+            for z in range(begin.z, end.z, nonZeroSign(end.z - begin.z)):
+                yield ivec3(x, y, z)
+
+
+def cuboid2D(corner1: ivec2, corner2: ivec2):
+    """Yields all points in the rectangle between <corner1> and <corner2> (inclusive)."""
+    return Rect.between(corner1, corner2).inner
+
+
+def cuboid3D(corner1: ivec3, corner2: ivec3):
+    """Yields all points in the box between <corner1> and <corner2> (inclusive)."""
+    return Box.between(corner1, corner2).inner
+
+
+def filled2DArray(points: Iterable[ivec2], seedPoint: ivec2, boundingRect: Optional[Rect] = None, includeInputPoints=True) -> np.ndarray:
+    """Fills the shape defined by <points>, starting at <seedPoint> and returns a (n,2) numpy array
+    containing the resulting points.\n
+    <boundingRect> should contain all <points>. If not provided, it is calculated."""
+    if boundingRect is None:
+        boundingRect = Rect.bounding(points)
+
+    pointMap = np.zeros(boundingRect.size, dtype=int)
+    pointMap[tuple(np.transpose(np.array(points) - np.array(boundingRect.offset)))] = 1
+    filled = skimage.segmentation.flood_fill(pointMap, tuple(seedPoint - boundingRect.offset), 1, footprint=np.array([[0,1,0],[1,1,1],[0,1,0]]))
+    if not includeInputPoints:
+        filled -= pointMap
+    return np.argwhere(filled) + np.array(boundingRect.offset)
+
+
+def filled2D(points: Iterable[ivec2], seedPoint: ivec2, boundingRect: Optional[Rect] = None, includeInputPoints=True):
+    """Fills the shape defined by <points>, starting at <seedPoint> and yields the resulting points.\n
+    <boundingRect> should contain all <points>. If not provided, it is calculated."""
+    return (ivec2(*point) for point in filled2DArray(points, seedPoint, boundingRect, includeInputPoints))
+
+
+def filled3DArray(points: Iterable[ivec3], seedPoint: ivec3, boundingBox: Optional[Box] = None, includeInputPoints=True) -> np.ndarray:
+    """Fills the shape defined by <points>, starting at <seedPoint> and returns a (n,3) numpy array
+    containing the resulting points.\n
+    <boundingBox> should contain all <points>. If not provided, it is calculated."""
+    if boundingBox is None:
+        boundingBox = Rect.bounding(points)
+
+    pointMap = np.zeros(boundingBox.size, dtype=int)
+    pointMap[tuple(np.transpose(np.array(points) - np.array(boundingBox.offset)))] = 1
+    filled = skimage.segmentation.flood_fill(pointMap, tuple(seedPoint - boundingBox.offset), 1, connectivity=1)
+    if not includeInputPoints:
+        filled -= pointMap
+    return np.argwhere(filled) + np.array(boundingBox.offset)
+
+
+def filled3D(points: Iterable[ivec3], seedPoint: ivec3, boundingBox: Optional[Box] = None, includeInputPoints=True):
+    """Fills the shape defined by <points>, starting at <seedPoint> and yields the resulting points.\n
+    <boundingBox> should contain all <points>. If not provided, it is calculated."""
+    return (ivec3(*point) for point in filled3DArray(points, seedPoint, boundingBox, includeInputPoints))
+
+
+# TODO: separate out thickening code?
+def _lineArray(begin: Union[ivec2, ivec3], end: Union[ivec2, ivec3], width: int = 1) -> np.ndarray:
+    delta = np.array(end - begin)
+    maxDelta = int(max(abs(delta)))
+    if maxDelta == 0:
+        return np.array([])
+    points = delta[np.newaxis,:] * np.arange(maxDelta + 1)[:,np.newaxis] / maxDelta + np.array(begin)
+    points = np.rint(points).astype(np.signedinteger)
+
+    if width > 1:
+        minPoint = np.array(glm.min(begin, end))
+
+        # convert point array to a map
+        array_width = maxDelta + width*2
+        array = np.zeros([array_width]*len(begin), dtype=int)
+        array[tuple(np.transpose(points - minPoint + width))] = 1
+
+        # dilate map (make it thick)
+        if width > 1:
+            array = ndimage.binary_dilation(array, iterations = width - 1)
+
+        # rebuild point array from map
+        points = np.argwhere(array) + minPoint - width
+
+    return points
+
+
+def line2DArray(begin: ivec2, end: ivec2, width: int = 1):
+    """Returns (n,2) numpy array of points on the line between [begin] and [end] (inclusive)"""
+    return _lineArray(begin, end, width)
+
+
+def line2D(begin: ivec2, end: ivec2, width: int = 1):
+    """Yields the points on the line between [begin] and [end] (inclusive)"""
+    return (ivec2(*point) for point in _lineArray(begin, end, width))
+
+
+def line3Darray(begin: ivec3, end: ivec3, width: int = 1):
+    """Returns (n,3) numpy array of points on the line between [begin] and [end] (inclusive)"""
+    return _lineArray(begin, end, width)
+
+
+def line3D(begin: ivec3, end: ivec3, width: int = 1):
+    """Yields the points on the line between [begin] and [end] (inclusive)"""
+    return (ivec3(*point) for point in _lineArray(begin, end, width))
+
+
+def lineSequence2D(points: Iterable[ivec2], closed=False):
+    """Yields all points on the lines that connect <points>"""
+    for i in range((-1 if closed else 0), len(points)-1):
+        yield from line2D(points[i], points[i+1])
+
+
+def lineSequence3D(points: Iterable[ivec3], closed=False):
+    """Yields all points on the lines that connect <points>"""
+    for i in range((-1 if closed else 0), len(points)-1):
+        yield from line3D(points[i], points[i+1])
+
+
+def circle(center: ivec2, diameter: int, filled=False):
+    """Yields the points of the specified circle.\n
+    If <diameter> is even, <center> will be the bottom left center point."""
+
+    # With 'inspiration' from:
+    # https://www.geeksforgeeks.org/bresenhams-circle-drawing-algorithm/
+
+    if diameter == 0:
+        empty: List[ivec2] = []
+        return (point for point in empty)
+
+    e = 1 - (diameter % 2) # for even centers
+    points: Set[ivec2] = set()
+
+    def eightPoints(x: int, y: int):
+        points.add(center + ivec2(e + x, e + y))
+        points.add(center + ivec2(0 - x, e + y))
+        points.add(center + ivec2(e + x, 0 - y))
+        points.add(center + ivec2(0 - x, 0 - y))
+        points.add(center + ivec2(e + y, e + x))
+        points.add(center + ivec2(0 - y, e + x))
+        points.add(center + ivec2(e + y, 0 - x))
+        points.add(center + ivec2(0 - y, 0 - x))
+
+    radius = (diameter-1) // 2
+    x, y = 0, radius
+    d = 3 - 2 * radius
+    eightPoints(x, y)
+    while y >= x:
+        # for each pixel we will
+        # draw all eight pixels
+
+        x += 1
+
+        # check for decision parameter
+        # and correspondingly
+        # update d, x, y
+        if d > 0:
+            y -= 1
+            d = d + 4 * (x - y) + 10
+        else:
+            d = d + 4 * x + 6
+        eightPoints(x, y)
+
+    if filled:
+        return filled2D(points, center, Rect(center - radius, ivec2(diameter, diameter)))
+    return (point for point in points)
+
+
+def fittingCircle(corner1: ivec2, corner2: ivec2, filled=False):
+    """Yields the points of the largest circle that fits between <corner1> and <corner2>.\n
+    The circle is centered in the larger axis."""
+    corner1, corner2 = orderedCorners2D(corner1, corner2)
+    diameter = min(corner2.x - corner1.x, corner2.y - corner1.y) + 1
+    return circle((corner1 + corner2) // 2, diameter, filled)
+
+
+def ellipse(center: ivec2, diameters: ivec2, filled=False):
+    """Yields the points of the specified ellipse.\n
+    If <diameter>[axis] is even, <center>[axis] will be the lower center point in that axis."""
+
+    # Modified version 'inspired' by chandan_jnu from
+    # https://www.geeksforgeeks.org/midpoint-ellipse-drawing-algorithm/
+
+    if diameters.x == 0 or diameters.y == 0:
+        empty: List[ivec2] = []
+        return (point for point in empty)
+
+    if diameters.x == diameters.y:
+        return circle(center, diameters.x, filled)
+
+    e = 1 - (diameters % 2)
+
+    points: Set[ivec2] = set()
+
+    def fourpoints(x, y):
+        points.add(center + ivec2(e.x + x, e.y + y))
+        points.add(center + ivec2(0   - x, e.y + y))
+        points.add(center + ivec2(e.x + x, 0   - y))
+        points.add(center + ivec2(0   - x, 0   - y))
+
+        if filled:
+            points.update(line2D(center + ivec2(0 - x, e.y + y), center + ivec2(e.x + x, e.y + y)))
+            points.update(line2D(center + ivec2(0 - x, 0   - y), center + ivec2(e.x + x, 0   - y)))
+
+    rx, ry = (diameters-1) // 2
+
+    x, y = 0, ry
+
+    # Initial decision parameter of region 1
+    d1 = ((ry * ry) - (rx * rx * ry) + (0.25 * rx * rx))
+    dx = 2 * ry * ry * x
+    dy = 2 * rx * rx * y
+
+    # For region 1
+    while dx < dy:
+        fourpoints(x, y)
+
+        # Checking and updating value of
+        # decision parameter based on algorithm
+        if d1 < 0:
+            x += 1
+            dx = dx + (2 * ry * ry)
+            d1 = d1 + dx + (ry * ry)
+        else:
+            x += 1
+            y -= 1
+            dx = dx + (2 * ry * ry)
+            dy = dy - (2 * rx * rx)
+            d1 = d1 + dx - dy + (ry * ry)
+
+    # Decision parameter of region 2
+    d2 = (((ry * ry) * ((x + 0.5) * (x + 0.5)))
+          + ((rx * rx) * ((y - 1) * (y - 1))) - (rx * rx * ry * ry))
+
+    # Plotting points of region 2
+    while y >= 0:
+        fourpoints(x, y)
+
+        # Checking and updating parameter
+        # value based on algorithm
+        if d2 > 0:
+            y -= 1
+            dy = dy - (2 * rx * rx)
+            d2 = d2 + (rx * rx) - dy
+        else:
+            y -= 1
+            x += 1
+            dx = dx + (2 * ry * ry)
+            dy = dy - (2 * rx * rx)
+            d2 = d2 + dx - dy + (rx * rx)
+
+    return (point for point in points)
+
+
+def fittingEllipse(corner1: ivec2, corner2: ivec2, filled=False):
+    """Yields the points of the largest ellipse that fits between <corner1> and <corner2>."""
+    corner1, corner2 = orderedCorners2D(corner1, corner2)
+    diameters = (corner2 - corner1) + 1
+    return ellipse((corner1 + corner2) // 2, diameters, filled)
+
+
+def cylinder(baseCenter: ivec3, diameters: Union[ivec2, int], length: int, axis=1, tube=False, hollow=False):
+    """Yields the points from the specified cylinder.\n
+    If <diameter> is even, <center> will be the bottom left center point.\n
+    <tube> has precedence over <hollow>."""
+
+    if isinstance(diameters, int):
+        diameters = ivec2(diameters, diameters)
+
+    if diameters.x == 0 or diameters.y == 0 or length == 0:
+        empty: List[ivec3] = []
+        return (point for point in empty)
+
+    corner1 = baseCenter - addDimension((diameters-1)/2, axis, 0)
+    corner2 = corner1 + addDimension(diameters-1, axis, length-1)
+    return fittingCylinder(corner1, corner2, axis, tube, hollow)
+
+
+def fittingCylinder(corner1: ivec3, corner2: ivec3, axis=1, tube=False, hollow=False):
+    """Yields the points of the largest cylinder that fits between <corner1> and <corner2>.\n
+    <tube> has precedence over <hollow>."""
+
+    corner1, corner2 = orderedCorners3D(corner1, corner2)
+    dimensionality, flatSides = getDimensionality(corner1, corner2)
+
+    if dimensionality == 0:
+        yield corner1
+        return
+
+    if (dimensionality == 1 or (dimensionality == 2 and flatSides[0] != axis)):
+        yield from cuboid3D(corner1, corner2)
+        return
+
+    baseCorner1 = dropDimension(corner1, axis)
+    baseCorner2 = dropDimension(corner2, axis)
+    h0 = corner1[axis]
+    hn = corner2[axis]
+
+    ellipsePoints2D = list(fittingEllipse(baseCorner1, baseCorner2, filled=False))
+    ellipsePoints3D = [addDimension(point, axis, h0) for point in ellipsePoints2D]
+
+    if tube:
+        basePoints = ellipsePoints3D
+        bodyPoints = ellipsePoints3D
+    else:
+        basePoints = [addDimension(point, axis, h0) for point in filled2D(ellipsePoints2D, (baseCorner1 + baseCorner2) // 2, Rect.between(baseCorner1, baseCorner2))]
+        bodyPoints = ellipsePoints3D if hollow else basePoints
+
+    yield from basePoints
+    if hn != h0:
+        direction = ivec3(0,0,0)
+        direction[axis] = 1
+        yield from (point + (hn - h0)*direction for point in basePoints)
+        yield from (point + i*direction for i in range(1, hn-h0) for point in bodyPoints)
+
+
+def neighbors2D(point: ivec2, boundingRect: Rect, diagonal: bool = False, stride: int = 1):
+    """Yields the neighbors of [point] within [bounding_rect].\n
+    Useful for pathfinding."""
+
+    end = boundingRect.end
+
+    left  = point.x - stride >= boundingRect.offset.x
+    down  = point.y - stride >= boundingRect.offset.y
+    right = point.x + stride <  end.x
+    up    = point.y + stride <  end.y
+
+    if left:   yield ivec2(point.x - stride, point.y         )
+    if down:   yield ivec2(point.x         , point.y - stride)
+    if right:  yield ivec2(point.x + stride, point.y         )
+    if up:     yield ivec2(point.x         , point.y + stride)
+
+    if not diagonal:
+        return
+
+    if left  and down: yield ivec2(point.x - stride, point.y - stride)
+    if left  and up:   yield ivec2(point.x - stride, point.y + stride)
+    if right and down: yield ivec2(point.x + stride, point.y - stride)
+    if right and up:   yield ivec2(point.x + stride, point.y + stride)
+
+
+def neighbors3D(point: ivec3, boundingBox: Box, diagonal: bool = False, stride: int = 1):
+    """Yields the neighbors of [point] within [bounding_box].\n
+    Useful for pathfinding."""
+
+    end = boundingBox.end
+
+    left  = point.x - stride >= boundingBox.offset.x
+    down  = point.y - stride >= boundingBox.offset.y
+    back  = point.z - stride >= boundingBox.offset.z
+    right = point.x + stride <  end.x
+    up    = point.y + stride <  end.y
+    front = point.z + stride <  end.z
+
+    if left:  yield ivec3(point.x - stride, point.y         , point.z         )
+    if down:  yield ivec3(point.x         , point.y - stride, point.z         )
+    if back:  yield ivec3(point.x         , point.y         , point.z - stride)
+    if right: yield ivec3(point.x + stride, point.y         , point.z         )
+    if up:    yield ivec3(point.x         , point.y + stride, point.z         )
+    if front: yield ivec3(point.x         , point.y         , point.z + stride)
+
+    if not diagonal:
+        return
+
+    if left  and down:           yield ivec3(point.x - stride, point.y - stride, point.z         )
+    if left  and back:           yield ivec3(point.x - stride, point.y         , point.z - stride)
+    if left  and up:             yield ivec3(point.x - stride, point.y + stride, point.z         )
+    if left  and front:          yield ivec3(point.x - stride, point.y         , point.z + stride)
+    if right and down:           yield ivec3(point.x + stride, point.y - stride, point.z         )
+    if right and back:           yield ivec3(point.x + stride, point.y         , point.z - stride)
+    if right and up:             yield ivec3(point.x + stride, point.y + stride, point.z         )
+    if right and front:          yield ivec3(point.x + stride, point.y         , point.z + stride)
+    if down  and back:           yield ivec3(point.x         , point.y - stride, point.z - stride)
+    if down  and front:          yield ivec3(point.x         , point.y - stride, point.z + stride)
+    if up    and back:           yield ivec3(point.x         , point.y + stride, point.z - stride)
+    if up    and front:          yield ivec3(point.x         , point.y + stride, point.z + stride)
+    if left  and down and back:  yield ivec3(point.x - stride, point.y - stride, point.z - stride)
+    if left  and down and front: yield ivec3(point.x - stride, point.y - stride, point.z + stride)
+    if left  and up   and back:  yield ivec3(point.x - stride, point.y + stride, point.z - stride)
+    if left  and up   and front: yield ivec3(point.x - stride, point.y + stride, point.z + stride)
+    if right and down and back:  yield ivec3(point.x + stride, point.y - stride, point.z - stride)
+    if right and down and front: yield ivec3(point.x + stride, point.y - stride, point.z + stride)
+    if right and up   and back:  yield ivec3(point.x + stride, point.y + stride, point.z - stride)
+    if right and up   and front: yield ivec3(point.x + stride, point.y + stride, point.z + stride)
diff --git a/gdpc/worldLoader.py b/gdpc/worldLoader.py
deleted file mode 100644
index cf08519..0000000
--- a/gdpc/worldLoader.py
+++ /dev/null
@@ -1,206 +0,0 @@
-"""Provides tools for reading chunk data.
-
-This module contains functions to:
-* Calculate a heightmap ideal for building
-* Visualise numpy arrays
-"""
-
-from io import BytesIO
-from math import ceil, log2
-
-import nbt
-import numpy as np
-
-from . import direct_interface as di
-from .bitarray import BitArray
-
-
-class CachedSection:
-    """Represents a cached chunk section (16x16x16)."""
-
-    def __init__(self, x, y, z, blockPalette, blockStatesBitArray, biomesPalette, biomesBitArray):
-        self.blockPalette = blockPalette
-        self.blockStatesBitArray = blockStatesBitArray
-        self.biomesPalette = biomesPalette
-        self.biomesBitArray = biomesBitArray
-        self.x = x
-        self.y = y
-        self.z = z
-
-    def getBlockCompoundAtIndex(self, index):
-        return self.blockPalette[self.blockStatesBitArray.getAt(index)]
-
-    def getBiomeAtIndex(self, index):
-        return self.biomesPalette[self.biomesBitArray.getAt(index)]
-
-    # __repr__ displays the class well enough so __str__ is omitted
-    def __repr__(self):
-        return f"CachedSection({repr(self.blockPalette)}, " \
-               f"{repr(self.blockStatesBitArray)})"
-
-
-class WorldSlice:
-    """Contains information on a slice of the world."""
-
-    def __init__(self, x1, z1, x2, z2,
-                 heightmapTypes=None):
-        """Initialise WorldSlice with region and heightmap.
-        x2 and z2 are exclusive
-        """
-        if heightmapTypes is None:
-            heightmapTypes = ["MOTION_BLOCKING",
-                              "MOTION_BLOCKING_NO_LEAVES",
-                              "OCEAN_FLOOR",
-                              "WORLD_SURFACE"]
-        self.rect = x1, z1, x2 - x1, z2 - z1
-        self.chunkRect = (self.rect[0] >> 4, self.rect[1] >> 4,
-                          ((self.rect[0] + self.rect[2] - 1) >> 4)
-                          - (self.rect[0] >> 4) + 1,
-                          ((self.rect[1] + self.rect[3] - 1) >> 4)
-                          - (self.rect[1] >> 4) + 1)
-        self.heightmapTypes = heightmapTypes
-
-        chunkBytes = di.getChunks(*self.chunkRect, rtype='bytes')
-        file_like = BytesIO(chunkBytes)
-
-        self.nbtfile = nbt.nbt.NBTFile(buffer=file_like)
-
-        rectOffset = [self.rect[0] % 16, self.rect[1] % 16]
-
-        # For each type of heightmap, create a 2D array of zeros in the shape
-        # of the build area.
-        self.heightmaps = {}
-        for hmName in self.heightmapTypes:
-            self.heightmaps[hmName] = np.zeros(
-                (self.rect[2] + 1, self.rect[3] + 1), dtype=int)
-
-        # For each x-z position in the build area, get the height from the
-        # heightmap data from the corresponding chunk for all types of
-        # heightmap data.
-        for x in range(self.chunkRect[2]):
-            for z in range(self.chunkRect[3]):
-                chunkID = x + z * self.chunkRect[2]
-
-                hms = self.nbtfile['Chunks'][chunkID]['Heightmaps']
-                for hmName in self.heightmapTypes:
-                    hmRaw = hms[hmName]
-                    heightmapBitArray = BitArray(9, 16 * 16, hmRaw)
-                    heightmap = self.heightmaps[hmName]
-                    for cz in range(16):
-                        for cx in range(16):
-                            try:
-                                # In the heightmap data the lowest point is
-                                # encoded as 0, while since Minecraft 1.18 the
-                                # actual lowest y position is below zero at -64.
-                                # Subtract 64 from the heightmap value to
-                                # compensate for this difference.
-                                heightmap[-rectOffset[0] + x * 16 + cx,
-                                          -rectOffset[1] + z * 16 + cz] \
-                                    = heightmapBitArray.getAt(cz * 16 + cx) - 64
-                            except IndexError:
-                                pass
-
-        # sections
-        # Flat dict of all chunk sections in this world slice
-        self.sections = dict()
-        for x in range(self.chunkRect[2]):
-            for z in range(self.chunkRect[3]):
-                chunkID = x + z * self.chunkRect[2]
-                chunk = self.nbtfile['Chunks'][chunkID]
-                chunkSections = chunk['sections']
-
-                for section in chunkSections:
-                    y = section['Y'].value
-
-                    if (not ('block_states' in section)
-                            or len(section['block_states']) == 0):
-                        continue
-
-                    blockPalette = section['block_states']['palette']
-                    blockData = None
-                    if 'data' in section['block_states']:
-                        blockData = section['block_states']['data']
-                    blockPaletteBitsPerEntry = max(4, ceil(log2(len(blockPalette))))
-                    blockDataBitArray = BitArray(blockPaletteBitsPerEntry, 16 * 16 * 16, blockData)
-
-                    biomesPalette = section['biomes']['palette']
-                    biomesData = None
-                    if 'data' in section['biomes']:
-                        biomesData = section['biomes']['data']
-                    biomesBitsPerEntry = max(1, ceil(log2(len(biomesPalette))))
-                    biomesDataBitArray = BitArray(biomesBitsPerEntry, 64, biomesData)
-
-                    self.sections[(x, y, z)] = CachedSection(
-                        x, y, z, blockPalette, blockDataBitArray, biomesPalette, biomesDataBitArray
-                    )
-
-    # __repr__ displays the class well enough so __str__ is omitted
-    def __repr__(self):
-        """Represent the WorldSlice as a constructor."""
-        x1, z1 = self.rect[:2]
-        x2, z2 = self.rect[0] + self.rect[2], self.rect[1] + self.rect[3]
-        return f"WorldSlice{(x1, z1, x2, z2)}"
-
-    def getChunkSectionPos(self, x, y, z):
-        """Get chunk section x,y,z index from global x, y, z position."""
-        chunkX = (x >> 4) - self.chunkRect[0]
-        chunkZ = (z >> 4) - self.chunkRect[1]
-        chunkY = y >> 4
-        return chunkX, chunkY, chunkZ
-
-    def getBlockCompoundAt(self, x, y, z):
-        """Return block data at global x, y, z position."""
-        cachedSection = self.sections.get(self.getChunkSectionPos(x, y, z))
-        if cachedSection is None:
-            return None
-
-        blockIndex = (y % 16) * 16 * 16 + \
-                     (z % 16) * 16 + x % 16
-        return cachedSection.getBlockCompoundAtIndex(blockIndex)
-
-    def getBlockAt(self, x, y, z):
-        """Return the block's namespaced id at global x, y, z position."""
-        blockCompound = self.getBlockCompoundAt(x, y, z)
-        if blockCompound is None:
-            return "minecraft:void_air"
-        else:
-            return blockCompound["Name"].value
-
-    def getBiomeAt(self, x, y, z):
-        """Return biome at global x, y, z position."""
-        cachedSection = self.sections.get(self.getChunkSectionPos(x, y, z))
-        if cachedSection is None:
-            return None
-
-        # Constrain pos to inside this chunk, then shift 2 bits since biome data is encoded
-        # in groups of 4x4x4 per chunk.
-        biomeX = (x % 16) >> 2
-        biomeY = (y % 16) >> 2
-        biomeZ = (z % 16) >> 2
-        biomeIndex = (biomeY << 4) | (biomeZ << 2) | biomeX
-        return cachedSection.getBiomeAtIndex(biomeIndex)
-
-    def getBiomesNear(self, x, y, z):
-        """Return a dict of biomes in the same chunk."""
-        cachedSection = self.sections.get(self.getChunkSectionPos(x, y, z))
-        if cachedSection is None:
-            return None
-
-        # Find and count each biome type for each biome area (a 4x4x4 block) of the chunk.
-        foundBiomes = dict()
-        for biomeX in range(0, 4):
-            for biomeY in range(0, 4):
-                for biomeZ in range(0, 4):
-                    biomeIndex = (biomeY << 4) | (biomeZ << 2) | biomeX
-                    foundBiome: str = cachedSection.getBiomeAtIndex(biomeIndex)
-                    if foundBiome not in foundBiomes:
-                        foundBiomes[foundBiome] = 1
-                    else:
-                        foundBiomes[foundBiome] = foundBiomes.get(foundBiome) + 1
-        return foundBiomes
-
-    def getPrimaryBiomeNear(self, x, y, z):
-        """Return the most prevelant biome in the same chunk."""
-        foundBiomes = self.getBiomesNear(x, y, z)
-        # Return the biome that was found the most.
-        return max(foundBiomes, key=foundBiomes.get)
diff --git a/gdpc/world_slice.py b/gdpc/world_slice.py
new file mode 100644
index 0000000..c484158
--- /dev/null
+++ b/gdpc/world_slice.py
@@ -0,0 +1,306 @@
+"""Provides the WorldSlice class"""
+
+from typing import Dict, Iterable, Optional
+from dataclasses import dataclass
+from io import BytesIO
+from math import floor, ceil, log2
+
+from glm import ivec2, ivec3
+from nbt import nbt
+import numpy as np
+
+from .vector_tools import addY, loop2D, loop3D, trueMod2D, Rect
+from .block import Block
+from . import interface
+
+
+# Chunk format information:
+# https://minecraft.fandom.com/wiki/Chunk_format
+
+
+class _BitArray:
+    """Store an array of binary values and its metrics.
+
+    Minecraft stores block and heightmap data in compacted arrays of longs (bitarrays).
+    This class performs index mapping and bit shifting to access the data.
+    """
+
+    def __init__(self, bitsPerEntry: int, logicalArraySize: int, data):
+        """Initialise a BitArray."""
+        self._logicalArraySize = logicalArraySize
+        self._bitsPerEntry     = bitsPerEntry
+        self._entriesPerLong   = 64 // bitsPerEntry
+        self._maxEntryValue    = (1 << bitsPerEntry) - 1
+        if data is None:
+            self.longArray = []
+        else:
+            expectedLongCount = floor((logicalArraySize + self._entriesPerLong - 1) / self._entriesPerLong)
+            if len(data) != expectedLongCount:
+                raise ValueError(f"Invalid data length: got {len(data)} but expected {expectedLongCount}")
+            self.longArray = data
+
+    def __repr__(self):
+        """Represents the BitArray as a constructor."""
+        return f"BitArray{(self._bitsPerEntry, self._logicalArraySize, self.longArray)}"
+
+    def __getitem__(self, index: int):
+        """Returns the binary value stored at <index>."""
+        # If longArray size is 0, this is because the corresponding palette
+        # only contains a single value.
+        if len(self.longArray) == 0:
+            return 0
+        longIndex = index // self._entriesPerLong
+        long = self.longArray[longIndex]
+        k = (index - longIndex * self._entriesPerLong) * self._bitsPerEntry
+        return long >> k & self._maxEntryValue
+
+    def __len__(self):
+        """Returns the logical array size."""
+        return self._logicalArraySize
+
+
+@dataclass
+class _ChunkSection:
+    """Represents a chunk section or sub-chunk (16x16x16)."""
+
+    blockPalette:        nbt.TAG_List
+    blockStatesBitArray: _BitArray
+    biomesPalette:       nbt.TAG_List
+    biomesBitArray:      _BitArray
+
+    def getBlockStateTagAtIndex(self, index) -> nbt.TAG_Compound:
+        return self.blockPalette[self.blockStatesBitArray[index]]
+
+    def getBiomeAtIndex(self, index) -> nbt.TAG_String:
+        return self.biomesPalette[self.biomesBitArray[index]]
+
+
+class WorldSlice:
+    """Contains information on a slice of the world."""
+
+    def __init__(self, rect: Rect, heightmapTypes: Optional[Iterable[str]] = None, retries=0, timeout=None, host=interface.DEFAULT_HOST):
+        """Initialise WorldSlice with region and heightmap."""
+
+        if heightmapTypes is None:
+            heightmapTypes = [
+                "MOTION_BLOCKING",
+                "MOTION_BLOCKING_NO_LEAVES",
+                "OCEAN_FLOOR",
+                "WORLD_SURFACE"
+            ]
+
+        self._rect = rect
+        self._chunkRect = Rect(
+            self._rect.offset >> 4,
+            ((self._rect.last) >> 4) - (self._rect.offset >> 4) + 1
+        )
+
+        chunkBytes = interface.getChunks(self._chunkRect.offset, self._chunkRect.size, asBytes=True, retries=retries, timeout=timeout, host=host)
+        chunkBuffer = BytesIO(chunkBytes)
+
+        self._nbt = nbt.NBTFile(buffer=chunkBuffer)
+
+        self._heightmaps: Dict[str, np.ndarray] = {}
+        for hmName in heightmapTypes:
+            self._heightmaps[hmName] = np.zeros(self._rect.size, dtype=int)
+
+        self._sections: Dict[ivec3, _ChunkSection] = {}
+
+        self._blockEntities: Dict[ivec3, nbt.TAG_Compound] = {}
+
+        inChunkRectOffset = trueMod2D(self._rect.offset, 16)
+
+        # This assumes that the Y minimum is the same for every chunk.
+        yMin = 16 * int(self._nbt["Chunks"][0]["yPos"].value)
+
+        # Loop through chunks
+        for chunkPos in loop2D(self._chunkRect.size):
+            chunkID = chunkPos.x + chunkPos.y * self._chunkRect.size.x
+            chunkTag = self._nbt['Chunks'][chunkID]
+
+            # Read heightmaps
+            heightmapsTag = chunkTag['Heightmaps']
+            for hmName in heightmapTypes:
+                hmRaw = heightmapsTag[hmName]
+                hmBitArray = _BitArray(9, 16*16, hmRaw)
+                heightmap = self._heightmaps[hmName]
+                for inChunkPos in loop2D(ivec2(16,16)):
+                    try:
+                        # In the heightmap data, the lowest point is encoded as 0, while since
+                        # Minecraft 1.18 the actual lowest y position is below zero. We subtract
+                        # yMin from the heightmap value to compensate for this difference.
+                        hmPos = -inChunkRectOffset + chunkPos * 16 + inChunkPos # pylint: disable=invalid-unary-operand-type
+                        heightmap[hmPos.x, hmPos.y] = hmBitArray[inChunkPos.y * 16 + inChunkPos.x] + yMin
+                    except IndexError:
+                        pass
+
+            # Read chunk sections
+            for sectionTag in chunkTag['sections']:
+                y = int(sectionTag['Y'].value)
+
+                if (not ('block_states' in sectionTag) or len(sectionTag['block_states']) == 0):
+                    continue
+
+                blockPalette = sectionTag['block_states']['palette']
+                blockData = None
+                if 'data' in sectionTag['block_states']:
+                    blockData = sectionTag['block_states']['data']
+                blockPaletteBitsPerEntry = max(4, ceil(log2(len(blockPalette))))
+                blockDataBitArray = _BitArray(blockPaletteBitsPerEntry, 16*16*16, blockData)
+
+                biomesPalette = sectionTag['biomes']['palette']
+                biomesData = None
+                if 'data' in sectionTag['biomes']:
+                    biomesData = sectionTag['biomes']['data']
+                biomesBitsPerEntry = max(1, ceil(log2(len(biomesPalette))))
+                biomesDataBitArray = _BitArray(biomesBitsPerEntry, 64, biomesData)
+
+                self._sections[addY(chunkPos, y)] = _ChunkSection(
+                    blockPalette, blockDataBitArray, biomesPalette, biomesDataBitArray
+                )
+
+            # Read block entities
+            if 'block_entities' in chunkTag:
+                for blockEntityTag in chunkTag['block_entities']:
+                    blockEntityPos = ivec3(
+                        blockEntityTag['x'].value,
+                        blockEntityTag['y'].value,
+                        blockEntityTag['z'].value
+                    )
+                    self._blockEntities[blockEntityPos] = blockEntityTag
+
+
+    def __repr__(self):
+        return f"WorldSlice{repr(self._rect)}"
+
+
+    @property
+    def rect(self):
+        """Returns the Rect of block coordinates this WorldSlice covers."""
+        return self._rect
+
+    @property
+    def chunkRect(self):
+        """Returns the Rect of chunk coordinates this WorldSlice covers."""
+        return self._chunkRect
+
+    @property
+    def nbt(self) -> nbt.TAG_Compound:
+        """Returns the parsed NBT data for the chunks of this WorldSlice.\n
+        Its structure is described in the GDMC HTTP interface API."""
+        return self._nbt
+
+    @property
+    def heightmaps(self):
+        """Returns the heightmaps of this WorldSlice."""
+        return self._heightmaps
+
+
+    def getChunkSectionPositionGlobal(self, blockPosition: ivec3):
+        """Returns the local position of the chunk section that contains the global <blockPosition>."""
+        return (blockPosition >> 4) - addY(self._chunkRect.offset)
+
+    def getChunkSectionPosition(self, blockPosition: ivec3):
+        """Returns the local position of the chunk section that contains the local <blockPosition>."""
+        return self.getChunkSectionPositionGlobal(blockPosition + addY(self._rect.offset))
+
+
+    def _getChunkSectionGlobal(self, blockPosition: ivec3):
+        """Returns the chunk section that contains the global <blockPosition>."""
+        return self._sections.get(self.getChunkSectionPositionGlobal(blockPosition))
+
+
+    def getBlockStateTagGlobal(self, position: ivec3):
+        """Returns the block state compound tag at global <position>.\n
+        If <position> is not contained in this WorldSlice, returns None."""
+        chunkSection = self._getChunkSectionGlobal(position)
+        if chunkSection is None:
+            return None
+        blockIndex = (
+            (position.y % 16) * 16 * 16 +
+            (position.z % 16) * 16 +
+            (position.x % 16)
+        )
+        return chunkSection.getBlockStateTagAtIndex(blockIndex)
+
+    def getBlockStateTag(self, position: ivec3):
+        """Returns the block state compound tag at local <position>.\n
+        If <position> is not contained in this WorldSlice, returns None."""
+        return self.getBlockStateTagGlobal(position + addY(self._rect.offset))
+
+
+    def getBlockGlobal(self, position: ivec3):
+        """Returns the block at global <position>.\n
+        If <position> is not contained in this WorldSlice, returns Block("minecraft:void_air")."""
+        blockStateTag = self.getBlockStateTagGlobal(position)
+        if blockStateTag is None:
+            return Block("minecraft:void_air")
+        blockEntityTag = self._blockEntities.get(position)
+        return Block.fromBlockStateTag(blockStateTag, blockEntityTag)
+
+    def getBlock(self, position: ivec3):
+        """Returns the block at local <position>.\n
+        If <position> is not contained in this WorldSlice, returns Block("minecraft:void_air")."""
+        return self.getBlockGlobal(position + addY(self._rect.offset))
+
+
+    def getBiomeGlobal(self, position: ivec3):
+        """Returns namespaced id of the biome at global <position>.\n
+        If <position> is contained in this WorldSlice, returns None.\n
+        Note that Minecraft stores biomes in groups of 4x4x4 blocks. This function returns the
+        biome of <position>'s group."""
+        chunkSection = self._getChunkSectionGlobal(position)
+        if chunkSection is None:
+            return None
+        # Constrain pos to inside this chunk, then shift 2 bits since biome data is encoded
+        # in 64 groups of 4x4x4 per chunk.
+        biomePos = ivec3(
+            (position.z % 16) >> 2,
+            (position.y % 16) >> 2,
+            (position.z % 16) >> 2
+        )
+        biomeIndex = (biomePos.y << 4) | (biomePos.z << 2) | biomePos.x # pylint: disable=unsupported-binary-operation
+        return str(chunkSection.getBiomeAtIndex(biomeIndex).value)
+
+    def getBiome(self, position: ivec3):
+        """Returns namespaced id of the biome at local <position>.\n
+        If <position> is contained in this WorldSlice, returns None.\n
+        Note that Minecraft stores biomes in groups of 4x4x4 blocks. This function returns the
+        biome of <position>'s group."""
+        return self.getBiomeGlobal(position + addY(self._rect.offset))
+
+
+    def getBiomeCountsInChunkGlobal(self, position: ivec3):
+        """Returns a dict of biomes in the same chunk as the global <position>.\n
+        If <position> is contained in this WorldSlice, returns None.\n
+        Minecraft stores biomes in groups of 4x4x4 blocks. The returned dict maps the namespaced id
+        of a biome to the number of groups with that biome in the chunk."""
+        chunkSection = self._getChunkSectionGlobal(position)
+        if chunkSection is None:
+            return None
+        biomeCounts: Dict[str, int] = dict()
+        for biomePos in loop3D(ivec3(4,4,4)):
+            biomeIndex = (biomePos.y << 4) | (biomePos.z << 2) | biomePos.x
+            biome = str(chunkSection.getBiomeAtIndex(biomeIndex).value)
+            biomeCounts[biome] = biomeCounts.get(biome, 0) + 1
+        return biomeCounts
+
+    def getBiomeCountsInChunk(self, position: ivec3):
+        """Returns a dict of biomes in the same chunk as the local <position>.\n
+        If <position> is contained in this WorldSlice, returns None.\n
+        Minecraft stores biomes in groups of 4x4x4 blocks. The returned dict maps the namespaced id
+        of a biome to the number of groups with that biome in the chunk."""
+        return self.getBiomeCountsInChunkGlobal(position + addY(self._rect.offset))
+
+
+    def getPrimaryBiomeInChunkGlobal(self, position: ivec3):
+        """Returns the most prevalent biome in the same chunk as the global <position>.\n
+        If <position> is contained in this WorldSlice, returns None."""
+        foundBiomes = self.getBiomeCountsInChunkGlobal(position)
+        biome: str = max(foundBiomes.keys(), key=foundBiomes.get)
+        return biome
+
+    def getPrimaryBiomeInChunk(self, position: ivec3):
+        """Returns the most prevalent biome in the same chunk as the local <position>.\n
+        If <position> is contained in this WorldSlice, returns None."""
+        return self.getPrimaryBiomeInChunkGlobal(position + addY(self._rect.offset))
diff --git a/requirements.txt b/requirements.txt
old mode 100755
new mode 100644
index 7aca53a..93e02d4
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,11 @@
 matplotlib==3.6.2
+more-itertools==9.0.0
 NBT==1.5.1
 numpy==1.19.3
 opencv_python==4.5.1.48
+PyGLM==2.7.0
+pyglm-typing==0.2.1
 requests==2.22.0
+scikit-image==0.19.3
+scipy==1.9.3
+termcolor==2.1.1
diff --git a/setup.py b/setup.py
index 462df09..caf93a4 100644
--- a/setup.py
+++ b/setup.py
@@ -38,12 +38,18 @@ def get_metadata(name: str) -> str:
     packages=["gdpc"], # Note: subpackages must be listed explicitly
     install_requires=[
         "matplotlib",
+        "more-itertools",
         "NBT",
         "numpy",
         "opencv_python",
-        "requests"
+        "PyGLM >= 2.7.0",
+        "pyglm-typing",
+        "requests",
+        "scikit-image",
+        "scipy",
+        "termcolor"
     ],
-    python_requires=">=3.6, <4",
+    python_requires=">=3.7, <4",
     classifiers=[
         "Development Status :: 5 - Production/Stable",
         "Intended Audience :: Developers",
@@ -52,7 +58,7 @@ def get_metadata(name: str) -> str:
         "Programming Language :: Python :: 3",
         "Topic :: Games/Entertainment :: Simulation",
         "Topic :: Software Development :: Libraries :: Application Frameworks",
-        "Topic :: Software Development :: Version Control :: Git"
+        "Topic :: Software Development :: Version Control :: Git",
     ],
     keywords="GDMC, generative design, Minecraft, HTTP, development",
     project_urls={


Test patch:
diff --git a/tests/1.16 Block Test Map/DIM-1/data/raids.dat b/tests/1.16 Block Test Map/DIM-1/data/raids.dat
deleted file mode 100644
index d6a996c..0000000
Binary files a/tests/1.16 Block Test Map/DIM-1/data/raids.dat and /dev/null differ
diff --git a/tests/1.16 Block Test Map/DIM1/data/raids_end.dat b/tests/1.16 Block Test Map/DIM1/data/raids_end.dat
deleted file mode 100644
index d6a996c..0000000
Binary files a/tests/1.16 Block Test Map/DIM1/data/raids_end.dat and /dev/null differ
diff --git a/tests/1.16 Block Test Map/README.md b/tests/1.16 Block Test Map/README.md
deleted file mode 100644
index 8e7a952..0000000
--- a/tests/1.16 Block Test Map/README.md
+++ /dev/null
@@ -1,4 +0,0 @@
-Map courtesy of Fween
-Downloaded from https://www.planetminecraft.com/project/every-block-in-minecraft-1-16-5/x
-
-Blocks are eligned from -147 4 -12 to -147 4 664
\ No newline at end of file
diff --git a/tests/1.16 Block Test Map/advancements/f85d27c5-f726-4594-9562-c207c8f36c84.json b/tests/1.16 Block Test Map/advancements/f85d27c5-f726-4594-9562-c207c8f36c84.json
deleted file mode 100644
index 083d9c5..0000000
--- a/tests/1.16 Block Test Map/advancements/f85d27c5-f726-4594-9562-c207c8f36c84.json
+++ /dev/null
@@ -1,3975 +0,0 @@
-{
-  "minecraft:recipes/tools/fishing_rod": {
-    "criteria": {
-      "has_string": "2021-05-02 09:54:38 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/purple_stained_glass_pane": {
-    "criteria": {
-      "has_glass": "2021-05-02 09:28:54 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/smoker": {
-    "criteria": {
-      "has_furnace": "2021-05-02 10:00:31 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/polished_andesite_slab": {
-    "criteria": {
-      "has_polished_andesite": "2021-05-02 09:03:28 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/black_stained_glass": {
-    "criteria": {
-      "has_glass": "2021-05-02 09:06:13 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/brick_stairs_from_bricks_stonecutting": {
-    "criteria": {
-      "has_bricks": "2021-05-02 09:24:11 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/polished_blackstone_brick_stairs_from_polished_blackstone_stonecutting": {
-    "criteria": {
-      "has_polished_blackstone": "2021-05-02 09:34:48 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/lever": {
-    "criteria": {
-      "has_cobblestone": "2021-05-02 09:04:10 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/acacia_button": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:31 -0600"
-    },
-    "done": true
-  },
-  "minecraft:nether/obtain_crying_obsidian": {
-    "criteria": {
-      "crying_obsidian": "2021-05-02 09:34:30 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/brick_wall": {
-    "criteria": {
-      "has_bricks": "2021-05-02 09:24:11 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/gray_glazed_terracotta": {
-    "criteria": {
-      "has_gray_terracotta": "2021-05-02 09:27:23 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/light_blue_dye_from_blue_orchid": {
-    "criteria": {
-      "has_red_flower": "2021-05-02 09:55:56 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/magenta_dye_from_lilac": {
-    "criteria": {
-      "has_double_plant": "2021-05-02 10:05:03 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/stone_bricks": {
-    "criteria": {
-      "has_stone": "2021-05-02 09:03:19 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/spruce_door": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:13 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/mossy_cobblestone": {
-    "criteria": {
-      "has_vine": "2021-05-02 10:02:40 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/red_nether_brick_slab_from_red_nether_bricks_stonecutting": {
-    "criteria": {
-      "has_nether_bricks": "2021-05-02 09:30:04 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/light_gray_dye_from_white_tulip": {
-    "criteria": {
-      "has_red_flower": "2021-05-02 09:56:02 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/lime_concrete_powder": {
-    "criteria": {
-      "has_sand": "2021-05-02 09:04:37 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/cracked_polished_blackstone_bricks": {
-    "criteria": {
-      "has_blackstone_bricks": "2021-05-02 09:34:52 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/yellow_stained_glass_pane_from_glass_pane": {
-    "criteria": {
-      "has_glass_pane": "2021-05-02 10:02:40 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/chiseled_stone_bricks": {
-    "criteria": {
-      "has_stone_bricks": "2021-05-02 09:25:35 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/nether_brick_slab": {
-    "criteria": {
-      "has_nether_brick": "2021-05-02 09:25:48 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/gray_stained_glass": {
-    "criteria": {
-      "has_glass": "2021-05-02 09:06:13 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/crimson_hyphae": {
-    "criteria": {
-      "has_log": "2021-05-02 09:05:16 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/spruce_fence_gate": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:13 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/yellow_concrete_powder": {
-    "criteria": {
-      "has_sand": "2021-05-02 09:04:37 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/pumpkin_seeds": {
-    "criteria": {
-      "has_pumpkin": "2021-05-02 09:25:07 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/polished_blackstone_brick_slab": {
-    "criteria": {
-      "has_polished_blackstone_bricks": "2021-05-02 09:34:52 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/crafting_table": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:12 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/magenta_wool": {
-    "criteria": {
-      "has_white_wool": "2021-05-02 09:06:19 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/polished_granite_stairs": {
-    "criteria": {
-      "has_polished_granite": "2021-05-02 09:03:22 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/purple_stained_glass": {
-    "criteria": {
-      "has_glass": "2021-05-02 09:06:13 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/blast_furnace": {
-    "criteria": {
-      "has_smooth_stone": "2021-05-02 09:24:09 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/ladder": {
-    "criteria": {
-      "has_stick": "2021-05-02 09:54:16 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/dark_oak_slab": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:32 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/dark_prismarine_stairs_from_dark_prismarine_stonecutting": {
-    "criteria": {
-      "has_dark_prismarine": "2021-05-02 09:29:38 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/red_nether_brick_wall": {
-    "criteria": {
-      "has_red_nether_bricks": "2021-05-02 09:30:04 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/chest": {
-    "criteria": {
-      "has_lots_of_items": "2021-05-02 09:03:53 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/magenta_stained_glass_pane": {
-    "criteria": {
-      "has_glass": "2021-05-02 09:28:24 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/chiseled_stone_bricks_stone_from_stonecutting": {
-    "criteria": {
-      "has_stone": "2021-05-02 09:03:19 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/spruce_button": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:13 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/red_sandstone": {
-    "criteria": {
-      "has_sand": "2021-05-02 09:04:38 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/tools/wooden_shovel": {
-    "criteria": {
-      "has_stick": "2021-05-02 09:54:16 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/gold_ingot_from_blasting": {
-    "criteria": {
-      "has_gold_ore": "2021-05-02 09:04:40 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/black_carpet_from_white_carpet": {
-    "criteria": {
-      "has_white_carpet": "2021-05-02 10:04:20 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/dark_oak_button": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:32 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/smooth_red_sandstone_stairs_from_smooth_red_sandstone_stonecutting": {
-    "criteria": {
-      "has_smooth_red_sandstone": "2021-05-02 09:24:05 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/purpur_stairs": {
-    "criteria": {
-      "has_purpur_block": "2021-05-02 09:24:15 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/stick": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:12 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/crimson_pressure_plate": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:33 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/redstone_lamp": {
-    "criteria": {
-      "has_glowstone": "2021-05-02 09:25:18 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/stone_brick_slab_from_stone_stonecutting": {
-    "criteria": {
-      "has_stone": "2021-05-02 09:03:19 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/mossy_cobblestone_slab_from_mossy_cobblestone_stonecutting": {
-    "criteria": {
-      "has_mossy_cobblestone": "2021-05-02 09:24:13 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/iron_ingot_from_blasting": {
-    "criteria": {
-      "has_iron_ore": "2021-05-02 09:04:55 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/polished_blackstone_brick_slab_from_polished_blackstone_bricks_stonecutting": {
-    "criteria": {
-      "has_polished_blackstone_bricks": "2021-05-02 09:34:52 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/flower_banner_pattern": {
-    "criteria": {
-      "has_oxeye_daisy": "2021-05-02 09:56:19 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/smooth_sandstone_stairs_from_smooth_sandstone_stonecutting": {
-    "criteria": {
-      "has_mossy_cobblestone": "2021-05-02 09:24:08 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/polished_blackstone_button": {
-    "criteria": {
-      "has_polished_blackstone": "2021-05-02 09:34:48 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/blue_carpet": {
-    "criteria": {
-      "has_blue_wool": "2021-05-02 09:06:58 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/crimson_fence": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:33 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/lime_terracotta": {
-    "criteria": {
-      "has_terracotta": "2021-05-02 09:28:14 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/smooth_quartz_slab_from_smooth_quartz_stonecutting": {
-    "criteria": {
-      "has_smooth_quartz": "2021-05-02 09:08:40 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/light_blue_glazed_terracotta": {
-    "criteria": {
-      "has_light_blue_terracotta": "2021-05-02 09:27:18 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/dark_oak_planks": {
-    "criteria": {
-      "has_log": "2021-05-02 09:05:02 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/transportation/detector_rail": {
-    "criteria": {
-      "has_rail": "2021-05-02 10:31:10 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/purpur_pillar_from_purpur_block_stonecutting": {
-    "criteria": {
-      "has_purpur_block": "2021-05-02 09:24:15 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/cut_red_sandstone_slab_from_red_sandstone_stonecutting": {
-    "criteria": {
-      "has_red_sandstone": "2021-05-02 09:29:44 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/dark_oak_door": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:32 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/sandstone_stairs": {
-    "criteria": {
-      "has_sandstone": "2021-05-02 09:06:16 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/light_gray_terracotta": {
-    "criteria": {
-      "has_terracotta": "2021-05-02 09:28:14 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/prismarine_stairs": {
-    "criteria": {
-      "has_prismarine": "2021-05-02 09:29:26 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/brown_banner": {
-    "criteria": {
-      "has_brown_wool": "2021-05-02 09:06:59 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/cobblestone_slab": {
-    "criteria": {
-      "has_cobblestone": "2021-05-02 09:04:10 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/red_sandstone_stairs": {
-    "criteria": {
-      "has_red_sandstone": "2021-05-02 09:29:44 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/polished_blackstone_from_blackstone_stonecutting": {
-    "criteria": {
-      "has_blackstone": "2021-05-02 09:34:31 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/trapped_chest": {
-    "criteria": {
-      "has_tripwire_hook": "2021-05-02 10:29:40 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/gray_bed": {
-    "criteria": {
-      "has_gray_wool": "2021-05-02 09:06:41 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/food/dried_kelp_from_smoking": {
-    "criteria": {
-      "has_kelp": "2021-05-02 09:57:49 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/pink_terracotta": {
-    "criteria": {
-      "has_terracotta": "2021-05-02 09:28:14 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/stripped_crimson_hyphae": {
-    "criteria": {
-      "has_log": "2021-05-02 09:05:21 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/polished_blackstone_wall_from_polished_blackstone_stonecutting": {
-    "criteria": {
-      "has_polished_blackstone": "2021-05-02 09:34:48 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/coarse_dirt": {
-    "criteria": {
-      "has_gravel": "2021-05-02 09:04:39 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/mossy_cobblestone_stairs_from_mossy_cobblestone_stonecutting": {
-    "criteria": {
-      "has_mossy_cobblestone": "2021-05-02 09:24:13 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/light_gray_concrete_powder": {
-    "criteria": {
-      "has_sand": "2021-05-02 09:04:37 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/magenta_terracotta": {
-    "criteria": {
-      "has_terracotta": "2021-05-02 09:28:14 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/magenta_carpet": {
-    "criteria": {
-      "has_magenta_wool": "2021-05-02 09:06:34 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/stripped_birch_wood": {
-    "criteria": {
-      "has_log": "2021-05-02 09:05:18 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/creeper_banner_pattern": {
-    "criteria": {
-      "has_creeper_head": "2021-05-02 10:11:15 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/end_stone_brick_stairs_from_end_stone_brick_stonecutting": {
-    "criteria": {
-      "has_end_stone_brick": "2021-05-02 09:26:16 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/pink_concrete_powder": {
-    "criteria": {
-      "has_sand": "2021-05-02 09:04:37 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/sponge": {
-    "criteria": {
-      "has_wet_sponge": "2021-05-02 09:06:12 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/sandstone_slab": {
-    "criteria": {
-      "has_sandstone": "2021-05-02 09:06:16 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/smooth_red_sandstone_slab_from_smooth_red_sandstone_stonecutting": {
-    "criteria": {
-      "has_smooth_red_sandstone": "2021-05-02 09:24:05 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/emerald": {
-    "criteria": {
-      "has_emerald_block": "2021-05-02 09:26:21 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/prismarine_wall": {
-    "criteria": {
-      "has_prismarine": "2021-05-02 09:29:26 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/prismarine_brick_stairs": {
-    "criteria": {
-      "has_prismarine_bricks": "2021-05-02 09:29:37 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/yellow_stained_glass": {
-    "criteria": {
-      "has_glass": "2021-05-02 09:06:13 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/emerald_from_blasting": {
-    "criteria": {
-      "has_emerald_ore": "2021-05-02 09:26:20 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/stone_stairs_from_stone_stonecutting": {
-    "criteria": {
-      "has_stone": "2021-05-02 09:03:19 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/acacia_fence": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:31 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/magenta_bed_from_white_bed": {
-    "criteria": {
-      "has_bed": "2021-05-02 10:10:00 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/andesite_stairs_from_andesite_stonecutting": {
-    "criteria": {
-      "has_andesite": "2021-05-02 09:03:26 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/crimson_button": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:33 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/warped_fence_gate": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:35 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/red_dye_from_rose_bush": {
-    "criteria": {
-      "has_double_plant": "2021-05-02 10:05:04 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/brick_stairs": {
-    "criteria": {
-      "has_brick_block": "2021-05-02 09:24:11 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/blue_wool": {
-    "criteria": {
-      "has_white_wool": "2021-05-02 09:06:19 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/mossy_stone_brick_wall": {
-    "criteria": {
-      "has_mossy_stone_bricks": "2021-05-02 09:25:37 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/yellow_dye_from_sunflower": {
-    "criteria": {
-      "has_double_plant": "2021-05-02 09:01:24 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/brewing/cauldron": {
-    "criteria": {
-      "has_water_bucket": "2021-05-02 09:32:08 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/armor_stand": {
-    "criteria": {
-      "has_stone_slab": "2021-05-02 09:08:03 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/gray_stained_glass_pane": {
-    "criteria": {
-      "has_glass": "2021-05-02 09:28:49 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/sandstone": {
-    "criteria": {
-      "has_sand": "2021-05-02 09:04:37 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/red_stained_glass": {
-    "criteria": {
-      "has_glass": "2021-05-02 09:06:13 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/spruce_planks": {
-    "criteria": {
-      "has_logs": "2021-05-02 09:04:59 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/lime_stained_glass_pane_from_glass_pane": {
-    "criteria": {
-      "has_glass_pane": "2021-05-02 10:02:40 -0600"
-    },
-    "done": true
-  },
-  "minecraft:story/lava_bucket": {
-    "criteria": {
-      "lava_bucket": "2021-05-02 10:31:35 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/cobblestone_slab_from_cobblestone_stonecutting": {
-    "criteria": {
-      "has_cobblestone": "2021-05-02 09:04:10 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/food/dried_kelp_from_smelting": {
-    "criteria": {
-      "has_kelp": "2021-05-02 09:57:49 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/polished_diorite_slab": {
-    "criteria": {
-      "has_polished_diorite": "2021-05-02 09:03:25 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/oak_stairs": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:12 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/quartz_stairs": {
-    "criteria": {
-      "has_chiseled_quartz_block": "2021-05-02 09:26:47 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/polished_andesite_slab_from_andesite_stonecutting": {
-    "criteria": {
-      "has_andesite": "2021-05-02 09:03:26 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/red_carpet": {
-    "criteria": {
-      "has_red_wool": "2021-05-02 09:07:01 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/transportation/acacia_boat": {
-    "criteria": {
-      "in_water": "2021-05-02 09:53:13 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/prismarine_slab_from_prismarine_stonecutting": {
-    "criteria": {
-      "has_prismarine": "2021-05-02 09:29:26 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/light_blue_carpet_from_white_carpet": {
-    "criteria": {
-      "has_white_carpet": "2021-05-02 10:04:20 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/birch_sign": {
-    "criteria": {
-      "has_birch_planks": "2021-05-02 09:04:14 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/smooth_quartz_slab": {
-    "criteria": {
-      "has_smooth_quartz": "2021-05-02 09:08:40 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/polished_basalt_from_basalt_stonecutting": {
-    "criteria": {
-      "has_basalt": "2021-05-02 09:25:16 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/black_bed": {
-    "criteria": {
-      "has_black_wool": "2021-05-02 09:07:02 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/end_stone_brick_stairs": {
-    "criteria": {
-      "has_end_stone_bricks": "2021-05-02 09:26:16 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/end_stone_brick_slab": {
-    "criteria": {
-      "has_end_stone_bricks": "2021-05-02 09:26:16 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/gray_bed_from_white_bed": {
-    "criteria": {
-      "has_bed": "2021-05-02 10:10:00 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/white_concrete_powder": {
-    "criteria": {
-      "has_sand": "2021-05-02 09:04:37 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/jungle_fence": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:16 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/stone_brick_stairs_from_stone_stonecutting": {
-    "criteria": {
-      "has_stone": "2021-05-02 09:03:19 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/polished_blackstone_brick_stairs": {
-    "criteria": {
-      "has_polished_blackstone_bricks": "2021-05-02 09:34:52 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/white_wool_from_string": {
-    "criteria": {
-      "has_string": "2021-05-02 09:54:38 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/brown_glazed_terracotta": {
-    "criteria": {
-      "has_brown_terracotta": "2021-05-02 09:27:54 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/chiseled_quartz_block_from_quartz_block_stonecutting": {
-    "criteria": {
-      "has_quartz_block": "2021-05-02 09:26:49 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/netherite_scrap": {
-    "criteria": {
-      "has_ancient_debris": "2021-05-02 09:34:29 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/birch_wood": {
-    "criteria": {
-      "has_log": "2021-05-02 09:05:00 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/orange_bed": {
-    "criteria": {
-      "has_orange_wool": "2021-05-02 09:06:20 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/redstone_block": {
-    "criteria": {
-      "has_redstone": "2021-05-02 09:03:13 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/yellow_banner": {
-    "criteria": {
-      "has_yellow_wool": "2021-05-02 09:06:38 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/stone_brick_wall": {
-    "criteria": {
-      "has_stone_bricks": "2021-05-02 09:25:35 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/brown_stained_glass_pane_from_glass_pane": {
-    "criteria": {
-      "has_glass_pane": "2021-05-02 10:02:40 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/lime_stained_glass": {
-    "criteria": {
-      "has_glass": "2021-05-02 09:06:13 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/pink_bed_from_white_bed": {
-    "criteria": {
-      "has_bed": "2021-05-02 10:10:00 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/crimson_sign": {
-    "criteria": {
-      "has_crimson_planks": "2021-05-02 09:04:33 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/pink_stained_glass_pane": {
-    "criteria": {
-      "has_glass": "2021-05-02 09:28:47 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/light_gray_dye_from_azure_bluet": {
-    "criteria": {
-      "has_red_flower": "2021-05-02 09:55:58 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/lime_carpet_from_white_carpet": {
-    "criteria": {
-      "has_white_carpet": "2021-05-02 10:04:20 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/dark_oak_fence_gate": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:32 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/brown_bed": {
-    "criteria": {
-      "has_brown_wool": "2021-05-02 09:06:59 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/cracked_nether_bricks": {
-    "criteria": {
-      "has_nether_bricks": "2021-05-02 09:25:48 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/prismarine_wall_from_prismarine_stonecutting": {
-    "criteria": {
-      "has_prismarine": "2021-05-02 09:29:26 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/polished_diorite_stairs_from_polished_diorite_stonecutting": {
-    "criteria": {
-      "has_polished_diorite": "2021-05-02 09:03:25 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/quartz_pillar_from_quartz_block_stonecutting": {
-    "criteria": {
-      "has_quartz_block": "2021-05-02 09:26:49 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/granite_wall_from_granite_stonecutting": {
-    "criteria": {
-      "has_granite": "2021-05-02 09:03:21 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/brick_wall_from_bricks_stonecutting": {
-    "criteria": {
-      "has_bricks": "2021-05-02 09:24:11 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/polished_blackstone_brick_stairs_from_blackstone_stonecutting": {
-    "criteria": {
-      "has_blackstone": "2021-05-02 09:34:31 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/smooth_quartz": {
-    "criteria": {
-      "has_quartz_block": "2021-05-02 09:26:49 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/polished_diorite_stairs": {
-    "criteria": {
-      "has_polished_diorite": "2021-05-02 09:03:25 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/yellow_bed": {
-    "criteria": {
-      "has_yellow_wool": "2021-05-02 09:06:38 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/cyan_terracotta": {
-    "criteria": {
-      "has_terracotta": "2021-05-02 09:28:14 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/polished_blackstone_pressure_plate": {
-    "criteria": {
-      "has_polished_blackstone": "2021-05-02 09:34:48 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/nether_brick_wall_from_nether_bricks_stonecutting": {
-    "criteria": {
-      "has_nether_bricks": "2021-05-02 09:25:48 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/oak_fence": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:12 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/red_stained_glass_pane_from_glass_pane": {
-    "criteria": {
-      "has_glass_pane": "2021-05-02 10:02:39 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/polished_blackstone_bricks_from_blackstone_stonecutting": {
-    "criteria": {
-      "has_blackstone": "2021-05-02 09:34:31 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/coal_from_blasting": {
-    "criteria": {
-      "has_coal_ore": "2021-05-02 09:04:56 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/gold_ingot_from_gold_block": {
-    "criteria": {
-      "has_gold_block": "2021-05-02 09:07:15 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/light_blue_stained_glass": {
-    "criteria": {
-      "has_glass": "2021-05-02 09:06:13 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/orange_concrete_powder": {
-    "criteria": {
-      "has_sand": "2021-05-02 09:04:37 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/oak_slab": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:12 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/light_gray_carpet_from_white_carpet": {
-    "criteria": {
-      "has_white_carpet": "2021-05-02 10:04:20 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/blue_stained_glass_pane": {
-    "criteria": {
-      "has_glass": "2021-05-02 09:28:56 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/magenta_stained_glass_pane_from_glass_pane": {
-    "criteria": {
-      "has_glass_pane": "2021-05-02 10:02:40 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/chiseled_polished_blackstone": {
-    "criteria": {
-      "has_polished_blackstone": "2021-05-02 09:34:48 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/granite_stairs": {
-    "criteria": {
-      "has_granite": "2021-05-02 09:03:21 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/cyan_glazed_terracotta": {
-    "criteria": {
-      "has_cyan_terracotta": "2021-05-02 09:27:49 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/jungle_pressure_plate": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:16 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/black_wool": {
-    "criteria": {
-      "has_white_wool": "2021-05-02 09:06:19 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/purple_bed": {
-    "criteria": {
-      "has_purple_wool": "2021-05-02 09:06:44 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/stripped_dark_oak_wood": {
-    "criteria": {
-      "has_log": "2021-05-02 09:05:21 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/acacia_pressure_plate": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:31 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/diorite_stairs_from_diorite_stonecutting": {
-    "criteria": {
-      "has_diorite": "2021-05-02 09:03:24 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/brewing/glass_bottle": {
-    "criteria": {
-      "has_glass": "2021-05-02 09:06:13 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/blue_dye_from_cornflower": {
-    "criteria": {
-      "has_blue_flower": "2021-05-02 09:56:20 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/light_blue_stained_glass_pane": {
-    "criteria": {
-      "has_glass": "2021-05-02 09:28:26 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/stripped_acacia_wood": {
-    "criteria": {
-      "has_log": "2021-05-02 09:05:20 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/polished_blackstone_brick_slab_from_polished_blackstone_stonecutting": {
-    "criteria": {
-      "has_polished_blackstone": "2021-05-02 09:34:48 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/light_gray_glazed_terracotta": {
-    "criteria": {
-      "has_light_gray_terracotta": "2021-05-02 09:27:41 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/polished_diorite_slab_from_polished_diorite_stonecutting": {
-    "criteria": {
-      "has_polished_diorite": "2021-05-02 09:03:25 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/crimson_slab": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:33 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/cyan_stained_glass_pane_from_glass_pane": {
-    "criteria": {
-      "has_glass_pane": "2021-05-02 10:02:39 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/nether_brick_stairs_from_nether_bricks_stonecutting": {
-    "criteria": {
-      "has_nether_bricks": "2021-05-02 09:25:48 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/birch_button": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:14 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/light_gray_bed": {
-    "criteria": {
-      "has_light_gray_wool": "2021-05-02 09:06:42 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/quartz_slab": {
-    "criteria": {
-      "has_chiseled_quartz_block": "2021-05-02 09:26:47 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/blackstone_wall_from_blackstone_stonecutting": {
-    "criteria": {
-      "has_blackstone": "2021-05-02 09:34:31 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/crimson_stairs": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:33 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/crimson_fence_gate": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:33 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/red_dye_from_poppy": {
-    "criteria": {
-      "has_red_flower": "2021-05-02 09:55:55 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/blackstone_stairs": {
-    "criteria": {
-      "has_blackstone": "2021-05-02 09:34:31 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/jungle_wood": {
-    "criteria": {
-      "has_log": "2021-05-02 09:05:01 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/acacia_wood": {
-    "criteria": {
-      "has_log": "2021-05-02 09:05:01 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/smooth_red_sandstone_stairs": {
-    "criteria": {
-      "has_smooth_red_sandstone": "2021-05-02 09:24:05 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/spruce_wood": {
-    "criteria": {
-      "has_log": "2021-05-02 09:04:59 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/red_nether_brick_wall_from_red_nether_bricks_stonecutting": {
-    "criteria": {
-      "has_nether_bricks": "2021-05-02 09:30:04 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/polished_blackstone_brick_wall": {
-    "criteria": {
-      "has_polished_blackstone_bricks": "2021-05-02 09:34:52 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/purple_glazed_terracotta": {
-    "criteria": {
-      "has_purple_terracotta": "2021-05-02 09:27:10 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/yellow_dye_from_dandelion": {
-    "criteria": {
-      "has_yellow_flower": "2021-05-02 09:55:54 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/green_stained_glass": {
-    "criteria": {
-      "has_glass": "2021-05-02 09:06:13 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/lime_dye_from_smelting": {
-    "criteria": {
-      "has_sea_pickle": "2021-05-02 09:52:34 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/light_blue_wool": {
-    "criteria": {
-      "has_white_wool": "2021-05-02 09:06:19 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/blackstone_slab_from_blackstone_stonecutting": {
-    "criteria": {
-      "has_blackstone": "2021-05-02 09:34:31 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/combat/wooden_sword": {
-    "criteria": {
-      "has_stick": "2021-05-02 09:54:16 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/gray_banner": {
-    "criteria": {
-      "has_gray_wool": "2021-05-02 09:06:41 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/oak_planks": {
-    "criteria": {
-      "has_logs": "2021-05-02 09:04:57 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/yellow_glazed_terracotta": {
-    "criteria": {
-      "has_yellow_terracotta": "2021-05-02 09:27:19 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/end_stone_brick_wall": {
-    "criteria": {
-      "has_end_stone_bricks": "2021-05-02 09:26:16 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/gray_concrete_powder": {
-    "criteria": {
-      "has_sand": "2021-05-02 09:04:37 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/light_blue_carpet": {
-    "criteria": {
-      "has_light_blue_wool": "2021-05-02 09:06:35 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/light_gray_banner": {
-    "criteria": {
-      "has_light_gray_wool": "2021-05-02 09:06:42 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/chiseled_red_sandstone_from_red_sandstone_stonecutting": {
-    "criteria": {
-      "has_red_sandstone": "2021-05-02 09:29:44 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/cyan_banner": {
-    "criteria": {
-      "has_cyan_wool": "2021-05-02 09:06:43 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/jungle_planks": {
-    "criteria": {
-      "has_logs": "2021-05-02 09:05:01 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/black_banner": {
-    "criteria": {
-      "has_black_wool": "2021-05-02 09:07:02 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/gray_carpet": {
-    "criteria": {
-      "has_gray_wool": "2021-05-02 09:06:41 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/red_banner": {
-    "criteria": {
-      "has_red_wool": "2021-05-02 09:07:01 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/quartz_slab_from_stonecutting": {
-    "criteria": {
-      "has_quartz_block": "2021-05-02 09:26:49 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/tripwire_hook": {
-    "criteria": {
-      "has_string": "2021-05-02 09:54:38 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/black_dye_from_wither_rose": {
-    "criteria": {
-      "has_black_flower": "2021-05-02 09:56:22 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/chiseled_quartz_block": {
-    "criteria": {
-      "has_chiseled_quartz_block": "2021-05-02 09:26:47 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/birch_planks": {
-    "criteria": {
-      "has_logs": "2021-05-02 09:05:00 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/black_concrete_powder": {
-    "criteria": {
-      "has_sand": "2021-05-02 09:04:37 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/orange_banner": {
-    "criteria": {
-      "has_orange_wool": "2021-05-02 09:06:20 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/prismarine_brick_slab_from_prismarine_stonecutting": {
-    "criteria": {
-      "has_prismarine_brick": "2021-05-02 09:29:37 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/sandstone_stairs_from_sandstone_stonecutting": {
-    "criteria": {
-      "has_sandstone": "2021-05-02 09:06:16 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/orange_carpet_from_white_carpet": {
-    "criteria": {
-      "has_white_carpet": "2021-05-02 10:04:20 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/birch_fence_gate": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:14 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/polished_blackstone_brick_wall_from_blackstone_stonecutting": {
-    "criteria": {
-      "has_blackstone": "2021-05-02 09:34:31 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/stone_button": {
-    "criteria": {
-      "has_stone": "2021-05-02 09:03:19 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/combat/crossbow": {
-    "criteria": {
-      "has_stick": "2021-05-02 09:54:16 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/polished_blackstone_slab_from_blackstone_stonecutting": {
-    "criteria": {
-      "has_blackstone": "2021-05-02 09:34:31 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/spruce_pressure_plate": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:13 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/yellow_wool": {
-    "criteria": {
-      "has_white_wool": "2021-05-02 09:06:19 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/dark_oak_trapdoor": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:32 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/oak_sign": {
-    "criteria": {
-      "has_oak_planks": "2021-05-02 09:04:12 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/oak_trapdoor": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:12 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/granite_stairs_from_granite_stonecutting": {
-    "criteria": {
-      "has_granite": "2021-05-02 09:03:21 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/polished_diorite_from_diorite_stonecutting": {
-    "criteria": {
-      "has_diorite": "2021-05-02 09:03:24 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/prismarine_brick_stairs_from_prismarine_stonecutting": {
-    "criteria": {
-      "has_prismarine_brick": "2021-05-02 09:29:37 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/warped_planks": {
-    "criteria": {
-      "has_logs": "2021-05-02 09:05:16 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/chiseled_nether_bricks": {
-    "criteria": {
-      "has_nether_bricks": "2021-05-02 09:25:48 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/red_concrete_powder": {
-    "criteria": {
-      "has_sand": "2021-05-02 09:04:37 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/nether_brick_slab_from_nether_bricks_stonecutting": {
-    "criteria": {
-      "has_nether_bricks": "2021-05-02 09:25:48 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/light_blue_bed_from_white_bed": {
-    "criteria": {
-      "has_bed": "2021-05-02 10:10:00 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/magenta_glazed_terracotta": {
-    "criteria": {
-      "has_magenta_terracotta": "2021-05-02 09:27:11 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/warped_fence": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:35 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/smooth_sandstone_slab": {
-    "criteria": {
-      "has_smooth_sandstone": "2021-05-02 09:24:08 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/black_glazed_terracotta": {
-    "criteria": {
-      "has_black_terracotta": "2021-05-02 09:27:58 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/blue_terracotta": {
-    "criteria": {
-      "has_terracotta": "2021-05-02 09:28:14 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/diorite_wall": {
-    "criteria": {
-      "has_diorite": "2021-05-02 09:03:24 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/polished_granite_stairs_from_polished_granite_stonecutting": {
-    "criteria": {
-      "has_polished_granite": "2021-05-02 09:03:22 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/stone": {
-    "criteria": {
-      "has_cobblestone": "2021-05-02 09:04:10 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/green_carpet": {
-    "criteria": {
-      "has_green_wool": "2021-05-02 09:07:00 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/stonecutter": {
-    "criteria": {
-      "has_stone": "2021-05-02 09:03:19 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/light_blue_terracotta": {
-    "criteria": {
-      "has_terracotta": "2021-05-02 09:28:14 -0600"
-    },
-    "done": true
-  },
-  "minecraft:adventure/adventuring_time": {
-    "criteria": {
-      "minecraft:plains": "2021-05-02 09:00:00 -0600"
-    },
-    "done": false
-  },
-  "minecraft:recipes/decorations/purple_bed_from_white_bed": {
-    "criteria": {
-      "has_bed": "2021-05-02 10:10:00 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/purpur_slab": {
-    "criteria": {
-      "has_purpur_block": "2021-05-02 09:24:15 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/cut_red_sandstone": {
-    "criteria": {
-      "has_red_sandstone": "2021-05-02 09:29:44 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/combat/stone_sword": {
-    "criteria": {
-      "has_cobblestone": "2021-05-02 09:04:10 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/lapis_from_smelting": {
-    "criteria": {
-      "has_lapis_ore": "2021-05-02 09:06:14 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/light_gray_carpet": {
-    "criteria": {
-      "has_light_gray_wool": "2021-05-02 09:06:42 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/cobblestone_wall": {
-    "criteria": {
-      "has_cobblestone": "2021-05-02 09:04:10 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/barrel": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:12 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/mossy_stone_brick_slab": {
-    "criteria": {
-      "has_mossy_stone_bricks": "2021-05-02 09:25:37 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/magenta_banner": {
-    "criteria": {
-      "has_magenta_wool": "2021-05-02 09:06:34 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/light_blue_banner": {
-    "criteria": {
-      "has_light_blue_wool": "2021-05-02 09:06:35 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/purple_carpet": {
-    "criteria": {
-      "has_purple_wool": "2021-05-02 09:06:44 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/tools/stone_pickaxe": {
-    "criteria": {
-      "has_cobblestone": "2021-05-02 09:04:10 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/redstone_from_smelting": {
-    "criteria": {
-      "has_redstone_ore": "2021-05-02 09:24:44 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/quartz_bricks": {
-    "criteria": {
-      "has_quartz_block": "2021-05-02 09:26:49 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/stone_pressure_plate": {
-    "criteria": {
-      "has_stone": "2021-05-02 09:03:19 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/brown_stained_glass": {
-    "criteria": {
-      "has_glass": "2021-05-02 09:06:13 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/polished_blackstone_slab": {
-    "criteria": {
-      "has_polished_blackstone": "2021-05-02 09:34:48 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/purple_stained_glass_pane_from_glass_pane": {
-    "criteria": {
-      "has_glass_pane": "2021-05-02 10:02:40 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/red_dye_from_tulip": {
-    "criteria": {
-      "has_red_flower": "2021-05-02 09:55:59 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/cobblestone_wall_from_cobblestone_stonecutting": {
-    "criteria": {
-      "has_cobblestone": "2021-05-02 09:04:10 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/cut_sandstone_slab": {
-    "criteria": {
-      "has_cut_sandstone": "2021-05-02 09:06:18 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/polished_andesite_slab_from_polished_andesite_stonecutting": {
-    "criteria": {
-      "has_polished_andesite": "2021-05-02 09:03:28 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/orange_dye_from_orange_tulip": {
-    "criteria": {
-      "has_red_flower": "2021-05-02 09:56:01 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/light_gray_stained_glass": {
-    "criteria": {
-      "has_glass": "2021-05-02 09:06:13 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/brown_carpet": {
-    "criteria": {
-      "has_brown_wool": "2021-05-02 09:06:59 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/grindstone": {
-    "criteria": {
-      "has_stone_slab": "2021-05-02 09:08:02 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/food/honey_bottle": {
-    "criteria": {
-      "has_honey_block": "2021-05-02 10:13:14 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/end_stone_brick_slab_from_end_stone_brick_stonecutting": {
-    "criteria": {
-      "has_end_stone_brick": "2021-05-02 09:26:16 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/red_stained_glass_pane": {
-    "criteria": {
-      "has_glass": "2021-05-02 09:29:22 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/quartz_bricks_from_quartz_block_stonecutting": {
-    "criteria": {
-      "has_quartz_block": "2021-05-02 09:26:49 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/food/dried_kelp_from_campfire_cooking": {
-    "criteria": {
-      "has_kelp": "2021-05-02 09:57:49 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/acacia_sign": {
-    "criteria": {
-      "has_acacia_planks": "2021-05-02 09:04:31 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/yellow_carpet": {
-    "criteria": {
-      "has_yellow_wool": "2021-05-02 09:06:38 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/stripped_oak_wood": {
-    "criteria": {
-      "has_log": "2021-05-02 09:05:17 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/brick_slab": {
-    "criteria": {
-      "has_brick_block": "2021-05-02 09:24:11 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/orange_stained_glass_pane_from_glass_pane": {
-    "criteria": {
-      "has_glass_pane": "2021-05-02 10:02:39 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/stick_from_bamboo_item": {
-    "criteria": {
-      "has_bamboo": "2021-05-02 09:57:50 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/sandstone_wall": {
-    "criteria": {
-      "has_sandstone": "2021-05-02 09:06:16 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/red_sandstone_wall_from_red_sandstone_stonecutting": {
-    "criteria": {
-      "has_red_sandstone": "2021-05-02 09:29:44 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/diamond": {
-    "criteria": {
-      "has_diamond_block": "2021-05-02 09:24:42 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/diamond_from_blasting": {
-    "criteria": {
-      "has_diamond_ore": "2021-05-02 09:24:39 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/polished_diorite": {
-    "criteria": {
-      "has_stone": "2021-05-02 09:03:24 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/andesite_wall": {
-    "criteria": {
-      "has_andesite": "2021-05-02 09:03:26 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/light_gray_wool": {
-    "criteria": {
-      "has_white_wool": "2021-05-02 09:06:19 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/light_blue_stained_glass_pane_from_glass_pane": {
-    "criteria": {
-      "has_glass_pane": "2021-05-02 10:02:40 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/andesite_slab": {
-    "criteria": {
-      "has_andesite": "2021-05-02 09:03:26 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/red_bed_from_white_bed": {
-    "criteria": {
-      "has_bed": "2021-05-02 10:10:00 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/acacia_stairs": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:31 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/red_wool": {
-    "criteria": {
-      "has_white_wool": "2021-05-02 09:06:19 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/light_gray_dye_from_oxeye_daisy": {
-    "criteria": {
-      "has_red_flower": "2021-05-02 09:56:19 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/white_terracotta": {
-    "criteria": {
-      "has_terracotta": "2021-05-02 09:28:14 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/tools/stone_axe": {
-    "criteria": {
-      "has_cobblestone": "2021-05-02 09:04:10 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/blue_banner": {
-    "criteria": {
-      "has_blue_wool": "2021-05-02 09:06:58 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/white_dye_from_lily_of_the_valley": {
-    "criteria": {
-      "has_white_flower": "2021-05-02 09:56:21 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/soul_torch": {
-    "criteria": {
-      "has_soul_sand": "2021-05-02 09:25:11 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/polished_blackstone_bricks_from_polished_blackstone_stonecutting": {
-    "criteria": {
-      "has_polished_blackstone": "2021-05-02 09:34:48 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/yellow_terracotta": {
-    "criteria": {
-      "has_terracotta": "2021-05-02 09:28:14 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/purple_wool": {
-    "criteria": {
-      "has_white_wool": "2021-05-02 09:06:19 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/quartz_pillar": {
-    "criteria": {
-      "has_chiseled_quartz_block": "2021-05-02 09:26:47 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/purple_terracotta": {
-    "criteria": {
-      "has_terracotta": "2021-05-02 09:28:14 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/cobblestone_stairs_from_cobblestone_stonecutting": {
-    "criteria": {
-      "has_cobblestone": "2021-05-02 09:04:10 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/magenta_concrete_powder": {
-    "criteria": {
-      "has_sand": "2021-05-02 09:04:37 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/blue_bed": {
-    "criteria": {
-      "has_blue_wool": "2021-05-02 09:06:58 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/lime_carpet": {
-    "criteria": {
-      "has_lime_wool": "2021-05-02 09:06:39 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/green_bed": {
-    "criteria": {
-      "has_green_wool": "2021-05-02 09:07:00 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/wheat": {
-    "criteria": {
-      "has_hay_block": "2021-05-02 09:28:00 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/dark_prismarine_slab_from_dark_prismarine_stonecutting": {
-    "criteria": {
-      "has_dark_prismarine": "2021-05-02 09:29:38 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/polished_andesite_stairs_from_andesite_stonecutting": {
-    "criteria": {
-      "has_andesite": "2021-05-02 09:03:26 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/polished_andesite_stairs": {
-    "criteria": {
-      "has_polished_andesite": "2021-05-02 09:03:28 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/white_bed": {
-    "criteria": {
-      "has_white_wool": "2021-05-02 09:06:19 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/lime_banner": {
-    "criteria": {
-      "has_lime_wool": "2021-05-02 09:06:39 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/cut_sandstone_from_sandstone_stonecutting": {
-    "criteria": {
-      "has_sandstone": "2021-05-02 09:06:16 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/stone_brick_slab_from_stone_bricks_stonecutting": {
-    "criteria": {
-      "has_stone_bricks": "2021-05-02 09:25:35 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/green_dye": {
-    "criteria": {
-      "has_cactus": "2021-05-02 10:00:34 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/magenta_bed": {
-    "criteria": {
-      "has_magenta_wool": "2021-05-02 09:06:34 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/birch_pressure_plate": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:14 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/campfire": {
-    "criteria": {
-      "has_stick": "2021-05-02 09:54:16 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/redstone": {
-    "criteria": {
-      "has_redstone_block": "2021-05-02 10:30:14 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/purpur_stairs_from_purpur_block_stonecutting": {
-    "criteria": {
-      "has_purpur_block": "2021-05-02 09:24:15 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/cut_red_sandstone_from_red_sandstone_stonecutting": {
-    "criteria": {
-      "has_red_sandstone": "2021-05-02 09:29:44 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/diorite_slab_from_diorite_stonecutting": {
-    "criteria": {
-      "has_diorite": "2021-05-02 09:03:24 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/smooth_stone": {
-    "criteria": {
-      "has_stone": "2021-05-02 09:03:19 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/lime_stained_glass_pane": {
-    "criteria": {
-      "has_glass": "2021-05-02 09:28:45 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/stone_slab_from_stone_stonecutting": {
-    "criteria": {
-      "has_stone": "2021-05-02 09:03:19 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/brown_concrete_powder": {
-    "criteria": {
-      "has_sand": "2021-05-02 09:04:37 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/nether_brick_wall": {
-    "criteria": {
-      "has_nether_bricks": "2021-05-02 09:25:48 -0600"
-    },
-    "done": true
-  },
-  "minecraft:nether/obtain_ancient_debris": {
-    "criteria": {
-      "ancient_debris": "2021-05-02 09:34:29 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/end_stone_brick_slab_from_end_stone_stonecutting": {
-    "criteria": {
-      "has_end_stone": "2021-05-02 09:26:15 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/black_bed_from_white_bed": {
-    "criteria": {
-      "has_bed": "2021-05-02 10:10:00 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/pink_carpet": {
-    "criteria": {
-      "has_pink_wool": "2021-05-02 09:06:40 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/bone_meal_from_bone_block": {
-    "criteria": {
-      "has_bone_block": "2021-05-02 09:30:05 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/netherite_scrap_from_blasting": {
-    "criteria": {
-      "has_ancient_debris": "2021-05-02 09:34:29 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/blue_stained_glass_pane_from_glass_pane": {
-    "criteria": {
-      "has_glass_pane": "2021-05-02 10:02:40 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/cyan_stained_glass_pane": {
-    "criteria": {
-      "has_glass": "2021-05-02 09:28:52 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/purple_carpet_from_white_carpet": {
-    "criteria": {
-      "has_white_carpet": "2021-05-02 10:04:20 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/blue_stained_glass": {
-    "criteria": {
-      "has_glass": "2021-05-02 09:06:13 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/cyan_wool": {
-    "criteria": {
-      "has_white_wool": "2021-05-02 09:06:19 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/red_carpet_from_white_carpet": {
-    "criteria": {
-      "has_white_carpet": "2021-05-02 10:04:20 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/stripped_warped_hyphae": {
-    "criteria": {
-      "has_log": "2021-05-02 09:05:35 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/packed_ice": {
-    "criteria": {
-      "has_ice": "2021-05-02 09:24:45 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/transportation/spruce_boat": {
-    "criteria": {
-      "in_water": "2021-05-02 09:53:13 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/cracked_stone_bricks": {
-    "criteria": {
-      "has_stone_bricks": "2021-05-02 09:25:35 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/terracotta": {
-    "criteria": {
-      "has_clay_block": "2021-05-02 09:24:47 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/tools/flint_and_steel": {
-    "criteria": {
-      "has_obsidian": "2021-05-02 09:24:14 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/quartz_stairs_from_quartz_block_stonecutting": {
-    "criteria": {
-      "has_quartz_block": "2021-05-02 09:26:49 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/food/mushroom_stew": {
-    "criteria": {
-      "has_brown_mushroom": "2021-05-02 09:56:22 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/end_stone_brick_wall_from_end_stone_brick_stonecutting": {
-    "criteria": {
-      "has_end_stone_brick": "2021-05-02 09:26:16 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/gray_wool": {
-    "criteria": {
-      "has_white_wool": "2021-05-02 09:06:19 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/polished_granite_from_granite_stonecutting": {
-    "criteria": {
-      "has_granite": "2021-05-02 09:03:21 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/sugar_from_sugar_cane": {
-    "criteria": {
-      "has_reeds": "2021-05-02 09:57:48 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/gray_terracotta": {
-    "criteria": {
-      "has_terracotta": "2021-05-02 09:28:14 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/jungle_trapdoor": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:16 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/tools/wooden_hoe": {
-    "criteria": {
-      "has_stick": "2021-05-02 09:54:16 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/smooth_sandstone_slab_from_smooth_sandstone_stonecutting": {
-    "criteria": {
-      "has_smooth_sandstone": "2021-05-02 09:24:08 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/respawn_anchor": {
-    "criteria": {
-      "has_obsidian": "2021-05-02 09:34:30 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/warped_hyphae": {
-    "criteria": {
-      "has_log": "2021-05-02 09:05:16 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/jungle_button": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:16 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/red_nether_brick_slab": {
-    "criteria": {
-      "has_red_nether_bricks": "2021-05-02 09:30:04 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/diorite_slab": {
-    "criteria": {
-      "has_diorite": "2021-05-02 09:03:24 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/cobblestone_stairs": {
-    "criteria": {
-      "has_cobblestone": "2021-05-02 09:04:10 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/stone_brick_stairs_from_stone_bricks_stonecutting": {
-    "criteria": {
-      "has_stone_bricks": "2021-05-02 09:25:35 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/birch_slab": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:14 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/chiseled_sandstone_from_sandstone_stonecutting": {
-    "criteria": {
-      "has_sandstone": "2021-05-02 09:06:16 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/yellow_bed_from_white_bed": {
-    "criteria": {
-      "has_bed": "2021-05-02 10:10:00 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/mossy_stone_brick_stairs_from_mossy_stone_brick_stonecutting": {
-    "criteria": {
-      "has_mossy_stone_bricks": "2021-05-02 09:25:37 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/granite_wall": {
-    "criteria": {
-      "has_granite": "2021-05-02 09:03:21 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/note_block": {
-    "criteria": {
-      "has_redstone": "2021-05-02 09:03:13 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/orange_stained_glass_pane": {
-    "criteria": {
-      "has_glass": "2021-05-02 09:28:23 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/white_banner": {
-    "criteria": {
-      "has_white_wool": "2021-05-02 09:06:19 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/polished_blackstone_stairs_from_blackstone_stonecutting": {
-    "criteria": {
-      "has_blackstone": "2021-05-02 09:34:31 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/green_stained_glass_pane_from_glass_pane": {
-    "criteria": {
-      "has_glass_pane": "2021-05-02 10:02:39 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/polished_diorite_slab_from_diorite_stonecutting": {
-    "criteria": {
-      "has_diorite": "2021-05-02 09:03:25 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/light_blue_concrete_powder": {
-    "criteria": {
-      "has_sand": "2021-05-02 09:04:37 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/pink_banner": {
-    "criteria": {
-      "has_pink_wool": "2021-05-02 09:06:40 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/mossy_cobblestone_wall": {
-    "criteria": {
-      "has_mossy_cobblestone": "2021-05-02 09:24:13 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/orange_terracotta": {
-    "criteria": {
-      "has_terracotta": "2021-05-02 09:28:14 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/prismarine_brick_slab": {
-    "criteria": {
-      "has_prismarine_bricks": "2021-05-02 09:29:37 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/glass": {
-    "criteria": {
-      "has_sand": "2021-05-02 09:04:37 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/lime_bed": {
-    "criteria": {
-      "has_lime_wool": "2021-05-02 09:06:39 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/white_glazed_terracotta": {
-    "criteria": {
-      "has_white_terracotta": "2021-05-02 09:27:08 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/mossy_cobblestone_wall_from_mossy_cobblestone_stonecutting": {
-    "criteria": {
-      "has_mossy_cobblestone": "2021-05-02 09:24:13 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/combat/bow": {
-    "criteria": {
-      "has_string": "2021-05-02 09:54:38 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/oak_fence_gate": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:12 -0600"
-    },
-    "done": true
-  },
-  "minecraft:nether/get_wither_skull": {
-    "criteria": {
-      "wither_skull": "2021-05-02 10:10:57 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/acacia_slab": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:31 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/black_stained_glass_pane": {
-    "criteria": {
-      "has_glass": "2021-05-02 09:29:24 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/smooth_sandstone": {
-    "criteria": {
-      "has_sandstone": "2021-05-02 09:06:16 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/purple_concrete_powder": {
-    "criteria": {
-      "has_sand": "2021-05-02 09:04:37 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/cut_sandstone_slab_from_cut_sandstone_stonecutting": {
-    "criteria": {
-      "has_cut_sandstone": "2021-05-02 09:06:16 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/smooth_stone_slab": {
-    "criteria": {
-      "has_smooth_stone": "2021-05-02 09:24:09 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/diorite_wall_from_diorite_stonecutting": {
-    "criteria": {
-      "has_diorite": "2021-05-02 09:03:24 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/orange_bed_from_white_bed": {
-    "criteria": {
-      "has_bed": "2021-05-02 10:10:00 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/orange_carpet": {
-    "criteria": {
-      "has_orange_wool": "2021-05-02 09:06:20 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/prismarine_slab": {
-    "criteria": {
-      "has_prismarine": "2021-05-02 09:29:26 -0600"
-    },
-    "done": true
-  },
-  "minecraft:story/mine_stone": {
-    "criteria": {
-      "get_stone": "2021-05-02 09:04:10 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/cyan_carpet_from_white_carpet": {
-    "criteria": {
-      "has_white_carpet": "2021-05-02 10:04:20 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/target": {
-    "criteria": {
-      "has_redstone": "2021-05-02 09:03:13 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/furnace": {
-    "criteria": {
-      "has_cobblestone": "2021-05-02 09:04:10 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/orange_glazed_terracotta": {
-    "criteria": {
-      "has_orange_terracotta": "2021-05-02 09:27:15 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/andesite_stairs": {
-    "criteria": {
-      "has_andesite": "2021-05-02 09:03:26 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/charcoal": {
-    "criteria": {
-      "has_log": "2021-05-02 09:04:57 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/crimson_door": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:33 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/orange_wool": {
-    "criteria": {
-      "has_white_wool": "2021-05-02 09:06:19 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/green_stained_glass_pane": {
-    "criteria": {
-      "has_glass": "2021-05-02 09:29:21 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/tools/wooden_pickaxe": {
-    "criteria": {
-      "has_stick": "2021-05-02 09:54:16 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/cyan_carpet": {
-    "criteria": {
-      "has_cyan_wool": "2021-05-02 09:06:43 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/mossy_stone_brick_stairs": {
-    "criteria": {
-      "has_mossy_stone_bricks": "2021-05-02 09:25:37 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/redstone_torch": {
-    "criteria": {
-      "has_redstone": "2021-05-02 09:03:13 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/stone_brick_slab": {
-    "criteria": {
-      "has_stone_bricks": "2021-05-02 09:25:35 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/warped_pressure_plate": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:35 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/transportation/birch_boat": {
-    "criteria": {
-      "in_water": "2021-05-02 09:53:13 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/polished_granite_slab_from_granite_stonecutting": {
-    "criteria": {
-      "has_granite": "2021-05-02 09:03:21 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/green_concrete_powder": {
-    "criteria": {
-      "has_sand": "2021-05-02 09:04:37 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/enchanting_table": {
-    "criteria": {
-      "has_obsidian": "2021-05-02 09:24:14 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/polished_andesite_from_andesite_stonecutting": {
-    "criteria": {
-      "has_andesite": "2021-05-02 09:03:26 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/dark_oak_wood": {
-    "criteria": {
-      "has_log": "2021-05-02 09:05:02 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/acacia_door": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:31 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/orange_stained_glass": {
-    "criteria": {
-      "has_glass": "2021-05-02 09:06:13 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/chiseled_polished_blackstone_from_polished_blackstone_stonecutting": {
-    "criteria": {
-      "has_polished_blackstone": "2021-05-02 09:34:48 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/polished_blackstone": {
-    "criteria": {
-      "has_blackstone": "2021-05-02 09:34:31 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/cyan_bed_from_white_bed": {
-    "criteria": {
-      "has_bed": "2021-05-02 10:10:00 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/bowl": {
-    "criteria": {
-      "has_brown_mushroom": "2021-05-02 09:56:22 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/diamond_from_smelting": {
-    "criteria": {
-      "has_diamond_ore": "2021-05-02 09:24:39 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/lime_bed_from_white_bed": {
-    "criteria": {
-      "has_bed": "2021-05-02 10:10:00 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/tools/clock": {
-    "criteria": {
-      "has_redstone": "2021-05-02 09:03:13 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/tools/stone_hoe": {
-    "criteria": {
-      "has_cobblestone": "2021-05-02 09:04:10 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/crimson_trapdoor": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:33 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/spruce_fence": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:13 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/green_glazed_terracotta": {
-    "criteria": {
-      "has_green_terracotta": "2021-05-02 09:27:55 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/food/dried_kelp": {
-    "criteria": {
-      "has_dried_kelp_block": "2021-05-02 09:34:28 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/transportation/activator_rail": {
-    "criteria": {
-      "has_rail": "2021-05-02 10:31:10 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/white_stained_glass_pane": {
-    "criteria": {
-      "has_glass": "2021-05-02 09:28:22 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/dark_oak_stairs": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:32 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/polished_granite_slab_from_polished_granite_stonecutting": {
-    "criteria": {
-      "has_polished_granite": "2021-05-02 09:03:22 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/green_bed_from_white_bed": {
-    "criteria": {
-      "has_bed": "2021-05-02 10:10:00 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/red_glazed_terracotta": {
-    "criteria": {
-      "has_red_terracotta": "2021-05-02 09:27:57 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/blue_ice": {
-    "criteria": {
-      "has_packed_ice": "2021-05-02 09:28:17 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/granite_slab": {
-    "criteria": {
-      "has_granite": "2021-05-02 09:03:21 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/smooth_sandstone_stairs": {
-    "criteria": {
-      "has_smooth_sandstone": "2021-05-02 09:24:08 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/white_carpet": {
-    "criteria": {
-      "has_white_wool": "2021-05-02 09:06:19 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/dark_oak_sign": {
-    "criteria": {
-      "has_dark_oak_planks": "2021-05-02 09:04:32 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/spruce_stairs": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:13 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/black_stained_glass_pane_from_glass_pane": {
-    "criteria": {
-      "has_glass_pane": "2021-05-02 10:02:40 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/magenta_carpet_from_white_carpet": {
-    "criteria": {
-      "has_white_carpet": "2021-05-02 10:04:20 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/red_sandstone_slab": {
-    "criteria": {
-      "has_red_sandstone": "2021-05-02 09:29:44 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/dark_prismarine_slab": {
-    "criteria": {
-      "has_dark_prismarine": "2021-05-02 09:29:38 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/pink_stained_glass_pane_from_glass_pane": {
-    "criteria": {
-      "has_glass_pane": "2021-05-02 10:02:39 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/birch_door": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:14 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/green_banner": {
-    "criteria": {
-      "has_green_wool": "2021-05-02 09:07:00 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/jack_o_lantern": {
-    "criteria": {
-      "has_carved_pumpkin": "2021-05-02 09:25:09 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/glass_pane": {
-    "criteria": {
-      "has_glass": "2021-05-02 09:06:13 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/purple_banner": {
-    "criteria": {
-      "has_purple_wool": "2021-05-02 09:06:44 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/lapis_lazuli": {
-    "criteria": {
-      "has_lapis_block": "2021-05-02 09:06:15 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/cut_red_sandstone_slab": {
-    "criteria": {
-      "has_cut_red_sandstone": "2021-05-02 09:29:46 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/magenta_stained_glass": {
-    "criteria": {
-      "has_glass": "2021-05-02 09:06:13 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/red_sandstone_stairs_from_red_sandstone_stonecutting": {
-    "criteria": {
-      "has_red_sandstone": "2021-05-02 09:29:44 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/polished_granite_stairs_from_granite_stonecutting": {
-    "criteria": {
-      "has_granite": "2021-05-02 09:03:21 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/green_carpet_from_white_carpet": {
-    "criteria": {
-      "has_white_carpet": "2021-05-02 10:04:20 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/polished_blackstone_brick_stairs_from_polished_blackstone_bricks_stonecutting": {
-    "criteria": {
-      "has_polished_blackstone_bricks": "2021-05-02 09:34:52 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/soul_lantern": {
-    "criteria": {
-      "has_soul_torch": "2021-05-02 09:02:50 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/transportation/oak_boat": {
-    "criteria": {
-      "in_water": "2021-05-02 09:53:13 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/anvil": {
-    "criteria": {
-      "has_iron_block": "2021-05-02 09:07:17 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/birch_fence": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:14 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/sandstone_wall_from_sandstone_stonecutting": {
-    "criteria": {
-      "has_sandstone": "2021-05-02 09:06:16 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/pink_dye_from_pink_tulip": {
-    "criteria": {
-      "has_red_flower": "2021-05-02 09:56:03 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/blue_bed_from_white_bed": {
-    "criteria": {
-      "has_bed": "2021-05-02 10:10:00 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/redstone_from_blasting": {
-    "criteria": {
-      "has_redstone_ore": "2021-05-02 09:24:44 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/cyan_concrete_powder": {
-    "criteria": {
-      "has_sand": "2021-05-02 09:04:37 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/stone_brick_stairs": {
-    "criteria": {
-      "has_stone_bricks": "2021-05-02 09:25:35 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/smooth_stone_slab_from_smooth_stone_stonecutting": {
-    "criteria": {
-      "has_smooth_stone": "2021-05-02 09:24:09 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/tools/wooden_axe": {
-    "criteria": {
-      "has_stick": "2021-05-02 09:54:16 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/spruce_sign": {
-    "criteria": {
-      "has_spruce_planks": "2021-05-02 09:04:13 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/lime_wool": {
-    "criteria": {
-      "has_white_wool": "2021-05-02 09:06:19 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/mossy_stone_brick_wall_from_mossy_stone_brick_stonecutting": {
-    "criteria": {
-      "has_mossy_stone_bricks": "2021-05-02 09:25:37 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/mossy_stone_bricks": {
-    "criteria": {
-      "has_mossy_cobblestone": "2021-05-02 09:24:13 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/paper": {
-    "criteria": {
-      "has_reeds": "2021-05-02 09:57:48 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/polished_diorite_stairs_from_diorite_stonecutting": {
-    "criteria": {
-      "has_diorite": "2021-05-02 09:03:25 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/stone_bricks_from_stone_stonecutting": {
-    "criteria": {
-      "has_stone": "2021-05-02 09:03:19 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/chiseled_nether_bricks_from_nether_bricks_stonecutting": {
-    "criteria": {
-      "has_nether_bricks": "2021-05-02 09:25:48 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/pink_dye_from_peony": {
-    "criteria": {
-      "has_double_plant": "2021-05-02 10:05:05 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/chiseled_sandstone": {
-    "criteria": {
-      "has_stone_slab": "2021-05-02 09:08:05 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/loom": {
-    "criteria": {
-      "has_string": "2021-05-02 09:54:38 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/green_wool": {
-    "criteria": {
-      "has_white_wool": "2021-05-02 09:06:19 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/warped_button": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:35 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/stone_brick_wall_from_stone_bricks_stonecutting": {
-    "criteria": {
-      "has_stone_bricks": "2021-05-02 09:25:35 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/end_stone_bricks_from_end_stone_stonecutting": {
-    "criteria": {
-      "has_end_stone": "2021-05-02 09:26:15 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/end_stone_brick_stairs_from_end_stone_stonecutting": {
-    "criteria": {
-      "has_end_stone": "2021-05-02 09:26:15 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/red_nether_brick_stairs": {
-    "criteria": {
-      "has_red_nether_bricks": "2021-05-02 09:30:04 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/brown_wool": {
-    "criteria": {
-      "has_white_wool": "2021-05-02 09:06:19 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/chiseled_stone_bricks_from_stone_bricks_stonecutting": {
-    "criteria": {
-      "has_stone_bricks": "2021-05-02 09:25:35 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/scaffolding": {
-    "criteria": {
-      "has_bamboo": "2021-05-02 09:57:50 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/polished_blackstone_bricks": {
-    "criteria": {
-      "has_polished_blackstone": "2021-05-02 09:34:48 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/warped_stairs": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:35 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/painting": {
-    "criteria": {
-      "has_wool": "2021-05-02 09:06:19 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/pink_bed": {
-    "criteria": {
-      "has_pink_wool": "2021-05-02 09:06:40 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/white_stained_glass": {
-    "criteria": {
-      "has_glass": "2021-05-02 09:06:13 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/warped_trapdoor": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:35 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/blue_glazed_terracotta": {
-    "criteria": {
-      "has_blue_terracotta": "2021-05-02 09:27:52 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/red_terracotta": {
-    "criteria": {
-      "has_terracotta": "2021-05-02 09:28:14 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/red_sandstone_wall": {
-    "criteria": {
-      "has_red_sandstone": "2021-05-02 09:29:44 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/nether_brick": {
-    "criteria": {
-      "has_netherrack": "2021-05-02 09:25:10 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/stone_slab": {
-    "criteria": {
-      "has_stone": "2021-05-02 09:03:19 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/andesite_slab_from_andesite_stonecutting": {
-    "criteria": {
-      "has_andesite": "2021-05-02 09:03:26 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/acacia_fence_gate": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:31 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/black_carpet": {
-    "criteria": {
-      "has_black_wool": "2021-05-02 09:07:02 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/gold_ingot": {
-    "criteria": {
-      "has_gold_ore": "2021-05-02 09:04:40 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/tools/stone_shovel": {
-    "criteria": {
-      "has_cobblestone": "2021-05-02 09:04:10 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/yellow_stained_glass_pane": {
-    "criteria": {
-      "has_glass": "2021-05-02 09:28:43 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/blue_concrete_powder": {
-    "criteria": {
-      "has_sand": "2021-05-02 09:04:37 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/mossy_cobblestone_slab": {
-    "criteria": {
-      "has_mossy_cobblestone": "2021-05-02 09:24:13 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/emerald_from_smelting": {
-    "criteria": {
-      "has_emerald_ore": "2021-05-02 09:26:20 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/diorite_stairs": {
-    "criteria": {
-      "has_diorite": "2021-05-02 09:03:24 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/cyan_stained_glass": {
-    "criteria": {
-      "has_glass": "2021-05-02 09:06:13 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/spruce_slab": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:13 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/coal_from_smelting": {
-    "criteria": {
-      "has_coal_ore": "2021-05-02 09:04:56 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/green_terracotta": {
-    "criteria": {
-      "has_terracotta": "2021-05-02 09:28:14 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/granite_slab_from_granite_stonecutting": {
-    "criteria": {
-      "has_granite": "2021-05-02 09:03:21 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/polished_blackstone_wall_from_blackstone_stonecutting": {
-    "criteria": {
-      "has_blackstone": "2021-05-02 09:34:31 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/jungle_sign": {
-    "criteria": {
-      "has_jungle_planks": "2021-05-02 09:04:16 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/prismarine_stairs_from_prismarine_stonecutting": {
-    "criteria": {
-      "has_prismarine": "2021-05-02 09:29:26 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/brown_terracotta": {
-    "criteria": {
-      "has_terracotta": "2021-05-02 09:28:14 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/andesite": {
-    "criteria": {
-      "has_stone": "2021-05-02 09:03:24 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/polished_andesite_stairs_from_polished_andesite_stonecutting": {
-    "criteria": {
-      "has_polished_andesite": "2021-05-02 09:03:28 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/crimson_planks": {
-    "criteria": {
-      "has_logs": "2021-05-02 09:05:16 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/acacia_trapdoor": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:31 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/cut_sandstone_slab_from_sandstone_stonecutting": {
-    "criteria": {
-      "has_sandstone": "2021-05-02 09:06:16 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/warped_sign": {
-    "criteria": {
-      "has_warped_planks": "2021-05-02 09:04:35 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/oak_pressure_plate": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:12 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/lapis_from_blasting": {
-    "criteria": {
-      "has_lapis_ore": "2021-05-02 09:06:14 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/brown_carpet_from_white_carpet": {
-    "criteria": {
-      "has_white_carpet": "2021-05-02 10:04:20 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/white_stained_glass_pane_from_glass_pane": {
-    "criteria": {
-      "has_glass_pane": "2021-05-02 10:02:39 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/cut_sandstone": {
-    "criteria": {
-      "has_sandstone": "2021-05-02 09:06:16 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/brick_slab_from_bricks_stonecutting": {
-    "criteria": {
-      "has_bricks": "2021-05-02 09:24:11 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/smooth_red_sandstone": {
-    "criteria": {
-      "has_red_sandstone": "2021-05-02 09:29:44 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/black_terracotta": {
-    "criteria": {
-      "has_terracotta": "2021-05-02 09:28:14 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/blackstone_wall": {
-    "criteria": {
-      "has_blackstone": "2021-05-02 09:34:31 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/skull_banner_pattern": {
-    "criteria": {
-      "has_wither_skeleton_skull": "2021-05-02 10:10:57 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/polished_basalt": {
-    "criteria": {
-      "has_basalt": "2021-05-02 09:25:16 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/polished_blackstone_stairs_from_polished_blackstone_stonecutting": {
-    "criteria": {
-      "has_polished_blackstone": "2021-05-02 09:34:48 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/oak_wood": {
-    "criteria": {
-      "has_log": "2021-05-02 09:04:57 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/smooth_quartz_stairs": {
-    "criteria": {
-      "has_smooth_quartz": "2021-05-02 09:08:40 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/blackstone_slab": {
-    "criteria": {
-      "has_blackstone": "2021-05-02 09:34:31 -0600"
-    },
-    "done": true
-  },
-  "minecraft:story/root": {
-    "criteria": {
-      "crafting_table": "2021-05-02 10:00:29 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/slime_ball": {
-    "criteria": {
-      "has_slime": "2021-05-02 10:05:01 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/dropper": {
-    "criteria": {
-      "has_redstone": "2021-05-02 09:03:13 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/sandstone_slab_from_sandstone_stonecutting": {
-    "criteria": {
-      "has_sandstone": "2021-05-02 09:06:16 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/pink_glazed_terracotta": {
-    "criteria": {
-      "has_pink_terracotta": "2021-05-02 09:27:22 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/coal": {
-    "criteria": {
-      "has_coal_block": "2021-05-02 09:28:15 -0600"
-    },
-    "done": true
-  },
-  "minecraft:story/form_obsidian": {
-    "criteria": {
-      "obsidian": "2021-05-02 09:24:14 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/netherite_ingot_from_netherite_block": {
-    "criteria": {
-      "has_netherite_block": "2021-05-02 09:34:29 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/jungle_slab": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:16 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/quartz_from_blasting": {
-    "criteria": {
-      "has_nether_quartz_ore": "2021-05-02 09:26:45 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/polished_granite_slab": {
-    "criteria": {
-      "has_polished_granite": "2021-05-02 09:03:22 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/acacia_planks": {
-    "criteria": {
-      "has_log": "2021-05-02 09:05:01 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/tools/compass": {
-    "criteria": {
-      "has_redstone": "2021-05-02 09:03:13 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/repeater": {
-    "criteria": {
-      "has_redstone_torch": "2021-05-02 10:28:50 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/oak_button": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:12 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/iron_ingot": {
-    "criteria": {
-      "has_iron_ore": "2021-05-02 09:04:55 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/blue_carpet_from_white_carpet": {
-    "criteria": {
-      "has_white_carpet": "2021-05-02 10:04:20 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/polished_andesite": {
-    "criteria": {
-      "has_stone": "2021-05-02 09:03:26 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/polished_blackstone_slab_from_polished_blackstone_stonecutting": {
-    "criteria": {
-      "has_polished_blackstone": "2021-05-02 09:34:48 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/dark_oak_pressure_plate": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:32 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/mossy_stone_brick_slab_from_mossy_stone_brick_stonecutting": {
-    "criteria": {
-      "has_mossy_stone_bricks": "2021-05-02 09:25:37 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/lime_glazed_terracotta": {
-    "criteria": {
-      "has_lime_terracotta": "2021-05-02 09:27:20 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/smooth_quartz_stairs_from_smooth_quartz_stonecutting": {
-    "criteria": {
-      "has_smooth_quartz": "2021-05-02 09:08:40 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/composter": {
-    "criteria": {
-      "has_wood_slab": "2021-05-02 09:07:18 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/nether_brick_fence": {
-    "criteria": {
-      "has_nether_brick": "2021-05-02 09:25:48 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/chiseled_polished_blackstone_from_blackstone_stonecutting": {
-    "criteria": {
-      "has_blackstone": "2021-05-02 09:34:31 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/magenta_dye_from_allium": {
-    "criteria": {
-      "has_red_flower": "2021-05-02 09:55:57 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/transportation/powered_rail": {
-    "criteria": {
-      "has_rail": "2021-05-02 10:31:10 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/red_bed": {
-    "criteria": {
-      "has_red_wool": "2021-05-02 09:07:01 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/red_nether_brick_stairs_from_red_nether_bricks_stonecutting": {
-    "criteria": {
-      "has_nether_bricks": "2021-05-02 09:30:04 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/cyan_bed": {
-    "criteria": {
-      "has_cyan_wool": "2021-05-02 09:06:43 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/blackstone_stairs_from_blackstone_stonecutting": {
-    "criteria": {
-      "has_blackstone": "2021-05-02 09:34:31 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/oak_door": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:12 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/transportation/jungle_boat": {
-    "criteria": {
-      "in_water": "2021-05-02 09:53:13 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/stone_brick_walls_from_stone_stonecutting": {
-    "criteria": {
-      "has_stone": "2021-05-02 09:03:19 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/birch_stairs": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:14 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/iron_ingot_from_iron_block": {
-    "criteria": {
-      "has_iron_block": "2021-05-02 09:07:17 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/jungle_stairs": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:16 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/polished_blackstone_stairs": {
-    "criteria": {
-      "has_polished_blackstone": "2021-05-02 09:34:48 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/end_stone_bricks": {
-    "criteria": {
-      "has_end_stone": "2021-05-02 09:26:15 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/polished_blackstone_wall": {
-    "criteria": {
-      "has_polished_blackstone": "2021-05-02 09:34:48 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/purpur_pillar": {
-    "criteria": {
-      "has_purpur_block": "2021-05-02 09:24:15 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/food/pumpkin_pie": {
-    "criteria": {
-      "has_pumpkin": "2021-05-02 09:25:07 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/transportation/dark_oak_boat": {
-    "criteria": {
-      "in_water": "2021-05-02 09:53:13 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/dark_prismarine_stairs": {
-    "criteria": {
-      "has_dark_prismarine": "2021-05-02 09:29:38 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/andesite_wall_from_andesite_stonecutting": {
-    "criteria": {
-      "has_andesite": "2021-05-02 09:03:26 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/pink_wool": {
-    "criteria": {
-      "has_white_wool": "2021-05-02 09:06:19 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/jungle_door": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:16 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/cut_red_sandstone_slab_from_cut_red_sandstone_stonecutting": {
-    "criteria": {
-      "has_cut_red_sandstone": "2021-05-02 09:29:44 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/nether_brick_stairs": {
-    "criteria": {
-      "has_nether_brick": "2021-05-02 09:25:48 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/brown_bed_from_white_bed": {
-    "criteria": {
-      "has_bed": "2021-05-02 10:10:00 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/end_stone_brick_wall_from_end_stone_stonecutting": {
-    "criteria": {
-      "has_end_stone": "2021-05-02 09:26:15 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/polished_granite": {
-    "criteria": {
-      "has_stone": "2021-05-02 09:03:21 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/jungle_fence_gate": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:16 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/transportation/warped_fungus_on_a_stick": {
-    "criteria": {
-      "has_warped_fungus": "2021-05-02 09:56:25 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/birch_trapdoor": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:14 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/polished_blackstone_brick_slab_from_blackstone_stonecutting": {
-    "criteria": {
-      "has_blackstone": "2021-05-02 09:34:31 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/piston": {
-    "criteria": {
-      "has_redstone": "2021-05-02 09:03:13 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/stripped_jungle_wood": {
-    "criteria": {
-      "has_log": "2021-05-02 09:05:19 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/stone_stairs": {
-    "criteria": {
-      "has_stone": "2021-05-02 09:03:19 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/warped_door": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:35 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/redstone/spruce_trapdoor": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:13 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/chiseled_red_sandstone": {
-    "criteria": {
-      "has_red_sandstone": "2021-05-02 09:29:44 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/red_sandstone_slab_from_red_sandstone_stonecutting": {
-    "criteria": {
-      "has_red_sandstone": "2021-05-02 09:29:44 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/gray_stained_glass_pane_from_glass_pane": {
-    "criteria": {
-      "has_glass_pane": "2021-05-02 10:02:40 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/light_gray_stained_glass_pane_from_glass_pane": {
-    "criteria": {
-      "has_glass_pane": "2021-05-02 10:02:40 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/stripped_spruce_wood": {
-    "criteria": {
-      "has_log": "2021-05-02 09:05:18 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/purpur_slab_from_purpur_block_stonecutting": {
-    "criteria": {
-      "has_purpur_block": "2021-05-02 09:24:15 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/misc/quartz": {
-    "criteria": {
-      "has_nether_quartz_ore": "2021-05-02 09:26:45 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/brown_stained_glass_pane": {
-    "criteria": {
-      "has_glass": "2021-05-02 09:28:58 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/yellow_carpet_from_white_carpet": {
-    "criteria": {
-      "has_white_carpet": "2021-05-02 10:04:20 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/dark_oak_fence": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:32 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/smooth_red_sandstone_slab": {
-    "criteria": {
-      "has_smooth_red_sandstone": "2021-05-02 09:24:05 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/polished_blackstone_brick_wall_from_polished_blackstone_stonecutting": {
-    "criteria": {
-      "has_polished_blackstone": "2021-05-02 09:34:48 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/polished_blackstone_brick_wall_from_polished_blackstone_bricks_stonecutting": {
-    "criteria": {
-      "has_polished_blackstone_bricks": "2021-05-02 09:34:52 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/gray_carpet_from_white_carpet": {
-    "criteria": {
-      "has_white_carpet": "2021-05-02 10:04:20 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/soul_campfire": {
-    "criteria": {
-      "has_soul_sand": "2021-05-02 09:25:11 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/pink_carpet_from_white_carpet": {
-    "criteria": {
-      "has_white_carpet": "2021-05-02 10:04:20 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/mossy_cobblestone_stairs": {
-    "criteria": {
-      "has_mossy_cobblestone": "2021-05-02 09:24:13 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/light_blue_bed": {
-    "criteria": {
-      "has_light_blue_wool": "2021-05-02 09:06:35 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/pink_stained_glass": {
-    "criteria": {
-      "has_glass": "2021-05-02 09:06:13 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/light_gray_stained_glass_pane": {
-    "criteria": {
-      "has_glass": "2021-05-02 09:28:50 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/decorations/light_gray_bed_from_white_bed": {
-    "criteria": {
-      "has_bed": "2021-05-02 10:10:00 -0600"
-    },
-    "done": true
-  },
-  "minecraft:recipes/building_blocks/warped_slab": {
-    "criteria": {
-      "has_planks": "2021-05-02 09:04:35 -0600"
-    },
-    "done": true
-  },
-  "DataVersion": 2586
-}
\ No newline at end of file
diff --git a/tests/1.16 Block Test Map/data/raids.dat b/tests/1.16 Block Test Map/data/raids.dat
deleted file mode 100644
index d6a996c..0000000
Binary files a/tests/1.16 Block Test Map/data/raids.dat and /dev/null differ
diff --git a/tests/1.16 Block Test Map/icon.png b/tests/1.16 Block Test Map/icon.png
deleted file mode 100644
index f59650b..0000000
Binary files a/tests/1.16 Block Test Map/icon.png and /dev/null differ
diff --git a/tests/1.16 Block Test Map/level.dat b/tests/1.16 Block Test Map/level.dat
deleted file mode 100644
index 1d6f5ac..0000000
Binary files a/tests/1.16 Block Test Map/level.dat and /dev/null differ
diff --git a/tests/1.16 Block Test Map/level.dat_old b/tests/1.16 Block Test Map/level.dat_old
deleted file mode 100644
index 8d0c783..0000000
Binary files a/tests/1.16 Block Test Map/level.dat_old and /dev/null differ
diff --git a/tests/1.16 Block Test Map/playerdata/f85d27c5-f726-4594-9562-c207c8f36c84.dat b/tests/1.16 Block Test Map/playerdata/f85d27c5-f726-4594-9562-c207c8f36c84.dat
deleted file mode 100644
index b201c0b..0000000
Binary files a/tests/1.16 Block Test Map/playerdata/f85d27c5-f726-4594-9562-c207c8f36c84.dat and /dev/null differ
diff --git a/tests/1.16 Block Test Map/playerdata/f85d27c5-f726-4594-9562-c207c8f36c84.dat_old b/tests/1.16 Block Test Map/playerdata/f85d27c5-f726-4594-9562-c207c8f36c84.dat_old
deleted file mode 100644
index b201c0b..0000000
Binary files a/tests/1.16 Block Test Map/playerdata/f85d27c5-f726-4594-9562-c207c8f36c84.dat_old and /dev/null differ
diff --git a/tests/1.16 Block Test Map/poi/r.-1.-1.mca b/tests/1.16 Block Test Map/poi/r.-1.-1.mca
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/1.16 Block Test Map/poi/r.-1.0.mca b/tests/1.16 Block Test Map/poi/r.-1.0.mca
deleted file mode 100644
index 47fee34..0000000
Binary files a/tests/1.16 Block Test Map/poi/r.-1.0.mca and /dev/null differ
diff --git a/tests/1.16 Block Test Map/poi/r.-1.1.mca b/tests/1.16 Block Test Map/poi/r.-1.1.mca
deleted file mode 100644
index 17f0d2a..0000000
Binary files a/tests/1.16 Block Test Map/poi/r.-1.1.mca and /dev/null differ
diff --git a/tests/1.16 Block Test Map/poi/r.0.-1.mca b/tests/1.16 Block Test Map/poi/r.0.-1.mca
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/1.16 Block Test Map/poi/r.0.0.mca b/tests/1.16 Block Test Map/poi/r.0.0.mca
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/1.16 Block Test Map/poi/r.0.1.mca b/tests/1.16 Block Test Map/poi/r.0.1.mca
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/1.16 Block Test Map/region/r.-1.-1.mca b/tests/1.16 Block Test Map/region/r.-1.-1.mca
deleted file mode 100644
index 2bd09b7..0000000
Binary files a/tests/1.16 Block Test Map/region/r.-1.-1.mca and /dev/null differ
diff --git a/tests/1.16 Block Test Map/region/r.-1.0.mca b/tests/1.16 Block Test Map/region/r.-1.0.mca
deleted file mode 100644
index 9e44117..0000000
Binary files a/tests/1.16 Block Test Map/region/r.-1.0.mca and /dev/null differ
diff --git a/tests/1.16 Block Test Map/region/r.-1.1.mca b/tests/1.16 Block Test Map/region/r.-1.1.mca
deleted file mode 100644
index fcd7b6d..0000000
Binary files a/tests/1.16 Block Test Map/region/r.-1.1.mca and /dev/null differ
diff --git a/tests/1.16 Block Test Map/region/r.0.-1.mca b/tests/1.16 Block Test Map/region/r.0.-1.mca
deleted file mode 100644
index 65e9b6e..0000000
Binary files a/tests/1.16 Block Test Map/region/r.0.-1.mca and /dev/null differ
diff --git a/tests/1.16 Block Test Map/region/r.0.0.mca b/tests/1.16 Block Test Map/region/r.0.0.mca
deleted file mode 100644
index 7c7543f..0000000
Binary files a/tests/1.16 Block Test Map/region/r.0.0.mca and /dev/null differ
diff --git a/tests/1.16 Block Test Map/region/r.0.1.mca b/tests/1.16 Block Test Map/region/r.0.1.mca
deleted file mode 100644
index dba6ed3..0000000
Binary files a/tests/1.16 Block Test Map/region/r.0.1.mca and /dev/null differ
diff --git a/tests/1.16 Block Test Map/session.lock b/tests/1.16 Block Test Map/session.lock
deleted file mode 100644
index 0d7e5f8..0000000
--- a/tests/1.16 Block Test Map/session.lock
+++ /dev/null
@@ -1,1 +0,0 @@
-☃
\ No newline at end of file
diff --git a/tests/1.16 Block Test Map/stats/f85d27c5-f726-4594-9562-c207c8f36c84.json b/tests/1.16 Block Test Map/stats/f85d27c5-f726-4594-9562-c207c8f36c84.json
deleted file mode 100644
index 904a35b..0000000
--- a/tests/1.16 Block Test Map/stats/f85d27c5-f726-4594-9562-c207c8f36c84.json
+++ /dev/null
@@ -1,1 +0,0 @@
-{"stats":{"minecraft:picked_up":{"minecraft:string":1,"minecraft:sea_pickle":1,"minecraft:stick":9,"minecraft:cactus":1,"minecraft:wheat_seeds":1,"minecraft:sunflower":6},"minecraft:used":{"minecraft:peony":1,"minecraft:light_blue_banner":1,"minecraft:nether_brick_fence":2,"minecraft:light_blue_stained_glass_pane":1,"minecraft:jack_o_lantern":1,"minecraft:blue_terracotta":1,"minecraft:red_sand":1,"minecraft:polished_granite_stairs":1,"minecraft:bone_block":1,"minecraft:light_gray_stained_glass_pane":1,"minecraft:melon":1,"minecraft:cut_red_sandstone_slab":1,"minecraft:smooth_quartz_slab":1,"minecraft:spruce_slab":1,"minecraft:red_sandstone_slab":1,"minecraft:dead_horn_coral_block":1,"minecraft:dead_tube_coral_fan":1,"minecraft:dispenser":1,"minecraft:polished_blackstone":1,"minecraft:poppy":1,"minecraft:dead_brain_coral":1,"minecraft:ladder":1,"minecraft:oak_sign":4,"minecraft:black_carpet":1,"minecraft:polished_basalt":1,"minecraft:white_concrete_powder":1,"minecraft:green_banner":1,"minecraft:chest":1,"minecraft:white_terracotta":1,"minecraft:stone":1,"minecraft:brown_concrete":1,"minecraft:white_shulker_box":1,"minecraft:purple_shulker_box":1,"minecraft:creeper_head":1,"minecraft:stripped_jungle_wood":1,"minecraft:quartz_stairs":1,"minecraft:red_nether_brick_stairs":1,"minecraft:warped_sign":1,"minecraft:bubble_coral":2,"minecraft:bell":1,"minecraft:grass":2,"minecraft:mossy_stone_brick_slab":1,"minecraft:player_head":1,"minecraft:smooth_sandstone_stairs":1,"minecraft:farmland":1,"minecraft:stripped_crimson_stem":1,"minecraft:green_terracotta":1,"minecraft:tube_coral":2,"minecraft:debug_stick":80,"minecraft:spruce_leaves":2,"minecraft:soul_campfire":1,"minecraft:chiseled_red_sandstone":1,"minecraft:soul_sand":1,"minecraft:birch_pressure_plate":1,"minecraft:lily_of_the_valley":1,"minecraft:blue_stained_glass":1,"minecraft:brick_slab":1,"minecraft:orange_glazed_terracotta":1,"minecraft:jungle_planks":1,"minecraft:nether_bricks":1,"minecraft:prismarine_brick_stairs":1,"minecraft:jungle_slab":1,"minecraft:allium":1,"minecraft:orange_wool":1,"minecraft:pink_terracotta":1,"minecraft:acacia_sapling":1,"minecraft:acacia_button":1,"minecraft:beehive":1,"minecraft:cyan_wool":1,"minecraft:azure_bluet":1,"minecraft:weeping_vines":1,"minecraft:stripped_spruce_log":1,"minecraft:obsidian":1,"minecraft:polished_blackstone_slab":1,"minecraft:red_sandstone_stairs":1,"minecraft:jungle_sapling":1,"minecraft:glass":10,"minecraft:crimson_button":1,"minecraft:packed_ice":1,"minecraft:honey_block":1,"minecraft:cyan_terracotta":1,"minecraft:lava_bucket":1,"minecraft:stripped_dark_oak_wood":1,"minecraft:bricks":1,"minecraft:coal_ore":1,"minecraft:quartz_pillar":1,"minecraft:lime_banner":1,"minecraft:black_stained_glass_pane":1,"minecraft:red_wool":1,"minecraft:jungle_fence_gate":1,"minecraft:quartz_slab":1,"minecraft:warped_hyphae":1,"minecraft:spruce_wood":1,"minecraft:lantern":1,"minecraft:grass_block":15,"minecraft:blue_orchid":1,"minecraft:birch_sapling":1,"minecraft:quartz_block":1,"minecraft:polished_andesite_stairs":1,"minecraft:orange_shulker_box":1,"minecraft:green_wool":1,"minecraft:polished_blackstone_brick_wall":2,"minecraft:warped_nylium":1,"minecraft:wet_sponge":1,"minecraft:dark_oak_wood":1,"minecraft:dark_oak_leaves":1,"minecraft:light_blue_bed":1,"minecraft:dark_oak_slab":1,"minecraft:mossy_cobblestone_slab":1,"minecraft:gray_concrete_powder":1,"minecraft:crimson_roots":2,"minecraft:dark_oak_button":1,"minecraft:diorite_wall":1,"minecraft:pink_wool":1,"minecraft:stone_brick_stairs":1,"minecraft:stone_bricks":1,"minecraft:blue_banner":1,"minecraft:item_frame":1,"minecraft:bedrock":2,"minecraft:red_nether_brick_wall":1,"minecraft:horn_coral":2,"minecraft:honeycomb_block":1,"minecraft:birch_button":1,"minecraft:birch_trapdoor":1,"minecraft:infested_mossy_stone_bricks":1,"minecraft:red_carpet":1,"minecraft:red_stained_glass_pane":1,"minecraft:repeater":1,"minecraft:redstone_lamp":1,"minecraft:snow":1,"minecraft:magma_block":1,"minecraft:brown_carpet":1,"minecraft:blue_shulker_box":1,"minecraft:warped_fungus":1,"minecraft:hay_block":1,"minecraft:infested_cracked_stone_bricks":2,"minecraft:scaffolding":1,"minecraft:jungle_button":1,"minecraft:furnace":1,"minecraft:yellow_glazed_terracotta":1,"minecraft:oak_sapling":1,"minecraft:gray_bed":1,"minecraft:acacia_fence":1,"minecraft:polished_diorite_stairs":1,"minecraft:gold_block":1,"minecraft:skeleton_skull":1,"minecraft:gray_banner":1,"minecraft:pink_concrete_powder":1,"minecraft:end_stone_brick_stairs":1,"minecraft:fire_coral_fan":1,"minecraft:large_fern":1,"minecraft:dragon_head":1,"minecraft:polished_blackstone_stairs":1,"minecraft:dead_brain_coral_fan":1,"minecraft:green_stained_glass_pane":1,"minecraft:seagrass":1,"minecraft:dark_prismarine_stairs":2,"minecraft:brown_concrete_powder":1,"minecraft:magenta_shulker_box":1,"minecraft:stripped_spruce_wood":1,"minecraft:lime_glazed_terracotta":1,"minecraft:dark_oak_sapling":1,"minecraft:light_blue_carpet":1,"minecraft:chiseled_polished_blackstone":1,"minecraft:emerald_ore":1,"minecraft:dark_oak_stairs":1,"minecraft:mossy_stone_bricks":1,"minecraft:tube_coral_fan":1,"minecraft:diorite":1,"minecraft:end_rod":2,"minecraft:damaged_anvil":1,"minecraft:end_stone_bricks":1,"minecraft:gray_carpet":1,"minecraft:spruce_door":1,"minecraft:brick_wall":1,"minecraft:wither_skeleton_skull":1,"minecraft:dropper":1,"minecraft:stripped_acacia_log":1,"minecraft:powered_rail":1,"minecraft:stripped_birch_log":1,"minecraft:infested_stone":1,"minecraft:dark_oak_sign":1,"minecraft:birch_leaves":1,"minecraft:red_stained_glass":1,"minecraft:acacia_trapdoor":1,"minecraft:oak_planks":1,"minecraft:blackstone_stairs":1,"minecraft:prismarine":1,"minecraft:jungle_fence":1,"minecraft:prismarine_stairs":1,"minecraft:birch_wood":1,"minecraft:blackstone":1,"minecraft:polished_diorite_slab":1,"minecraft:orange_tulip":1,"minecraft:cactus":2,"minecraft:lime_concrete_powder":1,"minecraft:mossy_stone_brick_stairs":1,"minecraft:sandstone":1,"minecraft:black_glazed_terracotta":2,"minecraft:composter":1,"minecraft:jungle_pressure_plate":1,"minecraft:redstone_block":1,"minecraft:end_stone_brick_slab":1,"minecraft:blue_bed":2,"minecraft:clay":1,"minecraft:sugar_cane":1,"minecraft:purpur_pillar":1,"minecraft:oak_leaves":1,"minecraft:smooth_quartz_stairs":1,"minecraft:white_wool":1,"minecraft:brain_coral":2,"minecraft:respawn_anchor":1,"minecraft:dark_oak_trapdoor":1,"minecraft:purpur_stairs":1,"minecraft:sand":6,"minecraft:light_blue_concrete":1,"minecraft:yellow_terracotta":2,"minecraft:infested_chiseled_stone_bricks":1,"minecraft:cut_sandstone_slab":1,"minecraft:lime_stained_glass_pane":1,"minecraft:loom":1,"minecraft:blast_furnace":1,"minecraft:light_blue_wool":1,"minecraft:black_banner":1,"minecraft:smooth_red_sandstone":1,"minecraft:light_gray_terracotta":1,"minecraft:yellow_concrete":1,"minecraft:blue_ice":1,"minecraft:acacia_leaves":1,"minecraft:purple_terracotta":1,"minecraft:dead_bubble_coral":2,"minecraft:sponge":1,"minecraft:blackstone_wall":1,"minecraft:chiseled_quartz_block":1,"minecraft:note_block":1,"minecraft:gray_concrete":1,"minecraft:stone_brick_wall":1,"minecraft:black_bed":1,"minecraft:black_concrete_powder":1,"minecraft:end_stone":3,"minecraft:red_sandstone_wall":1,"minecraft:crimson_fence_gate":1,"minecraft:purple_concrete_powder":1,"minecraft:birch_fence":1,"minecraft:redstone_torch":1,"minecraft:sticky_piston":1,"minecraft:black_terracotta":1,"minecraft:dead_fire_coral_fan":1,"minecraft:blue_glazed_terracotta":1,"minecraft:lever":1,"minecraft:green_concrete":1,"minecraft:red_sandstone":1,"minecraft:black_concrete":1,"minecraft:gray_shulker_box":1,"minecraft:birch_slab":1,"minecraft:daylight_detector":1,"minecraft:birch_fence_gate":1,"minecraft:acacia_door":1,"minecraft:chiseled_sandstone":1,"minecraft:crying_obsidian":1,"minecraft:zombie_head":1,"minecraft:dark_oak_log":1,"minecraft:gray_stained_glass_pane":1,"minecraft:light_gray_concrete":1,"minecraft:lime_carpet":1,"minecraft:heavy_weighted_pressure_plate":1,"minecraft:nether_sprouts":1,"minecraft:cyan_bed":1,"minecraft:brown_wool":1,"minecraft:brown_stained_glass":1,"minecraft:warped_slab":1,"minecraft:stone_button":1,"minecraft:cyan_carpet":1,"minecraft:lectern":1,"minecraft:white_glazed_terracotta":1,"minecraft:red_terracotta":1,"minecraft:polished_granite":1,"minecraft:oak_pressure_plate":1,"minecraft:diamond_ore":1,"minecraft:iron_door":1,"minecraft:cut_red_sandstone":1,"minecraft:prismarine_wall":1,"minecraft:gravel":1,"minecraft:lilac":1,"minecraft:polished_andesite_slab":1,"minecraft:soul_lantern":1,"minecraft:blackstone_slab":1,"minecraft:magenta_stained_glass_pane":1,"minecraft:granite_slab":1,"minecraft:stripped_warped_hyphae":1,"minecraft:dead_horn_coral":1,"minecraft:jungle_sign":1,"minecraft:nether_wart_block":1,"minecraft:smooth_red_sandstone_slab":1,"minecraft:coal_block":1,"minecraft:light_blue_concrete_powder":1,"minecraft:dead_horn_coral_fan":1,"minecraft:stripped_oak_wood":1,"minecraft:crimson_door":1,"minecraft:birch_log":1,"minecraft:grindstone":1,"minecraft:dark_oak_fence":1,"minecraft:smithing_table":1,"minecraft:ice":1,"minecraft:bubble_coral_block":1,"minecraft:white_stained_glass":1,"minecraft:green_concrete_powder":1,"minecraft:andesite":1,"minecraft:chain":1,"minecraft:smooth_stone_slab":1,"minecraft:pink_stained_glass":1,"minecraft:mossy_cobblestone_wall":1,"minecraft:white_concrete":1,"minecraft:coarse_dirt":1,"minecraft:warped_button":1,"minecraft:chorus_plant":1,"minecraft:purple_wool":1,"minecraft:diorite_slab":2,"minecraft:cobweb":26,"minecraft:gray_stained_glass":1,"minecraft:diorite_stairs":1,"minecraft:oak_fence":1,"minecraft:oak_trapdoor":1,"minecraft:purpur_block":1,"minecraft:stripped_acacia_wood":1,"minecraft:petrified_oak_slab":1,"minecraft:orange_concrete_powder":1,"minecraft:iron_bars":1,"minecraft:jungle_leaves":1,"minecraft:polished_blackstone_brick_slab":1,"minecraft:rose_bush":1,"minecraft:enchanting_table":1,"minecraft:white_tulip":2,"minecraft:dead_tube_coral_block":1,"minecraft:crimson_fence":1,"minecraft:granite_stairs":1,"minecraft:acacia_log":1,"minecraft:dead_bush":10,"minecraft:orange_concrete":1,"minecraft:brain_coral_fan":1,"minecraft:red_concrete":1,"minecraft:carved_pumpkin":1,"minecraft:redstone_ore":1,"minecraft:armor_stand":1,"minecraft:redstone":2,"minecraft:light_gray_carpet":1,"minecraft:mycelium":3,"minecraft:light_blue_terracotta":1,"minecraft:stripped_warped_stem":1,"minecraft:spruce_planks":1,"minecraft:ender_chest":1,"minecraft:brown_shulker_box":1,"minecraft:stripped_birch_wood":1,"minecraft:polished_blackstone_wall":1,"minecraft:sandstone_wall":1,"minecraft:dried_kelp_block":1,"minecraft:netherrack":2,"minecraft:green_glazed_terracotta":1,"minecraft:oak_door":1,"minecraft:orange_bed":1,"minecraft:orange_stained_glass":1,"minecraft:torch":2,"minecraft:light_gray_shulker_box":1,"minecraft:anvil":1,"minecraft:end_stone_brick_wall":1,"minecraft:target":1,"minecraft:brewing_stand":1,"minecraft:ancient_debris":1,"minecraft:dirt":1,"minecraft:warped_wart_block":1,"minecraft:red_nether_brick_slab":1,"minecraft:white_carpet":1,"minecraft:hopper":1,"minecraft:chorus_flower":1,"minecraft:nether_quartz_ore":1,"minecraft:prismarine_bricks":1,"minecraft:spruce_stairs":1,"minecraft:twisting_vines":1,"minecraft:warped_door":1,"minecraft:cobblestone_wall":1,"minecraft:crimson_hyphae":1,"minecraft:stone_stairs":1,"minecraft:nether_brick_slab":1,"minecraft:dead_tube_coral":1,"minecraft:shulker_box":2,"minecraft:light_gray_glazed_terracotta":1,"minecraft:oak_wood":1,"minecraft:yellow_carpet":1,"minecraft:bee_nest":1,"minecraft:pink_carpet":1,"minecraft:blue_concrete":1,"minecraft:jungle_trapdoor":1,"minecraft:polished_granite_slab":1,"minecraft:warped_pressure_plate":1,"minecraft:spruce_fence":1,"minecraft:granite":1,"minecraft:dead_brain_coral_block":1,"minecraft:light_blue_stained_glass":1,"minecraft:nether_brick_stairs":1,"minecraft:oak_fence_gate":1,"minecraft:polished_diorite":1,"minecraft:fern":2,"minecraft:purple_banner":1,"minecraft:andesite_wall":1,"minecraft:birch_door":1,"minecraft:shroomlight":1,"minecraft:mossy_cobblestone_stairs":1,"minecraft:purple_stained_glass_pane":1,"minecraft:dark_oak_planks":1,"minecraft:stone_brick_slab":1,"minecraft:red_shulker_box":1,"minecraft:brown_terracotta":1,"minecraft:sea_lantern":1,"minecraft:chipped_anvil":1,"minecraft:red_tulip":1,"minecraft:light_gray_stained_glass":1,"minecraft:bookshelf":1,"minecraft:cyan_shulker_box":1,"minecraft:smooth_sandstone":1,"minecraft:yellow_stained_glass":1,"minecraft:iron_block":2,"minecraft:light_weighted_pressure_plate":1,"minecraft:oak_stairs":1,"minecraft:spruce_button":1,"minecraft:slime_block":1,"minecraft:brick_stairs":1,"minecraft:warped_fence":1,"minecraft:acacia_slab":1,"minecraft:crimson_slab":1,"minecraft:jungle_log":1,"minecraft:polished_blackstone_brick_stairs":1,"minecraft:smooth_quartz":1,"minecraft:pink_stained_glass_pane":1,"minecraft:crimson_planks":1,"minecraft:barrel":1,"minecraft:prismarine_slab":1,"minecraft:bamboo":2,"minecraft:yellow_concrete_powder":1,"minecraft:light_gray_wool":1,"minecraft:basalt":1,"minecraft:tripwire_hook":1,"minecraft:purple_concrete":1,"minecraft:glowstone":1,"minecraft:tall_grass":1,"minecraft:emerald_block":1,"minecraft:lime_concrete":1,"minecraft:warped_stairs":1,"minecraft:black_stained_glass":1,"minecraft:magenta_wool":1,"minecraft:soul_soil":1,"minecraft:gray_glazed_terracotta":1,"minecraft:iron_trapdoor":1,"minecraft:magenta_terracotta":1,"minecraft:dark_oak_pressure_plate":1,"minecraft:lime_stained_glass":1,"minecraft:white_stained_glass_pane":1,"minecraft:podzol":2,"minecraft:cyan_banner":1,"minecraft:acacia_wood":1,"minecraft:infested_cobblestone":1,"minecraft:granite_wall":1,"minecraft:trapped_chest":2,"minecraft:cyan_concrete_powder":1,"minecraft:crimson_pressure_plate":1,"minecraft:horn_coral_block":1,"minecraft:stone_pressure_plate":1,"minecraft:dead_bubble_coral_block":1,"minecraft:dead_bubble_coral_fan":1,"minecraft:acacia_planks":1,"minecraft:mossy_stone_brick_wall":1,"minecraft:observer":1,"minecraft:orange_stained_glass_pane":1,"minecraft:turtle_egg":1,"minecraft:orange_terracotta":1,"minecraft:pumpkin":1,"minecraft:light_gray_bed":1,"minecraft:horn_coral_fan":1,"minecraft:brown_bed":1,"minecraft:brain_coral_block":2,"minecraft:crimson_stem":1,"minecraft:quartz_bricks":1,"minecraft:water_bucket":21,"minecraft:yellow_banner":1,"minecraft:warped_stem":1,"minecraft:nether_brick_wall":1,"minecraft:light_blue_shulker_box":1,"minecraft:light_blue_glazed_terracotta":1,"minecraft:iron_ore":1,"minecraft:pink_tulip":1,"minecraft:tube_coral_block":2,"minecraft:cobblestone_stairs":1,"minecraft:pink_glazed_terracotta":1,"minecraft:sandstone_stairs":1,"minecraft:lodestone":1,"minecraft:snow_block":1,"minecraft:prismarine_brick_slab":1,"minecraft:birch_planks":1,"minecraft:yellow_shulker_box":1,"minecraft:polished_andesite":1,"minecraft:bubble_coral_fan":1,"minecraft:lime_wool":1,"minecraft:red_nether_bricks":1,"minecraft:cartography_table":1,"minecraft:blue_wool":1,"minecraft:pink_banner":1,"minecraft:andesite_slab":1,"minecraft:smooth_sandstone_slab":1,"minecraft:spruce_fence_gate":1,"minecraft:chiseled_nether_bricks":1,"minecraft:cut_sandstone":1,"minecraft:purple_glazed_terracotta":1,"minecraft:black_shulker_box":1,"minecraft:red_mushroom_block":1,"minecraft:brown_mushroom":1,"minecraft:blue_concrete_powder":1,"minecraft:cauldron":1,"minecraft:spruce_sign":1,"minecraft:magenta_stained_glass":1,"minecraft:red_glazed_terracotta":1,"minecraft:fletching_table":1,"minecraft:beacon":1,"minecraft:stonecutter":1,"minecraft:green_bed":1,"minecraft:cobblestone_slab":2,"minecraft:gold_ore":2,"minecraft:warped_roots":1,"minecraft:cracked_stone_bricks":1,"minecraft:conduit":1,"minecraft:polished_blackstone_bricks":1,"minecraft:gilded_blackstone":1,"minecraft:pink_concrete":1,"minecraft:diamond_block":1,"minecraft:orange_carpet":1,"minecraft:jungle_wood":1,"minecraft:polished_blackstone_pressure_plate":1,"minecraft:lime_bed":1,"minecraft:mossy_cobblestone":1,"minecraft:light_gray_banner":1,"minecraft:lime_terracotta":1,"minecraft:stripped_dark_oak_log":1,"minecraft:terracotta":1,"minecraft:rail":1,"minecraft:comparator":1,"minecraft:vine":1,"minecraft:dark_prismarine_slab":1,"minecraft:crafting_table":1,"minecraft:oak_slab":14,"minecraft:tnt":1,"minecraft:cyan_glazed_terracotta":1,"minecraft:red_bed":1,"minecraft:crimson_trapdoor":3,"minecraft:magenta_bed":1,"minecraft:dead_fire_coral_block":1,"minecraft:red_mushroom":1,"minecraft:brown_banner":1,"minecraft:birch_sign":1,"minecraft:yellow_stained_glass_pane":1,"minecraft:white_bed":1,"minecraft:campfire":1,"minecraft:light_gray_concrete_powder":1,"minecraft:dark_oak_fence_gate":1,"minecraft:fire_coral":2,"minecraft:smoker":1,"minecraft:lime_shulker_box":1,"minecraft:brown_glazed_terracotta":1,"minecraft:painting":1,"minecraft:magenta_carpet":1,"minecraft:warped_planks":1,"minecraft:infested_stone_bricks":1,"minecraft:smooth_red_sandstone_stairs":1,"minecraft:cake":1,"minecraft:spruce_log":1,"minecraft:mushroom_stem":1,"minecraft:dark_oak_door":3,"minecraft:cyan_stained_glass":1,"minecraft:nether_gold_ore":2,"minecraft:oak_log":1,"minecraft:purple_stained_glass":1,"minecraft:blue_stained_glass_pane":1,"minecraft:gray_terracotta":1,"minecraft:dark_prismarine":1,"minecraft:piston":3,"minecraft:netherite_block":1,"minecraft:cornflower":1,"minecraft:flower_pot":1,"minecraft:activator_rail":1,"minecraft:acacia_stairs":1,"minecraft:black_wool":1,"minecraft:warped_trapdoor":1,"minecraft:oxeye_daisy":1,"minecraft:acacia_sign":1,"minecraft:stripped_jungle_log":1,"minecraft:dead_fire_coral":1,"minecraft:command_block":1,"minecraft:sunflower":9,"minecraft:cracked_polished_blackstone_bricks":1,"minecraft:oak_button":1,"minecraft:lapis_ore":1,"minecraft:detector_rail":1,"minecraft:spruce_pressure_plate":1,"minecraft:cracked_nether_bricks":1,"minecraft:magenta_concrete":1,"minecraft:jungle_stairs":1,"minecraft:cyan_concrete":1,"minecraft:stone_slab":1,"minecraft:purpur_slab":1,"minecraft:spruce_trapdoor":1,"minecraft:magenta_concrete_powder":1,"minecraft:spruce_sapling":2,"minecraft:crimson_nylium":1,"minecraft:yellow_wool":1,"minecraft:acacia_fence_gate":1,"minecraft:crimson_sign":1,"minecraft:magenta_banner":1,"minecraft:blue_carpet":1,"minecraft:green_carpet":1,"minecraft:pink_bed":1,"minecraft:chiseled_stone_bricks":1,"minecraft:brown_stained_glass_pane":1,"minecraft:fire_coral_block":1,"minecraft:green_shulker_box":1,"minecraft:crimson_fungus":2,"minecraft:cobblestone":1,"minecraft:stripped_crimson_hyphae":1,"minecraft:orange_banner":1,"minecraft:end_crystal":1,"minecraft:crimson_stairs":1,"minecraft:purple_bed":1,"minecraft:acacia_pressure_plate":1,"minecraft:cyan_stained_glass_pane":1,"minecraft:red_concrete_powder":1,"minecraft:white_banner":1,"minecraft:andesite_stairs":1,"minecraft:jungle_door":1,"minecraft:glass_pane":1,"minecraft:pink_shulker_box":1,"minecraft:warped_fence_gate":1,"minecraft:sandstone_slab":1,"minecraft:red_banner":1,"minecraft:lapis_block":2,"minecraft:magenta_glazed_terracotta":1,"minecraft:grass_path":1,"minecraft:sea_pickle":3,"minecraft:yellow_bed":1,"minecraft:end_portal_frame":1,"minecraft:wither_rose":1,"minecraft:polished_blackstone_button":1,"minecraft:stripped_oak_log":1,"minecraft:gray_wool":1,"minecraft:kelp":1,"minecraft:purple_carpet":1,"minecraft:dandelion":3,"minecraft:brown_mushroom_block":1,"minecraft:jukebox":2,"minecraft:smooth_stone":1,"minecraft:birch_stairs":1,"minecraft:soul_torch":1,"minecraft:green_stained_glass":1},"minecraft:custom":{"minecraft:leave_game":4,"minecraft:time_since_death":82793,"minecraft:sprint_one_cm":8207,"minecraft:jump":47,"minecraft:walk_on_water_one_cm":1601,"minecraft:play_one_minute":82793,"minecraft:crouch_one_cm":1123,"minecraft:walk_one_cm":184884,"minecraft:sneak_time":634,"minecraft:time_since_rest":82793,"minecraft:fly_one_cm":196558}},"DataVersion":2586}
\ No newline at end of file
diff --git a/tests/README.md b/tests/README.md
deleted file mode 100644
index 2f1deec..0000000
--- a/tests/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-Tests are currently broken!
-
-The examples can be used for basic testing.
diff --git a/tests/tests.py b/tests/tests.py
deleted file mode 100644
index feb4b0e..0000000
--- a/tests/tests.py
+++ /dev/null
@@ -1,524 +0,0 @@
-#! /usr/bin/python3
-"""### Test various aspects of the framework automatically.
-
-The tests contained in this file include:
-
--
-
-It is not meant to be imported.
-"""
-
-import random
-import sys
-import time
-from copy import deepcopy
-
-from gdpc import (direct_interface, geometry, interface, interface_toolbox,
-                  lookup, toolbox)
-
-__all__ = []
-__version__ = "v5.1"
-
-VERSIONNAME = toolbox.check_version()[1]
-
-# import timeit
-
-# import mapUtils
-# import visualizeMap
-# import worldLoader
-# import example
-# import bitarray
-
-
-class TestException(Exception):
-    """Exception raised when a test fails.    """
-
-    def __init__(self, *args):
-        super().__init__(*args)
-
-    # inherited __repr__ from OrderedDict is sufficient
-
-
-def verifyPaletteBlocks():
-    """Check blockColours blocks."""
-    print(f"\n{lookup.TCOLORS['yellow']}Running blockColours palette test...")
-
-    print(f"\t{lookup.TCOLORS['gray']}Preparing...", end="\r")
-    tester = interface.Interface()
-    counter = 0
-    badcounter = 0
-    passed = []
-    tocheck = [block for i in lookup.PALETTE.values()
-               for block in i] + list(lookup.MAPTRANSPARENT)
-    print(f"\t{lookup.TCOLORS['gray']}Preparing done.")
-
-    for block in tocheck:
-        if block in passed:
-            badcounter += 1
-            print()
-            print(f"\t\t{lookup.TCOLORS['gray']}{block} is duplicated")
-        elif not tester.placeBlock(0, 0, 0, block).isnumeric():
-            badcounter += 1
-            print()
-            print(tester.placeBlock(0, 0, 0, block))
-            print(f"\t\t{lookup.TCOLORS['orange']}Cannot verify {block}")
-        counter += 1
-        passed.append(block)
-        print(f"\t{lookup.TCOLORS['blue']}{counter}"
-              f"{lookup.TCOLORS['CLR']} blocks verified.", end='\r')
-    tester.placeBlock(0, 0, 0, 'air')
-    if badcounter > 0:
-        raise TestException(f"{lookup.TCOLORS['red']}{badcounter}/"
-                            f"{lookup.TCOLORS['gray']}{counter}"
-                            f"{lookup.TCOLORS['red']} blocks duplicate "
-                            "or could not be verified.\n"
-                            f"{lookup.TCOLORS['orange']}"
-                            "Please check you are running"
-                            f" on Minecraft {VERSIONNAME}")
-
-    p = {v for nv in lookup.PALETTE.values() for v in nv} \
-        | lookup.MAPTRANSPARENT
-    b = lookup.BLOCKS
-    discrepancy = len(p-b) + len(b-p)
-    if discrepancy != 0:
-        raise TestException(f"{lookup.TCOLORS['orange']}"
-                            f"There is a discrepancy of {discrepancy} blocks "
-                            f"between BLOCKS and PALETTE:\n"
-                            f"{lookup.TCOLORS['gray']}Missing in BLOCKS:\n"
-                            f"{p-b}\n"
-                            f"Missing in PALETTE:\n{b-p}\n")
-
-    print(f"{lookup.TCOLORS['green']}"
-          f"All {counter} blocks successfully verified!")
-
-
-def testSynchronisation():
-    print(f"\n{lookup.TCOLORS['yellow']}Running synchronization test...")
-    print(f"\t{lookup.TCOLORS['gray']}Testing y-indexes...", end='\r')
-
-    buildarea = interface.requestBuildArea()
-    center = ((buildarea[0] + buildarea[3]) // 2,
-              (buildarea[2] + buildarea[5]) // 2)
-
-    # Creating world datafiles
-    ws = interface.makeGlobalSlice()
-
-    x, z = center
-
-    error = False
-    for _x in range(x - 1, x + 2):
-        for _z in range(z - 1, z + 2):
-            for i in range(0, 128):
-                bws = ws.getBlockAt(_x, i, _z)
-                bdi = direct_interface.getBlock(_x, i, _z).get('id')
-                if (bws != bdi and bws != 'minecraft:void_air'):
-                    print("{}: ws: {}, di: {}".format((_x, i, _z), bws, bdi))
-                    error = True
-
-    if error:
-        raise TestException("Worldslice's blocks do not align in the Y-axis.")
-
-    print("\tTesting y-indexes done.")
-    print(f"{lookup.TCOLORS['green']}Synchronization test complete!")
-
-
-def testShapes():
-    """**Check shape construction**."""
-    # TODO: Fill me!
-    print(f"\n{lookup.TCOLORS['yellow']}Running shape test..."
-          f"{lookup.TCOLORS['gray']}\n\tPlacing shapes...")
-    geometry.placeVolume(63, 159, 63, 0, 128, 0, 'air')
-
-    # placeLine
-    geometry.placeLine(0, 128, 0, 0, 128, 0, 'red_concrete')            # 0D
-
-    geometry.placeLine(0, 128, 16, 15, 128, 16, 'red_concrete')         # 1D x
-    geometry.placeLine(0, 128, 16, 0, 143, 16, 'red_concrete')          # 1D y
-    geometry.placeLine(0, 128, 16, 0, 128, 31, 'red_concrete')          # 1D z
-
-    geometry.placeLine(0, 128, 32, 15, 143, 32, 'red_concrete')         # 2D++0
-    geometry.placeLine(0, 128, 32, 0, 143, 47, 'red_concrete')          # 2D0++
-    geometry.placeLine(0, 128, 32, 15, 128, 47, 'red_concrete')         # 2D+0+
-    geometry.placeLine(15, 143, 47, 0, 128, 47, 'red_concrete')         # 2D--0
-    geometry.placeLine(15, 143, 47, 15, 128, 32, 'red_concrete')        # 2D0--
-    geometry.placeLine(15, 143, 47, 0, 143, 32, 'red_concrete')         # 2D-0-
-
-    geometry.placeLine(0, 128, 48, 7, 135, 55, 'red_concrete')          # 3D+++
-    geometry.placeLine(15, 143, 63, 7, 135, 55, 'red_concrete')         # 3D---
-    interface.globalinterface.placeBlock(7, 135, 55, 'gold_block')
-
-    # placeVolume
-    geometry.placeVolume(16, 128, 0, 16, 128, 0, 'orange_concrete')       # 0D
-
-    geometry.placeVolume(16, 128, 16, 31, 128, 16,
-                         'orange_concrete')     # 1D x
-    geometry.placeVolume(16, 128, 16, 16, 143, 16,
-                         'orange_concrete')     # 1D y
-    geometry.placeVolume(16, 128, 16, 16, 128, 31,
-                         'orange_concrete')     # 1D z
-
-    geometry.placeVolume(16, 128, 32, 31, 143, 32,
-                         'orange_concrete')     # 2D xy
-    geometry.placeVolume(16, 128, 32, 16, 143, 47,
-                         'orange_concrete')     # 2D yz
-    geometry.placeVolume(16, 128, 32, 31, 128, 47,
-                         'orange_concrete')     # 2D xz
-
-    geometry.placeVolume(16, 128, 48, 31, 143, 63, 'orange_concrete')     # 3D
-
-    # placeCuboid
-    geometry.placeCuboid(32, 128, 0, 32, 128, 0, 'yellow_concrete')     # 0D
-    geometry.placeCuboid(32, 144, 0, 32, 144, 0, 'yellow_stained_glass',
-                         hollow=True)
-
-    geometry.placeCuboid(32, 128, 16, 47, 128, 16, 'yellow_concrete')   # 1D x
-    geometry.placeCuboid(32, 144, 16, 47, 144, 16, 'yellow_stained_glass',
-                         hollow=True)
-    geometry.placeCuboid(32, 128, 16, 32, 143, 16, 'yellow_concrete')   # 1D y
-    geometry.placeCuboid(32, 144, 16, 32, 159, 16, 'yellow_stained_glass',
-                         hollow=True)
-    geometry.placeCuboid(32, 128, 16, 32, 128, 31, 'yellow_concrete')   # 1D z
-    geometry.placeCuboid(32, 144, 16, 32, 144, 31, 'yellow_stained_glass',
-                         hollow=True)
-
-    geometry.placeCuboid(32, 128, 32, 47, 143, 32, 'yellow_concrete')   # 2D xy
-    geometry.placeCuboid(32, 144, 32, 47, 159, 32, 'yellow_stained_glass',
-                         hollow=True)
-    geometry.placeCuboid(32, 128, 32, 32, 143, 47, 'yellow_concrete')   # 2D yz
-    geometry.placeCuboid(32, 144, 32, 32, 159, 47, 'yellow_stained_glass',
-                         hollow=True)
-    geometry.placeCuboid(32, 128, 32, 47, 128, 47, 'yellow_concrete')   # 2D xz
-    geometry.placeCuboid(32, 144, 32, 47, 144, 47, 'yellow_stained_glass',
-                         hollow=True)
-
-    geometry.placeCuboid(32, 128, 48, 47, 143, 63, 'yellow_concrete')   # 3D
-    geometry.placeCuboid(32, 144, 48, 47, 159, 63, 'yellow_stained_glass',
-                         hollow=True)
-
-    # placeCylinder axis = x
-    geometry.placeCylinder(48, 128, 0, 48, 128, 0, 'lime_concrete')     # 0D
-    geometry.placeCylinder(48, 144, 0, 48, 144, 0, 'lime_stained_glass',
-                           hollow=True)
-
-    geometry.placeCylinder(48, 128, 16, 63, 128, 16, 'lime_concrete')   # 1D x
-    geometry.placeCylinder(48, 144, 16, 63, 144, 16, 'lime_stained_glass',
-                           hollow=True)
-    geometry.placeCylinder(48, 128, 16, 48, 143, 16, 'lime_concrete')   # 1D y
-    geometry.placeCylinder(48, 144, 16, 48, 159, 16, 'lime_stained_glass',
-                           hollow=True)
-    geometry.placeCylinder(48, 128, 16, 48, 128, 31, 'lime_concrete')   # 1D z
-    geometry.placeCylinder(48, 144, 16, 48, 144, 31, 'lime_stained_glass',
-                           hollow=True)
-
-    # 2D xy (z axis)
-    geometry.placeCylinder(48, 128, 32, 63, 143, 32, 'lime_concrete', axis='z')
-    geometry.placeCylinder(48, 144, 32, 63, 159, 32, 'lime_stained_glass',
-                           hollow=True, axis='z')
-    # 2D yz (x axis)
-    geometry.placeCylinder(48, 128, 32, 48, 143, 47, 'lime_concrete', axis='x')
-    geometry.placeCylinder(48, 144, 32, 48, 159, 47, 'lime_stained_glass',
-                           hollow=True, axis='x')
-    # 2D xz (default axis)
-    geometry.placeCylinder(48, 128, 32, 63, 128, 47, 'lime_concrete')
-    geometry.placeCylinder(48, 144, 32, 63, 144, 47, 'lime_stained_glass',
-                           hollow=True)
-
-    # 3D x axis
-    geometry.placeCylinder(48, 128, 48, 63, 143, 63, 'lime_concrete', axis='x')
-    geometry.placeCylinder(48, 144, 48, 63, 159, 63, 'lime_stained_glass',
-                           hollow=True, axis='x')
-    # 3D default axis
-    geometry.placeCylinder(48, 128, 48, 63, 143, 63, 'lime_concrete')
-    geometry.placeCylinder(48, 144, 48, 63, 159, 63, 'lime_stained_glass',
-                           hollow=True)
-    # 3D z axis
-    geometry.placeCylinder(48, 128, 48, 63, 143, 63, 'lime_concrete', axis='z')
-    geometry.placeCylinder(48, 144, 48, 63, 159, 63, 'lime_stained_glass',
-                           hollow=True, axis='z')
-
-    reply = input(f"\t{lookup.TCOLORS['blue']}\aHave all shapes generated "
-                  f"correctly? (y/*): {lookup.TCOLORS['CLR']}")
-
-    if reply != 'y':
-        raise TestException(f"Shapes were failed:\n"
-                            f"\t{reply}")
-    print(f"{lookup.TCOLORS['gray']}\tRemoving shapes...")
-    geometry.placeVolume(63, 159, 63, 0, 128, 0, 'air')
-    print(f"{lookup.TCOLORS['green']}Shape test complete!")
-
-
-def testBooks():
-    """**Check book creation and storage**."""
-    print(f"\n{lookup.TCOLORS['yellow']}Running book test...")
-    TITLE = 'Testonomicon'
-    AUTHOR = 'Dr. Blinkenlights'
-    DESCRIPTION = 'All is doomed that enters here.'
-    DESCRIPTIONCOLOR = 'aqua'
-
-    CRITERIA = ('Lectern is at 0 255 0 and displays correctly?',
-                'Lectern is facing east?', 'Book is legible?',
-                'Book contents is correct?', 'Final page is displayed?')
-
-    text = ('New line:\n'
-            'Automatic sentence line breaking\n'
-            'Automatic_word_line_breaking\n'
-            '\\cCenter-aligned\n'
-            '\\rRight-aligned\n'
-            'New page:\f'
-            '§6gold text§r\n'
-            '§k█§r < obfuscated text§r\n'
-            '§lbold§r text§r\n'
-            '§mstriked§r text§r\n'
-            '§nunderlined§r text§r\n'
-            '§oitalic§r text§r\n'
-            '\f\\\\s╔══════════╗\\\\n'
-            '║                      `║\\\\n'
-            '║                      `║\\\\n'
-            '║:   Preformatted  :║\\\\n'
-            '║         Page       `║\\\\n'
-            '║                      `║\\\\n'
-            '║                      `║\\\\n'
-            '║        ☞⛏☜       .║\\\\n'
-            '║                      `║\\\\n'
-            '║                      `║\\\\n'
-            '║                      `║\\\\n'
-            '║                      `║\\\\n'
-            '║                      `║\\\\n'
-            '╚══════════╝')
-
-    print(f"\t{lookup.TCOLORS['gray']}Writing book...", end="\r")
-    book = toolbox.writeBook(text, TITLE, AUTHOR,
-                             DESCRIPTION, DESCRIPTIONCOLOR)
-    print("\tWriting book done.")
-
-    print("\tPlacing lectern...", end="\r")
-    interface_toolbox.placeLectern(0, 255, 0, book, 'east')
-    print("\tPlacing lectern done.")
-
-    print("\tPrompting user...", end="\r")
-    for no, prompt in enumerate(CRITERIA):
-        reply = input(f"\t{lookup.TCOLORS['blue']}\a{no+1}/{len(CRITERIA)} "
-                      f"{prompt} (y/*): {lookup.TCOLORS['CLR']}")
-        if reply == '' or reply[0].lower() != 'y':
-            raise TestException(f"Book criteria #{no} was failed:\n"
-                                f"\t{prompt}: {reply}")
-    print(f"{lookup.TCOLORS['green']}Book test complete!")
-    interface.globalinterface.placeBlock(0, 255, 0, "air")
-
-
-def testCache():
-    """**Check Interface cache functionality**."""
-    print(f"\n{lookup.TCOLORS['yellow']}Running Interface cache test...")
-    SIZE = 16
-    PALETTES = (("birch_fence", "stripped_birch_log"),
-                ("dark_oak_fence", "stripped_dark_oak_log"))
-
-    def clearTestbed():
-        """Clean testbed for placement from memory."""
-        print("\t\tWiping blocks...", end="\r")
-        geometry.placeVolume(0, 1, 0, SIZE - 1, 1, SIZE - 1,
-                             "shroomlight", tester)
-        tester.sendBlocks()
-        print("\n\t\tWiping blocks done.")
-
-    def placeFromCache():
-        """Replace all removed blocks from memory."""
-        print("\t\tReplacing blocks from memory...", end="\r")
-        tester.caching = True
-        for x, z in toolbox.loop2d(SIZE, SIZE):
-            tester.placeBlock(x, 1, z, tester.getBlock(x, 1, z))
-        tester.sendBlocks()
-        tester.caching = False
-        print("\n\t\tReplacing blocks from memory done.")
-
-    def checkDiscrepancies():
-        """Check test bed and comparison layer for discrepancies."""
-        for x, z in toolbox.loop2d(SIZE, SIZE):
-            print("\t\tTesting...▕" + (10 * x // SIZE) * "█"
-                  + (10 - 10 * x // SIZE) * "▕", end="\r")
-
-            for palette in PALETTES:
-                if tester.getBlock(x, 1, z) == "minecraft:shroomlight":
-                    raise TestException(
-                        f"{lookup.TCOLORS['red']}Block at "
-                        f"{lookup.TCOLORS['orange']}{x} 0 {z} "
-                        f"{lookup.TCOLORS['red']}was no longer in memory.")
-                if (tester.getBlock(x, 0, z) == palette[0]
-                        and tester.getBlock(x, 1, z) != palette[1]):
-                    raise TestException(
-                        f"{lookup.TCOLORS['red']}Cache test failed at "
-                        f"{lookup.TCOLORS['orange']}{x} 0 {z}"
-                        f"{lookup.TCOLORS['red']}.")
-        print("\t\tTesting...▕██████████")
-        print(f"\t{lookup.TCOLORS['darkgreen']}No discrepancies found.")
-
-    def muddle():
-        for i in range(4 * SIZE):
-            print("\t\tMuddling...▕" + (10 * i // SIZE) * "█"
-                  + (10 - 10 * i // SIZE) * "▕", end="\r")
-            x = random.randint(0, SIZE - 1)
-            z = random.randint(0, SIZE - 1)
-            if random.choice([True, False]):
-                type = random.choice(PALETTES)
-                tester.caching = True
-                tester.placeBlock(x, 1, z, type[1])
-                tester.caching = False
-                tester.placeBlock(x, 0, z, type[0])
-                tester.sendBlocks()
-            else:
-                tester.caching = True
-                tester.getBlock(x, 1, z)
-                tester.caching = False
-        print("\t\tMuddling...▕██████████")
-        print("\t\tMuddling complete.")
-
-    # ---- preparation
-    print(f"\t{lookup.TCOLORS['gray']}Preparing...", end="\r")
-    tester = interface.Interface(buffering=True, bufferlimit=SIZE ** 2)
-    geometry.placeVolume(0, 2, 0, SIZE - 1, 2, SIZE - 1, "bedrock", tester)
-    geometry.placeVolume(0, 0, 0, SIZE - 1, 1, SIZE - 1, "air", tester)
-    tester.sendBlocks()
-    tester.cache.maxsize = (SIZE ** 2)
-    print("\tPerparing done.")
-
-    # ---- test block scatter
-    print("\tScattering test blocks...", end="\r")
-    for x, z in toolbox.loop2d(SIZE, SIZE):
-        print("\tPlacing pattern...▕" + (10 * x // SIZE) * "█"
-              + (10 - 10 * x // SIZE) * "▕", end="\r")
-        type = random.choice(PALETTES)
-        tester.caching = True
-        tester.placeBlock(x, 1, z, type[1])
-        tester.caching = False
-        tester.placeBlock(x, 0, z, type[0])
-    tester.sendBlocks()
-    print("\tPlacing pattern...▕██████████")
-    print("\tScattering test blocks done.")
-
-    # ---- first run (caching through placeBlock)
-    print(f"\t{lookup.TCOLORS['gray']}First run: Cache updated via placeBlock")
-
-    clearTestbed()
-    placeFromCache()
-    checkDiscrepancies()
-
-    # ---- second run (caching through getBlock)
-    print(f"\t{lookup.TCOLORS['gray']}Second run: Cache updated via getBlock")
-
-    tester.cache.clear
-    tester.caching = True
-    for x, z in toolbox.loop2d(SIZE, SIZE):
-        print("\t\tReading...▕" + (10 * x // SIZE) * "█"
-              + (10 - 10 * x // SIZE) * "▕", end="\r")
-        tester.getBlock(x, 1, z)
-    tester.caching = False
-    print("\t\tReading...▕██████████")
-    print("\t\tCache refilled.")
-
-    clearTestbed()
-    placeFromCache()
-    checkDiscrepancies()
-
-    # ---- third run (randomized get-/placeBlock)
-    print(f"\t{lookup.TCOLORS['gray']}Third run: "
-          "Cache updated via random methods")
-
-    muddle()
-    clearTestbed()
-    placeFromCache()
-    checkDiscrepancies()
-
-    # ---- fourth run (using WorldSlice)
-    print(f"\t{lookup.TCOLORS['gray']}Fourth run: "
-          "Cache updated via WorldSlice")
-
-    muddle()
-
-    print("\t\tGenerating global slice...", end="\r")
-    d0 = time.perf_counter()
-    interface.makeGlobalSlice()
-    dt = time.perf_counter()
-    print(f"\t\tGenerated global slice in {(dt-d0):.2f} seconds.")
-
-    clearTestbed()
-    placeFromCache()
-    checkDiscrepancies()
-
-    # ---- cleanup
-    print(f"{lookup.TCOLORS['green']}Cache test complete!")
-    geometry.placeVolume(0, 0, 0, SIZE, 1, SIZE, "bedrock", tester)
-    interface.globalWorldSlice = None
-    interface.globalDecay = None
-
-
-def testBlocks():
-    pos1, pos2 = (-147, 4, -12), (-147, 4, 664)
-    print(f"\n{lookup.TCOLORS['yellow']}Running block test...")
-    print(f"\t{lookup.TCOLORS['gray']}Testing blocks from...", end='\r')
-
-    test_set = deepcopy(lookup.BLOCKS)
-    tested_set = set()
-    missing_set = set()
-
-    for x, y, z in toolbox.loop3d(*pos1, *pos2):
-        test_block = interface.getBlock(x, y, z)
-        if test_block in test_set:
-            tested_set.add(test_block)
-            test_set.remove(test_block)
-            continue
-        if test_block not in tested_set:
-            missing_set.add(test_block)
-            continue
-
-    if len(missing_set) > 0:
-        raise TestException(f"{lookup.TCOLORS['orange']}"
-                            "There is a discrepancy between "
-                            "the blocks provided and the blocks checked:"
-                            f"{lookup.TCOLORS['gray']}\n"
-                            f"Missing from BLOCKS:\n{missing_set}\n"
-                            f"Not found in world:\t\n{test_set}\n")
-    elif len(test_set) > 0:
-        print(f"{lookup.TCOLORS['orange']}"
-              "The block test world is incomplete and should be updated by "
-              f"{len(test_set)} blocks:"
-              f"{lookup.TCOLORS['gray']}\n"
-              f"Not found in world:\t\n{test_set}\n")
-
-    print("\tTesting blocks done.")
-    print(f"{lookup.TCOLORS['green']}Block test complete!")
-
-
-if __name__ == '__main__':
-    AUTOTESTS = {verifyPaletteBlocks, testCache, testSynchronisation,
-                 testBlocks}
-    MANUALTESTS = {testBooks, testShapes}
-    tests = AUTOTESTS | MANUALTESTS
-
-    if len(sys.argv) > 1:
-        if sys.argv[1] == '--manual':
-            tests = MANUALTESTS
-        elif sys.argv[1] == '--loadonly':
-            tests = set()
-    else:
-        tests = AUTOTESTS
-
-    print(f"Beginning test suite for "
-          f"{lookup.TCOLORS['blue']}GDPC {__version__} "
-          f"{lookup.TCOLORS['CLR']}on "
-          f"{lookup.TCOLORS['blue']}Minecraft {VERSIONNAME}: "
-          f"{len(tests)} tests{lookup.TCOLORS['CLR']}")
-    interface.setBuildArea(0, 0, 0, 255, 255, 255)
-    failed = 0
-    errors = ""
-    for test in tests:
-        try:
-            test()
-        except TestException as e:
-            errors += f"{lookup.TCOLORS['red']}> {test.__name__}() failed.\n" \
-                + f"{lookup.TCOLORS['gray']}Cause: {e}\n"
-            failed += 1
-    print(f"\n{lookup.TCOLORS['CLR']}\aTest suite completed with "
-          f"{lookup.TCOLORS['orange']}{failed}"
-          f"{lookup.TCOLORS['CLR']} fails!\n")
-    if errors != "":
-        print(f"==== Summary ====\n{errors}{lookup.TCOLORS['CLR']}")