Project Nephia

Introduction

Project Nephia is a Modification (Mod) to the popular game Minecraft.

Project Nephia aims to bring RPG elements to this game, utilizing its flexible world to create a one-of-a-kind experience for player to explore the world, go on adventure with their friends in the virtual world. Due to it being a sand-box, it opens up to endless possibilities.

Minecraft & Forge

Minecraft
3D Sandbox Game
Cubes as building blocks
Procedurally-generated World
Open-ended play experience
Forge
Modification API
Deconflicts between Mods
Deobfuscate Source Code

Problems
10111010 11100110 10101011
X
No easy way to make modification to core game files
LWJGL
Animations and UI require low level native methods
Limited event hooks for interaction with core game

GUI API

Application Programming Interface for the design of Graphical User Interfaces and on-screen overlays. Allows for design elements to be grouped in a parent-child hierarchy for easy manipulation (e.g. resizing or repositioning)

Details [show]


Definitions

HUD: Heads Up Display. It is the on-screen overlay over the 3D game world, displaying crucial information such as Health.

GUI: Graphical User Interface, an interface, usually in the form of a window, used to display information. Unlike the HUD, which is always on the screen, GUIs are typically only brought up on request to fulfill a specific function.

Problem

Forge (the API mentioned above), does not provide high-level abstraction for GUI and HUD designing. Currently, a programmer must specify the number and location of the vertices of each polygon relative to the screen and use LWJGL to draw them. This lacks flexibility and results in unnecessary math and copy-pasted code, leading to update errors in the long run.

Forge also does not provide displaying text at different font sizes. Normally a programmer would have to manually scale text as if it were an image, using LWJGL’s scale methods. This does not merely scale the size of the font, but also the on-screen position (e.g. An element at coordinate (5,5) scaled by 0.5 will now be at (2.5,2.5)), leading to frustrating problems and yet more unnecessary math.

Solution

My API aims to provide a higher level of abstraction and manage all the necessary math calculations internally, allowing programmers to do less copy-pasting. By using a parent-child to group similar or relevant elements, properties can be easily ascribed to groups of elements where they would otherwise have to be done separately.

Result

As a result, the code required to draw or edit existing elements becomes significantly simplified.

This can be illustrated from the below example:

HUD Design: A health bar requires 3 elements: The background, the health bar, and the text overlay on the bar. Each of these elements has to be drawn independently, with their position and size determined manually relative to the screen
Before
//draw the holder at (0,0) with 150 width and 9 height
//args: pos x on screen, pos y on screen, pos x of source, pos y of source, width, height
this.drawTexturedModalRect(5, 2, 0, 0, 150, 9); 

//draw the bar inside the holder at 2,2 away from the holder with 148 with and 7 height 
//x coordinate: 5+2=7, y coordinate: 2+2 = 4
this.drawTexturedModalRect(7, 4, 0, 9, health*148, 7);

//draw the health text
//pushes matrix
GL11.glPushMatrix();

    //calls LWJGL scale
    GL11.glScalef(0.65F, 0.65F, 0.65F);
	
    //renders the string at 0.5,0.5 away from the bar (refer above) 
	//scaled x coordinates: (7+0.5)/0.65 ~= 11.538, scaled y coordinate: (4+0.5)/0.65 ~= 6.923
	//arg: string, prescale posX, prescale posY, color (hex)
    fontRender.drawString(""+ df.format(health)+"/" + df.format(maxHealth) , 11.538, 6.923, 0x000000); 

//pops back matrix
GL11.glPopMatrix();
After
//args: width on screen, height on screen, pos x on screen, pos y on screen, texture, pos x on source, pos y on source, width on source, height on source
//draws bar holder at 0,0 on the screen with 150 width and 9 height
BaseElement barHolder = new BasicElement(150,9,0,0, hudv2 , 0,0,150,9); 


//draws bar at 2,2 relative to its parent, with 148 width and 9 height
BaseElement hpBar = new DynamicElement(148, 7, 2, 2,  hudv2, 0, 9, 148, 7).setDynamicLength(dv.hpRatio); //sets a pointer for dynamic length

//args: pos x on screen, pos y on screen, font size, color, stringwrapper (reference)
//draw text at 0.5,0.5 relative to the hpBar parent
BaseElement hpInfo = new StringElement(0.5, 0.5, 0.65f, 0x020000, dv.hpInfo); 

//adding them as parent child
hpBar.add(hpInfo);




barHolder.add(hpBar);

//draws the bar
barHolder.draw();

The above code first draws the holder at (7,4), draws the actual bar (2,2) away from the bar holder then draws the text (0.5,0.5) away from the actual bar. The results is as below:

