Skip to content

Instantly share code, notes, and snippets.

@AndrewJHart
Forked from nornagon/1-intro.md
Created April 24, 2022 20:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save AndrewJHart/ebc5150a1d40541e55cf50629dc121ea to your computer and use it in GitHub Desktop.
Save AndrewJHart/ebc5150a1d40541e55cf50629dc121ea to your computer and use it in GitHub Desktop.
How to make a Minecraft (1.8) mod

How to make a Minecraft mod

Minecraft mods, especially mods which change the client, are by and large written with Forge. If you visit their website, you'll be greeted abruptly by a mysterious message at the top of an SMF forum, with no clear path towards actually... making a mod. I'm documenting here the steps I went through to get started, in the hopes of helping the next person have an easier time of it.

I'll be using Scala for this guide, but it should be fairly easy to adapt these instructions to any JVM language (e.g. clojure or if you're feeling masochistic, Java). I'm also developing on OS X, so some of the commands will be a little different if you're on Linux or Windows. I'm assuming you have some proficiency with your operating system, so I won't go into details about how to adapt those commands to your system.

Background

Minecraft doesn't have an official mod API (despite early promises), so all mods are built off a decompiled version of the source code called Mod Coder Pack, maintained by... I have no idea who, possibly this guy who appears to work at Mojang i.e. Microsoft but still hosts files on Mediafire? MCP is essentially a set of scripts that takes the official Minecraft jar files, decompiles them, and renames all the obfuscated variables and classes. Mojang seems to be fine with this.

Forge is built on top of MCP--it takes the decompiled and deobfuscated source, then tweaks it a bunch, adding hooks in various places that your mod can attach itself to through an extensive set of source code patches. Forge is Minecraft with a mod API.

Getting started

First up you're going to want to fetch the latest Forge MDK from http://files.minecraftforge.net/, something like this:

$ curl -O http://files.minecraftforge.net/maven/net/minecraftforge/forge/1.8.9-11.15.1.1764/forge-1.8.9-11.15.1.1764-mdk.zip

This contains a template project that uses gradle as a build system, hooking into ForgeGradle for its dirty work (decompiling and deobfuscating Minecraft, applying various patches, and so on). To get this going, you'll want to unzip the template into a new directory

$ mkdir mymod && cd mymod
$ unzip ../forge-1.8.9-11.15.1.1764-mdk.zip

This will dump a bunch of files into the current directory, something like this:

.
├── README.txt
├── build.gradle
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── src
    └── main
        ├── java
        │   └── com
        │       └── example
        │           └── examplemod
        │               └── ExampleMod.java
        └── resources
            └── mcmod.info

Great! That's all the stuff we need. Because I want to write my mod in Scala, I'm going to open up the build.gradle and add this line before I do anything else:

--- a/build.gradle	2016-02-29 16:20:56.000000000 -0800
+++ b/build.gradle	2016-03-16 18:32:50.000000000 -0700
@@ -12,6 +12,7 @@
         classpath 'net.minecraftforge.gradle:ForgeGradle:2.1-SNAPSHOT'
     }
 }
+apply plugin: 'scala'
 apply plugin: 'net.minecraftforge.gradle.forge'

And then we want to ask gradle to pull down all the minecraft stuff it needs and do its deobfuscation magic:

$ ./gradlew setupDecompWorkspace

