-
Notifications
You must be signed in to change notification settings - Fork 55
Getting Started Guide
EMI is an item and recipe viewer mod. As a mod dev, you might want to integrate with EMI to make your mod more understandable for users. EMI integration can involve simple data formats, to in code plugin. This guide is going to go over a lot of the common tasks you'd want to do as a developer, and how to quickly achieve them.
Step one is getting the mod into your development environment. EMI is available on the TerraformersMC Gradle, and can be easily added to a project with the following.
repositories {
maven {
name = "TerraformersMC"
url = "https://maven.terraformersmc.com/"
}
}
How EMI gets added to your dependencies varies based on setup.
The Gradle property emi_version
should be something like 1.0.0+1.19.4
with EMI's version and Minecraft's version.
Here are common dependency setups
dependencies {
// Fabric
modCompileOnly "dev.emi:emi-fabric:${emi_version}:api"
modLocalRuntime "dev.emi:emi-fabric:${emi_version}"
// Forge (see below block as well if you use Forge Gradle)
compileOnly fg.deobf("dev.emi:emi-forge:${emi_version}:api")
runtimeOnly fg.deobf("dev.emi:emi-forge:${emi_version}")
// Architectury
modCompileOnly "dev.emi:emi-xplat-intermediary:${emi_version}:api"
// MultiLoader Template/VanillaGradle
compileOnly "dev.emi:emi-xplat-mojmap:${emi_version}:api"
}
For Forge Gradle users, you will need to enable Mixin refmaps in your client sourceset. This can be done by adding 2 lines inside of your client runs, to look like below.
runs {
client {
// Add these two lines
property 'mixin.env.remapRefMap', 'true'
property 'mixin.env.refMapRemappingFile', "${projectDir}/build/createSrgToMcp/output.srg"
// The rest of the code that was already here
// ...
}
}
Next, EMI has a plugin system for calling mods when they need to provide EMI with metadata. Not all integration is going to need code, but most of it will.
If you are on Fabric or Quilt, you'll need to edit your fabric.mod.json
or quilt.mod.json
to include the following entrypoint. The entrypoint should point to a class of yours that implements EmiPlugin
.
{
"entrypoints": {
"emi": [
"my.package.MyModEmiPlugin"
]
}
}
If you are on Forge, your implementation of EmiPlugin
must be annotated with @EmiEntrypoint
.
If you are an xplat mod, you must do both.
From here, you will have EMI in your game when you launch, and can start adding code or data.
It is also recommended that you read Basic API Concepts before writing any code, as it should provide a short rundown of what the types mentioned in following sections represent, and how to interact with them.
EMI expects mods to provide translations for all of the tags they create and use. Users in a development environment will be given a warning if they have tags that are untranslated.
This is very important for displaying understandable information to users, and having your mod be more accessible to people of other languages.
Doing this is well documented on the Tag Translation page, and is a short read.
It is also recommended that mods use tags for ingredients instead of lists of items whenever possible, as this will allow those ingredients to be translated and given quick to understand names.
EMI's recipe tree functions best when mods provide some metadata about which recipes should be automatically expanded in the recipe tree. Not doing so will make users who use the recipe tree with your mod be forced to manually expand trees and set their own defaults, which can waste their time and energy.
Doing this is well documented on the Recipe Defaults page, and is a short read.
EMI's API is completely client side. All of your code can reference client concepts like rendering, it will only ever be loaded on the client.
EMI is an abstraction layer separate from vanilla's recipe system. As such, it has its own concepts for existing vanilla classes. EMI's recipes extend EmiRecipe
and not vanilla's Recipe
, for example. Creating EMI integration involves providing EMI what it understands using vanilla concepts, such as making EmiRecipe
s that represent your Recipe
s. Like other recipe viewer mods, EMI needs to know more about recipes than what vanilla provides, such as how to render them, and what inputs/outputs they provide.
Stacks are a notable case. Vanilla has ItemStack
s, but no representation of other resources, like fluids. Forge has FluidStack
s. EMI abstracts these into an EmiStack
, which is a representation of a discrete part of a resource, including amount and nbt. To create an EmiStack
from vanilla concepts, you can call EmiStack.of
this will convert Block
s, Item
s, and ItemStack
s to EMI's representation.
On Fabric, you can use FabricEmiStack.of for FluidVariant
s, and ItemVariant
s.
On Forge, you can use ForgeEmiStack.of for FluidStack
s
The second concept EMI has relating to stacks is ingredients. An EmiIngredient
represents a collection of EmiStack
s, but also offers its own name, rendering, and amount. Notably, EmiStack
s are EmiIngredient
s, and they can be passed wherever an EmiIngredient
is used by the API. To create an EmiIngredient
, similar to EmiStack
, you can call EmiIngredient.of
on a vanilla ingredient (which will attempt to match it to a tag), or a list of resources. If you provide this method with a singleton, it will automatically forward the call to EmiStack.of
, manual checking for this is not necessary.
EMI's index is the list of all blocks, items, fluids, and other abstract resources, that are present in Minecraft. The index is searchable, and typically present at most points in time on the right half of the screen. EMI determines what items to put into the index by how the items respond to creative tabs, by default. This means the ideal way to add variants to the index is to simply configure how your item is added to the creative menu.
However, of course, this is not always adequate. EMI will add items that don't add themselves to the creative inventory for completeness sake if a mod does not provide tabs, and you may want to remove this, or adding your variants to the creative inventory may be impractical or simply not desired. The EmiRegistry
passed to your EmiPlugin
has addEmiStack
for adding single stacks, addEmiStackAfter
for adding stacks at some point and removeEmiStacks
for removing certain stacks or stacks that match a predicate. The javadocs should describe how to use each of these methods. Alternatively, there is an example below.
Do note that calls to removeEmiStacks
are deferred, and are called after all stacks are added to EMI's index.
public class MyModEmiPlugin implements EmiPlugin {
@Override
public void register(EmiRegistry registry) {
// Sets a comparison to match with nbt
EmiStack normal = EmiStack.of(MyMod.MY_ITEM).comparison(Comparison.compareNbt());
// Adds an item with special nbt to the index
registry.addEmiStackAfter(EmiStack.of(MyItem.createSpecialStack()), normal);
// Removes the version of the item with no nbt
// Calling this after adding is not necessary, since remove calls are deferred anyway, but it is good for conveying purpose
registry.removeEmiStacks(normal);
// Removes every EmiStack that is not an item
// You really don't want to do this, but it's an example of what is possible
registry.removeEmiStacks(s -> s.getItemStack().isEmpty());
}
}
Adding recipes is likely the majority of the work a developer will do with EMI.
The first step to adding a new recipe is adding a new EmiRecipeCategory
. Recipe categories are the way EMI organizes recipes in the recipe screen, and how processing steps are abbreviated in the crafting tree.
Creating an EmiRecipeCategory is pretty simple. In your EmiPlugin
implementation you simply need to call addCategory
on register, and addWorkstation
for all of the workstations your mod adds. Workstations are typically the blocks that a recipe takes place in, like a crafting table for crafting recipes, or a furnace for furnace recipes. When constructing an EmiRecipeCategory, it needs two EmiRenderable
s for the rendering first in the tab of the recipe screen, and second in the recipe tree. EmiIngredient
, EmiStack
, and EmiTexture
all implement EmiRenderable
, as shown in the example, you do not need to use your own draw calls. Icons in the recipe tree are typically composed of only pure white and transparency, you can use your own discretion to decide if this is possible for your category.
public class MyModEmiPlugin implements EmiPlugin {
public static final Identifier MY_SPRITE_SHEET = new Identifier("mymod", "textures/gui/emi_simplified_textures.png");
public static final EmiStack MY_WORKSTATION = EmiStack.of(MyModItems.MY_WORKSTATION);
public static final EmiRecipeCategory MY_CATEGORY
= new EmiRecipeCategory(new Identifier("mymod", "my_workstation"), MY_WORKSTATION, new EmiTexture(MY_SPRITE_SHEET, 0, 0, 16, 16));
@Override
public void register(EmiRegistry registry) {
// Tell EMI to add a tab for your category
registry.addCategory(MY_CATEGORY);
// Add all the workstations your category uses
registry.addWorkstation(MY_CATEGORY, MY_WORKSTATION);
}
}
Once you have an EmiRecipeCategory
, you can implement EmiRecipe
. For this example, we will be using a recipe type that has a single input and a single output. Do note that inputs are a list of EmiIngredient
s while outputs must be a list of EmiStack
s. Inputs and outputs should represent a single transaction with your recipe accurately, including amounts and remainders. As mentioned in the example, it is very important to call recipeContext
on the "output" slot of your recipes to make sure recipes can be favorited with stacks, and recipes can be used as temporary resolutions in the recipe tree.
If your recipe is not consistent in its output (being a pattern of recipes instead of a single recipe, such as armor dying), you should override EmiRecipe.supportsRecipeTree
to return false
. This will tell EMI to not use your recipe in the recipe tree.
If your recipe has some requirements that are not part of the workstation, but shouldn't be considered part of the cost breakdown (for example, the mana pool catalysts in Botania) you can implement EmiRecipe.getCatalysts()
. This will make looking up those items find your recipe.
The following is the verbose implementation of an EmiRecipe
.
A shorter version for simpler recipes like this is below.
public class MyRecipeEmiRecipe implements EmiRecipe {
private final Identifier id;
private final List<EmiIngredient> input;
private final List<EmiStack> output;
public MyRecipeEmiRecipe(MyRecipe recipe) {
this.id = recipe.getId();
this.input = List.of(EmiIngredient.of(recipe.getIngredients().get(0)));
this.output = List.of(EmiStack.of(recipe.getOutput()));
}
@Override
public EmiRecipeCategory getCategory() {
return MyModEmiPlugin.MY_CATEGORY;
}
@Override
public Identifier getId() {
return id;
}
@Override
public List<EmiIngredient> getInputs() {
return input;
}
@Override
public List<EmiStack> getOutputs() {
return output;
}
@Override
public int getDisplayWidth() {
return 76;
}
@Override
public int getDisplayHeight {
return 18;
}
@Override
public int addWidgets(WidgetHolder widgets) {
// Add an arrow texture to indicate processing
widgets.addTexture(EmiTexture.EMPTY_ARROW, 26, 1);
// Adds an input slot on the left
widgets.addSlot(input.get(0), 0, 0);
// Adds an output slot on the right
// Note that output slots need to call `recipeContext` to inform EMI about their recipe context
// This includes being able to resolve recipe trees, favorite stacks with recipe context, and more
widgets.addSlot(output.get(0), 58, 0).recipeContext(this);
}
}
You can alternatively extend BasicEmiRecipe
to have most of the methods handled for you for simpler cases, like the following
public class MyRecipeEmiRecipe extends BasicEmiRecipe {
public MyRecipeEmiRecipe(MyRecipe recipe) {
super(MyModEmiPlugin.MY_CATEGORY, recipe.getId(), 70, 18);
this.inputs.add(EmiIngredient.of(recipe.getIngredients().get(0)));
this.outputs.add(EmiStack.of(recipe.getOutput()));
}
@Override
public int addWidgets(WidgetHolder widgets) {
// Add an arrow texture to indicate processing
widgets.addTexture(EmiTexture.EMPTY_ARROW, 26, 1);
// Adds an input slot on the left
widgets.addSlot(inputs.get(0), 0, 0);
// Adds an output slot on the right
// Note that output slots need to call `recipeContext` to inform EMI about their recipe context
// This includes being able to resolve recipe trees, favorite stacks with recipe context, and more
widgets.addSlot(outputs.get(0), 58, 0).recipeContext(this);
}
}
Finally, with your class created, you can tell EMI about your recipes.
public class MyModEmiPlugin implements EmiPlugin {
public static final Identifier MY_SPRITE_SHEET = new Identifier("mymod", "textures/gui/emi_simplified_textures.png");
public static final EmiStack MY_WORKSTATION = EmiStack.of(MyModItems.MY_WORKSTATION);
public static final EmiRecipeCategory MY_CATEGORY
= new EmiRecipeCategory(MY_WORKSTATION, new EmiTexture(MY_SPRITE_SHEET, 0, 0, 16, 16));
@Override
public void register(EmiRegistry registry) {
// Tell EMI to add a tab for your category
registry.addCategory(MY_CATEGORY);
// Add all the workstations your category uses
registry.addWorkstation(MY_CATEGORY, MY_WORKSTATION);
RecipeManager manager = registry.getRecipeManager();
// Use vanilla's concept of your recipes and pass them to your EmiRecipe representation
for (MyRecipe recipe : manager.listAllOfType(MyRecipeTypes.MY_RECIPE)) {
registry.addRecipe(new MyRecipeEmiRecipe(recipe));
}
}
}
If you're adding an EmiRecipe
that doesn't represent a vanilla Recipe
, you won't have an Identifier
to use for your recipe ID. Examples in vanilla of this includes brewing recipes, log stripping, or bucket filling. In this case, you simply need to create a unique Identifier
for your recipe. EMI recommends that you do this in the form of [modid]:/[unique id]
. Note the /
at the start of the path, this is important, and means your synthetic id will never conflict with any real datapack files. If you have a recipe type and an output resource, you might use a format like [modid]:/[recipe_type]/[output_id]
. For instance, if you add a way to create resources with magic, the recipe ID for creating diamonds might be something like mymod:/magic_conjuring/minecraft/diamond
. EmiRecipe.getId
is nullable, if this isn't practical or desirable, but returning null makes recipes unable to be serialized, and as a consequence cannot be assigned as recipe defaults, or be saved along side stacks when favoriting.
This guide should cover the high frequency things a mod would want to do with EMI, and should be a good starting point. Of course, EMI provides much more than just what's on this page. Good classes to look at are EmiRegistry
for the things you can register and modify on reload, EmiApi
for utilities you can access during runtime, and WidgetHolder
for the kinds of widgets EMI can automatically generate for you. As always, more detailed support can be found in my Discord Server.