Graduate Program KB

Minecraft

Minecraft's History

First

  • First version released in May 2009
  • Hobby project from a single developer best known for his nickname of 'Notch'
  • Bare bones project without much gameplay
  • Poor texture quality

Infiniminer

  • What Minecraft's early build was heavily inspired by
  • A procedurally generated block-based map in which you play as a miner who searches for precious minerals
  • As you can already see, there are strong parallels between the two games

Alpha

  • Moved into alpha
  • People begun creating client-side mods
  • Very basic alterations such as a mod which allowed you to fly
  • Through

Beta

  • Provided an overhaul of textures and biomes
  • Update game mechanics (adding things like critical hits, sprinting etc)
  • Modding community was still quite small, but was slowly growing

Release

  • Around November 2011
  • Forge Mode Loader/Minecraft Forge
  • Allow multiple mods to be run simultaneously (Previously had issues of mods overwriting the same source code)
  • Modded servers became better (more on this)
  • After full release, modding started to take off
  • Feb 2012, Mojang (company which owns minecraft) hired developers of Bukkit (a popular server-side mod at the time) to create an official modding API with the goal of allowing easier access to the Minecraft game files

Microsoft Acquisition

  • Mid 2014
  • Concern around Microsoft changing modding behaviour/policies
  • July 2015
  • Windows 10 version was announced
  • To be made in c++ (unlike the previous version in Java)
  • Further concern for the modding community as c++ cannot be reverse engineered like Java can
  • Minecraft was also released on other platforms as variants including Nintendo Switch, Minecraft Pocket Edition for Phones/Tablets, Minecraft for Playstation/Xbox
  • These would all later get accumulated under the tile: Minecraft Bedrock, with a singular unified codebase written in C++
  • This not only brought all the development teams back together to work on a single project, but allowed for more regular updates on versions of the game that did not get much attention (like pocket edition for example)
  • C++ was chosen over Java as not all machines could easily run Java (performance would be a nightmare on the consoles), but all the aforementioned devices could run C++

Java

  • As we know, Java is a high-level class based, object-oriented programming language
  • It is general purpose and intended to be written once, and be able to run anywhere, this flexibility is one of its biggest assets
  • Any machine that supports Java, can run Java code

Java Compilation

  • Java source code first gets compiled, contrasted to Javascript which is an interpreted language, and thanks to Khai's talks we should have a pretty good idea about how interpretation works and what it means
  • Java compilation involves of two distinct steps
  • The first being put thought an OS-independent compiler, this turns the source code (what we as programmers write), into byte-code, which are platform-independent instructions for the JVM, instead of compiling directly to architecture specific machine code like many other compilers
  • The second is a Java Virtual Machine (JVM) which are custom built for each operating system, the byte code is passed in, and now converted to the machine code
  • Whilst it made porting the universal byte-code easy, it often would come at a cost of performance with the overhead of interpreting this bytecode coming later on your own machine, we will discuss performance as we learn more about the Java Compilation process

Step 1

  1. Parse: .java source files are read, and the resulting token sequence is converted into a syntax tree, a reminder that tokens represent the individual elements of a programming language's syntax
  2. Enter: The compiler constructs a symbol table, this table keeps track of various definitions in our code, like classes, methods or variables. Symbol table can be thought of a data structure which contains entries that store information corresponding to an entries symbol. The enter phase specifically refers to populating this table with symbols. This process has sub-tasks which include processing class definitions, method definitions and field definitions, recording details like names and accessibility
  3. Process Annotations: If requested, can be utilised in compilation to enforce coding conventions, automate boilerplate code generation, or perform complex code generation, it does this based on the annotations in the code base
  4. Once these previous steps have been completed, the Java Compiler can proceed to analyse the syntax trees which were parsed with the goal of generating the class files, whilst analysing the tree, any classes which are required for successful compilation, but were not explicitly specified for compilation will be (if found in the source path) added to a To Do list. The work to actually analyse the tree and generate class files is actually done by a series of 'visitors', these process entries on the compilers To Do list. Each entry will be at some point processed by each of the visitors.
  • Attr - Top level classes are attributed (names, expressions and other elements in syntax tree are resolved and associated with the corresponding types and symbols)
  • Flow - Check definite assignment to variables and unreachable statements
  • TransTypes - Generic typed code is translated to code without generic types
  • Lower - Removes syntactic sugar
  • Gen - Code for methods is generated by Gen, which creates the Code attributes containing the bytecodes needed by a JVM to execute the method. If that step is successful, the class is written out by ClassWriter. This step creates our .class files

Bytecode Obfuscation

This is the process of modifying Java bytecode so that it is much harder to read/understand, yet still retains its full functionality. Without any obfuscation, bytecode can easily be reverse-engineered back to its original form. Obfuscation makes this process harder (but still not impossible).

Step 2

Class files are independent of the machine or the OS which allows them to be run on any system. To be able to run the bytecode, the main class file (the class file which contains the method main), is passed to the JVM (java virtual machine), and then goes through three main stages. These being ClassLoader, Bytecode Verifier, and Just-In-Time-Compiler.

  1. Class Loader: Main class is passed to the JVM, all other classes referenced in the program are loaded through the class loader. The class loader is an object which creates a flat namespace of class bodies that are referenced by a string name. The primordial class loader is used by default, and is present in all JVM's, however a user defined/provided class loader known as a non-primordial class loader can be given to enable custom class-loading behaviour, when providing this becomes the default class-loader.

  2. Bytecode Verifier: After class bytecode is loaded, it must be inspected. The Bytecode verifier checks that the instructions to not perform damaging actions. Some checks include variables are initialized before they are used, method calls match the types of object references, local variable accesses fall within the runtime stack, and the run-time stack does not overflow.

  3. Interpreter/Just-In-Time-Compiler: The interpreter reads and executes the bytecode instructions line by line, this is typically quite slow. A JIT compiler on the other hand can be used when bytecode is converted into machine code. The JVM first uses the interpreter, and when repeated code is found, it uses the JIT compiler to change the bytecode into native machine code, this machine code is then used in place of the repeated code (like method calls for example), therefore greatly improving performance

