GoodCraft [Hackathon Project]

Bob Koutsky

One of many perks of working for GoodData are occasional hackathons. We get twenty four hours, a pyramid of Red Bull cans and a simple assignment: code anything you want, as long as it is GoodData related. Now, I’m pretty sure than when management came up with this assignment, what they really meant was “code anything USEFUL you want”, but hey, I’m not a mindreader, I’m just following orders here. And as it happened, the first GoodData related thing I came up with was a GoodData client for Minecraft.

Minewhat?

In case you have no idea what I am talking about: Minecraft is a sandbox-style computer game, which lets you explore huge (for all practical purposes infinite), procedurally generated world. There’s no “right” way to play the game: some players like to explore the world, other’s fight with monsters that live there, some mine minerals and build huge and complicated structures.

Minecraft Scenery

Thanks to multiplayer support, it is even possible to visit worlds of other players and cooperate with them (or fight them). The game is a huge hit - it has sold over 30 millions copies already and is still gaining popularity. Because it is possible to create plugins that add new functionality to the game, it makes complete sense to let Minecraft players to monetize their data while playing the game, by creating a GoodData plugin.

From zero to plugin in 24 hours

There was only one problem. Well, maybe two. First, I had absolutely no idea how to write a Minecraft plugin. I knew that Minecraft is written in Java and that there is a large modding community, but that was about it. Also, my Java was (and still is) somewhat… well, let’s say, rusty. But not knowing how to do something never stopped me from trying, so I entered “Minecraft plugin how to” into Google and started reading.

No Idea

Please keep this in mind when you start reading the code. I’m pretty sure an experienced Java coder and/or Minecraft plugin author would find a lot of space for improvement there. However, it sort of works, and that’s about the best I could have expected from code written by Java/Minecraft rookie in 24 hours, mostly in the state of Red Bull overdose.

So how does it work?

Well, it’s simple.

Rube Goldberg Machine

No, really, it’s easy. While my code may lack finesse, the design of Minecraft and Forge (see below) is nice and elegant and once you find out how is stuff supposed to work, it makes sense and is even a joy to work with. This article can not be a full-blown Minecraft modding tutorial. Instead, I’ll refer you to Minecraft Forge Wiki and in the next paragraphs will focus only on the problematic or GoodCraft specific pieces of code. All the required sources are available at GitHub repository.

Development Environment

At this time, Minecraft does not provide full, official plugin/mod API. Plugin authors must use one of third-party plugin frameworks. I picked up Minecraft Forge - it’s mostly up-to-date (available for Minecraft 1.7.2, while current version is 1.7.9), it integrates nicely with IntelliJ IDE and seems to be quite popular and thus better documented than alternatives. Download the src package (the one marked “Recommended”), and unpack into new directory. cd there and execute ./gradlew setupDecompWorkspace --refresh-dependencies to setup dependencies. Finally, do ./gradlew idea or ./gradlew eclipse to setup environment either for IntelliJ or Eclipse (I do now want to start a flamewar here, but IntelliJ totally rules). Now you you can start your favorite IDE (i.e. IntelliJ) and after you open the project, you can use regular “Run” function to start Minecraft with your plugin installed. Yay!

First steps

The first class to create is the main plugin class, in our case com.gooddata.goodcraft.GoodCraft. Minecraft specific code in this class is pretty much by the cookbook, except one large caveat.