This downloads minecraft, decompiles it, deobfuscates the decompiled source, applies a bunch of patches, recompiles everything, and stores it all in ~/.gradle/caches/minecraft (so it's shared between projects--not editable!)

Everything's technically ready to go now. You could boot a client with ExampleMod loaded by running ./gradlew runClient. The only thing ExampleMod does is print out DIRT BLOCK >> tile.dirt during initialization; it's otherwise identical to the unmodded client. But that's our Hello World!

On the shoulders of giants

There are many, many other modding tutorials and talented programmers that I've drawn from in writing this series. I'm particularly indebted to:

If this tutorial is at all useful, it is because these others have come before me and shared their wisdom.

Setting up an IDE

I really like using IntelliJ for making Minecraft mods, because it makes it super easy to browse through the deobfuscated Minecraft source code, which is often the only way to figure out how anything works. Fortunately it's super easy to set up IntelliJ with Forge.

Just hit the 'Import Project' button in IntelliJ...

"Import Project" button in IntelliJ

... and select the build.gradle file in your mod directory. Hit 'OK' and you're in!

Importing the project doesn't automatically create any run configurations, but ForgeGradle comes with a little helper to do that for IntelliJ. Running

$ ./gradlew genIntellijRuns

will create 'Run Client' and 'Run Server' run configurations, so booting the client will be as simple as hitting Ctrl+R.

Time for some Scala

Delete the java directory (nuke it from orbit, it's the only way to be sure). Make a new scala directory underneath src/main. If it doesn't show up with a blue folder icon in IntelliJ indicating it's a directory where source code is found, you might not have added the apply plugin: 'scala' line to gradle.build. Add it and re-import the gradle project (I just blew away the .idea directory and imported from scratch when this happened to me).

Happy blue folder icon

Underneath src/main/scala, make some more directories for your package, something like src/main/scala/com/mymod. And therein, place the following bare-bones template, which is a Scala transliteration of the Java ExampleMod:

package com.mymod

import net.minecraft.init.Blocks
import net.minecraftforge.fml.common.Mod
import net.minecraftforge.fml.common.Mod.EventHandler
import net.minecraftforge.fml.common.event.FMLInitializationEvent


@Mod(modid = MyMod.MODID, version = MyMod.VERSION, modLanguage = "scala")
object MyMod {
  final val MODID = "mymod"
  final val VERSION = "1.0"

  @EventHandler
  def init(event: FMLInitializationEvent): Unit = {
    println(s"DIRT BLOCK >> ${Blocks.dirt.getUnlocalizedName}")
  }
}

It's SUPER CRITICAL to add the modLanguage = "scala" argument to the @Mod annotation, or you'll get funky error messages about MyMod.<init> not being defined, like this:

net.minecraftforge.fml.common.LoaderException: java.lang.InstantiationException: com.mymod.MyMod
	at net.minecraftforge.fml.common.LoadController.transition(LoadController.java:162)
	at net.minecraftforge.fml.common.Loader.loadMods(Loader.java:543)
	at net.minecraftforge.fml.client.FMLClientHandler.beginMinecraftLoading(FMLClientHandler.java:208)
	at net.minecraft.client.Minecraft.startGame(Minecraft.java:451)
[...]
Caused by: java.lang.InstantiationException: com.mymod.MyMod
	at java.lang.Class.newInstance(Class.java:427)
	at net.minecraftforge.fml.common.ILanguageAdapter$JavaAdapter.getNewInstance(ILanguageAdapter.java:174)
	at net.minecraftforge.fml.common.FMLModContainer.constructMod(FMLModContainer.java:534)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:497)
[...]

See here for the code in Forge which needs this annotation.

Anyway, you should now be running the simplest possible Scala-based Minecraft mod. Congrats!

Fundamentals

The fundamental elements of Minecraft are blocks, items and entities. Blocks are parts of the world, like dirt and obsidian. Items are things that go in your inventory like a sword or a book. Sometimes an item represents a block, like when you have a dirt block in your inventory. And lastly, entities are things that move, like chickens and players.

Blocks

Blocks form a 3D grid, with X and Z forming the horizontal plane, and Y the vertical axis. Internally, the infinite world is divided into chunks, each 16 blocks to a side and 256 blocks tall. Chunks can be individually loaded and unloaded by the server, and when you fly around the world, the server will send you chunks one by one.

In code, what Minecraft calls a Block would probably more accurately be called a BlockType, but so be it. Each block in a chunk is stored very compactly, as just 16 bits of data--12 bits for a block ID and 4 bits for metadata. In order to figure out the various other bits of information about a block--say, how to render it, or how long it should take to break the block--Minecraft looks up the Block by its ID and passes in the block position and metadata to ask it questions. Minecraft 1.8 introduced a fancy thing called BlockState to aid in interpreting those 4 bits of metadata (more on that later). If you want to store more than 4 bits of information about your block, like signs and paintings do, you'll want a TileEntity. More on that later too. Due to the fact that a block's identity is stored as a 12-bit number, there can only be up to 4096 different block types in any given Minecraft instance.

Enough talk. How do I make a block??

First, make a new class, I'm going to call mine BlockCool.

package com.mymod

import net.minecraft.block.Block
import net.minecraft.block.material.Material
import net.minecraft.creativetab.CreativeTabs


class BlockCool extends Block(Material.ground) {
  setUnlocalizedName("cool")
  setCreativeTab(CreativeTabs.tabDecorations)
}

We'll put some more stuff in there later, but for now that's all we need. We extend from Block and pass Material.ground to its constructor. The Material defines a few basic properties of a block, such as whether or not it's solid, opaque, burnable, and so on. Browse the Minecraft source starting from net.minecraft.block.material.Material for examples of other materials and their use.

That's defined our block type--well, actually that's more like a block type type. In order to define a block type, we'll need an instance of BlockCool. Theoretically we could make many BlockCool instances, and each would get its own block ID and be its own block type. We could even make them render differently, despite all being of the same block type type. But we won't. Not today, anyway.

Next up we'll make an instance of BlockCool and register it with the GameRegistry to give it an ID. Add a preInit event handler to MyMod (the name of the function isn't important; Forge does reflection magic and looks at the type of the event argument):

