What do you get when you cross a thin wrapper around VTK, a Minecraft world, and a sudden job interview? The answer is a few lines of Python code, and a frantic elevator pitch.

In 2014, I started to aggregate event data from a Minecraft server using Prism - a Bukkit plug-in for Minecraft that gives administrators, and players end-to-end visibility into what is going on in the world. The plug-in was designed to let these users track down malicious players, and to regrow razed forests, and seaside cottages in the blink of an eye after a raid by “griefers”.

Aside from helping put a face to a place, the plug-in also helped frantic job candidates like myself reconstruct the world using event data, and impress the hell out of people.

Since Prism stores event data in a MySQL database, I simply exported a subset of the data into a flatfile, and then fed it into NumPy - a scientific computing library that brings multi-dimensional arrays to Python, among other things.

SELECT x, y, z
FROM prism_actions
INTO OUTFILE '/tmp/mc.asc'
FIELDS TERMINATED BY ' '
ENCLOSED BY '"'
LINES TERMINATED BY '\n';

Once the data was exported into a flatfile, I wrote a few lines of code, and I was off to the races. In order to produce the results below, I loaded the flatfile into a 3-dimensional NumPy array, applied a basic colourmap, and then politely asked MayaVi to render everything for me. Easy peasy.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import division
from mayavi import mlab
import numpy as np

df = np.genfromtxt('topology.asc', dtype=int, names=['x', 'y', 'z'], delimiter=' ')
x = df['x']
y = df['y']
z = df['z']

# Let's add some colour!
c = np.zeros(shape=len(df))

max_height = max(y)
for height in set(y):
  color_weight = height/max_height
  indices = np.where(y == height)[0]
  for i in indices:
    c[i] = color_weight

s = mlab.points3d(x, z, y, mode="point", line_width=1,opacity=1, scale_mode="none")
mlab.axes(s, ranges=[min(x), max(x), min(z), max(z), min(y), max(y)], xlabel="x", ylabel="z", zlabel="y")
mlab.show()

The first set of images was produced by forcing Minecraft to render a bunch of unloaded chunks, and then asking the Bukkit API to tell me what the highest Y-value was for a given (X, Z) coordinate. Since I opted to use asynchronous requests in lieu of synchronous calls, I had to register a callback with the server, and then wait for a response. This took a few hours to produce, and the rest, is history.

...
@Override
public List<Block> getHighestBlocksNear(int x, int z, int r) {
  List<Block> blocks = new ArrayList<Block>();
  
  Future<List<Block>> future = this.plugin.getServer().getScheduler().callSyncMethod(plugin, new Callable<List<Block>>() {

    @Override
    public List<Block> call() {
      List<Block> blocks = new ArrayList<Block>();
      for(int i = x-r; i < x+r; i++){
        for(int j = z-r; j < z+r; j++){
          int y = world.getHighestBlockYAt(i, j);
          Block block = world.getBlockAt(i, y-1, j);
          blocks.add(block);
        }
      }
      return blocks;
    }
  });

  try {
    blocks = future.get();
  } catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
  }
  return blocks;
}
...

Without further ado, here is the first batch of results.

The second set of images is the product of a few months of log aggregation on a quiet Minecraft server. On a hustling, bustling server, it may take only a few weeks to reproduce, and get meaningful results. If you look closely, you can see where players were mining, where they decided to build their homes, and the occasional skydome here, and there.