Here is the custom block registration code:

  @EventHandler // used in 1.6.2
    public void preInit(FMLPreInitializationEvent event) {
        makeFont();
        makeColors();
        masterBlock = new MasterBlock();
        GameRegistry.registerBlock(masterBlock, MasterBlock.BLOCK_NAME);
        projectBlock = new ProjectBlock();
        GameRegistry.registerBlock(projectBlock, ProjectBlock.BLOCK_NAME);
        reportBlock = new ReportBlock();
        GameRegistry.registerBlock(reportBlock, ReportBlock.BLOCK_NAME);
        chartBlock = new ChartBlock();
        GameRegistry.registerBlock(chartBlock, ChartBlock.BLOCK_NAME);
// ...

To conserve memory, individual blocks in the Minecraft world are not represented by instances of a Block class. Instead, each block type is a singleton, created during game startup and world consists of pointers to these singletons (yes, that makes it more complicated to have a state specific for a single block, but there are two ways around it, see below).

Each constructor in the above code and the makeFont and makeColors calls create these singletons. That’s quite simple, but the trick is where to call these constructors. The correct place is the preInit method, as shown above. If you use the load method, it will almost work, but you will waste three hours of your life, just like I did, trying to figure out why are the textures not loading.

Drawing, printing & state

As I said above, individual blocks in the Minecraft world are not instances of different Block class, instead they are just pointers to singletons of different Block types. If a block needs some local state, there are two possibilities:

If the amount of data is quite small, it can be saved into block metadata. Each block may have only four bits of metadata. That’s not a lot, but if all that’s needed is some on/off switch or a simple damage counter with up to 16 levels, it is enough.

For arbitrary amount of data, a TileEntity may be used. In that case, the block class must inherit from BlockContainer and implement createNewTileEntity method. This method will simply return new object (subclassed from TileEntity), that will contain all the data that specified block will need.

For the GoodCraft plugin, I needed several special block types. Some of them were “input” blocks, used by the player to control the plugin. Others were “output” blocks, used to display the name of selected project and report and most importantly, to draw the chart. For the latter, I first considered using either TileEntity based blocks or metadata, but to simplify implementation, I decided to use brute force instead. I used simple scripts and one-liners to create a single block type for each letter of alphabet and each of the 64 colors that I was going to use. Each such block type is probably the simplest possible custom block class. For example, letter ‘g’:

package com.gooddata.goodcraft.font;
import com.gooddata.goodcraft.GoodCraft;
import cpw.mods.fml.common.registry.GameRegistry;
 
public class FontBlock_g extends FontBlock {
    public FontBlock_g() {
        super();
        // Remember what letter I am. "letter" is a member variable declared in superclass.
        letter = 'g';
        // Each block needs a name.
        setBlockName("letterblock_g");
        // Where is the texture for this block.
        setBlockTextureName(GoodCraft.MOD_ID + ":" + "letterblock-g");
        // All set, let's register ourself with the game.
        GameRegistry.registerBlock(this, "letterblock_g");
    }
}

Note the {{setBlockTextureName}} call: Forge handles textures for the programmers, so all that’s needed is to specify texture name in the form “modulename:texturename” and put the png file to src/main/resources/assets/<MODULENAME>/textures/blocks/ directory, in our case to src/main/resources/assets/goodcraft/textures/blocks/letterblock-g.png .

This is certainly not a very elegant solution. A better solutions would be:

  • for letter blocks, to create a single block type, with letter stored in the TileEntity, with correct texture selected based on it.
  • for color blocks, to create a few block types, each representing 16 colors, with specific shade stored in the metadata. There is generally many more color blocks than letter blocks and TileEntity takes relatively lot of space.

Talking to GoodData

Before we start creating the input blocks, let’s look at how communication with GoodData API works.

GoodData API is (mostly) strictly RESTful API. It consists of a large number of different calls, but the low-level details are all the same for all of them: just send a https GET, POST, DELETE or other method call on some URL and receive results, most of the times in form of a JSON structure. The only important things to remember are to correctly set Accept and Content-type headers and to properly handle GoodData authentication. The first one is easy, the second one is easy too, if you know how it works. GoodData API login is a two step process. First, you need to get SuperSecureToken (SST):

public boolean login() {
   try {
       HttpPost request = new HttpPost(API + "/gdc/account/login");
       request.addHeader("Content-type", "application/json");
       request.addHeader("Accept", "application/json");
       String payload = "{\"postUserLogin\":{\"login\":\"" + GoodCraft.username + "\",\"password\":\"" + GoodCraft.password + "\",\"remember\":1}}";
       System.out.println(payload);
       request.setEntity(new StringEntity(payload));
       CloseableHttpResponse response = execute(request, false);

Nothing complicated here, just POST something to https://secure.gooddata.com/gdc/account/login. What’s important (and what is not shown in this code), is the SST cookie. The login resource returns a specific cookie with SST and http client we are using will remember it. The important thing about SST is that it is sent back to server only in requests on the /gdc/account/token resource. That’s when the second step takes place:

private boolean getTT() throws IOException {
    HttpGet req = new HttpGet(API + "/gdc/account/token");
    CloseableHttpResponse resp = execute(req);
    return resp.getStatusLine().getStatusCode() == 200;
}

This method calls the /gdc/account/token resource. SST is sent as part of the request and, if correct, in response the server returns TT, Temporary Token. Again, http client remembers it. In contrast to SST however, TT is sent as part of a request on any GoodData API resource and is used to authenticate user.

As the name suggests, TT is temporary - it expires after a couple of minutes. In that case, we need to get a new one:

private CloseableHttpResponse execute(HttpUriRequest request, boolean refreshTT) throws IOException {
    int retryCount = 0;
    CloseableHttpResponse response = null;
    do {
        response = execute(request);
        if (response.getStatusLine().getStatusCode() != 401) {
            return response;
        }
        // 401 Unauthorized may mean "TT expired", let's try it once more after we try to get a new TT
    } while (refreshTT && retryCount++ < 1 && getTT());
    return response;
} 

GoodData blocks

Special blocks with GoodData logo are used to get input from user. White one logs user in and creates dark gray block that, when hit, selects projects. Light gray block is used to select report and black block draws the chart.

GoodCubes

Asynchronous processing

When player activate a block, method onBlockActivated gets called. Because all the block state is stored in its TileEntity, all four GoodData blocks immediately pass control there:

@Override
public boolean onBlockActivated(World world, int x, int y, int z,
                                EntityPlayer player, int metadata, float what, float these, float are) {
    // Get TileEntity for this block
    ProjectTileEntity te = (ProjectTileEntity) world.getTileEntity(x, y, z);
    // Remember which direction is player looking
    te.vd = new ViewDirection(x,y,z, player);
    // Execute the action
    te.action(world, player);
    return true;
}

I did not find any detailed information about threading model of Minecraft, but I did not think it is a good idea to do a lot of processing on the “UI” thread. To make sure my long processing does not block something important, all action methods just setup a Future that executes on different thread.

Similarly, I did not think that it is good idea to execute world-modifying code from non-UI code (I have not even tried it, so I do not know if it is even possible). Fortunately, each TileEntity has method updateEntity that gets called “often”, probably on every game clock tick. In this method, I checked the result of the Future and if possible, I processed it.

ABCD…

Writing text is simple: just set a row of blocks to block types of correct letters:

public static void type(World world, int distance, ViewDirection vd, String text) {
    text = text.toLowerCase();
    text = (text + "                                 ").substring(0,30);
    int textLength = text.length();
    for (int i = 0; i < textLength; i++) {
        world.setBlock(vd.x(distance, i), vd.y(), vd.z(distance, i),
                getGlyph(text.charAt(i)));
    }
}

The only trick here is the ViewDirection class, which determines in which direction the text should appear, so that is always appears in front of the user in correct (somewhat) orientation. By “trick” I mean “because I lack spatial orientation, it took me a half an hour of trial and error to correctly figure out where in that class to put minuses and where pluses”, and by “somewhat” I mean “Half the time the letters are ordered correctly, but because we are looking at the bitmaps from the back side, the letters itself are mirrored.” That could be fixed my making the letter block return different bitmap for “back” sides.

PIXELS!!!

And finally, pixels. Essentially, they work the same as letters. There are 64 block types for 64 colors, when I need to draw a pixel of some RGB color, I first round each component to nearest 0x00, 0x55, 0xAA or 0xFF, select the corresponding block type and place it where it belongs. The complicated part is how to convert all that gigabytes of data in the datawarehouse to a few pixels. Fortunately, this is something I can offload to our backend:

String payload = "{\n" +
        "  \"report_req\": {" +
        "    \"report\":\"" + r + "\"," +
        "    \"context\": {" +
        "      \"imageSpecification\":{\"sizeX\":" + WIDTH + "," +
        "      \"sizeY\":" + HEIGHT + "}" +
        "    }" +
        "  }" +
        "}";
JSONObject result = http.post("/gdc/app/projects/" + project + "/execute", payload, true);

You see, the /gdc/app/projects/PROJECT/execute resource takes the optional imageSpecification parameter. If I specify it, part of the result is URL, where I can find nice image of the chart at correct resolution (provided that the report is formatted as a chart, of course). After that, it is easy: download the image, use png library to decode and then iterate over pixels and convert to blocks. Yay, done!

Chart

To Do

Well, there’s a lot of things to fix. First of all, there is very limited, if any, error handling. For example, if you try to draw a report that is not in chart format, it probably just silently fails (but it may also crash Minecraft). If you use newly created GoodData input block (the one with our logo) too soon after it appeared, it will crash Minecraft (so just wait a few seconds before doing that).

Another thing that is completely broken is client/server separation. Minecraft (allegedly) has a nice client/server separation, with nice support in Forge… however, the documentation is lacking. In the current version of the code everything executes twice, once on the client side, once on the server side. It sort of works, but it may break in mysterious ways. Moving everything to client (so we do not have to share credentials with server) is probably a good idea.

More importantly, there is no support for dashboards, filters and report editing. I will try to get this planned for Q3 2025, but I’m not holding my breath here.

Oh yeah, multiplayer monetization would be nice. Maybe in the next hackathon.

Enough with this complicated stuff, I just want to Monetize while Minecrafting!

It’s not that easy, as I have not figured out yet how to package my modifications for distribution. Once I manage that, I will update this post, but until then, follow the instruction on how to install the development environment, either here or at Minecraft Forge Wiki Installation page. Then, delete everything under src directory of your installation. Then, copy everything from repository to your installation (it will recreate the src directory and overwrite some files in the base directory). Create file .goodcraft.creds in your HOME directory, with first line being your GoodData login, second line your GoodData password. Start IDE, open project, run it and it should start local Minecraft game.

Enter the Creative mode, place white cube with GoodData logo and use it. It will spawn light-gray cube next to it. Wait a few seconds, use the second cube - it will cycle your available project and create dark-gray cube next to it. This one is used to cycle reports inside the selected project. Finally, the black cube draws selected chart. Probably. Maybe. If you are lucky. If not, let me know (bohumil.koutsky@gooddata.com). Also, please let me know if you have any comments, suggestions, questions or similar - I will be happy to hear them.

KTHXBYE, Bob


About The Author

Bob Koutsky Bob Koutsky is a troubleshooter and a facepalm specialist at GoodData. When not working on improving the performance of the GoodData Platform, he plays games, pets cats and builds an immunity to iocane powder.

Email bohumil.koutsky@gooddata.com

Dev's Newsletter

Subscribe Now