Difference between revisions of "Addon Development"

From TerraFirmaCraft Plus Wiki
Jump to: navigation, search
(12 intermediate revisions by 5 users not shown)
Line 1: Line 1:
{{wip}}
+
<!-- The code on this page was syntax highlighted with Pygments - https://pygments.org/demo/. If you are modifying on the code on this page, consider using that tool. -->
  
 
This page is an in-depth tutorial on how to setup an environment for creating [[Addons]] for TFC+. It will also show you how to make a basic addon for demonstration purposes, from which you can grow your own addon, and give you some general tips and tricks. It will ''not'' go in-depth about programming, or making substantial changes to the game as those are complex topics that have been better covered elsewhere.
 
This page is an in-depth tutorial on how to setup an environment for creating [[Addons]] for TFC+. It will also show you how to make a basic addon for demonstration purposes, from which you can grow your own addon, and give you some general tips and tricks. It will ''not'' go in-depth about programming, or making substantial changes to the game as those are complex topics that have been better covered elsewhere.
Line 41: Line 41:
 
# Then from the "Advanced" tab click on "Environment Variables..."  
 
# Then from the "Advanced" tab click on "Environment Variables..."  
 
# Locate the "Path" variable and then click on "Edit...".  
 
# Locate the "Path" variable and then click on "Edit...".  
# In the window that pops up, click on the "New" button and then paste in the <code>C:/Program Files/Java/jdk1.8.0_XXX/bin</code> path that you copied earlier, and press Enter.
+
# In the window that pops up, click on the "New" button and then paste in the <code>C:/Program Files/Java/jdk1.8.0_XXX/bin</code> path that you copied earlier, and press Enter. (if it says there are wrong characters try switching the slashes with backslashes first)
 
# Click on "OK" to close the "Environment Variables" window. ''Do not click on "Cancel" or click on the close window button as these will discard your changes''.
 
# Click on "OK" to close the "Environment Variables" window. ''Do not click on "Cancel" or click on the close window button as these will discard your changes''.
 
# Restart your computer so that Windows can properly register the new PATH.  
 
# Restart your computer so that Windows can properly register the new PATH.  
Line 70: Line 70:
 
# Then from the "Advanced" tab click on "Environment Variables..."  
 
# Then from the "Advanced" tab click on "Environment Variables..."  
 
# Now click on the "New..." button.  
 
# Now click on the "New..." button.  
# In the window that pops up, enter "JAVA_HOME" under the "Variable name" field, and paste the path to the JDK you copied earlier into the "Variable value" field. It should look something like the screenshot below.
+
# In the window that pops up, enter "JAVA_HOME" under the "Variable name" field, and paste the path to the JDK you copied earlier into the "Variable value" field. It should look something like the screenshot below. (by the way, the "XXX" part of the path represents a number that can vary. you must check the folder name on your computer to know that number)
 
# Click on "OK" to save your new variable.
 
# Click on "OK" to save your new variable.
 
# Click on "OK" in the "Environment Variables" window to save your changes. ''Do not press "Cancel" or click on the close window button as those will discard your changes.''
 
# Click on "OK" in the "Environment Variables" window to save your changes. ''Do not press "Cancel" or click on the close window button as those will discard your changes.''
Line 99: Line 99:
  
 
== Setting up Gradle ==
 
== Setting up Gradle ==
 +
 +
<div style="float: right; display: inline-block; background-color: #eeeeee; max-width: 30%; padding: 5px">
 +
<span style="font-size: 110%">'''Note'''</span>
 +
 +
Make sure you are using a ''text editor'', like Notepad, not a ''word processor'', like Microsoft Word. Editing the file in a word processor may cause problems.
 +
</div>
  
 
# Open up the <code>TFCAddon</code> directory that contains the extracted Forge Src.  
 
# Open up the <code>TFCAddon</code> directory that contains the extracted Forge Src.  
# Open the <code>build.gradlew</code> file inside of this directory with a text editor like notepad.
+
# Open the <code>build.gradle</code> file inside of this directory with a text editor like notepad.
 
# Go to '''line 3''' in this file, and replace <code>mavenCentral()</code> with <code>maven { url = "https://repo1.maven.org/maven2/" }</code>.
 
# Go to '''line 3''' in this file, and replace <code>mavenCentral()</code> with <code>maven { url = "https://repo1.maven.org/maven2/" }</code>.
 
# Save and close the file.
 
# Save and close the file.
Line 157: Line 163:
 
# Keep expanding <code>src.main.java.com.example.examplemod</code> until you reach <code>ExampleMod</code>.
 
# Keep expanding <code>src.main.java.com.example.examplemod</code> until you reach <code>ExampleMod</code>.
 
# Double click on <code>ExampleMod</code> to open up it's source code.
 
# Double click on <code>ExampleMod</code> to open up it's source code.
 
You can now run the code to make sure everything is set up properly. To do so, click on the small play icon in the top bar of the screen. You should get another Minecraft window popping up, and just like in the last section, you should be able to see the Example Mod in the Mods list in the main menu.
 
  
 
<gallery mode="packed" heights="200px">
 
<gallery mode="packed" heights="200px">
Line 164: Line 168:
 
File:Intellij_Project_Open.png|Step 6
 
File:Intellij_Project_Open.png|Step 6
 
File:Intellij_Expand_Project.png|Steps 7, 8 and 9
 
File:Intellij_Expand_Project.png|Steps 7, 8 and 9
 +
</gallery>
 +
 +
You can now run the code to make sure everything is set up properly.
 +
 +
To do so, click on the small box in the lower left hand corner. From there you will see a menu, you want to select Gradle. That will open the Gradle window on the right; in there select the file named "Forge Gradle" and expand it. Inside there you will double click on runClient. That will launch the Minecraft client. These steps are important, if you do not do this Intellij IDEA will not be able to read the mcmod.info file or the .lang files when you boot up the client normally by just pressing the green play arrow. Once it has been selected as a launch option however, it will be in the launch client till you change it.
 +
 +
<gallery mode="packed" heights="200px">
 +
File:Setting_Up_Environment_Step_1.PNG | Step 1
 +
File:Setting_Up_Environment_Step_2.PNG | Step 2
 +
File:Setting_Up_Environment_finsihed.PNG | Finished
 
</gallery>
 
</gallery>
  
Line 521: Line 535:
 
In order to properly correct our missing item name, we have to create a [[Translation|localization file]] that will give our item it's name. Minecraft will automatically scan a specific directory in your mod for a <code>.lang</code> file, and this is the only easy way to set a proper name for items, blocks, keybindings, etc. So we need to create this language file.
 
In order to properly correct our missing item name, we have to create a [[Translation|localization file]] that will give our item it's name. Minecraft will automatically scan a specific directory in your mod for a <code>.lang</code> file, and this is the only easy way to set a proper name for items, blocks, keybindings, etc. So we need to create this language file.
  
When looking for <code>.lang</code> files, as well as other resource files like textures or sounds, Minecraft will look in your mod's "Resource" directory. You should already have a directory called <code>resources</code> in the <code>main<code> directory of your project, however chances are you haven't properly marked it as ''the'' resource directory for your mod yet. In order to do so in Intellij, right click on the <code>resources</code> directory, and go to "Mark directory as", and then select "Resources Root". ''If you don't see the option to mark the <code>resources</code> directory as the resources root, then it already is one and you don't need to do anything''.
+
When looking for <code>.lang</code> files, as well as other resource files like textures or sounds, Minecraft will look in your mod's "Resource" directory. You should already have a directory called <code>resources</code> in the <code>main</code> directory of your project, however chances are you haven't properly marked it as ''the'' resource directory for your mod yet. In order to do so in Intellij, right click on the <code>resources</code> directory, and go to "Mark directory as", and then select "Resources Root". ''If you don't see the option to mark the <code>resources</code> directory as the resources root, then it already is one and you don't need to do anything''.
  
 
[[File:Mark_Resource_Directory.png|400px|thumb|center|Setting the Resource Root directory in Intellij.]]
 
[[File:Mark_Resource_Directory.png|400px|thumb|center|Setting the Resource Root directory in Intellij.]]
Line 1,230: Line 1,244:
 
You can run the game now and make sure that both variants of the paintbrush appear and have the correct name and texture from the creative menu.
 
You can run the game now and make sure that both variants of the paintbrush appear and have the correct name and texture from the creative menu.
  
[[File:Paintbrush_Variants.png|500px|center|thumb|The two paintbrush variants as seen in the creative inventory]]
+
[[File:Paintbrush_Variants.png|500px|center|thumb|The two paintbrush variants as seen in the creative inventory.]]
  
 
=== Dipping the paintbrush ===
 
=== Dipping the paintbrush ===
Line 1,285: Line 1,299:
 
Firstly, the reason we get an empty bucket at all is because we called the <code>setContainerItem()</code> method inside of our <code>ItemClayBucketPaint</code> constructor. We currently pass the <code>clayBucketEmpty</code> item to this method, and that makes it so that if our paint bucked it ever used up in a crafting recipe, the empty bucket will be added to the player's inventory. This is normally what we would want if the paint got used up, but in this case it didn't.  
 
Firstly, the reason we get an empty bucket at all is because we called the <code>setContainerItem()</code> method inside of our <code>ItemClayBucketPaint</code> constructor. We currently pass the <code>clayBucketEmpty</code> item to this method, and that makes it so that if our paint bucked it ever used up in a crafting recipe, the empty bucket will be added to the player's inventory. This is normally what we would want if the paint got used up, but in this case it didn't.  
  
We could just pass an instance to the full paint bucket as the argument to <code>setContainerItem()</code>, but even then the paint bucket will just appear ''somewhere'' in the players inventory, it won't stay in the crafting grid - which is what we wan't it to do in this case. Instead, we are going to remove the call to <code>setContainerItem()</code> inside of the <code>ItemClayBucketPaint</code> constructor and solve the problem in a different way.
+
We could just pass an instance to the full paint bucket as the argument to <code>setContainerItem()</code>, but even then the paint bucket will just appear ''somewhere'' in the players inventory, it won't stay in the crafting grid - which is what we want it to do in this case. Instead, we are going to remove the call to <code>setContainerItem()</code> inside of the <code>ItemClayBucketPaint</code> constructor and solve the problem in a different way.
  
 
The constructor should now look like this.
 
The constructor should now look like this.
Line 1,350: Line 1,364:
 
If you now start up the game again, and craft a dipped paintbrush from a paintbrush and a paint bucket, you will see that the dipped bucket stays in the exact slot crafting it started from, and it should still be full.
 
If you now start up the game again, and craft a dipped paintbrush from a paintbrush and a paint bucket, you will see that the dipped bucket stays in the exact slot crafting it started from, and it should still be full.
  
That concludes this section of the totorial. In the next one we will finally look at adding painting blocks that we can use our new paintbrushes on.
+
That concludes this section of the tutorial. In the next one we will finally look at adding painting blocks that we can use our new paintbrushes on.
  
 
=== Code reference ===
 