object MyMod {
  @EventHandler
  def preInit(event: FMLPreInitializationEvent): Unit = {
    val cool = new BlockCool
    GameRegistry.registerBlock(cool, "cool")
    // "cool" is the name of the block, used by Minecraft as
    // a human-friendly version of the block ID that's created
    // internally. Forge will actually tweak the name to make
    // sure it doesn't collide with any other mods, by
    // prepending your mod's ID, like "mymod:cool".
  }
}

NOTE: Technically, it also works to register the block in the init phase, rather than preInit. However, when we later come to registering item models, which must be done in the preInit phase, we're going to want our blocks to already be registered, so we're preparing for that. Also, Forge recommends you register blocks in preInit, so there's that.

And that's all you need! If you hit run and open the creative inventory, you'll see something like this:

The 'cool' block displaying in the creative inventory search tab

That beautiful purple and black thing is our baby! Go ahead and plop one down in the world.

A 'cool' block in the world

Amazing.

Just one thing though... I didn't actually want it to be black and purple and do nothing.

Models and textures

Let's solve the 'not black and purple' problem first, by way of a blockstates definition file. There are a bunch of different ways to affect the way a block is rendered (up to and including completely custom OpenGL code), but for this relatively normal block the easiest thing for us will be to use the model system built into vanilla Minecraft.

BlockStates and variants

