Skip to content

Getting Started Guide

Emily Rose Ploszaj edited this page Jun 17, 2023 · 12 revisions

Getting Started

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.

Workspace Setup

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
		// ...
	}
}

EMI Entrypoint

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.

Translating Tags

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.

Recipe Defaults

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.

Basic API Concepts

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 EmiRecipes that represent your Recipes. 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 ItemStacks, but no representation of other resources, like fluids. Forge has FluidStacks. 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 Blocks, Items, and ItemStacks to EMI's representation. On Fabric, you can use FabricEmiStack.of for FluidVariants, and ItemVariants. On Forge, you can use ForgeEmiStack.of for FluidStacks

The second concept EMI has relating to stacks is ingredients. An EmiIngredient represents a collection of EmiStacks, but also offers its own name, rendering, and amount. Notably, EmiStacks are EmiIngredients, 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.

Adding/Removing from the Index

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

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 EmiRenderables 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 EmiIngredients while outputs must be a list of EmiStacks. 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));
        }
    }
}

Non-Wrapper Recipes

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.

Closing Thoughts

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.