How-to: Add Relics and Relic Affixes

This guide covers the full relic pipeline: items, types, rarities, stats, and affixes. For a minimal end-to-end walkthrough, see Tutorial 3.


1. Register a RelicItem

Every relic needs an Item registered with Forge. RelicItem is the minimal subclass:

public static final RegistryObject<Item> RING_ITEM =
    ITEMS.register("ring", RelicItem::new);

If you need custom behavior (tooltips, durability, etc.) extend RelicItem directly.


2. Create a RelicType

RelicType links an Item to the relic system and sets the equipped limit.

JSON (data/my_mod/library_of_exile_relic_type/ring.json):

{
  "id": "my_mod:ring",
  "item_id": "my_mod:ring",
  "weight": 1000,
  "max_equipped": 2
}

Hardcoded alternative:

public class MyRelicTypeHolder extends ExileKeyHolder<RelicType> {
    public MyRelicTypeHolder(String modid) {
        super(modid, LibDatabase.RELIC_TYPE);
    }

    public ExileKey<RelicType, KeyInfo> RING = ExileKey.ofId(
        this, "my_mod:ring",
        x -> {
            var rt = new RelicType();
            rt.id = x.GUID();
            rt.item_id = "my_mod:ring";
            rt.weight = 1000;
            rt.max_equipped = 2;
            return rt;
        }
    );
}

3. Define RelicStat entries

Each stat is a named numeric quantity that can be rolled onto a relic.

Generic stat (manual_stat)

{
  "id": "my_mod:attack_speed",
  "serializer": "manual_stat",
  "is_percent": true,
  "min": 0,
  "max": 100,
  "base": 0
}

Map content weight stat (content_weight)

Increases the spawn-weight of a specific map_content entry when this relic is equipped:

{
  "id": "my_mod:more_treasure_rooms",
  "serializer": "content_weight",
  "is_percent": false,
  "min": 0,
  "max": 200,
  "base": 0,
  "map_content_id": "library_of_exile:treasure_room"
}

Extra content stat (extra_content)

Adds additional map content rolls:

{
  "id": "my_mod:extra_shrines",
  "serializer": "extra_content",
  "is_percent": false,
  "min": 0,
  "max": 3,
  "base": 0,
  "map_content_id": "my_mod:shrine",
  "data": {
    "type": "ADDITION",
    "extra": 1
  }
}

type is either "ADDITION" or "MULTIPLY".


4. Define RelicAffix entries

A RelicAffix packages one or more stat rolls and is associated with a specific RelicType.

{
  "id": "my_mod:ring_attack_speed",
  "weight": 600,
  "relic_type": "my_mod:ring",
  "mods": [
    { "stat": "my_mod:attack_speed", "min": 5.0, "max": 30.0 },
    { "stat": "my_mod:life_regen",   "min": 2.0, "max": 10.0 }
  ]
}

Multiple mods in one affix are rolled together as a bundle — either all appear or none do.


5. Customize RelicRarity

The built-in rarities (Common through Mythic) are defined in LibRelicRarities. You can add your own rarity tier via JSON:

{
  "id": "my_mod:divine",
  "base_data": {
    "id": "my_mod:divine",
    "text_format": "AQUA",
    "tier": 7,
    "weight": 25
  },
  "affixes": 4,
  "min_affix_percent": 50,
  "max_affix_percent": 100
}

Place in data/my_mod/library_of_exile_relic_rarity/divine.json.


6. Read stats at runtime

// Obtain the rolled mods from your capability or item NBT (implementation-specific).
List<ExactRelicStat> mods = getRelicMods(itemStack);

RelicStatsContainer stats = RelicStatsContainer.calculate(mods);

// By GUID:
float atkSpeed = stats.get(Database.get(LibDatabase.RELIC_STAT, "my_mod:attack_speed"));

// By ExileKey reference (avoids string lookups):
float atkSpeed = stats.get(MY_HOLDER.ATTACK_SPEED);

Notes

  • RelicAffix entries reference a relic_type — an affix is only eligible to roll onto relics of that type.
  • Relic system stat lookup respects the base field: if no affix provides a stat, stats.get() returns base.
  • RelicType, RelicStat, RelicAffix, and RelicRarity all have SyncTime.ON_LOGIN — they are synced to clients automatically.