Make a JSON file underneath src/main/resources/assets/mymod/blockstates, called cool.json. (The name is important, it has to match the block's name). In it, this:

{
  "variants": {
    "normal": { "model": "lapis_block" }
  }
}

The "variants" key in the blockstates JSON file can define a different model to be rendered depending on the metadata of a block. There's a fantastic explanation of the format of this file on the Minecraft wiki, which I won't repeat here, but it can do some pretty neat stuff. For now we're just going to use the default "normal" variant, which is what you get if your block doesn't use metadata at all. And just to test it's working, we're going to make our block look like a block of lapis. Since "lapis_block" doesn't have a : in it, Forge will recognise that we're referring to a vanilla asset. We'll refer to our own models later as "mymod:mymodel".

The 'cool' block looking like a lapis block

It still looks all dumb and black and purple in our hotbar though, and anyway we don't want it to look like lapis at all. To fix those issues, we're going to need to make a custom model.

Item models

Let's tackle the hotbar issue first. Make another JSON file, this time under the assets/mymod/models/item directory, and call it cool.json.

{
  "parent": "block/lapis_block",
  "display": {
    "thirdperson": {
      "rotation": [ 10, -45, 170 ],
      "translation": [ 0, 1.5, -2.75 ],
      "scale": [ 0.375, 0.375, 0.375 ]
    }
  }
}

This defines an item model, which tells Minecraft how to render the item when it's in our inventory, and when it's in the world as an item (for example, in the player's hand, or lying on the floor).

To hook it up so that Minecraft knows to render the item using this item model when it's in your inventory, add this snippet to the preInit function, after the call to registerBlock():

// GameRegistry.registerBlock() also registers an item which
// represents the block. findItem just looks it up by name.
val coolItem = GameRegistry.findItem(MODID, "cool")
ModelLoader.setCustomModelResourceLocation(
  coolItem,
  0,
  new ModelResourceLocation(coolItem.getRegistryName)
)

NOTE: It's important that you call ModelLoader.setCustomModelResourceLocation() during the preInit phase, otherwise it doesn't work. ModelLoader.setCustomModelResourceLocation() adds your item model registration information to a list, which is consulted during the constructor of RenderItem (of which there is one per game instance). RenderItem is constructed after preInit, but before init.

See the definition of net.minecraft.client.Minecraft.startGame() for more details (preInit is called by net.minecraftforge.fml.client.FMLClientHandler.instance().beginMinecraftLoading(), and init is called by net.minecraftforge.fml.client.FMLClientHandler.instance().finishMinecraftLoading()).

The black and purple item in the hotbar has been replaced by what appears to be a lapis block

Begone, vile magenta! I banish ye forever.

Aside: vanilla assets

It's super helpful to peek at the vanilla Minecraft assets to see how to structure your own. You can unzip the Minecraft JAR file (which ForgeGradle kindly downloaded for you) to browse them easily. For example:

$ unzip ~/.gradle/caches/minecraft/net/minecraft/minecraft/1.8.9/minecraft-1.8.9.jar 'assets/*' -d vanilla-assets

wherein you will find all the vanilla blockstate definitions, textures and models.

I copied the item model definition from the vanilla lapis block item file, which you can see at vanilla-assets/assets/minecraft/models/item/lapis_block.json if you unzip the vanilla assets (see below).

Block models

To banish the lapis, though, we'll need to make a new model. We could do all sorts of fancy stuff here, but we're just going to use the basic 'cube' model and change its texture. Baby steps.

Drop a texture (I recommend this one: awesome face) in assets/mymod/textures/block/cool.png, and make a new JSON file in assets/mymod/models/block/cool.json:

{
  "parent": "block/cube_all",
  "textures": {
    "all": "mymod:block/cool"
  }
}

This defines a block model based on the cube_all builtin model, and overrides the "all" texture with our own. (You can check out the cube_all model at vanilla-assets/assets/minecraft/models/block/cube_all.json, and see that it's in turn based on the cube model, which defines a single cuboid element. More on the model format here)

Update your item model to have "parent": "mymod:block/cool" instead of "parent": "block/lapis_block", and also update the blockstates file to specify the model as "mymod:cool" (not "mymod:block/cool"; Forge in its infinite wisdom adds the block/ part for you).

Et voilà:

The cool block looking like awesome face instead of lapis

Interlude: Client and Server

While developing, you've been using what's called the 'combined client'. Single-player Minecraft is actually just multi-player Minecraft with the server and client running in the same process. If you try to run the dedicated server with the current code (either through the 'Minecraft Server' run configuration in IntelliJ or via ./gradlew runServer), you'll get this nasty exception:

java.lang.NoClassDefFoundError: net/minecraft/client/resources/model/ModelResourceLocation
[...]
Caused by: java.lang.ClassNotFoundException: net.minecraft.client.resources.model.ModelResourceLocation
[...]
Caused by: net.minecraftforge.fml.common.asm.ASMTransformerWrapper$TransformerException: Exception in class transformer net.minecraftforge.fml.common.asm.transformers.SideTransformer@1200458e from coremod FMLCorePlugin
[...]
Caused by: java.lang.RuntimeException: Attempted to load class net/minecraft/client/resources/model/ModelResourceLocation for invalid side SERVER

This is because the dedicated server doesn't include any of the client-side rendering logic. The server JAR doesn't contain the ModelResourceLocation class, which we're using in our preInit function, so when the server tries to initialize our mod, it can't find that class and crashes.

To address this issue, Forge has a utility class called SidedProxy, which allows a class to behave differently depending on whether it's loaded on the client or the server. Refactoring to use this, we get:

@Mod(modid = MyMod.MODID, version = MyMod.VERSION, modLanguage = "scala")
object MyMod {
  final val MODID = "mymod"
  final val VERSION = "1.0"

  def registerBlocks() = {
    val cool = new BlockCool
    GameRegistry.registerBlock(cool, "cool")
  }

  def registerItemModels(): Unit = {
    val coolItem = GameRegistry.findItem(MODID, "cool")
    ModelLoader.setCustomModelResourceLocation(
      coolItem,
      0,
      new ModelResourceLocation(coolItem.getRegistryName)
    )
  }

  @SidedProxy(
    clientSide = "com.mymod.ClientOnlyProxy",
    serverSide = "com.mymod.CommonProxy"
  )
  // Forge will fill this in during mod loading.
  var proxy: CommonProxy = null

  @EventHandler
  def preInit(event: FMLPreInitializationEvent): Unit = {
    proxy.preInit()
  }
}

class CommonProxy {
  def preInit(): Unit = {
    MyMod.registerBlocks()
  }
}

class ClientOnlyProxy extends CommonProxy {
  override def preInit(): Unit = {
    super.preInit()
    MyMod.registerItemModels()
  }
}

In the client, proxy will contain an instance of ClientOnlyProxy, but in the dedicated server, it will contain an instance of the base class CommonProxy. It's also possible to have another subclass for dedicated-server-only code, as long as it also extends CommonProxy.

You might also see annotations on functions like @SideOnly(Side.CLIENT) when browsing other Forge mod code. Forge does horrible things with ClassReader to make those functions only appear on either the annotated side. Use at your peril.

Items

Like Block, instances of the Item class represent types of items. A specific item (or stack of items) is represented by an instance of the ItemStack class. An ItemStack can have metadata, often used to store durability (up to 15 bits, it's stored as a short but restricted to be non-negative) and sometimes called 'damage' in the code. An ItemStack can also have an NBT tag associated with it, containing arbitrary extra data--think enchantments, Tinkers Construct item compositions, and so on.

Let's make an item. I want to make a "wheat and steel" item, like flint and steel but instead of lighting things on fire it plants wheat. I'm going to start by looking at the vanilla source code for flint and steel, which you can see in IntelliJ by double-tapping Shift and typing in ItemFlintAndSteel. This is roughly the strategy I'd suggest for doing anything with Minecraft (or really, anything in programming): find something that's a little like what you want, learn from it and adapt it to your own nefarious wheat-making purposes. The vanilla source code is your friend, as is GitHub search.

Start by making a new Item subclass, called ItemWheatAndSteel:

class ItemWheatAndSteel extends Item {
  maxStackSize = 1
  setMaxDamage(64)
  setCreativeTab(CreativeTabs.tabTools)
}

Instantiate one and register it with the GameRegistry in preInit (or in registerBlocks if you followed the SidedProxy note above):

val itemWheatAndSteel = (new ItemWheatAndSteel).setUnlocalizedName("wheat_and_steel")
GameRegistry.registerItem(itemWheatAndSteel, "wheat_and_steel")

Finally, make an item model for it in assets/mymod/models/item (cribbed from vanilla-assets/assets/minecraft/models/item/flint_and_steel.json):

{
  "parent": "builtin/generated",
  "textures": {
    "layer0": "blocks/wheat_stage_4"
  },
  "display": {
    "thirdperson": {
      "rotation": [ -90, 0, 0 ],
      "translation": [ 0, 1, -3 ],
      "scale": [ 0.55, 0.55, 0.55 ]
    },
    "firstperson": {
      "rotation": [ 0, -135, 25 ],
      "translation": [ 0, 4, 2 ],
      "scale": [ 1.7, 1.7, 1.7 ]
    }
  }
}

And register it with the ModelLoader in registerItemModels (or otherwise during the preInit phase):

ModelLoader.setCustomModelResourceLocation(
  itemWheatAndSteel,
  0,
  new ModelResourceLocation(itemWheatAndSteel.getRegistryName)
)

And there it is, next to our glorious BlockCool.

The wheat and steel item in the hotbar

It doesn't do anything yet, though. Let's fix that. Override the onItemUse function in ItemWheatAndSteel like this:

  override def onItemUse(
    stack: ItemStack,      // The ItemStack the player is using
    player: EntityPlayer,  // The player using the item
    world: World,          // The world in which the block exists
    pos: BlockPos,         // The position of the block the item is being used on
    side: EnumFacing,      // The side of the block being pointed at
    hitX: Float,           // Exact coordinates the mouse is pointing at
    hitY: Float,
    hitZ: Float
  ): Boolean = {
    // If the player is pointing at the top face of a block, and there's
    // nothing above it...
    if (side == EnumFacing.UP && world.isAirBlock(pos.up)) {
      // ... get the Minecraft item called "wheat_seeds" ...
      val wheat_seeds =
        GameRegistry.findItem("minecraft", "wheat_seeds")
          // ... which we know implements IPlantable ...
          .asInstanceOf[Item with IPlantable]
      // ... check if the block the player is pointing at can grow wheat (i.e.
      // it's farmland)
      val block = world.getBlockState(pos).getBlock
      val canGrow = block.canSustainPlant(world, pos, EnumFacing.UP, wheat_seeds)
      if (canGrow) {
        // ... if it is, set the block above the pointed-at block to wheat ...
        world.setBlockState(pos.up, wheat_seeds.getPlant(world, pos))
        // ... and add one point of damage to the wheat & steel.
        stack.damageItem(1, player)
        // The return value of onItemUse indicates whether the use was successful,
        // which is used to determine whether to play the "swing item" character
        // animation.
        return true
      }
    }
    false
  }

Some planted wheat, holding the wheat and steel item

NOTE: You'll have to be in survival mode for the items to get damaged. See the source for ItemStack.damageItem(), which begins with a check for creative mode.

You still have to hoe the ground before planting the wheat, though. It'd be neat if it would automatically hoe the dirt, too... (hint: look at how ItemHoe works.)

Entities

TK

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment