Skip to content

Getting Started

Setup

In your build.gradle:

repositories {
  maven {
    url "https://www.cursemaven.com"
    content {
      includeGroup "curse.maven"
    }
  }
}

dependencies {
  // Visit https://www.curseforge.com/minecraft/mc-mods/jade/files/all?filter-status=1&filter-game-version=2020709689%3A7499
  // to get the latest version's jade_id
  modImplementation "curse.maven:jade-324717:${jade_id}"
}

In your build.gradle:

repositories {
  maven {
    url "https://www.cursemaven.com"
    content {
      includeGroup "curse.maven"
    }
  }
}

dependencies {
  // Visit https://www.curseforge.com/minecraft/mc-mods/jade/files/all?filter-status=1&filter-game-version=2020709689%3A7498
  // to get the latest version's jade_id
  implementation fg.deobf("curse.maven:jade-324717:${jade_id}")
}

Fix mixin error

You might get an error like this:

[mixin/]: Mixing XxxMixin from jade.mixins.json into Xxx
[mixin/]: jade.mixins.json:XxxMixin: Class version 61 required is higher than the class version supported by the current version of Mixin (JAVA_16 supports class version 60)

You need to add this to your build.gradle:

minecraft {
  runs {
    client {
      property 'mixin.env.remapRefMap', 'true'
      property 'mixin.env.refMapRemappingFile', "${projectDir}/build/createSrgToMcp/output.srg"
    }
  }
}

Then re-run genIntellijRuns/genEclipseRuns and refresh gradle project. (Source)

Visit CurseMaven to find more information about how to set up your workspace.

Registering

package snownee.jade.test;

import snownee.jade.api.IWailaClientRegistration;
import snownee.jade.api.IWailaCommonRegistration;
import snownee.jade.api.IWailaPlugin;
import snownee.jade.api.WailaPlugin;

@WailaPlugin
public class ExamplePlugin implements IWailaPlugin {

  @Override
  public void register(IWailaCommonRegistration registration) {
    //TODO register data providers
  }

  @Override
  public void registerClient(IWailaClientRegistration registration) {
    //TODO register component providers, icon providers, callbacks, and config options here
  }
}

Note

For Fabric edition, you need to add entrypoints in your fabric.mod.json

{
  "entrypoints": {
    "jade": [
      "full.class.path.to.ExamplePlugin"
    ]
  }
}

Component Provider

Component providers can append information (texts or images) to the tooltip.

Let's create a simple block component provider that adds an extra line to all the furnaces:

package snownee.jade.test;

import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import snownee.jade.api.BlockAccessor;
import snownee.jade.api.IBlockComponentProvider;
import snownee.jade.api.ITooltip;
import snownee.jade.api.config.IPluginConfig;

public enum ExampleComponentProvider implements IBlockComponentProvider {
  INSTANCE;

  @Override
  public void appendTooltip(
    ITooltip tooltip,
    BlockAccessor accessor,
    IPluginConfig config
  ) {
    tooltip.append(Component.translatable("mymod.fuel"));
  }

  @Override
  public ResourceLocation getUid() {
    return ExamplePlugin.FURNACE_FUEL;
  }
}

Here you have the tooltip that you can do many various operations to the tooltip. You can take the tooltip as a list of Component. But here our elements are IElements, to support displaying images, not just texts. In this case we only added a single line.

You also have the accessor, which you can get access to the context. We will use it later.

Then register our ExampleComponentProvider:

@Override
public void registerClient(IWailaClientRegistration registration) {
  registration.registerBlockComponent(ExampleComponentProvider.INSTANCE, AbstractFurnaceBlock.class);
}

AbstractFurnaceBlock.class means we will append our text only when the block is extended from AbstractFurnaceBlock.

Once your component provider is registered, Jade will create a config option for the user to toggle the provider. So don't forget to add translation for your option.

Note

If you want to control the position your elements are inserted, you can override getDefaultPriority method in your component provider. Greater is lower. -5000 ~ 5000 is for normal providers and they will be folded in the Lite mode.

