-
Notifications
You must be signed in to change notification settings - Fork 81
Your First Mod: Creating a Block
Better to ask on Discord MODS channels ( https://discord.gg/rfSh5Q9H )
NOTE: Readers should understand that this mod guide was written by someone who is still learning too, and that there may be (and probably are) other and/or better ways of doing the same things. It is also written to include people such as myself who know c# to some degree and want to try their hand out at modding but who may not necessarily be professional programmers or have training and degrees in the topic. So the more advanced readers will have to filter through a lot of explanations they probably will consider obvious. That being said, I hope this guide is helpful to some of you out there, and I offer it humbly.
PREREQUISITES: A basic level of c# knowledge is recommended and this mod also assumes you have already read and followed the basic 'Beginner's Guide to Modding' tutorial on this wiki. This tutorial is designed to be followed while using Visual Studio as an IDE, however users should be aware that there are other alternatives such as SharpDevelop.
Let's assume that so far we have just completed the Beginner's Guide to Modding tutorial and have created a new mod called 'MyMod'. Assuming we did nothing beyond that we would now be left with a modinfo.json file, and a Class1.cs file which will look something like this:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace MyMod
{
public class Class1
{
// stuff here
}
}
Let's right click on 'Class1.cs' in the solution explorer and rename that to something more useful. Such as 'Main' to signify it as the main entry point of code execution for our mod. Visual Studio will at this point also ask us if we want to rename all other references to 'Class1' and we'll say yes.
Next, highlight the 'MyMod' namespace and let's rename that to something unique as well. Say 'MyMods.ThisMod'. Since we'll be creating other .cs files down the road, it would probably also be good to right click on 'MyMod' in the solution explorer and select properties. In the window which opens we'll locate the 'default namespace' field and change its value to 'MyMods.ThisMod' as well.
Also, since the purpose of this class is to contain methods which the modloader and game can run at will without instantiating anything, let's just go ahead and make the class static.
So we now have one .cs file called 'Main.cs' which looks something like this:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace MyMods.ThisMod
{
public static class Main
{
// stuff here
}
}
We're making progress, but of course whatever code we put in class Main will never be executed unless we make use of a callback. Callbacks are really c# attributes which tell the game's modloader to execute certain portions of code at certain times. Users new to c# may want to brush up on attributes. Their use as it applies to Colony Survival mods is covered here.
So, to bring the modloader's attention to the fact that it will need to execute some code within this class, let's add our first attribute. Put the line [ModLoader.ModManager] right before the declaration of class Main. Do the same for any class containing code which must be called by the game itself.
Now we'll create a method where we'll create and register our block, let's call it 'afterAddingBaseTypes', only because that's the name of the callback we'll be using and we might want to do more in that callback than just register blocks.
The afterAddingBaseTypes callback method will accept a parameter that will be important for us later on: Dictionary<string, ItemTypesServer.ItemTypeRaw> items
Since the Dictionary class is part of the c# language I won't go into it here, but make sure you include this parameter because without it we will not be able to register our block with the game, and the compiler will happily let you omit it.
Here, finally, is where we'll add a proper callback. Just before the declaration of our new method, we'll add another couple attributes:
[ModLoader.ModCallback(ModLoader.EModCallbackType.AfterAddingBaseTypes, "MyMods.ThisMod.AfterAddingBaseTypes")]
And
ModLoader.ModCallbackDependsOn("pipliz.blocknpcs.addlittypes")
By using both a ModCallback and ModCallbackDependsOn attribute we can make sure that the code is going to run precisely when we want it to in game execution, and also that other tasks which need to get done ahead of us are done.
Now the next bit of code could use some explanation for those who have not dealt with .JSON (Javascript Object Notation) files before, because the game uses some internal classes to manipulate data in JSON format, store it, and pass it around. Because many tasks involved in modding Colony Survival involve creating and/or modifying JSON files and working with their in-game data component counterparts I would suggest not making the same mistake I did by insuring you are familiar with JSON before going too much further. Familiarity with that method of data structuring will reduce the likelihood of confusion later on. There is a good introduction on Wired.
Colony Survival already provides all the classes you'll need to work with JSON files and data, so to start let's include the namespace of those classes for the benefit of less typing. Add the statement
using Pipliz.JSON;
to the top of your Main.cs file.
To create our block the first thing we're going to do is create a new, empty JSON node. We'll call it 'MyBlock'. The line of code will look like this:
JSONNode MyBlockJSON = new JSONNode();
Now that we've got our block, let's fill in some of it's data using the setAs() method of the node object.
MyBlockJSON.SetAs("isPlaceable", true);
MyBlockJSON.SetAs("isSolid", true);
MyBlockJSON.SetAs("sideall", "planks");
As is most likely clear, we're just setting a series of name-value pairs at this point to outline the block's properties. 'isPlaceable' and 'isSolid' being set to true will allow the user to place our block in the world but will not allow them to walk through it. 'sideall' however needs further explanation because it refers to a texture. Since we haven't added a new texture for our block yet, I used the already existing texture 'planks'. Obviously, we'll want to change this.
Before we worry about the texture however, let's give the block an icon. This image will represent it in inventory. This is going to be a several step process:
-
Create a suitable image. It should be a 64x64 image (.png files are accepted, I don't know what other types.) Right click on 'MyMod' in the solution explorer and select 'open folder in file explorer'. This will put you in your project directory. Next, navigate to \bin\debug within your project directory and then create a new directory called 'icons'.
-
Place your chosen image there and name it 'MyItem.png'.
-
Reference the icon file in your mod. Before we add the line of code that will do that, we need another method & callback. A useful one called 'OnAssemblyLoaded' which is where we'll do all the setup for our mod. This one will also accept a parameter – a string we'll call 'path.' This will be very useful to us because it contains the path to the folder where our mod files physically reside when the game is run.
Create a static string member for the Main class called 'MODPATH', an initial value is not necessary. While you're up there, add another class member of type 'ItemTypesServer.ItemTypeRaw' and name it 'MyBlock'. Again you can leave it uninitialized. The ItemTypeRaw object will hold the data for our block in its final form. We won't use it yet, but this is as good a time as any to add it.
Within the OnAssemblyLoaded callback, let's set that variable using the following line of code:
MODPATH = System.IO.Path.GetDirectoryName(path).Replace("\\", "/");
Notice that we used the GetDirectoryName()
and Replace()
methods to format the path string in a way that the game will like when we use it down the road.
Now let's finally add that line to our afterAddingBaseTypes()
method that associates the image file as the icon for our block:
MyBlockJSON.SetAs("icon", MODPATH + "/icons/MyBlock.png");
Now, our code should look something like this:
using Pipliz.JSON;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace MyMods.ThisMod
{
/// <summary>
/// Execution entry points for our mod.
/// </summary>
[ModLoader.ModManager]
public static class Main
{
// Location of MOD .DLL file at runtime.
public static string MODPATH;
// BLOCKS
public static ItemTypesServer.ItemTypeRaw MyBlock;
/// <summary>
/// OnAssemblyLoaded callback entrypoint. Used for mod configuration / setup.
/// </summary>
/// <param name="path">The starting point of our mod file structure.</param>
[ModLoader.ModCallback(ModLoader.EModCallbackType.OnAssemblyLoaded, "MyMods.ThisMod.OnAssemblyLoaded")]
public static void OnAssemblyLoaded(string path)
{
// Get a nicely formatted version of our mod directory.
MODPATH = System.IO.Path.GetDirectoryName(path).Replace("\\", "/");
}
/// <summary>
/// afterAddingBaseTypes callback. Used for adding blocks.
/// </summary>
[ModLoader.ModCallback(ModLoader.EModCallbackType.AfterAddingBaseTypes, "MyMods.ThisMod.AfterAddingBaseTypes")]
[ModLoader.ModCallbackDependsOn("pipliz.blocknpcs.addlittypes")]
public static void afterAddingBaseTypes (Dictionary<string, ItemTypesServer.ItemTypeRaw> items)
{
// Create a node to store our block's data.
JSONNode MyBlockJSON = new JSONNode();
// Fill in some data.
MyBlockJSON.SetAs("isPlaceable", true);
MyBlockJSON.SetAs("isSolid", true);
MyBlockJSON.SetAs("sideall", "planks");
MyBlockJSON.SetAs("icon", MODPATH + "/icons/MyBlock.png");
}
}
}
As mentioned above, our block still does not have a texture, so let's make one. This is another multi-step process:
-
Add the texture file.
-
Create a suitable image. It should be a 256x256 image (.png files are accepted, I don't know what other types.) Careful not to make the image too large, or the game will not accept it and your texture won't show up.
-
Right click on 'MyMod' in the solution explorer and select 'open folder in file explorer'. This will put you in your project directory. Next, navigate to \bin\debug within your project directory and then create a new directory called 'textures'.
-
Place your chosen image there and name it 'MyBlockAlbedo.png'.
-
Reference the icon file in your mod:
Before we add the line of code that will do that, we must again add another method & callback. 'afterSelectedWorld' this time, which will accept no parameters. It will however have an additional attribute:
ModLoader.ModCallbackProvidesFor("pipliz.server.registertexturemappingtextures")
We'll then add some additional code to our new method:
// (Create a new textureMapping object.)
ItemTypesServer.TextureMapping MyBlockTexture = new ItemTypesServer.TextureMapping(new JSONNode());
// (Store the path of the texture to our JSON node.)
MyBlockTexture.AlbedoPath = MODPATH + "/textures/MyBlockAlbedo.png";
// (Associate the block with the texture we just stored.)
ItemTypesServer.SetTextureMapping("MyMods.ThisMod.MyBlockTexture", MyBlockTexture);
Lastly, let's modify the line in the afterAddingBaseTypes() method that associates a texture with our block:
MyBlockJSON.SetAs("sideall", "planks");
Becomes:
MyBlockJSON.SetAs("sideall", "MyMods.ThisMod.MyBlockTexture");
So, now that all is said and done, our Main.cs should look something like this:
using Pipliz.JSON;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace MyMods.ThisMod
{
/// <summary>
/// Execution entry points for our mod.
/// </summary>
[ModLoader.ModManager]
public static class Main
{
// Location of MOD .DLL file at runtime.
public static string MODPATH;
// BLOCKS
public static ItemTypesServer.ItemTypeRaw MyBlock;
/// <summary>
/// OnAssemblyLoaded callback entrypoint. Used for mod configuration / setup.
/// </summary>
/// <param name="path">The starting point of our mod file structure.</param>
[ModLoader.ModCallback(ModLoader.EModCallbackType.OnAssemblyLoaded, "MyMods.ThisMod.OnAssemblyLoaded")]
public static void OnAssemblyLoaded(string path)
{
// Get a nicely formatted version of our mod directory.
MODPATH = System.IO.Path.GetDirectoryName(path).Replace("\\", "/");
}
/// <summary>
/// afterAddingBaseTypes callback. Used for adding blocks.
/// </summary>
[ModLoader.ModCallback(ModLoader.EModCallbackType.AfterAddingBaseTypes, "MyMods.ThisMod.AfterAddingBaseTypes")]
[ModLoader.ModCallbackDependsOn("pipliz.blocknpcs.addlittypes")]
public static void afterAddingBaseTypes (Dictionary<string, ItemTypesServer.ItemTypeRaw> items)
{
// Create a node to store our block's data.
JSONNode MyBlock = new JSONNode();
// Fill in some data.
MyBlock.SetAs("isPlaceable", true);
MyBlock.SetAs("isSolid", true);
MyBlock.SetAs("sideall", "planks");
MyBlock.SetAs("icon", MODPATH + "/icons/MyBlock.png");
MyBlock.SetAs("sideall", "MyMods.ThisMod.MyBlockTexture");
}
/// <summary>
/// AfterSelectedWorld callback entry point. Used for adding textures.
/// </summary>
[ModLoader.ModCallback(ModLoader.EModCallbackType.AfterSelectedWorld, "MyMods.ThisMod.afterSelectedWorld"),
ModLoader.ModCallbackProvidesFor("pipliz.server.registertexturemappingtextures")]
public static void afterSelectedWorld()
{
// (Create a new textureMapping object.)
ItemTypesServer.TextureMapping MyBlockTexture = new ItemTypesServer.TextureMapping(new JSONNode());
// (Store the path of the texture to our JSON node.)
MyBlockTexture.AlbedoPath = MODPATH + "/textures/MyBlockAlbedo.png";
// (Associate the block with the texture we just stored.)
ItemTypesServer.SetTextureMapping("MyMods.ThisMod.MyBlockTexture", MyBlockTexture);
}
}
}
So, now there's a few last bits of code to add. Remember that Dictionary object we were given in afterAddingBaseTypes()
, and the ItemTypeRaw
object we created but never used? Well, now we'll use them both to register our block in that same afterAddingBaseTypes()
method by creating a raw item, giving it the data we've assembled, and registering it like so:
// Assemble
MyBlock = new ItemTypesServer.ItemTypeRaw("MyMods.ThisMod.MyBlock", MyBlockJSON);
// Register
items.Add("MyMods.ThisMod.MyBlock", MyBlock);
Now all of this effort will give us a block, yes, but no way for the user to craft it.
If you guessed we were going to be adding another method & callback, you nailed it. This time we'll be using the callback AfterItemTypesDefined
, and adding several lines of code like so:
/// <summary>
/// The afterItemType callback entrypoint. Used for registering jobs and recipes.
/// </summary>
[ModLoader.ModCallback(ModLoader.EModCallbackType.AfterItemTypesDefined, "MyMods.ThisMod.AfterItemTypesDefined"),
ModLoader.ModCallbackProvidesFor("pipliz.apiprovider.jobs.resolvetypes")]
public static void AfterItemTypesDefined()
{
// Create a couple wooden planks
InventoryItem MyBlockRequirements = new InventoryItem(BlockTypes.Builtin.BuiltinBlocks.Planks, 2);
// Create a recipe
Recipe MyBlockRecipe = new Recipe("MyMods.ThisMod.MyBlock", MyBlockRequirements, new InventoryItem(MyBlock.ItemIndex, 1), 100);
// Register the recipe
RecipeStorage.AddDefaultLimitTypeRecipe("MyMods.ThisMod.MyBlock", MyBlockRecipe);
// Make it craftable by the user
RecipePlayer.AddDefaultRecipe(MyBlockRecipe);
}
Notice the structure of InventoryItem
and Recipe objects above. Also note that the Recipe class constructor has many overloads for flexibility. The metadata reveals them for us:
public Recipe(JSONNode node);
public Recipe(string name, InventoryItem requirement, List<InventoryItem> results, int defaultLimit = 2000000000, bool isOptional = false);
public Recipe(string name, List<InventoryItem> requirements, InventoryItem result, int defaultLimit = 2000000000, bool isOptional = false);
public Recipe(string name, InventoryItem requirement, InventoryItem result, int defaultLimit = 2000000000, bool isOptional = false);
public Recipe(string name, List<InventoryItem> requirements, List<InventoryItem> results, int defaultLimit = 2000000000, bool isOptional = false);
Also note the different types of recipes. 'Default' recipes can always be crafted. 'Optional' ones must be researched. As in:
RecipeStorage.AddOptionalLimitTypeRecipe("MyMods.ThisMod.MyBlock", MyBlockRecipe);
So now our accumulated c# code is going to look something like this:
using Pipliz.JSON;
using System.Collections.Generic;
namespace MyMods.ThisMod
{
/// <summary>
/// Execution entry points for our mod.
/// </summary>
[ModLoader.ModManager]
public static class Main
{
// Location of MOD .DLL file at runtime.
public static string MODPATH;
// BLOCKS
public static ItemTypesServer.ItemTypeRaw MyBlock;
/// <summary>
/// OnAssemblyLoaded callback entrypoint. Used for mod configuration / setup.
/// </summary>
/// <param name="path">The starting point of our mod file structure.</param>
[ModLoader.ModCallback(ModLoader.EModCallbackType.OnAssemblyLoaded, "MyMods.ThisMod.OnAssemblyLoaded")]
public static void OnAssemblyLoaded(string path)
{
// Get a nicely formatted version of our mod directory.
MODPATH = System.IO.Path.GetDirectoryName(path).Replace("\\", "/");
}
/// <summary>
/// The afterItemType callback entrypoint. Used for registering jobs and recipes.
/// </summary>
[ModLoader.ModCallback(ModLoader.EModCallbackType.AfterItemTypesDefined, "MyMods.ThisMod.AfterItemTypesDefined"),
ModLoader.ModCallbackProvidesFor("pipliz.apiprovider.jobs.resolvetypes")]
public static void AfterItemTypesDefined()
{
// Create a couple wooden planks.
InventoryItem MyBlockRequirements = new InventoryItem(BlockTypes.Builtin.BuiltinBlocks.Planks, 2);
// Create a recipe
Recipe MyBlockRecipe = new Recipe("MyMods.ThisMod.MyBlock", MyBlockRequirements, new InventoryItem(MyBlock.ItemIndex, 1), 100);
// Register the recipe
RecipeStorage.AddDefaultLimitTypeRecipe("MyMods.ThisMod.MyBlock", MyBlockRecipe);
// Make it craftable by the user
RecipePlayer.AddDefaultRecipe(MyBlockRecipe);
}
/// <summary>
/// afterAddingBaseTypes callback. Used for adding blocks.
/// </summary>
[ModLoader.ModCallback(ModLoader.EModCallbackType.AfterAddingBaseTypes, "MyMods.ThisMod.AfterAddingBaseTypes")]
[ModLoader.ModCallbackDependsOn("pipliz.blocknpcs.addlittypes")]
public static void afterAddingBaseTypes (Dictionary<string, ItemTypesServer.ItemTypeRaw> items)
{
// Create a node to store our block's data.
JSONNode MyBlockJSON = new JSONNode();
// Fill in some data.
MyBlockJSON.SetAs("isPlaceable", true);
MyBlockJSON.SetAs("isSolid", true);
MyBlockJSON.SetAs("sideall", "planks");
MyBlockJSON.SetAs("icon", MODPATH + "/icons/MyBlock.png");
MyBlockJSON.SetAs("sideall", "MyMods.ThisMod.MyBlockTexture");
// Assemble
MyBlock = new ItemTypesServer.ItemTypeRaw("MyMods.ThisMod.MyBlock", MyBlockJSON);
// Register
items.Add("MyMods.ThisMod.MyBlock", MyBlock);
}
/// <summary>
/// AfterSelectedWorld callback entry point. Used for adding textures.
/// </summary>
[ModLoader.ModCallback(ModLoader.EModCallbackType.AfterSelectedWorld, "MyMods.ThisMod.afterSelectedWorld"),
ModLoader.ModCallbackProvidesFor("pipliz.server.registertexturemappingtextures")]
public static void afterSelectedWorld()
{
// (Create a new textureMapping object.)
ItemTypesServer.TextureMapping MyBlockTexture = new ItemTypesServer.TextureMapping(new JSONNode());
// (Store the path of the texture to our JSON node.)
MyBlockTexture.AlbedoPath = MODPATH + "/textures/MyBlockAlbedo.png";
// (Associate the block with the texture we just stored.)
ItemTypesServer.SetTextureMapping("MyMods.ThisMod.MyBlockTexture", MyBlockTexture);
}
}
}
It's also worth noting that the blocks themselves have many more properties than the ones which we used. For example, note the following code:
MyBlockJSON.SetAs("sideall", "crate");
MyBlockJSON.SetAs("sidey+", "MyMods.ThisMod.MyBlockTexture");
This code would result in a block that looked like an ordinary crate on all sides except the custom texture on its top. Blocks can also have meshes, and can be rotatable. However these topics are beyond the scope of this tutorial.
If we've done everything right, Visual Studio should be reporting no errors and we can just go ahead and hit the good 'ol F6 for build.
It's not important, but I would advise changing the 'copy local' attribute of every reference in your project to 'false' so that the output directory stays less cluttered. Finally, copy the “icons” and “textures” directories you created earlier from your project folder to the output directory ([project_folder]/bin/debug by default). Now in that debug folder you are looking at all the files needed for someone to use your mod. It should look like this:
Just put those files in their own folder structure (ex. MyMods\ThisMod[files here]), show it to someone who will tell you how incredibly awesome you are, and tada! You're a Colony Survival modder.
“Now hold on a minute!” you say, because you ran the game with your mod and your new block appears as 'MyMods.ThisMod.MyBlock' and not 'MyBlock.' Well, that's where localization comes in. Since games like Colony Survival are designed for users speaking many different languages, the game needs to translate your names into the local language.
At the time of this writing localization must be done by hand, by modifying json files, or through code too complicated to go over in this tutorial. However that is likely to change at some point. Watch for updates to the wiki.