Final product of code above

At glance, it appears that the only benefit was neatness (and maybe the proper use of OO paradigm?), which I would argue that neatness is important, but consider:

Suppose we are unhappy with the position and size of the bar drawn, and we want to scale the size by 1.25 and shift it down by 50px. The changes made would be extremely complex:

Before
#new y: 2+50 = 52, new width: 150*1.25 = 187.5, new height: 9*1.25 = 11.25
/*this.drawTexturedModalRect(5,*/ 52 /*, 0, 0,*/ 187.5 /*,*/ 11.25 /*);*/ 

#new y: 2+2+50 = 54, new width: 148*1.25 = 185, new height: 7*1.25 = 8.75
/*this.drawTexturedModalRect(7,*/ 54/*, 0, 9, health**/ 185 /*,*/ 8.75 /*);*/

/*GL11.glPushMatrix();*/

    #new rescale: 0.65*1.25 =0.8125
    /*GL11.glScalef(*/0.8125 /*,*/ 0.8125 /*, 0.65F);*/

    #new x = (2+5+0.5)/(0.65*1.25) = 9.23, new y = (2+2+50+0.5)/(0.65*1.25) ~= 67.077
    /*fontRender.drawString(""+ df.format(health)+"/" + df.format(maxHealth) ,*/ 9.23 /*,*/ 67.077 /*, 0x000000); 

GL11.glPopMatrix();*/
After
#new y: 2+50 =52
/*BaseElement barHolder = new BasicElement(150,9, 5,*/52/*, hudv2 , 0,0,150,9); 

BaseElement hpBar = new DynamicElement(148, 7, 2, 2,  hudv2, 0, 9, 148, 7).setDynamicLength(dv.hpRatio); //sets a pointer for dynamic length

BaseElement hpInfo = new StringElement(0.5, 0.5, 0.65f, 0x020000, dv.hpInfo); 

hpBar.add(hpInfo);
barHolder.add(hpBar);*/

barHolder.scale(1.25,1.25);

/*barHolder.draw();*/

The above highlights code that needs to be change, math done will have a '#' to preceed it.

As the above shows, there are numerous changes to be made and a considerable amount of math to do for the version not using the API, whereas the process is vastly simplified using the API as the child elements 'hpBar' and 'hpInfo' will inherit the changes from the parent.

Design Philosophy

Elements are designed with inheritance and encapsulation in mind – open for extension, closed for modification. The abstract class contains basic values such as its default size and position (relative to its parent), as well as its current size and position. A child element’s position and size can only be modified by its singular parent element. The parent edits the base values of the child, and the child uses these values to draw themselves on the screen. With the strong Object Oriented paradigm, errors can be isolated fairly easily, as it is easy to see at which level the error started to propagate.

Programmers seeking to expand the core functionality of this API can extend the existing classes and ascribe to it new properties (such as references to external fields) or by designing functions for additional behaviors.

Interpreter

Custom Markup Language Interpreter that is able to process simple mathematical expressions, comparators and logical operators.

Details [show]


Problem

Data storage is something that ought to be external to the code because it is global and tends to be unchanging. Due to the nature of the stored data required for my mod, there is a need for interpretation of simple math expressions and Boolean logic.

Solution

I designed a markup language that can evaluate simple mathematical functions (+ - * /), comparators (>=, <=) and logical operators (&& ||) through parsing external text files.

Result

Without using this interpreter, we would have to create an object that stores values or extract values from the player.

Functional:

//non-OO, a static method for each type of description
public static getHoverDescription(EntityPlayer player, int skillID){
    //Obtain’s skill data from the player using skillID
    SkillData data = player.getSkillData(skillID);
    switch(skillID){
	case 0: return data.getLevel()*5 + "% movespeed";
	case 1: return data.getLevel()*10 + " health, making you stronger";	
        default: throw new IllegalArgumentException("Uknown skill ID " + skillID);
    }
}

Object Oriented

//OO
//within specific skill class, 1 method for each type of description
public String getHoverDescription(){
    return this.level*5 + "% movespeed";
}
Public String getFlavourText(){
    return "People of the past had magic that allow them to cross over vast distances";
}

Our API, read from external data

description.additional::#$level$*5#% movespeed;

Allows for compact and changes without editing code, only exist on Client side. Do not need to in-depth understanding of the code to be able to write description for skills (very much like mark-up language)

Design Methodology

The interpreter invokes a PredicateInterpreter to evalute predicate, and a MathInterpreter to evalute the math function, after subbing in these values, it returns a object containing data to the client, allow our GUI to properly display this information.

The predicate interpreter first takes use regex to extract everything in brackets, use the content to call pass it into itself recursive and uses the result to replace the current expression. It laters split the newly replaced predicate by AND (&&), within each array these 'expressions', it is then split by ORs (||), which each 'term' is evaluated by splitting them by comparators, then after, by switching the comparator, map it to the real comparator in the language (Java). All these are then iteratively evaluated, getting a final true or false value.

I wrote a full article on this here.

I will move on to the logic behind the MathInterpreter. It acts as a recursive descent parser.

The parseExpression function evaluates an expression(string) by progressing along the string, calling the helper functions (parseTerm and parseFactor) to evaluate each term and, within a term, each factor within the expression. Each term is defined by the presence of any number (including 0) of either multiplicative or divisive operators, whereas each factor is defined by the presence of parentheses as well as unary operators(+/-).

I also wrote a short write up on this here.

Permission System

Tree based system for the allocation of Boolean flags determining what actions each entity is authorized to perform.

Details [show]


Problem

As a modification, we want to restrict what different players can do for a myriad of reasons. We implemented a zones system (zones are physical areas in the game world), to limit what players can do in different locations. This requires a large number of Boolean flags to check the ability of each player to perform actions. These flags are then checked against every time a player attempts an action. As a result, the array of flags quickly grew to an impractical size.

Solution

A tree-based permission system was devised such that leaf nodes denoted specific action permissions while parent nodes inherited the permissions of its leaves. An exceptions system was also included to allow for higher levels of customization of authority levels such that two people holding the same permission level could vary in the permissions granted. This means that one could simply set a large number of permissions using a single command, while retaining the flexibility of omitting certain permissions depending on the circumstance.

Result

Before the implementation of the permission tree, each player had to have every permission defined and added separately.

//Permission init
playerA.addPermission(“breakwood”);
playerA.addPermission(“breakstone”);
playerA.addPermission(“placewood”):
playerA.addPermission(“placestone”):
playerA.addPermission(“castflash”);
playerA.addPermission(“castpunch”);
playerB.addPermission(“breakwood”);
playerB.addPermission(“breakstone”):
playerB.addPermission(“castflash”);
playerB.addPermission(“castpunch”);

After we implemented the tree system, permissions could be granted in bulk, with exceptions given as necessary.

PermissionGroup breakPerm = new PermissionGroup(“all.terrain.break”, “all.combat.cast”);
breakPerm.add(playerA,playerB);
PermissionGroup place  = new PermissionGroup(“all.terrain.place”);
place.add(playerA);

Design Philosophy

The permissions tree is a global object that is checked against every time an action is performed. Players and other entities are grouped into ‘permission groups’ which are then added to nodes of the tree to represent awarding the members of that group the permission. As a result, players can be members of multiple groups, and while each group may have different permissions, the player’s personal set of permissions is the sum of the permissions inherited from each group. Blacklisting is also introduced such that certain permissions will not be propagated upwards, meaning that during a traversal, if the parent node has blacklisted the respective player, the traversal will ignore the current layer and check if the player has any superior permissions(from parent nodes of the current node). This allows for control over what specific permissions a player is assigned.

Stats System

Defining player variables that sync over a client-server connection to allow for greater variety in combat mechanics and strategy

Details [show]


Definition

Stats: properties or values belonging to a player or living entity. These are commonly numerical values used mostly for combat calculation. Things such as current health, max health, et cetera.

Problem

As we cannot modify the base game files, we cannot add additional fields to the EntityPlayer class, which defines the player in game.

Furthermore, since this is a multiplayer game, server-client handling is required to be done manually.

Without using our API, one would have to generate an interface and hook onto an event to implement it on the player. To obtain any such value on the client side, one would have to send a message to the server, then the server message handler would call a static method to obtain the interface, and serialize the returned values into a message and send it to the client.

Solution

As it is important that the server holds the authority, the client can only query data. This prevents client-sided hacking. As such, a server-sided system that uses Java Enumerations as indexes in an array was conceived. This system, with its associated set of utility methods, simplifies the process of adding, querying and saving any additional stats that a programmer would want to add to a player. Due to the inability to modify base game files, these stats are associated with a player’s name, and stored within a global map.

Design Philosophy

The reason for using a global map is simple: there are I/O methods regarding the saving and loading of stats, and since there is no access to the base code, there is no preferable Object Oriented approach to this problem.

Each datatype attached to a ‘EntityPlayer’ are intended to encapsulate data. As such, only when the server is making the request, it is allowed to call setters, which automatically raise the update flag, pushing the updated data down to the clients. This implied contract allows other programmers to simply add an enumeration value and increase the array size to create a stat.