=== Code reference ===
Line 1,579: Line 1,593:
  
 
== Making a New Block ==
 
== Making a New Block ==
 +
 +
== Making an Anvil Recipe ==
  
 
== Changing Your Mod's ID ==
 
== Changing Your Mod's ID ==
Line 1,687: Line 1,703:
 
* [https://www.youtube.com/watch?v=rE9fXrHXX5U How to set up a mod configuration menu]
 
* [https://www.youtube.com/watch?v=rE9fXrHXX5U How to set up a mod configuration menu]
 
* [[Translation|.lang files]]
 
* [[Translation|.lang files]]
 +
 +
{{Guides}}
 +
{{Blocks}}

Revision as of 20:27, 11 October 2020


This page is an in-depth tutorial on how to setup an environment for creating Addons for TFC+. It will also show you how to make a basic addon for demonstration purposes, from which you can grow your own addon, and give you some general tips and tricks. It will not go in-depth about programming, or making substantial changes to the game as those are complex topics that have been better covered elsewhere.

Previous knowledge of Minecraft modding is very helpful for making Addons but this tutorial assumes no prior modding experience. While programming knowledge is not required in order to follow this tutorial, any code shown here will be explained only very briefly. If you do not know any programming, then Minecraft modding might be a good place to start, in which case this tutorial will help get you started.

Setting everything up can be very tricky and can take longer than you might expect, so please follow the tutorial carefully to make sure everything works properly. You can always start a section over if you mess up.

A Note on Programming Languages

Minecraft, and TFC+ by extension were written in the Java programming language. This is also the language you will probably use to write TFC+ addons. Although Java is the most popular language choice for making Minecraft mods, any other language that can compile to JVM bytecode can in theory be used. Popular alternatives include Scala and Kotlin.

Through this tutorial will be setting up and using Java, both because it is the most popular choice, and because integrating it with Minecraft and TFC+ is easier than with other languages. If you use another programming language most of these steps will be similar, however there may be extra in-between steps that you need to follow for your particular language of choice. These extra steps are outside of the scope of this tutorial.

Downloading the Java JDK

In to make Minecraft mods, you first need to download and install the Java JDK 1.8. Download the last version of the appropriate JDK for you system Here and install it.

If you are on Windows, you can confirm that the JDK is successfully installed by going to the C:/Program Files/Java folder, you should see a sub-folder called jdk1.8.0_XXX where XXX will depend on the exact version of the JDK you installed. On MacOS, this should be in /Library/Java/JavaVirtualMachines, and on Linux it should be in /usr/lib/jvm.

Setting up the PATH

You need to make sure that Java is accessible from your PATH. This is necessary to make sure later steps will work properly.

  1. If you are on Windows, open up the start menu and type "Command Prompt" into the search bar, and open up a command prompt. On a Mac or Linux, open up a terminal.
  2. Type java (all lowercase) into your command prompt/terminal.
  3. Press Enter.
  4. If no errors show up on your terminal, and the output looks like what is shown below, then Java is accessible from the PATH and you can skip forward to the next sub-section.
This is what the output is supposed to look like. You may have to scroll up to see it.

If an error message appeared in your terminal then you need to add Java to your PATH before you can continue.

  1. Locate the bin directory of the JDK, which should be inside of the directory you installed the JDK earlier. For example C:/Program Files/Java/jdk1.8.0_XXX/bin.
  2. Copy this path to the clipboard.
  3. Follow the operating system specific instructions below.

Adding Java to the PATH on Windows

  1. Open up the Control Panel and then click on "System"
  2. Click the "Advanced system settings" button on the left pane.
  3. Then from the "Advanced" tab click on "Environment Variables..."
  4. Locate the "Path" variable and then click on "Edit...".
  5. In the window that pops up, click on the "New" button and then paste in the C:/Program Files/Java/jdk1.8.0_XXX/bin path that you copied earlier, and press Enter. (if it says there are wrong characters try switching the slashes with backslashes first)
  6. Click on "OK" to close the "Environment Variables" window. Do not click on "Cancel" or click on the close window button as these will discard your changes.
  7. Restart your computer so that Windows can properly register the new PATH.
  8. Open up another Command Prompt and run java again. You should get no errors this time.

Adding Java to the PATH on Mac or Linux

  1. Open ~/.bash_profile in a text editor.
  2. Add the following line at the end of the file export PATH=$PATH:/usr/java/jdk1.8.0_XXX/bin.
  3. Save and close the file.
  4. Restart your computer.
  5. Open up another terminal and run java again. You should get no errors this time.

Setting up JAVA_HOME

You now need to set your JAVA_HOME variable to point to the JDK folder you set up earlier, for example /usr/java/jdk1.8.0_XXX. Copy the path to this directory, and then follow the operating system specific instructions below.

Creating JAVA_HOME on Windows

  1. Open up the Control Panel and then click on "System".
  2. Click the "Advanced system settings" button on the left pane.
  3. Then from the "Advanced" tab click on "Environment Variables..."
  4. Now click on the "New..." button.
  5. In the window that pops up, enter "JAVA_HOME" under the "Variable name" field, and paste the path to the JDK you copied earlier into the "Variable value" field. It should look something like the screenshot below. (by the way, the "XXX" part of the path represents a number that can vary. you must check the folder name on your computer to know that number)
  6. Click on "OK" to save your new variable.
  7. Click on "OK" in the "Environment Variables" window to save your changes. Do not press "Cancel" or click on the close window button as those will discard your changes.
  8. Restart your computer.
  9. To confirm that JAVA_HOME is set up properly, open up the Command Prompt and run echo %JAVA_HOME%. The output should be the same as the path to the JDK.

Creating JAVA_HOME on Mac or Linux

  1. Open ~/.bash_profile in a text editor.
  2. Add the following line at the very end export JAVA_HOME=/usr/java/jdk1.8.0_XXX.
  3. Save and close the file.
  4. Restart your computer.
  5. To confirm that JAVA_HOME is set up properly, open up another terminal and run echo $JAVA_HOME. The output should be the same as the path to the JDK.

Downloading Forge

Minecraft Forge is what allows making and running Minecraft mods, so we will need it for making addons. You will need the "Src" version of Forge, which is different from the version used to play the game. To download the appropriate version of Forge, go Here and click on the "Src" download button for the latest version of Forge for Minecraft 1.7.10 as shown below.

The Forge file you downloaded should be a zip file, for example forge-1.7.10-10.13.4.1614-1.7.10-src.zip. Extract the contents of this zip file to a new directory. You can call this directory anything you like, but for the purposes of this tutorial we will be calling it TFCAddon.

This is what the Forge Webpage looks like. Make sure to download the "Src" version and not any of the installers.

Setting up Gradle

Note

Make sure you are using a text editor, like Notepad, not a word processor, like Microsoft Word. Editing the file in a word processor may cause problems.

  1. Open up the TFCAddon directory that contains the extracted Forge Src.
  2. Open the build.gradle file inside of this directory with a text editor like notepad.
  3. Go to line 3 in this file, and replace mavenCentral() with maven { url = "https://repo1.maven.org/maven2/" }.
  4. Save and close the file.
  5. Open up the gradle/wrapper/gradle-wrapper.properties file in a text editor like notepad.
  6. Go to line 6, and replace .../gradle-2.0-bin.zip with .../gradle-3.0-bin.zip.
  7. Save and close the file.
  8. Now, open up a command prompt/terminal in the TFCAddon directory. On Windows you can do this by holding shift and right clicking in the directory, then clicking on "Open PowerShell window here".
  9. Run ./gradlew setupDecompWorkspace, and wait for this command to finish. This may take up to 5 minutes.

If you get any errors in the terminal, then make sure you have followed the tutorial properly up to this point. Specifically, make sure you have set up your PATH and JAVA_HOME variables properly, and that you have replaced lines 3 and 6 in the respective files as said above.

Downloading the TFC+ Source Code

You will need a special deobfuscated version of the TFC+ source code in order to develop Addons.

  1. Go to the TFC+ download page Here.
  2. In the "Files" tab, scroll past the "Main File" section and into the "Additional Files" section.
  3. Download the "deobf" version - not the "src" version. The downloaded file should be called something like [1.7.10]TerraFirmaCraftPlus-deobf-X.XX.X.jar
  4. Open up the TFCAddon directory where you extracted the Forge Src.
  5. Create a new sub-directory called libs. This exact name is very important.
  6. Move the deobfuscated TFC+ source jar into the newly created libs directory.
This is what you should download from the TFC+ CurseForge page. Make sure you get this "deobf" version and not the "src" version.

Making Sure Everything is Working

If you go to TFCAddon/src/main/java/com/example/examplemod/ExampleMod.java you can see that Forge Src already comes with Java code for a trivial mod that prints out the name of a dirt block. We will now build and compile this example mod to make sure everything is working, and later on we will edit it to make our own mod.

  1. Open up a terminal in TFCAddon
  2. Type ./gradlew build into the terminal. This may take a minute, and the output will be long but should end with "BUILD SUCCESSFUL".
  3. Type ./gradlew runClient into the terminal. This will actually open up and run Minecraft with TFC+ and the example mod.
  4. If you go to the Mods tab on Minecraft's main menu, and scroll all the way down, you should see both TerraFirmaCraft+ as well as the Example Mod listed.

Congratulations! You just made your first TFC+ addon. It doesn't do anything yet but you can already start editing ExampleMod.java to make it do whatever you want.

You can test your Addon by running ./gradlew runClient in the terminal. You can find the jar file of your mod in the TFCAddon/build/libs directory, which you can place in your Minecrafts mods directory to try it in your own Minecraft game setup.

You can see that Forge successfully loaded our Example Mod, along with TFC+.

Setting up an IDE

This step is highly recommended for beginners, however you can skip to the next section if you know what you're doing and prefer to use a simple text editor like Notepad++ or Visual Studio Code for writing Java code.

Most people find it easier and more comfortable to write Java from an Integrated Development Environment (IDE) and this step will explain how to get one set up. The rest of the tutorial also assumes that you are using an IDE.

The two most popular IDEs for Java are Eclipse and Intellij IDEA. We will be using Intellij IDEA in this tutorial.

  1. Download Intellij IDEA Here and install it.
  2. Open a terminal in TFCAddon and run ./gradlew idea.
  3. Open up Intellij IDEA. You should get a window like in the screenshot below.
  4. Click on the "Open or Import" button.
  5. Navigate to the TFCAddon directory, click on it, and then press "OK". Wait for intellij to open the project. You should now see a welcome screen similar to the one below.
  6. To see all of the files in your project, click on the button on the left edge of the window - it can be a bit difficult to spot.
  7. Expand the TFCAddon project by clicking the little upside-down triangle next to it's name.
  8. Keep expanding src.main.java.com.example.examplemod until you reach ExampleMod.
  9. Double click on ExampleMod to open up it's source code.

You can now run the code to make sure everything is set up properly.

To do so, click on the small box in the lower left hand corner. From there you will see a menu, you want to select Gradle. That will open the Gradle window on the right; in there select the file named "Forge Gradle" and expand it. Inside there you will double click on runClient. That will launch the Minecraft client. These steps are important, if you do not do this Intellij IDEA will not be able to read the mcmod.info file or the .lang files when you boot up the client normally by just pressing the green play arrow. Once it has been selected as a launch option however, it will be in the launch client till you change it.

Once again, Forge successfully loaded our Example Mod and TFC+.

Inspecting Minecraft and TFC+ Source Code

Since you are making a mod for Minecraft and TFC+ it is important to eventually get yourself familiarized with their source code. Inspecting how Minecraft's source does something can give you a great idea about how to do similar things. We will now see where we can find these sources.

To find Minecraft's source code in your Intellij project:

  1. Scroll down on the project pane to "External Libraries".
  2. Click the little arrow next to it to expand it.
  3. Then scroll down until you find a library root called forgeSrc-1.7.10-XX.XX.XX.XXXX-1.7.10.jar.
  4. This contains the deobfuscated Minecraft source code, expand it.
  5. Let's now see how the dirt blocks in vanilla Minecraft are coded. Keep expanding until you get to net.minecraft.block, this is the directory where all vanilla blocks are coded.
  6. Double click on BlockDirt to see what the code structure of a simple block looks like.

To find the source code of TFC+:

  1. Scroll up on the project pane and expand libs directory you made.
  2. Inside of it should be the deobfuscated TFC+ source code you copied there earlier - expand it.
  3. Let's now see where all of the TFC+ items are instantiated. Keep expanding until you get to com.dunk.tfc.
  4. Double click on ItemSetup, this is a long file in which all TFC+ items are instantiated.
  5. Let's see how a simple coal item is coded in TFC+, scroll down to line 188 where it is instantiated.
  6. While holding Control/Command, click on ItemCoal this will take you to the ItemCoal file where you can see how the coal was coded.
  7. You can Control/Command click on any other item if you want to see how it was coded as well.

Note: If you cannot expand the deobfuscated TFC+ source code jar file from your project pane, then you first need to add it as a library to your project. Right click on the jar, and select the "Add as library" option. You should now be able to expand it.

Making a Basic Addon

The starter code that forge has set up for you inside of ExampleMod.java should look similar to the code below. This is the starting point from which we will build a very simple addon.

package com.example.examplemod;

import net.minecraft.init.Blocks;
import cpw.mods.fml.common.Mod;
import cpw.mods.fml.common.Mod.EventHandler;
import cpw.mods.fml.common.event.FMLInitializationEvent;

@Mod(modid = ExampleMod.MODID, version = ExampleMod.VERSION)
public class ExampleMod
{
    public static final String MODID = "examplemod";
    public static final String VERSION = "1.0";
    
    @EventHandler
    public void init(FMLInitializationEvent event)
    {
        System.out.println("DIRT BLOCK >> " + Blocks.dirt.getUnlocalizedName());
    }
}

The variable MODID specifies a unique mod ID for your mod. Each mod's ID has to be unique, you cannot play with multiple mods that have the same ID. For now we will be keeping our "examplemod" ID. Please do not change your mod's ID for right now, as there are a lot of steps involved and it is a very error prone process. To avoid frustration, keep it as is for now. Changing your mod's ID will be discussed in a later section.

All code written in this section will be inside of ExampleMod.java. If you prefer to split your code up into multiple files, you can also do that while following along.

Adding a dependency on TFC+

Since this is a TFC+ addon, we will be using some classes from TFC+. This means that our mod has a hard dependency on TFC+. Our mod should load after TFC+ loads, and if TFC+ doesn't load then our mod shouldn't load either. We can add this dependency by changing the @Mod annotation on our ExampleMod class as is shown below.

@Mod(modid = ExampleMod.MODID, version = ExampleMod.VERSION, dependencies = "required-after:terrafirmacraftplus;")
public class ExampleMod
{
    ...
}

This will ensure that our mod only loads after TFC+ had already successfully loaded.

If you start up the game again, and click on the "Mods" menu, you should see that our Example Mod is now lower in the list than TerraFirmaCraft+, while before adding this dependency, it appeared higher in the list. Since the ordering of this list depends on the order in which mods are loaded, this confirms we have successfully added a dependency on TFC+.

The Example Mod is loaded after TerraFirmaCraft+.

Writing an event handler

Most mods want to be notified of certain changes or events that happen in the game. For example we may want to make Skeletons drop Bone Fragments when they die from crushing damage, but how do we figure out when exactly they die?. In our mods, we can opt in to have a particular method called when a certain event happens, like when mobs are killed and drop items. Forge posts a number of different events for different scenarios that can happen.

We want to make Skeletons drop Bone Fragments in addition to Bones when they die. The specific forge event we can use to accomplish this is the LivingDropsEvent. As it's name suggests, this event fires whenever an entity dies and right before it drops loot. This is perfect for our needs. We will now see how we can hook up to this event.

We need to create a method that Forge will call whenever the LivingDropsEvent happens. This event handling method must follow a few rules:

  • It must return nothing (void).
  • It must have only a single parameter of type LivingDropsEvent.
  • It should not be static.
  • It has to be annotated with the @SubscribeEvent annotation.
  • It needs to be registered on the proper event bus so that Forge can know about it.

Right now we will take care of the first 4 points, and we will leave the last point for a bit later in the tutorial. Let's write a method called onLivingDrops() in our ExampleMod class.

@SubscribeEvent
public void onLivingDrops(LivingDropsEvent event)
{
    System.out.println("LivingDropsEvent triggered!");
}

You can check that this method satisfies the first 4 points. The only thing that we will do for now is print something out to the debug console when the event is triggered, to make sure everything is working. Now, lets take care of that last point.

Forge posts game events to two main Event Buses. One of these event buses can be accessed via MinecraftForge.EVENT_BUS, while the other can be accessed through FMLCommonHandler.instance().bus(). Both of these buses handle different events, but the one that handles LivingDropsEvent is MinecraftForge.EVENT_BUS.

To get our onLivingDrops() method registered on the MinecraftForge.EVENT_BUS, we can call the MinecraftForge.EVENT_BUS.register() method inside of our init() method, and pass it an instance of the class that has the event handling method. In our case this means we have to pass in an instance of our ExampleMod class, since that is where our event handling method is declared.

@EventHandler
public void init(FMLInitializationEvent event)
{
    MinecraftForge.EVENT_BUS.register(this);
}

Our onLivingDrops() method should now be registered on the proper event bus, and should be printing "LivingDropsEvent triggered!" whenever an entity dies and drops loot. Let's test this out to make sure everything is working.

Start up Minecraft again and load up a new Creative world. Get yourself a Sword and a Mace, and use the /time set night command to make it easier to find some mobs. Now find some unwilling test subjects and kill them. You should see "LivingDropsEvent triggered!" printed somewhere in the console of your IDE whenever you kill a mob.

You should see this when you kill a mob and it drops items.

Congratulations! You've set up your first event handler. Note that all other events provided by Forge need to be hooked up in exactly the same way, following the steps above.

You might have noticed that our init() method actually handles another event - the FMLInitializationEvent, however it is annotated with @EventHandler, instead of SubscribeEvent. The FMLInitializationEvent is a special type of event that Forge calls during mod initialization. There are others like it, and they will be discussed in a later section.

Adding and removing mob drops

Now that we have our event handling onLivingDrops() method properly set up, we can use it to do what we originally intended - make Skeletons drop Bone Fragments instead of Bones when they are killed with a weapon that deals crushing damage.

We can use the .entityLiving field of the LivingDropsEvent which contains the entity that was killed and is dropping items to check whether this entity is a Skeleton or not. And we can use .source.getEntity() method of the LivingDropsEvent to get the entity responsible for killing the skeleton, and we can check if it was a player.

If the entity that killed the Skeleton was a player, we can get that player's currently equipped item, the one they used to deal the killing blow, with player.inventory.mainInventory[player.inventory.currentItem]. We can check to make sure that the currently equipped item is a weapon, and get its damage type with weapon.damageType.

Our onLivingDrops() method will now look like this.

@SubscribeEvent
public void onLivingDrops(LivingDropsEvent event)
{
    Entity mob = event.entityLiving;
    Entity killer = event.source.getEntity();

    if (killer instanceof EntityPlayer && mob instanceof EntitySkeletonTFC)
    {
        EntityPlayer player = (EntityPlayer)killer;
        ItemStack currentItem = player.inventory.mainInventory[player.inventory.currentItem];
        
        if (currentItem != null && currentItem.getItem() instanceof ItemWeapon)
        {
            ItemWeapon weapon = (ItemWeapon)currentItem.getItem();
            if (weapon.damageType == EnumDamageType.CRUSHING)
            {
                System.out.println("Skeleton was killed with crushing damage");
                // TODO: replace bone drops with bone fragments
            }
        }
    }
}

You should now launch Minecraft again and kill a few more skeletons to make sure you've set this up properly. "Skeleton was killed with crushing damage" should be printed to the IDE debug console only if you kill a Skeleton with your mace, but it should not print anything if you kill a Zombie, or if you kill a Skeleton but with a Sword.

The perfect test subject for our mod.

After you've made sure everything works fine. Let's replace all of the Bone drops with Bone Fragments.

The LivingDropsEvent contains a .drops field, which is a list containing all of the items entities that drop as a result of the mob's death. Since this event fires before these are actually dropped in the world, we can change the drop list to our liking inside of onLivingDrops() in order to replace Bones with Bone Shards. So let's loop over all of the drops, figure out which ones are Bones and replace them.

All TFC+ items are fields of the ItemSetup class, so we can check if any drop is a ItemSetup.bone and replace it with a ItemSetup.boneFragment.

You can find the code below. As you can see the logic in there is starting to get pretty bulky, so be careful when you're copying it over.

@SubscribeEvent
public void onLivingDrops(LivingDropsEvent event)
{
    EntityLivingBase mob = event.entityLiving;
    Entity killer = event.source.getEntity();
    
    if (killer instanceof EntityPlayer && mob instanceof EntitySkeletonTFC)
    {
        EntityPlayer player = (EntityPlayer)killer;
        ItemStack currentItem = player.inventory.mainInventory[player.inventory.currentItem];
        
        if (currentItem != null && currentItem.getItem() instanceof ItemWeapon)
        {
            ItemWeapon weapon = (ItemWeapon)currentItem.getItem();
            if (weapon.damageType == EnumDamageType.CRUSHING)
            {
                for (int i = 0; i < event.drops.size(); ++i)
                {
                    EntityItem drop = event.drops.get(i);
                    ItemStack dropStack = drop.getEntityItem();
                    
                    if (dropStack.getItem() == ItemSetup.bone)
                    {
                        ItemStack boneFragment = new ItemStack(ItemSetup.boneFragment, dropStack.stackSize);
                        drop.setEntityItemStack(boneFragment);
                    }
                }
            }
        }
    }
}

That should do it. Let's test if it works. Find another Skeleton test subject and smack it with your Mace. It should be dropping Bone Fragments instead of Bones now. Note that you should also make sure that arrows are still dropping, and that if you use your Sword instead of your Mace, Bones will still drop.

Our test subject dropped Bone Fragments when killed with a Mace.

Congratulations once again! You've now made your first visible change to the game. You now know how to add or remove mob drops. In the next sections we will see how to create new Items and Blocks.

You might have noticed that not every method or field or class used in the code is explained in this tutorial. If you are confused about why something was done, go investigate it! Hold Control/Command and click on the field, method and Intellij will take you to where it is declared. If you Right-click on field or class or method, and select "Find Usages", Intellij will give you a list of where these are used, and you can jump to any particular usage place. The best way to learn is to examine things yourself and to play around.

Code reference

Click on the right to expand the code reference. Your final code inside ExampleMod.java should look like this at the end of this section.

package com.example.examplemod;

import com.dunk.tfc.Entities.Mobs.EntitySkeletonTFC;
import com.dunk.tfc.ItemSetup;
import com.dunk.tfc.Items.Tools.ItemWeapon;
import com.dunk.tfc.api.Enums.EnumDamageType;
import net.minecraft.entity.Entity;
import net.minecraft.entity.item.EntityItem;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.init.Blocks;
import net.minecraft.item.ItemStack;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.event.entity.living.LivingDropsEvent;
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
import cpw.mods.fml.common.Mod;
import cpw.mods.fml.common.Mod.EventHandler;
import cpw.mods.fml.common.event.FMLInitializationEvent;

@Mod(modid = ExampleMod.MODID, version = ExampleMod.VERSION, dependencies = "required-after:terrafirmacraftplus;")
public class ExampleMod
{
    public static final String MODID = "examplemod";
    public static final String VERSION = "1.0";

    @EventHandler
    public void init(FMLInitializationEvent event)
    {
        MinecraftForge.EVENT_BUS.register(this);
    }

    @SubscribeEvent
    public void onLivingDrops(LivingDropsEvent event)
    {
        Entity mob = event.entityLiving;
        Entity killer = event.source.getEntity();

        if (killer instanceof EntityPlayer && mob instanceof EntitySkeletonTFC)
        {
            EntityPlayer player = (EntityPlayer)killer;
            ItemStack currentItem = player.inventory.mainInventory[player.inventory.currentItem];

            if (currentItem != null && currentItem.getItem() instanceof ItemWeapon)
            {
                ItemWeapon weapon = (ItemWeapon)currentItem.getItem();
                if (weapon.damageType == EnumDamageType.CRUSHING)
                {
                    for (int i = 0; i < event.drops.size(); ++i)
                    {
                        EntityItem drop = event.drops.get(i);
                        ItemStack dropStack = drop.getEntityItem();

                        if (dropStack.getItem() == ItemSetup.bone)
                        {
                            ItemStack boneFragment = new ItemStack(ItemSetup.boneFragment, dropStack.stackSize);
                            drop.setEntityItemStack(boneFragment);
                        }
                    }
                }
            }
        }
    }
}

Making a New Item

In this tutorial we will learn how to make a simple paintbrush item. We won't make this item have any special use for now, but we will do so in the later tutorials.

Although we will technically be building on the code from the last tutorial, we actually won't be using any of the old code. So you can still follow this tutorial even if you haven't written the code from the last one. Do note that the last tutorial introduces important concepts that this tutorial will take for granted.

The paintbrush class

We will be making a new class for our paintbrush item. For the sake of simplicity we will simply be adding this new class as a static class inside of our main ExampleMod class, although if you prefer to separate your classes into different files, you can also do that as you follow along.

Every item in Minecraft inherits from the Item base class, and most TFC+ items inherit from the ItemTerra class. Since ItemTerra extends the Item class with the weight and size properties we will want our class to inherit from ItemTerra as well.

In Minecraft and TFC+, all item classes are prefixed with "Item-", so we will do the same. Our paintbrush class will be called ItemPaintbrush.

public static class ItemPaintbrush extends ItemTerra
{
    ...
}

In the constructor of our paintbrush class, we will set our item's unlocalized name. This name isn't normally visible to the player, as we will soon see, but it can be used in code to uniquely identify items.

We will also set our item to appear under the "(TFC) Tools" category in the creative menu, so we can quickly find it when testing. And, since we don't want our paintbrushes to stack, we will disable that.

The constructor of our paintbrush should look like this.

public static class ItemPaintbrush extends ItemTerra
{
    public ItemPaintbrush()
    {
        setUnlocalizedName("ItemPaintbrush");
        setCreativeTab(TFCTabs.TFC_TOOLS);
        stackable = false;
    }
}

We now need to instantiate our paintbrush item, and to register it with Forge, so that Minecraft knows about it's existence.

Registering the paintbrush

Item registration should be done during in either the FMLInitializationEvent or the FMLPreInitializationEvent. Since we already handle the FMLInitializationEvent in our init() method, we will do our item setup there.

We will create a new static field to store our paintbrush instance, so that we can access it later. We will then register our paintbrush instance using GameRegistry.registerItem(). The first parameter of this method is the item to register, so we will pass in our paintbrush instance. The second parameter is some unique name for the item that should be unique from other items. For this we will simply pass the unlocalized name of our paintbrush, which we set earlier, since it shouldn't conflict with any Minecraft or TFC+ items.

public static Item itemPaintbrush;

@EventHandler
public void init(FMLInitializationEvent event)
{
    MinecraftForge.EVENT_BUS.register(this);

    itemPaintbrush = new ItemPaintbrush();
    GameRegistry.registerItem(itemPaintbrush, itemPaintbrush.getUnlocalizedName());
}

That's it. We've created a new item. If you start up a creative world in Minecraft now, and go to the "(TFC) Tools" category in the creative inventory, you should be able to see a very strange looking item as the last one in the list. That one is ours.

Since we never set any texture for our paintbrush, Minecraft gave it a default magenta and black texture in order to draw our attention to it, so we don't accidentally forget to texture it. Also, you will notice that if you hover over the item, the name of our paintbrush is item.ItemPaintbrush.name and not "ItemPaintbrush" like we specified.

Our paintbrush is showing up, but it's name and texture of our paintbrush are missing.

Correcting the name

In order to properly correct our missing item name, we have to create a localization file that will give our item it's name. Minecraft will automatically scan a specific directory in your mod for a .lang file, and this is the only easy way to set a proper name for items, blocks, keybindings, etc. So we need to create this language file.

When looking for .lang files, as well as other resource files like textures or sounds, Minecraft will look in your mod's "Resource" directory. You should already have a directory called resources in the main directory of your project, however chances are you haven't properly marked it as the resource directory for your mod yet. In order to do so in Intellij, right click on the resources directory, and go to "Mark directory as", and then select "Resources Root". If you don't see the option to mark the resources directory as the resources root, then it already is one and you don't need to do anything.

Setting the Resource Root directory in Intellij.

Now, inside the resource directory, Minecraft will look inside of assets/examplemod/lang as the language file that gives your items their name. So, add new directories to your project structure until you have all of these necessary directories. Be very careful with their names as they have to be an exact mach. If you've changed your mod's ID then you need to replace examplemod in this path with whatever your mod's ID is.

Now we need to create a .lang file inside of assets/examplemod/lang/. The exact name of the language file will depend on the language you configured Minecraft with, but in general, all language files follow the pattern ll_DD, where ll are two character identifying the language, and DD are two characters identifying the dialect of the language. For example a Russian translation file would be called ru_RU.lang and a Simplified Chineese translation file would be called zh_CN.lang.

Since this tutorial is in English, we will make an American English translation file, which needs to be called en_US.lang. Create this file, and add the line item.ItemPaintbrush.name=Paintbrush to it. Save and close the file.

We don't need to do anything else, Minecraft will automatically parse our language file and properly assign a name to our item now. If we open up our world again, we should see that the item has a proper name, like in the screenshot below. If you do not see this name then triple check the naming of the path to your language file. The path should be exactly resources/assets/examplemod/lang/en_US.lang.

The paintbrush now has the correct name.

Adding the texture

Just like for language files, Minecraft will look in specific directories for your item and block textures. The item texture directory Minecraft will scan through is resources/assets/examplemod/textures/items/. Create all of the missing directories in this path. As always be very careful that their names match exactly with you can see here.

Now we need to get a texture for our paintbrush. We will be using the texture that you can see and download here. It was made by combining the textures of a Stick and Straw. Download this texture and save it as ItemPaintbrush.png in the folder chain you just created, the full path should be resources/assets/examplemod/textures/items/ItemPaintbrush.png.

ItemPaintbrush.png

Since we could theoretically have more than one item whose textures are in this directory, we will need to tell Minecraft what the name of the texture file is specifically. We will do this in our ItemPaintbrush constructor.

This name needs to be prefixed with examplemod:- as Minecraft also needs to know which mod's texture directory to use, since you could also theoretically use a texture from another mod. You do not need to specify the directories in between examplemod/ and items, those are implied. You also shouldn't specify a file extension for the texture since it is implied that it is a .png.

public ItemPaintbrush()
{
    ...
    setTextureName(MODID + ":" + "ItemPaintbrush");
}

Unfortunately, since our ItemPaintbrush inherits from ItemTerra, this is not enough to get our texture to show up. ItemTerra overrides the registerIcons() method to automatically prefix every texture name supplied in set texture with "terrafirmacraftplus:-". This means we first need to override this method back to how it was originally written in the Icon class whenever we inherit from ItemTerra. We can just copy-paste the registerIcons() method from Item as is.

@SideOnly(Side.CLIENT)
@Override
public void registerIcons(IIconRegister register)
{
    this.itemIcon = register.registerIcon(getIconString());
}

Note the @SideOnly annotation on this method. This annotation means that this method is only accessible from the client-side, and not from the server-side. If you try to call this method from the server-side, you will crash the game. In general most GUI and user interface classes or methods are declared with this annotation as the server doesn't need to know about them.

Now, with that out of the way, our paintbrush should finally have a texture. If you load up your game now, you should see the correct texture appearing on our paintbrush item. If you still see a missing texture, triple check to make sure that you set the path to the texture correctly in the previous steps.

The texture of the paintbrush is now showing correctly, but the item is rendered the wrong way around when in the player's hand.

The correct texture is now there, but if select the paintbrush from your hotbar, you will notice that it is rendered the wrong way around in the players hand. We want the paintbrush to be pointing outwards from the player character, but it is pointing inwards. Thankfully this is easily fixable. We just have to override the shouldRotateAroundWhenRendering() method in our ItemPaintbrush class to return true instead of false.

@Override
public boolean shouldRotateAroundWhenRendering() { return true; }

If we start our game again, we should see that the paintbrush is now rendered correctly.

The finished paintbrush item.

Congratulations! You should now know how to add items to Minecraft and TFC+.

Making a crafting recipe

To top things off, lets add a new crafting recipe so that we can craft our paintbrush from a Stick and some Straw. This is relatively easy. To make a shapeless recipe, simply add the following code at the end of your init() method.

@EventHandler
public void init(FMLInitializationEvent event)
{
    ...
    GameRegistry.addShapelessRecipe(new ItemStack(itemPaintbrush), ItemSetup.stick, ItemSetup.straw);
}

This will allow us to craft a paintbrush in survival mode like so.

Crafting the paintbrush One alternative configuration
Stick ItemPaintbrush
Grid layout Arrow (small).png
Straw
Stick ItemPaintbrush
Grid layout Arrow (small).png
Straw

Since the recipe is shapeless we could also craft it by button the stick and the straw in any other configuration such as this. If you would like to make the recipe only craftable with certain specific configurations of the straw and the stick, then you can use the GameRegistry.addShapedRecipe() method.

If you go to net.minecraft.item.crafting.CraftingManager, you can see how Minecraft registers most of it's recipes. And, if you go to com.dunk.tfc.Core.Recipes, you can see how TFC+ registers it's own recipes.

In the next tutorial, we will learn how to make a new TFC+ liquid - Paint.

Code reference

Click on the right to expand the code reference. Your final code inside ExampleMod.java should look like this at the end of this section.

package com.example.examplemod;

import cpw.mods.fml.common.eventhandler.SubscribeEvent;
import cpw.mods.fml.common.registry.GameRegistry;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import cpw.mods.fml.common.Mod;
import cpw.mods.fml.common.Mod.EventHandler;
import cpw.mods.fml.common.event.FMLInitializationEvent;
import net.minecraft.client.renderer.texture.IIconRegister;
import net.minecraft.entity.Entity;
import net.minecraft.entity.item.EntityItem;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.event.entity.living.LivingDropsEvent;
import com.dunk.tfc.Core.TFCTabs;
import com.dunk.tfc.Entities.Mobs.EntitySkeletonTFC;
import com.dunk.tfc.ItemSetup;
import com.dunk.tfc.Items.ItemTerra;
import com.dunk.tfc.Items.Tools.ItemWeapon;
import com.dunk.tfc.api.Enums.EnumDamageType;

@Mod(modid = ExampleMod.MODID, version = ExampleMod.VERSION, dependencies = "required-after:terrafirmacraftplus;")
public class ExampleMod
{
    public static final String MODID = "examplemod";
    public static final String VERSION = "1.0";

    public static Item itemPaintbrush;

    @EventHandler
    public void init(FMLInitializationEvent event)
    {
        MinecraftForge.EVENT_BUS.register(this);

        itemPaintbrush = new ItemPaintbrush();
        GameRegistry.registerItem(itemPaintbrush, itemPaintbrush.getUnlocalizedName());

        GameRegistry.addShapelessRecipe(new ItemStack(itemPaintbrush), ItemSetup.stick, ItemSetup.straw);
    }

    @SubscribeEvent
    public void onLivingDrops(LivingDropsEvent event)
    {
        Entity mob = event.entityLiving;
        Entity killer = event.source.getEntity();

        if (killer instanceof EntityPlayer && mob instanceof EntitySkeletonTFC)
        {
            EntityPlayer player = (EntityPlayer)killer;
            ItemStack currentItem = player.inventory.mainInventory[player.inventory.currentItem];

            if (currentItem != null && currentItem.getItem() instanceof ItemWeapon)
            {
                ItemWeapon weapon = (ItemWeapon)currentItem.getItem();
                if (weapon.damageType == EnumDamageType.CRUSHING)
                {
                    for (int i = 0; i < event.drops.size(); ++i)
                    {
                        EntityItem drop = event.drops.get(i);
                        ItemStack dropStack = drop.getEntityItem();

                        if (dropStack.getItem() == ItemSetup.bone)
                        {
                            ItemStack boneFragment = new ItemStack(ItemSetup.boneFragment, dropStack.stackSize);
                            drop.setEntityItemStack(boneFragment);
                        }
                    }
                }
            }
        }
    }

    public static class ItemPaintbrush extends ItemTerra
    {
        public ItemPaintbrush()
        {
            setUnlocalizedName("ItemPaintbrush");
            setCreativeTab(TFCTabs.TFC_TOOLS);
            stackable = false;
            setTextureName(MODID + ":" + "ItemPaintbrush");
        }

        @SideOnly(Side.CLIENT)
        @Override
        public void registerIcons(IIconRegister register)
        {
            this.itemIcon = register.registerIcon(this.getIconString());
        }

        @Override
        public boolean shouldRotateAroundWhenRendering() { return true; }
    }
}

Making a New Fluid

In this section of the tutorial we will be making a custom fluid - Paint.

Although we will technically be building on the code from the last tutorial, we won't actually be using any of the old code. So you can still follow this tutorial even if you haven't written the code from the last one. Do note that the last tutorial introduces important concepts that this tutorial will take for granted, such as how to make custom items.

Making the FluidPaint class

The first step to making our paint is to write a class for the custom fluid. We will call our class FluidPaint.

In Minecraft, all fluids inherit from the Fluid class, and in TFC+, all fluids inherit from the FluidBaseTFC class. As such, our FluidPaint class will also inherit from FluidBaseTFC.

public static class FluidPaint extends FluidBaseTFC
{
    ...
}

In the FluidPaint constructor, we need to provide the super-class constructor with the unlocalized name of our fluid. We will then set our paints color to white, and change its viscocity and density.

public static class FluidPaint extends FluidBaseTFC
{
    public FluidPaint()
    {
        super("FluidPaint");
        setBaseColor(Color.WHITE.getRGB());
        setViscosity(6000);
        setDensity(3000);
    }
}

That's it for our fluid class, nothing more fancy is needed. I've made our fluid denser, and more viscous than water, which has both viscosity and density of 1000. The viscocity and density don't actually serve any gameplay purpose, although another mod might use them for some cool effects.

Registering the fluid

Now that we have our FluidPaint class, we need to instantiate it, and register it with Minecraft, just like we did with our paintbrush item.

Inside of our ExampleMod class, we will declare a new static field that will hold the instance of our paint fluid. And then in the init() method we will create the fluid instance and register it using FluidRegistry.registerFluid().

@Mod(modid = ExampleMod.MODID, version = ExampleMod.VERSION, dependencies = "required-after:terrafirmacraftplus;")
public class ExampleMod
{
    ...

    public static Fluid fluidPaint;

    @EventHandler
    public void init(FMLInitializationEvent event)
    {
        ...

        fluidPaint = new FluidPaint();
        FluidRegistry.registerFluid(fluidPaint);
    }

    ...
}

To finish up our fluid itself, we should add it's name to our translation file. Add fluid.FluidPaint=Paint to your en_US.lang file or equivalent, on a separate line. The fluid registration is now finished.

Making a barrel recipe

We've technically created our custom paint fluid, however we have no way to make it in order to test that it actually works yet. Even in creative mode, we can't spawn fluids. So let's make a Barrel recipe so we can create some of our paint fluid.

Our paint recipe will use salt water and Wheat Flour to make flour paint. You can even make this paint at home with the instructions from [Here]. We will require 10000 milli-buckets (10 buckets) of salt-water, with 100 ounces of wheat flour. These particular quantities are arbitrary, you can use different ones if you want.

Inside of our init() method, after we register the fluid, we will first instantiate the particular quantities of water and flour needed, as well the amount of paint that will be produced. We can use the ItemFoodTFC.createTag() method to instantiate a food item stack with a particular quantity. The secon parameter of this method is the particular quantity of the food, in ounces. We will pass 100 for this parameter. We then instantiate two FluidStacks to hold our water and paint quantities. The first paramter of the FluidStack constructor is the fluid, and the second parameter is the quantity of the fluid in millibuckets.

We will then instantiate a new BarrelRecipe for our paint. This constructor has the following signature.

BarrelRecipe(
    ItemStack inputItemStack,
    FluidStack inputFluidStack,
    ItemStack outputItemStack,
    FluidStack outputFluidStack,
    int inGameHoursNeededForRecipe
);

We will then specify that our recipe requires that the barrel remains unsealed, and then finally we will use the BarrelManager.getInstance().addRecipe() method to register our new barrel recipe.

@EventHandler
public void init(FMLInitializationEvent event)
{
    ...

    fluidPaint = new FluidPaint();
    FluidRegistry.registerFluid(fluidPaint);

    ItemStack flour = ItemFoodTFC.createTag(new ItemStack(ItemSetup.wheatGround), 100);
    FluidStack saltwater = new FluidStack(TFCFluids.SALTWATER, 10000);
    FluidStack paint = new FluidStack(fluidPaint, 10000);

    BarrelRecipe paintRecipe = new BarrelRecipe(flour, saltwater, null, paint, 1);
    paintRecipe.sealedRecipe = false;
    paintRecipe.setMinTechLevel(0);
    BarrelManager.getInstance().addRecipe(paintRecipe);
}

That's it, we should now be able to make some of our Paint. It would be a very good idea to test things out at this point to make sure you've followed everything correctly.

Load up a game in creative mode and find a beach or salty-swamp where you can get some salty sea-water. Grab a barrel and fill it up with sea-water, and put a stack of wheat flour weighing 100oz or more into the item slot of the filled barrel. You should see the output of this will be "Paint" in the GUI. If you wait for one in game hour (approx. 40 real world seconds) you should see that the fluid in the barrel turned white, and that the barrel is indeed full of Paint. If the paint is not showing up for you then go back and triple check your code.

Making a new bucket for the paint

Since making a barrel full of liquid didn't require modifying the TFC+ barrels, you might think that you can pick up any Bucket and grab a bucket full of paint from the barrel. If you try to do this you will quickly realize that you cannot. If you search for "bucket" inside of ItemSetup, you can see that there are different versions of every bucket for every fluid in the game. So in order to grab some paint from the barrel we just made, we first need to make a custom bucket full of paint.

We will make ItemClayBucketPaint class to represent a clay bucket filled with our custom paint. If you've followed the tutorial where we made the paint brush item, the code for the clay bucket item will be exactly the same. The only changes are that in the constructor we will set our paint bucket's container to be the empty clay bucket, and that our bucked will appear in the "(TFC) Misc" category in the creative menu, where all the other buckets are.

public static class ItemClayBucketPaint extends ItemTerra
{
    public ItemClayBucketPaint()
    {
        setUnlocalizedName("ItemClayBucketPaint");
        setCreativeTab(TFCTabs.TFC_MISC);
        stackable = false;
        setNoRepair();
        setTextureName(MODID + ":" + "ItemClayBucketPaint");
        setContainerItem(ItemSetup.clayBucketEmpty);
    }

    @SideOnly(Side.CLIENT)
    @Override
    public void registerIcons(IIconRegister register)
    {
        itemIcon = register.registerIcon(getIconString());
    }
}

For the texture of our paint bucket we will be using the file below which you can download and place in your assets/textures/items/ directory.

ItemClayBucketPaint.png

We will make another static field to hold our paint bucket item, and we will initialize and register it in the init() method, just like we did for the paint brush item.

@Mod(modid = ExampleMod.MODID, version = ExampleMod.VERSION, dependencies = "required-after:terrafirmacraftplus;")
public class ExampleMod
{
    ...

    public static Item itemClayBucketPaint;

    @EventHandler
    public void init(FMLInitializationEvent event)
    {
        ...

        itemClayBucketPaint = new ItemClayBucketPaint();
        GameRegistry.registerItem(itemClayBucketPaint, itemClayBucketPaint.getUnlocalizedName());
    }
}

To finalize our item, add item.ItemClayBucketPaint.name=Ceramic Bucket (Paint) to your language file on a separate line.

We now need to register our bucket as a fluid container using the FluidContainerRegistry.registerFluidContainer() method. This method has the following signature.

boolean registerFluidContainer(
    FluidStack fluidCapacity,
    ItemStack fullItem,
    ItemStack emptyItem
);

We will make our bucket have a fluid capacity of 1000mB of Paint. This is the standard value for buckets. For the fullItem parameter we will pass our newly created paint bucket, and for the emptyItem parameter we will pass the empty clay bucket from TFC+.

@EventHandler
public void init(FMLInitializationEvent event)
{
    ...

    itemClayBucketPaint = new ItemClayBucketPaint();
    GameRegistry.registerItem(itemClayBucketPaint, itemClayBucketPaint.getUnlocalizedName());

    FluidStack bucketPaint = new FluidStack(fluidPaint, 1000);
    ItemStack fullPaintBucket = new ItemStack(itemClayBucketPaint);
    ItemStack emptyPaintBucket = new ItemStack(ItemSetup.clayBucketEmpty);
    FluidContainerRegistry.registerFluidContainer(bucketPaint, fullPaintBucket, emptyPaintBucket);
}

That is all that we need to do in this section. If you load up your game now, you should be able to pick up 1000mB of Paint from the barrel we created earlier if you Right-click on the barrel with a clay bucket in hand.

Filling up a ceramic bucket with our custom Paint.

Code reference

Click on the right to expand the code reference. Your final code inside ExampleMod.java should look like this at the end of this section.

package com.example.examplemod;

import com.dunk.tfc.Core.FluidBaseTFC;
import com.dunk.tfc.Food.ItemFoodTFC;
import com.dunk.tfc.api.Crafting.BarrelManager;
import com.dunk.tfc.api.Crafting.BarrelRecipe;
import com.dunk.tfc.api.TFCFluids;
import com.dunk.tfc.Core.TFCTabs;
import com.dunk.tfc.Entities.Mobs.EntitySkeletonTFC;
import com.dunk.tfc.ItemSetup;
import com.dunk.tfc.Items.ItemTerra;
import com.dunk.tfc.Items.Tools.ItemWeapon;
import com.dunk.tfc.api.Enums.EnumDamageType;
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
import cpw.mods.fml.common.registry.GameRegistry;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import cpw.mods.fml.common.Mod;
import cpw.mods.fml.common.Mod.EventHandler;
import cpw.mods.fml.common.event.FMLInitializationEvent;
import net.minecraft.client.renderer.texture.IIconRegister;
import net.minecraft.entity.Entity;
import net.minecraft.entity.item.EntityItem;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.event.entity.living.LivingDropsEvent;
import net.minecraftforge.fluids.Fluid;
import net.minecraftforge.fluids.FluidContainerRegistry;
import net.minecraftforge.fluids.FluidRegistry;
import net.minecraftforge.fluids.FluidStack;
import java.awt.*;

@Mod(modid = ExampleMod.MODID, version = ExampleMod.VERSION, dependencies = "required-after:terrafirmacraftplus;")
public class ExampleMod
{
    public static final String MODID = "examplemod";
    public static final String VERSION = "1.0";

    public static Item itemPaintbrush;
    public static Item itemClayBucketPaint;
    public static Fluid fluidPaint;

    @EventHandler
    public void init(FMLInitializationEvent event)
    {
        MinecraftForge.EVENT_BUS.register(this);

        itemPaintbrush = new ItemPaintbrush();
        GameRegistry.registerItem(itemPaintbrush, itemPaintbrush.getUnlocalizedName());

        GameRegistry.addShapelessRecipe(new ItemStack(itemPaintbrush), ItemSetup.stick, ItemSetup.straw);

        fluidPaint = new FluidPaint();
        FluidRegistry.registerFluid(fluidPaint);

        ItemStack flour = ItemFoodTFC.createTag(new ItemStack(ItemSetup.wheatGround), 120);
        FluidStack saltwater = new FluidStack(TFCFluids.SALTWATER, 10000);
        FluidStack paint = new FluidStack(fluidPaint, 10000);

        BarrelRecipe paintRecipe = new BarrelRecipe(flour, saltwater, null, paint, 1);
        paintRecipe.sealedRecipe = false;
        paintRecipe.setMinTechLevel(0);
        BarrelManager.getInstance().addRecipe(paintRecipe);

        itemClayBucketPaint = new ItemClayBucketPaint();
        GameRegistry.registerItem(itemClayBucketPaint, itemClayBucketPaint.getUnlocalizedName());

        FluidStack bucketPaint = new FluidStack(fluidPaint, 1000);
        ItemStack fullPaintBucket = new ItemStack(itemClayBucketPaint);
        ItemStack emptyPaintBucket = new ItemStack(ItemSetup.clayBucketEmpty);
        FluidContainerRegistry.registerFluidContainer(bucketPaint, fullPaintBucket, emptyPaintBucket);
    }

    @SubscribeEvent
    public void onLivingDrops(LivingDropsEvent event)
    {
        Entity mob = event.entityLiving;
        Entity killer = event.source.getEntity();

        if (killer instanceof EntityPlayer && mob instanceof EntitySkeletonTFC)
        {
            EntityPlayer player = (EntityPlayer)killer;
            ItemStack currentItem = player.inventory.mainInventory[player.inventory.currentItem];

            if (currentItem != null && currentItem.getItem() instanceof ItemWeapon)
            {
                ItemWeapon weapon = (ItemWeapon)currentItem.getItem();
                if (weapon.damageType == EnumDamageType.CRUSHING)
                {
                    for (int i = 0; i < event.drops.size(); ++i)
                    {
                        EntityItem drop = event.drops.get(i);
                        ItemStack dropStack = drop.getEntityItem();

                        if (dropStack.getItem() == ItemSetup.bone)
                        {
                            ItemStack boneFragment = new ItemStack(ItemSetup.boneFragment, dropStack.stackSize);
                            drop.setEntityItemStack(boneFragment);
                        }
                    }
                }
            }
        }
    }

    public static class ItemPaintbrush extends ItemTerra
    {
        public ItemPaintbrush()
        {
            setUnlocalizedName("ItemPaintbrush");
            setCreativeTab(TFCTabs.TFC_TOOLS);
            stackable = false;
            setTextureName(MODID + ":" + "ItemPaintbrush");
        }

        @SideOnly(Side.CLIENT)
        @Override
        public void registerIcons(IIconRegister register)
        {
            this.itemIcon = register.registerIcon(this.getIconString());
        }

        @Override
        public boolean shouldRotateAroundWhenRendering() { return true; }
    }

    public static class ItemClayBucketPaint extends ItemTerra
    {
        public ItemClayBucketPaint()
        {
            setUnlocalizedName("ItemClayBucketPaint");
            setCreativeTab(TFCTabs.TFC_MISC);
            stackable = false;
            setNoRepair();
            setContainerItem(ItemSetup.clayBucketEmpty);
            setTextureName(MODID + ":" + "ItemClayBucketPaint");
        }

        @SideOnly(Side.CLIENT)
        @Override
        public void registerIcons(IIconRegister register)
        {
            itemIcon = register.registerIcon(getIconString());
        }
    }

    public static class FluidPaint extends FluidBaseTFC
    {
        public FluidPaint()
        {
            super("FluidPaint");
            setBaseColor(Color.WHITE.getRGB());
            setViscosity(6000);
            setDensity(3000);
        }
    }
}

Making an Item With Variants

In this section of the Addon Tutorial we will be making a variant of the paintbrush item that we made in one of earlier sections. We will be using the old code from the previous sections as a starting point. It is highly recommended that you follow the earlier tutorials before following this one, however you can also just copy the full code from the last tutorial's "Code reference" and start with this tutorial.

We obviously want to be able to paint with our paintbrush, so first we have to make a way to dip it in paint.

There are two ways we could accomplish this. We could either create an entirely new item to represent a dipped paintbrush, or we could modify our existing paintbrush to support multiple variants. The latter option is quicker, easier, and is also easy to extend if we need more than 2 variants of the paintbrush in the future, so that's the approach we will be taking here.

Making a dipped paintbrush variant

There are a number of changes we have to make to our existing ItemPaintbrush class in order to support multiple variants. As a reference, this class should currently look like this.

public static class ItemPaintbrush extends ItemTerra
{
    public ItemPaintbrush()
    {
        setUnlocalizedName("ItemPaintbrush");
        setCreativeTab(TFCTabs.TFC_TOOLS);
        stackable = false;
        setTextureName(MODID + ":" + "ItemPaintbrush");
    }

    @SideOnly(Side.CLIENT)
    @Override
    public void registerIcons(IIconRegister register)
    {
        this.itemIcon = register.registerIcon(this.getIconString());
    }

    @Override
    public boolean shouldRotateAroundWhenRendering() { return true; }
}

Firstly, each of our 2 paintbrush variants will have it's own texture, so we will add 2 fields to our class to store the two different textures. Since these textures can be shared between all of the ItemPaintbrush instances, we will make them static. Since the server side doesn't need to know about the existence of any textures, we will annotate both of these fields with the @SideOnly(Side.CLIENT) annotation.

Also, since setTextureName() only supports a single texture, we will not be using this method in the constructor anymore. You can comment it out, or delete it. Instead we will be declaring that our ItemPaintbrush has variants by calling the setHasSubtypes() method.

public static class ItemPaintbrush extends ItemTerra
{
    @SideOnly(Side.CLIENT)
    public static IIcon icon;
    @SideOnly(Side.CLIENT)
    public static IIcon dippedIcon;

    public ItemPaintbrush()
    {
        ...

        //setTextureName(MODID + ":" + "ItemPaintbrush");
        setHasSubtypes(true);
    }

    ...
}

We will also need to change the registerIcons() method of our ItemPaintbrush to register both of our new textures.

@SideOnly(Side.CLIENT)
@Override
public void registerIcons(IIconRegister register)
{
    icon = register.registerIcon(MODID + ":" + "ItemPaintbrush");
    dippedIcon = register.registerIcon(MODID + ":" + "ItemPaintbrushDipped");
}

We're done changing existing methods now, shouldRotateAroundWhenRendering() can be left as-is. Our paintbrush will no longer function correctly in the game now. We will be fixing it and adding the second variant in the next sub section.

Using item damage

In order to make our paintbrush have variants, we will be taking advantage a variable known as "Item Damage". The name of this variable is slightly missleading. While it's main role is to keep track of how damaged tools and weapons are, it can also be used to keep track of various item metadata, including different states and different variants.

The Item Damage is a propery of an ItemStack, not a property of the item itself. However, most relevant methods in the Item class get a ItemStack parameter passed in by the game, and we can use that to keep track of which variant of the item we are dealing with. We can also use the stack.getItemDamage() and stack.setItemDamage() methods to manipulate the Item Damage on an ItemStack.

In order to get 2 possible variants of our item, we will need to have 2 possible Item Damage values for the paintbrush: 0 and 1. When the Item Damage is 0, we will treat the item as being a regular paintbrush, but when the Item Damage is 1, we will treat it as the dipped paintbrush variant.

There are 3 methods that we now have to override to properly deal with both of our paintbrush variants. Firstly, we need to override the getUnlocalizedName() method to return a name appropriate for each variant based on the Item Damage of the stack that was passed in. This unlocalized name is the one that's looked up in the translation file, so we can use it to set a different name for our 2 item variants.

@Override
public String getUnlocalizedName(ItemStack stack)
{
    if (stack.getItemDamage() == 1)
        return "item.ItemPaintbrushDipped";
    else
        return "item.ItemPaintbrush";
}

Secondly, we need to override getIconFromDamage() so that we can return an icon/texture appropriate to the item variant.

@SideOnly(Side.CLIENT)
@Override
public IIcon getIconFromDamage(int damage)
{
    if (damage == 1)
        return dippedIcon;
    else
        return icon;
}

Finally, we should override the getSubItems() method. This method called by Minecraft to populate the creative mode menu, so we should override it to make sure both of our paintbrush variants will appear in the menu.

Here we will need to add ItemStacks to the given subItems parameter. In order to instantiate an ItemStack with a particular Item Damage, you can pass it in as the last parameter.

@SideOnly(Side.CLIENT)
@Override
public void getSubItems(Item item, CreativeTabs tabs, List subItems)
{
    subItems.add(new ItemStack(this, 1, 0));
    subItems.add(new ItemStack(this, 1, 1));
}

And that's all for the code. Now you can add item.ItemPaintbrushDipped.name=Dipped Paintbrush to your language file on a separate line. Keep the existing line with item.ItemPaintbrush.name=Paintbrush as this doesn't need to change.

The last thing we need is a texture for our dipped paintbrush variant. For that, we can use the texture you can see below. Download it, and save it in your assets/textures/items/ directory. Make sure the texture's file name is exactly "ItemPaintbrushDipped.png".

ItemPaintbrushDipped.png

You can run the game now and make sure that both variants of the paintbrush appear and have the correct name and texture from the creative menu.

The two paintbrush variants as seen in the creative inventory.

Dipping the paintbrush

Now that we've added the dipped variant of our paintbrush, let's make it so the player can actually dip the paintbrush in a barrel of paint to get the dipped variant.

To do this, we will simply register our paintbrush as a fluid container, the same way we did with our paint bucket in an earlier tutorial. Add the following code at the end of your init() method.

@EventHandler
public void init(FMLInitializationEvent event)
{
    ...

    FluidStack paintbrushPaint = new FluidStack(fluidPaint, 10);
    ItemStack dippedPaintbrush = new ItemStack(itemPaintbrush, 1, 1);
    ItemStack paintbrush = new ItemStack(itemPaintbrush, 1, 0);
    FluidContainerRegistry.registerFluidContainer(paintbrushPaint, dippedPaintbrush, paintbrush);
}

We've registered our paint brush as a container that can store 10 millibuckets of paint. That's all we need to do to make our paint brush dippable. If you open up your game again, you should be able to Right click on a barrel while holding a paint brush, and get a dipped paint brush. This will also consume 10mB of paint from the barrel.

You can now dip the paintbrush in a barrel of paint.

Adding a recipe with multiple outputs

For the sake of completeness, we also may want to add a way to dip a paintbrush in the paint bucket that we made earlier. Since the paint bucket is an item, and not a block like the barrel, the best way to go about this is probably to make a recipe for creating the dipped paintbrush variant from the undipped paintbrush and a bucket of paint. We can make this a shapeless recipe. Add the following like at the end of your init() method.

@EventHandler
public void init(FMLInitializationEvent event)
{
    ...

    GameRegistry.addShapelessRecipe(dippedPaintbrush, paintbrush, fullPaintBucket);
}

You can now try this from within the game, you should be able to make the following recipe.

Dipping the paintbrush in a bucket of paint
ItemPaintbrush ItemPaintbrushDipped
Grid layout Arrow (small).png
ItemClayBucketPaint

If you try this, you might notice that you get an empty ceramic bucket back after doing this recipe. Like the paintbrush requires the full bucket of paint to get dipped. This is not the end of the world, but we can do better.

Firstly, the reason we get an empty bucket at all is because we called the setContainerItem() method inside of our ItemClayBucketPaint constructor. We currently pass the clayBucketEmpty item to this method, and that makes it so that if our paint bucked it ever used up in a crafting recipe, the empty bucket will be added to the player's inventory. This is normally what we would want if the paint got used up, but in this case it didn't.

We could just pass an instance to the full paint bucket as the argument to setContainerItem(), but even then the paint bucket will just appear somewhere in the players inventory, it won't stay in the crafting grid - which is what we want it to do in this case. Instead, we are going to remove the call to setContainerItem() inside of the ItemClayBucketPaint constructor and solve the problem in a different way.

The constructor should now look like this.

public ItemClayBucketPaint()
{
    setUnlocalizedName("ItemClayBucketPaint");
    setCreativeTab(TFCTabs.TFC_MISC);
    stackable = false;
    setNoRepair();
    //setContainerItem(ItemSetup.clayBucketEmpty);
    setTextureName(MODID + ":" + "ItemClayBucketPaint");
}

In order to make it so our paint bucket doesn't get used up in the recipe, we are going to hook up to the ItemCraftedEvent, and handle this from there. This event fires right before the player finishes crafting an item by clicking on the crafting output slot. Let's write a onItemCrafted() event handling method in our ExampleMod class.

@SubscribeEvent
public void onItemCrafted(PlayerEvent.ItemCraftedEvent event)
{
    System.out.println("The player crafted an item!");
}

We also need to register this event on the appropriate event bus. Unlike the LivingDropsEvent we used earlier, the ItemCraftedEvent is posted on the FMLCommonHandler.instance().bus() event bus, and not the MinecraftForge.EVENT_BUS we registered with earlier. We can register with this other event bus in our init() method.

@EventHandler
public void init(FMLInitializationEvent event)
{
    MinecraftForge.EVENT_BUS.register(this);
    FMLCommonHandler.instance().bus().register(this);

    ...
}

You can run the game now to make sure everything is working fine, and the event handling method is being called. You should see "The player crafted an item!" printed out in the debug console whenever you craft an item in the game.

Now, inside of the onItemCrafted() method, we are going to first check the output item that was crafted. We are going to make sure that the output item is a paintbrush, but also that it is the dipped variant of the paintbrush. If this is the case, we are going to loop over all the items in the crafting slots and check if they are paint buckets. Once we find a paint bucket, we are going to increment it's stack size.

Since Minecraft decrements the stack size of all items in the crafting slots after a recipe was made, if we increment the stack size ourselves before that we will effectively counteract this. In the end, we are going to end with a single bucket full of paint, right in the slot where it started, exactly how we wanted it.

The code for doing this is the following.

@SubscribeEvent
public void onItemCrafted(PlayerEvent.ItemCraftedEvent event)
{
    if (event.crafting.getItem() == itemPaintbrush && event.crafting.getItemDamage() == 1)
    {
        for (int i = 0; i < event.craftMatrix.getSizeInventory(); ++i)
        {
            ItemStack stackInSlot = event.craftMatrix.getStackInSlot(i);
            if (stackInSlot != null && stackInSlot.getItem() == itemClayBucketPaint)
                ++stackInSlot.stackSize;
        }
    }
}

If you now start up the game again, and craft a dipped paintbrush from a paintbrush and a paint bucket, you will see that the dipped bucket stays in the exact slot crafting it started from, and it should still be full.

That concludes this section of the tutorial. In the next one we will finally look at adding painting blocks that we can use our new paintbrushes on.

Code reference

Click on the right to expand the code reference. Your final code inside ExampleMod.java should look like this at the end of this section.

package com.example.examplemod;

import com.dunk.tfc.Core.FluidBaseTFC;
import com.dunk.tfc.Food.ItemFoodTFC;
import com.dunk.tfc.api.Crafting.BarrelManager;
import com.dunk.tfc.api.Crafting.BarrelRecipe;
import com.dunk.tfc.api.TFCFluids;
import com.dunk.tfc.Core.TFCTabs;
import com.dunk.tfc.Entities.Mobs.EntitySkeletonTFC;
import com.dunk.tfc.ItemSetup;
import com.dunk.tfc.Items.ItemTerra;
import com.dunk.tfc.Items.Tools.ItemWeapon;
import com.dunk.tfc.api.Enums.EnumDamageType;
import cpw.mods.fml.common.FMLCommonHandler;
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
import cpw.mods.fml.common.gameevent.PlayerEvent;
import cpw.mods.fml.common.registry.GameRegistry;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import cpw.mods.fml.common.Mod;
import cpw.mods.fml.common.Mod.EventHandler;
import cpw.mods.fml.common.event.FMLInitializationEvent;
import net.minecraft.client.renderer.texture.IIconRegister;
import net.minecraft.creativetab.CreativeTabs;
import net.minecraft.entity.Entity;
import net.minecraft.entity.item.EntityItem;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.util.IIcon;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.event.entity.living.LivingDropsEvent;
import net.minecraftforge.fluids.Fluid;
import net.minecraftforge.fluids.FluidContainerRegistry;
import net.minecraftforge.fluids.FluidRegistry;
import net.minecraftforge.fluids.FluidStack;
import java.awt.*;
import java.util.List;

@Mod(modid = ExampleMod.MODID, version = ExampleMod.VERSION, dependencies = "required-after:terrafirmacraftplus;")
public class ExampleMod
{
    public static final String MODID = "examplemod";
    public static final String VERSION = "1.0";

    public static Item itemPaintbrush;
    public static Item itemClayBucketPaint;
    public static Fluid fluidPaint;

    @EventHandler
    public void init(FMLInitializationEvent event)
    {
        MinecraftForge.EVENT_BUS.register(this);
        FMLCommonHandler.instance().bus().register(this);

        itemPaintbrush = new ItemPaintbrush();
        GameRegistry.registerItem(itemPaintbrush, itemPaintbrush.getUnlocalizedName());

        GameRegistry.addShapelessRecipe(new ItemStack(itemPaintbrush), ItemSetup.stick, ItemSetup.straw);

        fluidPaint = new FluidPaint();
        FluidRegistry.registerFluid(fluidPaint);

        ItemStack flour = ItemFoodTFC.createTag(new ItemStack(ItemSetup.wheatGround), 120);
        FluidStack saltwater = new FluidStack(TFCFluids.SALTWATER, 10000);
        FluidStack paint = new FluidStack(fluidPaint, 10000);

        BarrelRecipe paintRecipe = new BarrelRecipe(flour, saltwater, null, paint, 1);
        paintRecipe.sealedRecipe = false;
        paintRecipe.setMinTechLevel(0);
        BarrelManager.getInstance().addRecipe(paintRecipe);

        itemClayBucketPaint = new ItemClayBucketPaint();
        GameRegistry.registerItem(itemClayBucketPaint, itemClayBucketPaint.getUnlocalizedName());

        FluidStack bucketPaint = new FluidStack(fluidPaint, 1000);
        ItemStack fullPaintBucket = new ItemStack(itemClayBucketPaint);
        ItemStack emptyPaintBucket = new ItemStack(ItemSetup.clayBucketEmpty);
        FluidContainerRegistry.registerFluidContainer(bucketPaint, fullPaintBucket, emptyPaintBucket);

        FluidStack paintbrushPaint = new FluidStack(fluidPaint, 10);
        ItemStack dippedPaintbrush = new ItemStack(itemPaintbrush, 1, 1);
        ItemStack paintbrush = new ItemStack(itemPaintbrush, 1, 0);
        FluidContainerRegistry.registerFluidContainer(paintbrushPaint, dippedPaintbrush, paintbrush);

        GameRegistry.addShapelessRecipe(dippedPaintbrush, paintbrush, fullPaintBucket);
    }

    @SubscribeEvent
    public void onLivingDrops(LivingDropsEvent event)
    {
        Entity mob = event.entityLiving;
        Entity killer = event.source.getEntity();

        if (killer instanceof EntityPlayer && mob instanceof EntitySkeletonTFC)
        {
            EntityPlayer player = (EntityPlayer)killer;
            ItemStack currentItem = player.inventory.mainInventory[player.inventory.currentItem];

            if (currentItem != null && currentItem.getItem() instanceof ItemWeapon)
            {
                ItemWeapon weapon = (ItemWeapon)currentItem.getItem();
                if (weapon.damageType == EnumDamageType.CRUSHING)
                {
                    for (int i = 0; i < event.drops.size(); ++i)
                    {
                        EntityItem drop = event.drops.get(i);
                        ItemStack dropStack = drop.getEntityItem();

                        if (dropStack.getItem() == ItemSetup.bone)
                        {
                            ItemStack boneFragment = new ItemStack(ItemSetup.boneFragment, dropStack.stackSize);
                            drop.setEntityItemStack(boneFragment);
                        }
                    }
                }
            }
        }
    }

    @SubscribeEvent
    public void onItemCrafted(PlayerEvent.ItemCraftedEvent event)
    {
        if (event.crafting.getItem() == itemPaintbrush && event.crafting.getItemDamage() == 1)
        {
            for (int i = 0; i < event.craftMatrix.getSizeInventory(); ++i)
            {
                ItemStack stackInSlot = event.craftMatrix.getStackInSlot(i);
                if (stackInSlot != null && stackInSlot.getItem() == itemClayBucketPaint)
                    ++stackInSlot.stackSize;
            }
        }
    }

    public static class ItemPaintbrush extends ItemTerra
    {
        public IIcon icon;
        public IIcon dippedIcon;

        public ItemPaintbrush()
        {
            setCreativeTab(TFCTabs.TFC_TOOLS);
            stackable = false;
            setNoRepair();
            setHasSubtypes(true);
        }

        @SideOnly(Side.CLIENT)
        @Override
        public void registerIcons(IIconRegister register)
        {
            icon = register.registerIcon(MODID + ":" + "ItemPaintbrush");
            dippedIcon = register.registerIcon(MODID + ":" + "ItemPaintbrushDipped");
        }

        @Override
        public boolean shouldRotateAroundWhenRendering() { return true; }

        @Override
        public String getUnlocalizedName(ItemStack stack)
        {
            if (stack.getItemDamage() != 0)
                return "item.ItemPaintbrushDipped";
            else
                return "item.ItemPaintbrush";
        }

        @SideOnly(Side.CLIENT)
        @Override
        public IIcon getIconFromDamage(int damage)
        {
            if (damage != 0)
                return dippedIcon;
            else
                return icon;
        }

        @SideOnly(Side.CLIENT)
        @Override
        public void getSubItems(Item item, CreativeTabs tabs, List subItems)
        {
            subItems.add(new ItemStack(this, 1, 0));
            subItems.add(new ItemStack(this, 1, 1));
        }
    }

    public static class ItemClayBucketPaint extends ItemTerra
    {
        public ItemClayBucketPaint()
        {
            setUnlocalizedName("ItemClayBucketPaint");
            setCreativeTab(TFCTabs.TFC_MISC);
            stackable = false;
            setNoRepair();
            //setContainerItem(ItemSetup.clayBucketEmpty);
            setTextureName(MODID + ":" + "ItemClayBucketPaint");
        }

        @SideOnly(Side.CLIENT)
        @Override
        public void registerIcons(IIconRegister register)
        {
            itemIcon = register.registerIcon(getIconString());
        }
    }

    public static class FluidPaint extends FluidBaseTFC
    {
        public FluidPaint()
        {
            super("FluidPaint");
            setBaseColor(Color.WHITE.getRGB());
            setViscosity(6000);
            setDensity(3000);
        }
    }
}

Making a New Block

Making an Anvil Recipe

Changing Your Mod's ID

Through this tutorial, our addon's mod ID has been the generic "examplemod". Now that you're ready to start making addons on your own, it is time we learned how to change that. If you open up your ExampleMod.java file, you should see that your mod's main class is declared like this, after all of the import statements.

@Mod(modid = ExampleMod.MODID, version = ExampleMod.VERSION, dependencies = "required-after:terrafirmacraftplus;")
public class ExampleMod
{
    public static final String MODID = "examplemod";
    public static final String VERSION = "1.0";

    ...
}

Here, the variable MODID stores your mod's ID. Let's change it to something else, for example "paintingaddon".

public static final String MODID = "paintingaddon";

If your run your game with this mod and go to the Mod List screen, you can see that our mod is indeed now called "paintingaddon", but there's something strange. If you click on mod, you will see that it appears like Forge couldn't find our mcmod.info file. Also, if you run the game you will notice that all of your custom textures are missing, even if the items and blocks still exist. Strangely enough, all the names from your translation .lang file should still show up.

When you change your mod's ID, you also need to change your asset path, where mcmod.info file and your textures directory is located. Our asset path is currently still resources/assets/examplemod/, lets change that to resources/assets/paintingaddon/.

Now your custom texture should show up again, but Forge will still complain about the missing mcmod.info file.

Forge complaining about the "missing" mcmod.info file.

The mcmod.info file

We need to update our mcmod.info file to match our new mods ID. Open it up in your IDE or text editor, it should currently look like this.

[
{
  "modid": "examplemod",
  "name": "Example Mod",
  "description": "Example placeholder mod.",
  "version": "${version}",
  "mcversion": "${mcversion}",
  "url": "",
  "updateUrl": "",
  "authorList": ["ExampleDude"],
  "credits": "The Forge and FML guys, for making this example",
  "logoFile": "",
  "screenshots": [],
  "dependencies": []
}
]

We need to change the modid entry to match our mod's id. We can also change some of the other fields.

[
{
  "modid": "paintingaddon",
  "name": "TFC+ Painting Addon",
  "description": "Adds paintings, paintbrushes, and paint, as well as a safe to store all of your paintings. Everything an aspiring artist needs",
  "version": "1.0",
  "mcversion": "1.7.10-10.13.4.1614-1.7.10",
  "url": "https://plus.terrafirmacraft.com/Making_TFC%2B_Addons",
  "updateUrl": "",
  "authorList": ["ExampleDude"],
  "credits": "",
  "logoFile": "",
  "screenshots": [],
  "dependencies": []
}
]

You can obviously put anything you want in these fields to better reflect what your mod does, but the modid field should always match your mod's ID. That's it, if we run our game now and go to the Mods list, we should see that everything is in order.

Note that while these are all the necessary steps, you may also want to rename your source code's root package to com.example.paintingaddon, or whatever is most fitting for your addon.

Our mod's description with the fixed mcmod.info file.

Closing remarks

You should now be truly fully equipped to make your own TFC+ addon. We hope that you've found this tutorial helpfull, and wish you good luck with your projects!

Tips and Tricks

  • Whenever you're stuck on how to do something, check how Minecraft or TFC+ does it in their respective sources. Chances are there is already a block/item/entity that is similar to what you want to create, and this is great starting point.
  • If the above point fails, look around Github for how other minecraft mods solved your problem. In particular, you can find source code for TFC+ addons Here, or from the Addons page.
  • Be wary of what you read online. Many tutorials and web-pages are either for older, or newer versions of Minecraft, where things have changed substantially.
  • You can find a list of Forge events in the forgeSrc library under cpw.mods.fml.client.event, cpw.mods.fml.common.event, and net.minecraftforge.event.
  • Be mindful of which events are posted on which event bus when you register your event handlers. Some events should be registered with MinecraftForge.EVENT_BUS.register(), while others should be registered with FMLCommonHandler.instance().bus().register().
  • If you are unsure what bus a particular event is posted to, find where they are instantiated in the Minecraft source code to see where Forge posts it.
  • If you are registering something in the FMLInitializationEvent but it doesn't seem to be working, try registering it in the FMLPreInitializationEvent instead. Same goes for the other way around.
  • Don't forged to annotate your even handling methods with @SubscribeEvent. This is the first thing you should check if your event handling method isn't running for mysterious reasons.
  • Be aware that you will need to find some way to synchronize the client and the server in many cases. If you try to teleport a player simply by changing their coordinates on the client side, the server will "correct" your "mistake" unless it is aware of this change.
  • Be very careful when making mods that are supposed to run on a dedicated server. The Minecraft jar running on the server is different than the jar running on the client; it's missing many of the classes that the client jar has. This means that if you try to import a class from the net.minecraft.client package, you will crash the server as those classes don't exist in the server's jar file. Use Forge's @SidedProxy annotation to write correct sided code.
  • If there is a vanilla or TFC+ field or method that you need to access but that is private or protected, you can always use Reflection to access it anyway. Although you should only do this as a last resort since it is messy and comes with a performance penalty. Look up Access Transformers for a cleaner solution.

Helpful Links