Intermediate Recipes - tModLoader/tModLoader GitHub Wiki
This Guide has been updated to 1.4. If you need to view the old 1.3 version of this wiki page, click here
Recipe groups allow you to create a recipe that accepts any item out of a list of specified ingredients. For example, all varieties of wood are in the vanilla "Wood" Group. Instead of creating 8 separate recipes to use each type of vanilla wood, we would create 1 recipe that uses the "Wood" recipe group. Mods can also add recipe groups to simplify their recipes.
Vanilla groups consist of: "Wood", "IronBar", "PresurePlate", "Sand", "Fragment", "Birds", "Scorpions", "Squirrels", "Bugs", "Ducks", "Butterflies", "Fireflies", "Snails", "Dragonflies", "Turtles", and "Fruit". To use a vanilla recipe group, simply add the following instead of a similar Recipe.AddIngredient
line:
// Option 1: The name of the group as a string
recipe.AddRecipeGroup("Wood", 5); // parameter 1 is the group name, parameter 2 is the item stack count
// Option 2: Use the RecipeGroupID value.
recipe.AddRecipeGroup(RecipeGroupID.IronBar, 10);
Note that, like with Recipe.AddIngredient
, the parameter for providing an explicit stack count is optional and defaults to 1.
You may have noticed that aside from "Wood" and "IronBar", there aren't many useful vanilla recipe groups. Luckily, we can make our own recipe groups. Let's imagine we want to make a recipe that used a Magic Mirror or Ice Mirror as an ingredient. Without recipe groups, we would have to do this:
Recipe recipe = CreateRecipe();
recipe.AddIngredient(ItemID.MagicMirror);
recipe.AddIngredient(ItemID.Gel, 10);
recipe.AddTile(TileID.Chairs);
recipe.AddTile(TileID.Tables);
recipe.Register();
recipe = CreateRecipe();
recipe.AddIngredient(ItemID.IceMirror);
recipe.AddIngredient(ItemID.Gel, 10);
recipe.AddTile(TileID.Chairs);
recipe.AddTile(TileID.Tables);
recipe.Register();
In this example, it isn't too bad, but imagine you have 10 or 20 similar items that you'd like to use for your recipe - it would get unmanageable very quickly. To solve this, first, we must add a new recipe group. We do this in a ModSystem
class by overriding the ModSystem.AddRecipeGroups
method:
// The Terraria.Localization using directive (using Terraria.Localization;) is required for Language methods.
public override void AddRecipeGroups()
{
RecipeGroup group = new RecipeGroup(() => $"{Language.GetTextValue("LegacyMisc.37")} {Lang.GetItemNameValue(ItemID.MagicMirror)}", ItemID.IceMirror, ItemID.MagicMirror);
RecipeGroup.RegisterGroup(nameof(ItemID.MagicMirror), group);
}
As seen above, first we construct the group, then we call RecipeGroup.RegisterGroup
with the desired name. As a convention, please use "ModName:GroupName"
for recipe groups that mainly concern items or usages specific to your mod. If you are using vanilla items to make a group that could conceivably be used by other mods for the same purpose, use nameof(ItemID.IconicItem)
, where IconicItem
is the earliest item to be added to the game in the group (smallest ItemID value). For example, if you were adding a group for SilverBar and TungstenBar, use SilverBar because that was the first item added to the game. Doing this allows multiple mods to collaborate seamlessly. If Mod A adds a SilverBar group, and Mod B adds a similar group but with a 3rd SilverBar variant, the game will seamlessly merge the recipegroups, allowing all recipes using the groups to allow all variants.
As a note, Language.GetTextValue("LegacyMisc.37")
is just the word "Any" in the selected language and requires the Terraria.Localization
using directive at the top of your code. The syntax here is a little difficult for newcomers (as it makes use of lambda expressions), so please follow it exactly.
As a note: use ModContent.ItemType<YourModItem>()
instead of ItemID.ItemName
for ModItem
s.
Next, we need to use the RecipeGroup
. We do this just like with vanilla recipe groups, except our recipe group's name will be different:
recipe.AddRecipeGroup("ModName:GroupName");
using Terraria;
using Terraria.ID;
using Terraria.ModLoader;
using Terraria.Localization;
namespace ExampleMod
{
public class ExampleRecipes : ModSystem
{
public override void AddRecipeGroups()
{
RecipeGroup group = new RecipeGroup(() => $"{Language.GetTextValue("LegacyMisc.37")} {Lang.GetItemNameValue(ItemID.MagicMirror)}", ItemID.IceMirror, ItemID.MagicMirror);
RecipeGroup.RegisterGroup(nameof(ItemID.MagicMirror), group);
}
public override void AddRecipes()
{
Recipe recipe = Recipe.Create(ModContent.ItemType<Items.MinionControlRod>());
recipe.AddIngredient(ItemID.SlimeStaff);
recipe.AddRecipeGroup(nameof(ItemID.MagicMirror));
recipe.AddTile(TileID.Chairs);
recipe.AddTile(TileID.Tables);
recipe.Register();
recipe = Recipe.Create(ModContent.ItemType<Items.SummonersAssociationCard>());
recipe.AddIngredient(ItemID.Gel, 10);
// Here, instead of adding 5 ItemID.Wood, we use a RecipeGroup to specify all types of Wood in a single recipe.
recipe.AddRecipeGroup("Wood", 5);
recipe.AddTile(TileID.Chairs);
recipe.AddTile(TileID.Tables);
recipe.Register();
}
}
}
Note that the name used in Recipe.AddRecipeGroup
is identical to what we defined when registering our modded RecipeGroup
.
In some situations, you may want to add your own items to existing recipe groups. In this example, we will add another wood type to the "Wood" recipe group.
public override void AddRecipeGroups()
{
if (RecipeGroup.recipeGroupIDs.ContainsKey("Wood"))
{
int index = RecipeGroup.recipeGroupIDs["Wood"];
RecipeGroup group = RecipeGroup.recipeGroups[index];
group.ValidItems.Add(ModContent.ItemType<MyWood>());
}
}
Note that checking if (RecipeGroup.recipeGroupIDs.ContainsKey(...))
is not necessary, but it will prevent errors if some other mod completely removes that recipe group for one reason or another. This can also be used for cross-mod compatibility.
In the Conditions section of the Basic Recipes guide, using existing conditions in recipes is taught. Custom conditions can also be used in mods.
The most common custom condition for mods would likely be a condition for if the player is in a custom ModBiome
. To do this, use the AddCondition
method and pass in a new instance of the Condition
class. Within the Condition
class constructor, a localization key and a method with no arguments that returns a bool will need to be provided. In this case, we will use the InModBiome
method to query if the local player is in our ModBiome
. This example only allows the recipe to be crafted if the player is in the ExampleSufaceBiome
:
Recipe.Create(ItemID.AlphabetStatueG)
.AddIngredient(ItemID.StoneBlock, 3)
.AddCondition(new Condition("Mods.ExampleMod.Conditions.InExampleBiome", () => Main.LocalPlayer.InModBiome<ExampleSurfaceBiome>()))
.Register();
This approach works well for effects used in a single recipe, but adding the same code to many different recipes will be messy and error-prone. The solution to this is to reuse the Condition
. It is recommended to place all Condition
instances in an appropriately named static class so that they can easily be referenced for any recipe that would use it in your whole mod.
Static class with shared Condition
instances:
namespace ExampleMod.Content
{
public static class ExampleConditions
{
public static Condition InExampleBiome = new Condition("Mods.ExampleMod.Conditions.InExampleBiome", () => Main.LocalPlayer.InModBiome<ExampleSurfaceBiome>());
// Other Condition instances can go here.
}
}
Using a Condition
from the static class. This code can be used in any recipe in any file, provided the correct using statement is used:
Recipe.Create(ItemID.AlphabetStatueG)
.AddIngredient(ItemID.StoneBlock, 3)
.AddCondition(ExampleConditions.InExampleBiome)
.Register();
The condition description will be shown to the user if they look at the recipe from the Guide's recipe help or a mod like Recipe Browser.
By default, the first recipe that results in an item will be the recipe used to decraft that item. This behavior can be customized with conditions. The comments and examples in ShimmerShowcase.cs teach how to customize to shimmer decrafting feature.
Recipes don't always consume all the items used as ingredients. For example, all recipes that use TileID.Bottles
as a crafting station will be subject to the Alchemy Table effect. All ingredients have a 1/3rd chance to not be consumed. Note that the modder still needs the full amount of ingredients to craft the item, they just might not be consumed.
Modders can make their own custom recipe item consumption rules and apply them to individual recipes. This is done by the AddConsumeItemCallback
method. The modder provides a delegate (The delegate is of the ConsumeItemCallback
Type) that will run when the item is being crafted, for each ingredient. That delegate has the ability to adjust the amount of each item that is consumed.
Recipe.Create(ItemID.AlphabetStatueB)
.AddIngredient(ItemID.StoneBlock, 10)
.AddIngredient(ItemID.Chain)
.AddConsumeItemCallback((Recipe recipe, int type, ref int amount) => {
if (type == ItemID.Chain) {
amount = 0;
}
})
.AddTile(TileID.HeavyWorkBench)
.Register();
In this example, we add a recipe that will not consume the ItemID.Chain
item. This is done by adding a ConsumeItemCallback
delegate that will set the amount to 0 if the ingredient being checked is ItemID.Chain
. This approach works well for effects used in a single recipe, but adding the same code to many different recipes will be messy and error-prone. The solution to this is to reuse the delegate. It is recommended to place all ConsumeItemCallback
delegates in an appropriately named static class so that they can easily be referenced for any recipe that would use it in your whole mod.
Static class with shared ConsumeItemCallback
delegates:
namespace ExampleMod.Content
{
public static class ExampleConsumptionRules
{
public static void DontConsumeChain(Recipe recipe, int type, ref int amount) {
if (type == ItemID.Chain) {
amount = 0;
}
}
// Other ConsumeItemCallback delegates can go here.
}
}
Using a ConsumeItemCallback
delegate. This code can be used in any recipe in any file, provided the correct using statement is used:
Recipe.Create(ItemID.AlphabetStatueC)
.AddIngredient(ItemID.StoneBlock, 10)
.AddIngredient(ItemID.Chain)
.AddConsumeItemCallback(ExampleConsumptionRules.DontConsumeChain)
.AddTile(TileID.HeavyWorkBench)
.Register();
The Alchemy Table effect uses a bit of advanced logic. First, the Alchemy Table tile itself uses AdjTiles to act like TileID.Bottles
. This means it will satisfy the crafting station requirement for recipes requiring TileID.Bottles
. The code also sets the Player.alchemyTable
bool to true, indicating that the player is under the effects of the alchemy table. Finally, the ConsumeItemCallback
code applied to each of the potion recipes does the following to check for the alchemyTable
bool and give each item a 1/3 chance to be consumed:
public static void Alchemy(Recipe recipe, int type, ref int amount) {
if (!Main.LocalPlayer.alchemyTable) {
return;
}
int amountUsed = 0;
for (int i = 0; i < amount; i++) {
if (!Main.rand.NextBool(3)) {
amountUsed++;
}
}
amount = amountUsed;
};
To adapt this to a modded situation, modders might need to use a ModPlayer
bool or something like Main.LocalPlayer.adjTile[TileID.HeavyWorkBench]
as the condition instead of the alchemyTable
bool.
As demonstrated, modders can do advanced logic in ConsumeItemCallback
code. Remember that your users will not know about these effects unless it is communicated to them. The Alchemy Table item tooltip, for example, tells the player "33% chance to not consume potion crafting ingredients".
We can use the AddOnCraftCallback
method to run code after the recipe is crafted. This is not done for any vanilla Terraria content, but it can be useful for advanced features added by mods. Similar to the approach shown for using AddConsumeItemCallback, we want to store these methods statically so they are easily reusable. The following example is a trivial example, but should give an idea about what can be done. This example randomly spawns fireworks projectiles and the confetti item into the players inventory when the recipe is crafted. You can test this out by crafting the item in ExampleMod:
Recipe.Create(ItemID.AlphabetStatueZ)
.AddIngredient(ItemID.StoneBlock, 10)
.AddIngredient(ItemID.Chain)
.AddOnCraftCallback(ExampleRecipeCallbacks.RandomlySpawnFireworks)
.AddTile(TileID.HeavyWorkBench)
.Register();
namespace ExampleMod.Common
{
public static class ExampleRecipeCallbacks
{
public static void RandomlySpawnFireworks(Recipe recipe, Item item, List<Item> consumedItems, Item destinationStack) {
if (Main.rand.NextBool(3)) {
int fireworkProjectile = ProjectileID.RocketFireworksBoxRed + Main.rand.Next(4);
Projectile.NewProjectile(Main.LocalPlayer.GetSource_FromThis(), Main.LocalPlayer.Top, new Microsoft.Xna.Framework.Vector2(0, -Main.rand.NextFloat(2f, 4f)).RotatedByRandom(0.3f), fireworkProjectile, 0, 0, Main.myPlayer);
Main.LocalPlayer.QuickSpawnItem(Main.LocalPlayer.GetSource_FromThis(), ItemID.Confetti, 5);
}
}
}
}
// TODO
We can edit vanilla recipes or recipes from other mods from within a ModSystem.PostAddRecipes
method. Basically, we iterate over the recipes to find the recipe we want to tweak, then modify ingredients, tiles, or even disable the recipe entirely.
This code removes the "Chain" item from all vanilla recipes. The second half of this example finds and disables an exact recipe. Do not remove entries from Main.recipe
, that will break many things.
public override void PostAddRecipes() {
for (int i = 0; i < Recipe.numRecipes; i++) {
Recipe recipe = Main.recipe[i];
if (recipe.TryGetIngredient(ItemID.Chain, out Item ingredient)) {
recipe.RemoveIngredient(ItemID.Chain);
}
if(recipe.HasRecipeGroup("IronBar") && recipe.HasTile(TileID.Anvils) && recipe.HasResult(ItemID.Chain) && recipe.createItem.stack == 10) {
recipe.DisableRecipe();
}
}
}
If you wish to use items or tiles from other mods in your recipes, you need to plan ahead. Read Expert Cross Mod Content for an idea of some considerations you'll need to keep in mind. For example, the mod you are referencing might change the item name in an update to their mod, breaking your mod in the process. Another issue is planning for the other mod not being enabled. In that case, you might want to skip the recipe altogether or plan an alternate recipe. The example below shows the alternate ingredients approach.
Recipe recipe = Recipe.Create(ItemID.Wood, 999);
if (ModLoader.TryGetMod("ExampleMod", out Mod exampleMod) && exampleMod.TryFind<ModItem>("ExampleWings", out ModItem ExampleWings)) {
recipe.AddIngredient(ExampleWings.Type);
}
else {
recipe.AddIngredient(ModContent.ItemType<MyItem>());
recipe.AddIngredient(ItemID.EnchantedSword);
}
recipe.Register();
As you can see, this recipe takes ExampleMod
's ExampleWings
item if ExampleMod
is enabled and ExampleWings
exists in that mod, and the current mod's MyItem
and vanilla's EnchantedSword
if ExampleMod
is not enabled. If you have opted to only load your mod if the other mod is enabled, you can simplify the logic since the other mod will always be available if you are using modReferences
.
ModSystem.AddRecipeGroups
has to go in the ModSystem
class, not your ModItem
class.