Things to Consider

  • The performance of Java Bytecode depends on how optimally its given tasks are managed by the host Java Virtual Machine (JVM), if the JVM exploits powerful features of the hardware and operating system, typically the JVM would become more performant
  • Just-In-Time Compiling was introduced in Java 1.1, and progressed to the default in version 1.3, it added a system called HotSpot, which would analyze program performance for sections which were repeatedly executed, these sections would then be targeted for optimising, leading to far superior performance
  • Java also has a built in garbage collector which automates memory management and helps reduce the risk of memory leaks or crashes

C++ Compilation

  • With C++ being used in all other editions of Minecraft other than Minecraft Java edition, it has become an issue that modding is non-existent in the c++ versions
  • This is partially due to the way C++ is compiled, in that when reverse-engineered and decompiled, it is extremely difficult to make out the intention of the code, especially without mappings (although not impossible)
  • Additionally there is no support from either third part API's or Minecraft itself, obviously no one wants to promote modding of the game
  • C++ is also arguable less accessible to the average modder, with a more complicated structure, I could also presume that people simple don't want to switch over with MC java edition having such a rich ecosystem of mods and API's from the past 10+ years

Things to Consider

  • Manual memory management, typically more tedious but allows for far greater control, giving developers more control over memory allocation and deallocation

Minecraft Modding

Remember back when we mentioned that a team of developers were tasked with creating a Minecraft API to make modding easy and accessible? Well it does not exist. The idea/promise was eventually abandoned as the modding scene on Minecraft Java edition had seemingly 'sorted itself out'. And in the peak era of Minecraft modding this was mostly true, many developers and aspiring kids used Forge's (most popular [service??] at the time) api to create their mods. But now there are many different like services with differing api's so the mods are only compatible with one and not the other!

Forge

  • As mentioned earlier, began with Minecraft Coder Project which provides utilities to decompile and de-obfuscate Minecraft. You would have to modifying minecrafts .jar file directly, and two mods could not modifying the same class at the same time.

  • Then came the mod loader innovation (some early services like LiteLoader), you could install these mods, which would then install other mods for you

  • The second innovation was that of Forge which allowed you to avoid modding base classes directly through the use of an API.

  • New releases of Minecraft can break older versions of mods or Forge itself

  • The ability to load new code at run time is part of the Java language as we demonstrated earlier with Class Loaders, so Forge modifies the Java ClassPath to allow our own custom Forge classes to be loaded alongside the games original classes

  • Forge can also directly modify the bytecode of existing minecraft classes, allowing the ability to inject custom behaviour directly into Minecraft's methods and fields, effectively altering how the game runs. It often does this using ASM, which is a library for bytecode manipulation

  • One of the most important aspects is the event bus system, obviously we have recently started covering the concept of a bus in our connect 4 game, but with Minecraft and Forge, we are introduced to an event bus which allows mods to listen for and respond to various events in the game. The mods we create register some event handlers with this event bus so that they can react to predefined game events (such as player actions), and once an event is seen, we can inject some additional functionality to modify some existing behaviours if we wish

  • So to actually create the 'Forge Ecosystem' for their first iteration and enable the use of an API, Minecraft's code had to be decompiled, de-obfuscated and then understood, and this has to happen for each update, although MC has now made de-obfuscating easier by providing mappings for each new update. These mappings essentially show what is meant to be what.

Forge Implementation

  • We must register the items we create in a mod, for example if we added a new item or block we would have to register these things so that the game knows about these objects, else we will run in to unexplained behaviour and crashes
  • We can do this through Forge Registries, part of the Forge API, a registry can be thought of as a map or object, with key-value pairs
  • Registry names/keys must be unique, however we will have different registries for Blocks and Items, so we could have an entry with an identical name in each different registry and be able to avoid any collisions

private static final DeferredRegister<Block> BLOCKS =
  DeferredRegister.create(ForgeRegistries.BLOCKS, {{modIdHere}});

public static final RegistryObject<Block> ROCK_BLOCK =
  BLOCKS.register("rock", () ->
  new Block(BlockBehaviour.Properties.of().mapColor(MapColor.STONE)));

What problems would an official API solve

  • Compare the current implementation we have to that of what an 'official' api might look like
  • The API update could be released as a new version comes out, meaning there is no 'down time' while Forge developers re-write parts of their system to work with the latest versions of minecraft
  • Free
  • More accessible
  • Would encourage more people to attempt modding (set-up is complicated if you are not a developer)
  • etc

Before we Begin

We have to first differentiate the physical client/server from the logical client/server. The physical client is the entire program which runs when you Launch minecraft. It contains all the threads, processes and services which run during the games lifetime. The physical server often known as the dedicated server, can be thought of as the entire program that is run, when a minecraft_server.jar (or equivalent is) is run, not the GUI.

Then we have: The logical server, this is what runs the game logic like mob spawning, weather, health, etc. This logical server is present in the physical server BUT can also run inside a physical client (along with a logical client), when the player is in a single player world. The logical server runs in a thread named the server thread. We will see this when we are running the game. And finally we have the logical client, this is what accepts the input from a player and relays it to the logical server, it also receives information back form the logical server and updates the clients graphics to reflect the change. The logical client runs in the Render thread.

These 'levels' as they are referred to are important, and we may have to perform some level checks as we are creating our mods, as we only want certain events to happen on one thread or the other.

Practical Component