Now launch the game:

Congrats you have implemented your first Jade plugin!

Server Data Provider

IServerDataProvider can help you sync data that is not on client side. In this tutorial it is the remaining burn time of the furnace. It will sync to the client every 250 milliseconds.

This is a chart shows the basic lifecycle:

Now it's time to implement our IServerDataProvider:

package snownee.jade.test;

import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity;
import snownee.jade.api.BlockAccessor;
import snownee.jade.api.IBlockComponentProvider;
import snownee.jade.api.IServerDataProvider;
import snownee.jade.api.ITooltip;
import snownee.jade.api.config.IPluginConfig;

public enum ExampleComponentProvider implements
  IBlockComponentProvider, IServerDataProvider<BlockAccessor> {
  INSTANCE;

  @Override
  public void appendTooltip(
    ITooltip tooltip,
    BlockAccessor accessor,
    IPluginConfig config
  ) {
    if (accessor.getServerData().contains("Fuel")) {
      tooltip.append(
        Component.translatable(
          "mymod.fuel",
          accessor.getServerData().getInt("Fuel")
        )
      );
    }
  }

  @Override
  public void appendServerData(CompoundTag data, BlockAccessor accessor) {
    AbstractFurnaceBlockEntity furnace = (AbstractFurnaceBlockEntity) accessor.getBlockEntity();
    data.putInt("Fuel", furnace.litTime);
  }

  @Override
  public ResourceLocation getUid() {
    return ExamplePlugin.FURNACE_FUEL;
  }
}

Here we used Access Transformer or Access Wideners to get access to the protected field.

Register IServerDataProvider:

@Override
public void register(IWailaCommonRegistration registration) {
  registration.registerBlockDataProvider(ExampleComponentProvider.INSTANCE, AbstractFurnaceBlockEntity.class);
}

Don't forget to add translations:

{
  "config.jade.plugin_mymod.furnace_fuel": "Test",
  "mymod.fuel": "Fuel: %d ticks"
}

Great!

Showing an Item

Now let's show a clock as a small icon:

@Override
public void appendTooltip(ITooltip tooltip, BlockAccessor accessor, IPluginConfig config) {
  if (accessor.getServerData().contains("Fuel")) {
    IElementHelper elements = tooltip.getElementHelper();
    IElement icon = elements.item(new ItemStack(Items.CLOCK), 0.5f);
    tooltip.add(icon);
    tooltip.append(Component.translatable("mymod.fuel", accessor.getServerData().getInt("Fuel")));
  }
}

Result:

Hmmm, would be better if we do some fine-tuning:

@Override
public void appendTooltip(ITooltip tooltip, BlockAccessor accessor, IPluginConfig config) {
  if (accessor.getServerData().contains("Fuel")) {
    IElementHelper elements = tooltip.getElementHelper();
    IElement icon = elements.item(new ItemStack(Items.CLOCK), 0.5f).size(new Vec2(10, 10)).translate(new Vec2(0, -1));
    tooltip.add(icon);
    tooltip.append(Component.translatable("mymod.fuel", accessor.getServerData().getInt("Fuel")));
  }
}

Result:

Much better now!

Improving Accessibility

Jade has a feature that allows user to narrate the tooltip (press keypad 5 by default). Now the problem is we don't want our clock icon to be narrated. So we can clear the narratable message in this way:

@Override
public void appendTooltip(ITooltip tooltip, BlockAccessor accessor, IPluginConfig config) {
  if (accessor.getServerData().contains("Fuel")) {
    IElementHelper elements = tooltip.getElementHelper();
    IElement icon = elements.item(new ItemStack(Items.CLOCK), 0.5f).size(new Vec2(10, 10)).translate(new Vec2(0, -1));
    icon.message(null);
    tooltip.add(icon);
    tooltip.append(Component.translatable("mymod.fuel", accessor.getServerData().getInt("Fuel")));
  }
}