Animation System

Animate Minecraft blocks, items and entities with 3D Blockbench models and .erianim.json files. Opening lootbox, spinning machine, entity with idle animation — the same pipeline for everything.

fr.eri.eriapi.anim Forge 1.12.2 EriAPI 1.2.0+

Introduction

The 3D Block Animation module allows you to create Minecraft blocks that are rendered with 3D Blockbench models and animated using .erianim.json files.

Unlike standard Minecraft blocks which are simple cubes with textures on each face, an animated block uses a TileEntity (an entity stored in the world that holds data) and a TESR (TileEntity Special Renderer — a rendering system that draws the 3D model instead of the cube).

Key concepts for beginners

  • TileEntity — A Java object attached to a block in the world. It stores data (which model to display, which animation to play, etc.) and survives server restarts.
  • TESR (TileEntity Special Renderer) — The code that draws the 3D model every frame. It reads data from the TileEntity and applies animation transformations.
  • Blockbench — A free software (blockbench.net) for creating 3D models in the Minecraft format. You design your model with cubes, texture it, then export it as JSON.
  • .erianim.json file — An EriAPI JSON file that describes animations: which groups of cubes move, how they move, at what speed, with which effects.

The creation pipeline

Here are the steps of the process, from creation to in-game animation:

  1. Modeling — Create the 3D model in Blockbench. Organize cubes into groups (each group can be animated independently).
  2. Export — Export the model in "Java Block/Item" format (.json).
  3. Animation — Create the .erianim.json file that describes the animations (manually or with the EriAnim editor).
  4. Integration — Register the block in Java code with EriAnimBlock.create().
  5. Runtime — The TESR reads the model and the animation, and draws the animated block on screen.
Everything is automatic

When you use EriAnimBlock.create(), EriAPI automatically registers the TileEntity, the TESR, the blockstate, and the item rendering in hand. You don't need to touch any OpenGL code or create additional classes.

Quick Start

Here is how to create a complete animated block in 5 steps. At the end, you will have a block that plays an animation when right-clicked.

Step 1 — Create the model in Blockbench

Open Blockbench and create a new project of type "Java Block/Item". Design your model by organizing cubes into named groups.

For example, for a lootbox: a base group for the pedestal, a lid group for the cover, a lock group for the padlock. Each group can be animated separately.

Export via File > Export > Export Java Block/Item Model. The generated file is a .json that you will place in your mod.

Step 2 — Create the .erianim.json file

Create a JSON file that describes the animations of your block. Here is a minimal example with an "open" animation that rotates the lootbox lid:

JSON — my_lootbox.erianim.json
{
  "format_version": 1,
  "model": "mymod:block/my_lootbox",
  "animations": {
    "open": {
      "duration": 15,
      "endBehavior": "freeze",
      "tracks": {
        "lid": {
          "rotation": [
            { "tick": 0,  "value": [0, 0, 0],    "easing": "linear" },
            { "tick": 15, "value": [-110, 0, 0],  "easing": "ease_out_back" }
          ]
        },
        "lock": {
          "translation": [
            { "tick": 0,  "value": [0, 0, 0],  "easing": "linear" },
            { "tick": 8,  "value": [0, -3, 0],  "easing": "ease_out" }
          ],
          "visible": [
            { "tick": 8, "value": false }
          ]
        }
      },
      "events": [
        { "tick": 0,  "type": "sound", "value": "minecraft:block.chest.open", "volume": 1.0, "pitch": 1.2 },
        { "tick": 15, "type": "callback", "value": "open_complete" }
      ]
    }
  }
}

Step 3 — Place the files in the correct folders

File structure
src/main/resources/assets/mymod/
  models/block/my_lootbox.json              ← Blockbench model (export)
  animations/block/my_lootbox.erianim.json  ← Animation file
  textures/blocks/my_lootbox/               ← Model PNG textures
    base.png
    lid.png
    lock.png
  blockstates/my_lootbox.json               ← Blockstate (auto-generated)

Step 4 — Register the block in Java

Java — Register the animated block
import fr.eri.eriapi.anim.AnimatedBlockTileEntity;
import fr.eri.eriapi.anim.EriAnimBlock;
import net.minecraft.block.material.Material;

public class MyBlocks {

    public static void init() {
        EriAnimBlock.create("mymod", "my_lootbox")
            .material(Material.IRON)
            .hardness(5.0f)
            .resistance(10.0f)
            .model("mymod:block/my_lootbox")
            .animation("mymod:block/my_lootbox")
            .renderDistance(64)
            .onRightClick(ctx -> {
                // Get the block's TileEntity
                AnimatedBlockTileEntity te = (AnimatedBlockTileEntity)
                    ctx.world.getTileEntity(ctx.pos);
                if (te != null) {
                    te.playAnimation("open");
                }
            })
            .register();
    }
}

Step 5 — Test in-game

Place the block in the world and right-click it. The lid opens with a chest sound, the padlock falls and disappears, and at the end of the animation the "open_complete" callback is triggered.

Reminder: ContentRegistry

Don't forget to call ContentRegistry.register(this) in your @Mod class before declaring your blocks. This is required for EriAPI to properly register blocks, TileEntities, and TESRs with Forge.

The TESR (AnimatedBlockTESR) is automatically registered by ContentRegistry for all blocks created via EriAnimBlock. No manual call to ClientRegistry.bindTileEntitySpecialRenderer() is needed.

EriAnimBlock — The Builder (recommended)

EriAnimBlock is the simplest way to create an animated block. It is a builder (a fluent API where you chain methods) that automatically configures everything needed: the TileEntity, the TESR, the blockstate, and the item rendering in hand.

EriAnimBlock
fr.eri.eriapi.anim.EriAnimBlock
Builder

Builder for animated blocks with 3D Blockbench models. Automatically configures TileEntity + TESR + blockstate + item renderer.

  • static EriAnimBlock create(String modId, String registryName)
    Creates a new builder. The modId is your mod's identifier (e.g., "mymod"). The registryName is the block's internal name (e.g., "lootbox").
    Static
  • EriAnimBlock material(Material mat)
    Sets the block's material (e.g., Material.IRON, Material.WOOD, Material.ROCK). Determines step sound, whether the block is flammable, etc.
    Setter
  • EriAnimBlock hardness(float hardness)
    Sets the block's hardness (time to break). Examples: stone = 1.5, iron = 5.0, obsidian = 50.0.
    Setter
  • EriAnimBlock resistance(float resistance)
    Sets the explosion resistance. Higher values make the block more resistant to TNT and creepers.
    Setter
  • EriAnimBlock harvestTool(String tool, int level)
    Sets the required tool and minimum harvest level to break the block. Tools: "pickaxe", "axe", "shovel". Levels: 0 = wood, 1 = stone, 2 = iron, 3 = diamond.
    Setter
  • EriAnimBlock soundType(SoundType sound)
    Sets the block's sound type (walking on it, breaking it, placing it). Examples: SoundType.METAL, SoundType.WOOD, SoundType.STONE.
    Setter
  • EriAnimBlock lightLevel(float light)
    Sets the light level emitted by the block (0.0 to 1.0). A torch = 0.9375, glowstone = 1.0.
    Setter
  • EriAnimBlock creativeTab(CreativeTabs tab)
    Sets the creative menu tab where the block appears.
    Setter
  • EriAnimBlock drops(Item item, int min, int max)
    Sets what the block drops when broken. min and max define the random quantity range.
    Setter
  • EriAnimBlock silkTouchable(boolean silk)
    If true, the block can be collected with Silk Touch.
    Setter
  • EriAnimBlock model(String modelId)
    Required. Sets the Blockbench model to use. Format: "modid:path/model" (without the .json extension). The file must be at assets/modid/models/path/model.json.
    Example: "mymod:block/lootbox"assets/mymod/models/block/lootbox.json
    Setter
  • EriAnimBlock animation(String animFileId)
    Required. Sets the .erianim.json animation file. Format: "modid:path/anim" (without the .erianim.json extension). The file must be at assets/modid/animations/path/anim.erianim.json.
    Example: "mymod:animations/lootbox"assets/mymod/animations/animations/lootbox.erianim.json
    Setter
  • EriAnimBlock renderDistance(double distance)
    Sets the maximum distance (in blocks) at which the 3D model is rendered. Default: 64 blocks. Beyond that, the block is invisible.
    Setter
  • EriAnimBlock defaultAnimation(String animName)
    Sets an animation that plays automatically when the block is loaded (with LOOP EndBehavior). Ideal for idle/breathing. Leave empty for no automatic animation.
    Setter
  • EriAnimBlock tileEntity(Class<?> tileClass)
    Replaces the automatic TileEntity with a custom class. The class must extend AnimatedBlockTileEntity and have a no-argument constructor. Use this when you need custom callbacks (see Callbacks section).
    Setter
  • EriAnimBlock onRightClick(Consumer<BlockActionContext> handler)
    Code executed when a player right-clicks the block. The BlockActionContext contains world, pos, player, hand, facing.
    Callback
  • EriAnimBlock onBreak(Consumer<BlockActionContext> handler)
    Code executed when the block is broken by a player.
    Callback
  • EriAnimBlock onPlace(Consumer<BlockActionContext> handler)
    Code executed when the block is placed by a player.
    Callback
  • EriAnimBlock javaAnimation(String name, EriAnimJava anim) v1.6.5
    Registers a Java animation directly on the builder — no need to subclass AnimatedBlockTileEntity just to call addJavaAnimation(). The instance is automatically registered on the TileEntity (server + client) at creation time. Coexists with .animation() for .erianim files. Can be called multiple times.
    Setter
  • EriAnimBlock register()
    Registers the block. Automatically configures: TileEntity, TESR, render type ENTITYBLOCK_ANIMATED, item renderer, and opaque/fullCube = false. If no custom TileEntity is specified, uses AnimatedBlockTileEntityGeneric.
    Final
Java — Full example with all options
EriAnimBlock.create("mymod", "refining_machine")
    .material(Material.IRON)
    .hardness(5.0f)
    .resistance(10.0f)
    .harvestTool("pickaxe", 2)
    .soundType(SoundType.METAL)
    .lightLevel(0.5f)
    .model("mymod:block/refining_machine")
    // Java anims declared directly on the builder (v1.6.5)
    .javaAnimation("idle", new IdleAnim())
    .javaAnimation("work", new WorkAnim())
    .defaultAnimation("idle")              // auto-play on spawn
    .renderDistance(48)
    .onRightClick(ctx -> {
        AnimatedBlockTileEntity te = (AnimatedBlockTileEntity)
            ctx.world.getTileEntity(ctx.pos);
        if (te != null) te.playAnimation("work");
    })
    .onBreak(ctx -> {
        // Drop items when the block is broken
        dropItems(ctx.world, ctx.pos);
    })
    .register();
When to keep .tileEntity(MyTE.class)? The .javaAnimation() builder covers 99% of cases. You only need a custom TileEntity class if you want to override animation callbacks (onAnimationComplete, onAnimationEvent, onAnimationCallback) or add custom fields / NBT to the TE.

Java Animation API v1.6.4

Since v1.6.4, EriAPI lets you write animations directly in Java, bypassing the .erianim file pipeline. This is useful for procedural animations (sin/cos oscillations, reactions to entity state, parametric movements, etc.) that would be tedious or impossible to express in JSON.

A Java animation is a class extending EriAnimJava with two extension points:

  • defineKeyframes(AnimContext ctx) — static keyframe declarations (equivalent to .erianim JSON). Called once and cached.
  • compute(AnimContext ctx, AnimationPose pose) — procedural modifications applied on top of the interpolated pose. Called every frame.

Java animations coexist with .erianim animations on the same TileEntity. The same te.playAnimation("name") API automatically dispatches to the right path based on the registered name.

EriAnimJava — abstract class

Create a class that extends EriAnimJava and implements at minimum duration():

Java — hybrid animation (keyframes + procedural)
import fr.eri.eriapi.anim.*;
import fr.eri.eriapi.gui.anim.Easing;

public class WalkAnim extends EriAnimJava {

    @Override
    public void defineKeyframes(AnimContext ctx) {
        // Equivalent to .erianim JSON, but in code
        ctx.at(0)
            .group("leg_left").rotateTo(-30, 0, 0).easing(Easing.EASE_IN_OUT)
            .group("leg_right").rotateTo(30, 0, 0).easing(Easing.EASE_IN_OUT);
        ctx.at(15)
            .group("leg_left").rotateTo(30, 0, 0)
            .group("leg_right").rotateTo(-30, 0, 0);
        ctx.at(30)
            .group("leg_left").rotateTo(-30, 0, 0)
            .group("leg_right").rotateTo(30, 0, 0);
    }

    @Override
    public void compute(AnimContext ctx, AnimationPose pose) {
        // Procedural: head bobbing on sin(t)
        float t = ctx.animTick + ctx.partial;
        pose.group("head").addRotateY((float) Math.sin(t * 0.2f) * 5f);
    }

    @Override public float duration() { return 30f; }
    @Override public EndBehavior endBehavior() { return EndBehavior.LOOP; }
}

Overridable methods

MethodReturnDescription
defineKeyframes(AnimContext ctx)voidDeclarative. Optional. Cached (rebuilt on invalidateDef()).
compute(AnimContext ctx, AnimationPose pose)voidProcedural. Optional. Called every frame.
duration()floatRequired. Total duration in ticks.
endBehavior()EndBehaviorEnd behavior (default FREEZE). See EndBehaviors.
invalidateDef()voidForces a rebuild of defineKeyframes() on the next frame.

Registration and playback

Java — register then play
// In the TileEntity constructor (or a shared server+client init)
public MyTileEntity() {
    addJavaAnimation("walk", new WalkAnim());
    addJavaAnimation("idle", new IdleAnim());
}

// Playback (server-side, like .erianim)
te.playAnimation("walk");
te.pauseAnimation();
te.resumeAnimation();
te.resetAnimationInstant();
Important: the Java registry must be populated identically on server and client. The standard pattern is to register inside the TileEntity constructor (which runs on both sides).

AnimContext

Context object passed to defineKeyframes() and compute(). Public mutable fields:

FieldTypeDescription
animTickfloatCurrent animation tick (with partial part). Available in compute().
partialfloatRaw partial ticks [0.0, 1.0) for fine interpolation.
worldTicklongWorld tick at render time.
targetObjectThe TileEntity or entity. Cast via the helpers below.

Methods

MethodReturnDescription
at(int tick)TickBuilderStarts a keyframe at this tick. Used in defineKeyframes().
asEntity()EntityLivingCast of target, or null if not an entity.
asTileEntity()TileEntityCast of target, or null if not a TE.

DSL at(tick).group(name).property(...)

The fluent defineKeyframes() API lets you chain keyframes and groups at a same tick:

Java — chained keyframes
ctx.at(0)
    .group("arm_left").rotateTo(0, 0, 0)
    .group("arm_right").rotateTo(0, 0, 0)
    .group("body").translateTo(0, 0, 0).easing(Easing.EASE_OUT)
.at(20)
    .group("arm_left").rotateTo(0, 45, 0)
    .group("arm_right").rotateTo(0, -45, 0).scaleTo(1.1f, 1.1f, 1.1f)
    .group("body").translateY(0.2f);

GroupKeyframeBuilder — methods

MethodDescription
rotateTo(x, y, z) / rotateX/Y/Z(v)Sets the target rotation (in degrees).
translateTo(x, y, z) / translateX/Y/Z(v)Sets the target translation.
scaleTo(x, y, z) / scale(s)Sets the scale (uniform or per-axis).
visible(boolean)Sets visibility at this tick (instantaneous).
easing(Easing)Easing used to reach this keyframe (default LINEAR).
group(name)Switches to another group at the same tick.
at(tick)Moves to a new tick.

GroupPoseBuilder — modify the pose in compute()

In compute(), use pose.group("name").property(...) to modify the interpolated pose. Two families of methods:

  • add* (additive) — adds to the value produced by keyframes. Typical for stacking procedural effects on top of a declarative animation.
  • set* / no prefix (replace) — overwrites the value produced by keyframes.

Example — procedural head oscillation

Java — sin/cos for bobbing and swaying
@Override
public void compute(AnimContext ctx, AnimationPose pose) {
    float t = ctx.animTick + ctx.partial;

    // Head: sway left/right
    pose.group("head")
        .addRotateY((float) Math.sin(t * 0.15f) * 8f)
        .addRotateZ((float) Math.cos(t * 0.10f) * 3f);

    // Body: small vertical breathing
    pose.group("body").addTranslateY((float) Math.sin(t * 0.05f) * 0.05f);

    // React to entity state (if target is an entity)
    if (ctx.asEntity() != null) {
        float yaw = ctx.asEntity().rotationYaw;
        pose.group("head").addRotateY(yaw * 0.1f);
    }
}

GroupPoseBuilder methods

CategoryMethods
Rotation (set)rotateX(v), rotateY(v), rotateZ(v), rotateTo(x,y,z)
Rotation (additive)addRotateX(v), addRotateY(v), addRotateZ(v)
Translation (set)translateX(v), translateY(v), translateZ(v), translateTo(x,y,z)
Translation (additive)addTranslateX(v), addTranslateY(v), addTranslateZ(v)
ScalescaleTo(x,y,z), scale(s)
Visibilityvisible(boolean)
Chaingroup(String) — switch to another group
When to use which? Use add* by default, layered on top of defineKeyframes(). Use set* only when you want to completely override the keyframe result (e.g. last-mile adjustments based on a runtime parameter).

Overlay — play an animation on top of another

The overlay slot lets you play a second Java animation on top of the main one. The overlay wins on every group it touches; groups it does not touch keep the main animation's pose.

The overlay is client-local — it is not synced via packets. Use it for reactive visual effects (flash, weapon recoil, temporary glow) triggered on the client only.

Java — trigger an overlay
// Start the overlay
te.playOverlay("flash", new FlashOverlayAnim());

// Stop it
te.stopOverlay();

// Check
boolean active = te.hasOverlay();
String name = te.getOverlayAnimName();

Overlay API

MethodReturnDescription
playOverlay(String, EriAnimJava)voidPlays a Java anim as overlay. Loops automatically.
stopOverlay()voidStops the overlay.
hasOverlay()booleantrue if an overlay is active.
getOverlayAnimName()StringOverlay name, or null.
addJavaAnimation(name, anim)voidRegisters a Java anim in the main registry.
isJavaAnimation(name)booleantrue if name is in the Java registry.
getJavaAnimation(name)EriAnimJavaRetrieves a registered anim.
Note: only the EndBehavior values FREEZE, WAIT_SERVER, LOOP, LOOP_COUNT, LOOP_PAUSE and instant reset are supported for Java anims. ROLLBACK is treated as an instant reset (no exposed rollbackDuration).

Alternative method: EriBlock + Custom TileEntity

If you need complex server-side logic in your animated block (inventory management, conditional interactions, animation chaining...), you can create a subclass of AnimatedBlockTileEntity and pass it to the builder via .tileEntity().

Java — Custom TileEntity with callbacks
import fr.eri.eriapi.anim.AnimatedBlockTileEntity;
import fr.eri.eriapi.anim.AnimationEvent;

public class TileLootbox extends AnimatedBlockTileEntity {

    private boolean isOpen = false;

    // No-argument constructor is REQUIRED
    public TileLootbox() {
        super("mymod:block/lootbox", "mymod:block/lootbox");
        //     ^--- modelId            ^--- animFileId
    }

    @Override
    protected void onAnimationCallback(String callbackName) {
        if ("open_complete".equals(callbackName)) {
            isOpen = true;
            // Give rewards to the player, etc.
        }
    }

    @Override
    protected void onAnimationComplete(String animName) {
        if ("close".equals(animName)) {
            isOpen = false;
        }
    }

    @Override
    protected void onAnimationEvent(AnimationEvent event) {
        if (event.getType() == AnimationEvent.EventType.PARTICLE) {
            // Spawn custom particles
            spawnParticles(event.getCount(), event.getSpread());
        }
    }

    public boolean isOpen() { return isOpen; }
}
Java — Register with the custom TE
EriAnimBlock.create("mymod", "lootbox")
    .material(Material.WOOD)
    .hardness(3.0f)
    .model("mymod:block/lootbox")
    .animation("mymod:block/lootbox")
    .tileEntity(TileLootbox.class)  // ← Our custom TE
    .onRightClick(ctx -> {
        TileLootbox te = (TileLootbox) ctx.world.getTileEntity(ctx.pos);
        if (te != null && !te.isOpen()) {
            te.playAnimation("open");
        }
    })
    .register();

Animated block — all approaches v1.6.5

Since v1.6.5, EriAnimBlock supports two kinds of animations: .animation() for .erianim JSON files (original approach) and .javaAnimation() for Java-coded animations. Both coexist and can be mixed on the same block.

Java — With a .erianim file (original approach)
// Requires assets/mymod/animations/my_machine.erianim.json
EriAnimBlock.create("mymod", "my_machine")
    .material(Material.IRON)
    .hardness(5.0f)
    .model("mymod:block/my_machine")
    .animation("mymod:animations/my_machine")  // ← .erianim file
    .defaultAnimation("idle")
    .onRightClick(ctx -> {
        AnimatedBlockTileEntity te = (AnimatedBlockTileEntity)
            ctx.world.getTileEntity(ctx.pos);
        if (te != null) te.playAnimation("work");
    })
    .register();
Java — With Java animations (no .erianim file needed)
EriAnimBlock.create("mymod", "my_machine")
    .material(Material.IRON)
    .hardness(5.0f)
    .model("mymod:block/my_machine")
    // Java anims directly on the builder — no custom TE needed
    .javaAnimation("idle", new IdleAnim())
    .javaAnimation("work", new WorkAnim())
    .defaultAnimation("idle")             // auto-play on spawn
    .onRightClick(ctx -> {
        AnimatedBlockTileEntity te = (AnimatedBlockTileEntity)
            ctx.world.getTileEntity(ctx.pos);
        if (te != null) te.playAnimation("work");
    })
    .register();
Java — Mix .erianim + Java on the same block
EriAnimBlock.create("mymod", "my_machine")
    .model("mymod:block/my_machine")
    .animation("mymod:animations/my_machine")  // .erianim animations
    .javaAnimation("glow", new GlowPulseAnim()) // Java animation on top
    .register();
When to keep .tileEntity(MyTE.class)? The .javaAnimation() builder is enough to declare/play animations. You only need a custom TE class to override animation callbacks (onAnimationComplete, onAnimationEvent, onAnimationCallback) or to add custom fields / NBT. In that case, just call addJavaAnimation() in your TE constructor.
When to keep .animation()? The .animation() call is still required if the block uses .erianim animations in addition to Java animations. It can be omitted when every animation is defined in Java.

AnimatedBlockTileEntity

This is the core of the animation system. Every animated block in the world has an AnimatedBlockTileEntity that stores the animation state (which animation is playing, at which point, paused or not, etc.).

All control methods (playAnimation, pauseAnimation, etc.) must be called server-side. The TileEntity automatically sends network packets to clients to synchronize the animation.

AnimatedBlockTileEntity
fr.eri.eriapi.anim.AnimatedBlockTileEntity
TileEntity

TileEntity for blocks rendered with Blockbench models and animated via .erianim files. Implements ITickable but returns immediately when the state is IDLE/FROZEN/PAUSED/WAITING_SERVER to avoid any overhead.

Constructors

  • AnimatedBlockTileEntity()
    Empty constructor. The modelId and animFileId will be configured by the builder or manually via setters.
    Constructor
  • AnimatedBlockTileEntity(String modelId)
    Constructor with the Blockbench model identifier. E.g., "mymod:block/lootbox".
    Constructor
  • AnimatedBlockTileEntity(String modelId, String animFileId)
    Full constructor. modelId = 3D model, animFileId = animation file.
    Constructor

Animation control (server only)

  • void playAnimation(String animName)
    Plays the animation by name. Uses the default EndBehavior defined in the .erianim.json file. Automatically sends a packet to all clients that can see the block.
    Side: Server only. Does nothing if called client-side.
    Server
  • void playAnimation(String animName, EndBehavior endBehaviorOverride)
    Plays the animation with a different EndBehavior than the one in the .erianim.json file. Pass null to use the file's default behavior.
    Side: Server only.
    Server
  • void pauseAnimation()
    Pauses the animation at the current tick. The block freezes at its current position. Only has an effect if the state is PLAYING.
    Side: Server only.
    Server
  • void resumeAnimation()
    Resumes a paused animation. The block continues where it left off. Only has an effect if the state is PAUSED.
    Side: Server only.
    Server
  • void resetAnimation()
    Resets the animation. If the current animation has a EndBehavior of type ROLLBACK and a rollbackDuration > 0, the block replays the animation in reverse (smooth return to the initial position). Otherwise, returns instantly to the starting position.
    Side: Server only.
    Server
  • void resetAnimation(boolean instant)
    Resets the animation. If instant is true, always returns immediately to the starting position (ignores rollback). If false, uses rollback if the EndBehavior is ROLLBACK.
    Side: Server only.
    Server
  • void stopAnimation()
    Immediately stops the animation. No rollback, no onAnimationComplete callback. The block transitions directly to the IDLE state. Use to abruptly cut an animation.
    Side: Server only.
    Server

State and progress

  • boolean isAnimating()
    Returns true if the block is currently playing an animation (PLAYING or ROLLING_BACK). Returns false for all other states (IDLE, FROZEN, PAUSED, WAITING_SERVER).
    Getter
  • AnimState getAnimState()
    Returns the current animation state. See the AnimStates section for the list of possible states.
    Getter
  • String getCurrentAnimation()
    Returns the name of the current animation, or an empty string "" if no animation is active.
    Getter
  • float getAnimationProgress()
    Returns the animation progress between 0.0 (start) and 1.0 (end). Returns 0.0 if no animation is active. Returns 1.0 if the animation is in FROZEN or WAITING_SERVER state.
    Side: Client and server.
    Getter
  • String getCurrentAnimName()
    Returns the internal name of the current animation. Identical to getCurrentAnimation().
    Getter

Dynamic textures

  • void setTexture(String target, String value)
    Replaces a model texture with another one in real-time. target is the original texture key (defined in the Blockbench model, e.g., "side_core"). value is the replacement texture key (e.g., "side_core_active"). The replacement texture must exist in the model's texture map. The change is automatically synchronized to clients.
    Key resolution: The target parameter is resolved by both the JSON key and the value from the Blockbench model. For example, if the model contains "beam": "rarity_beam", then both setTexture("beam", ...) and setTexture("rarity_beam", ...) work.
    Side: Server only.
    Server
  • void clearTexture(String target)
    Removes a specific texture override. The model reverts to the original texture for this key.
    Side: Server only.
    Server
  • void clearAllTextures()
    Removes all texture overrides. The model reverts to all its original textures.
    Side: Server only.
    Server
  • Map<String, String> getTextureOverrides()
    Returns the map of current texture overrides (target → value). Read-only.
    Getter

Configuration

  • void setAnimFileId(String animFileId)
    Changes the animation file used by this TileEntity. Useful for loading different animations on the same model at runtime.
    Setter
  • String getAnimFileId()
    Returns the current animation file identifier.
    Getter
  • void setModelId(String modelId)
    Changes the 3D model used by this TileEntity.
    Setter
  • String getModelId()
    Returns the current 3D model identifier.
    Getter
  • void setMaxRenderDistance(double distance)
    Changes the maximum render distance (in blocks).
    Setter

TileEntity Callbacks

If you create a subclass of AnimatedBlockTileEntity, you can override 3 callback methods to react to animation events. These callbacks are called server-side only.

Overridable callbacks
In your AnimatedBlockTileEntity subclass
Protected
  • void onAnimationEvent(AnimationEvent event)
    Called every time an event from the .erianim.json file is triggered (sound, particle, callback, hide/show group). The event parameter contains the type (event.getType()), the value (event.getValue()), and type-specific properties (volume, pitch, count, spread).
    Server
  • void onAnimationComplete(String animName)
    Called when an animation finishes and its EndBehavior is resolved. For example, called after a freeze, after a rollback completes, or when a LOOP_COUNT reaches its last iteration.
    Server
  • void onAnimationCallback(String callbackName)
    Called when a "callback" type event fires in the .erianim.json file. The callbackName corresponds to the "value" field of the event. This is the primary way to synchronize server logic with the animation.
    Server
Java — Callback examples
public class TileMachine extends AnimatedBlockTileEntity {

    public TileMachine() {
        super("mymod:block/machine", "mymod:block/machine");
    }

    @Override
    protected void onAnimationCallback(String callbackName) {
        switch (callbackName) {
            case "produce_item":
                // The animation reached the point where the item is created
                produceOutput();
                break;
            case "consume_fuel":
                // The animation reached the point where fuel is consumed
                consumeFuel();
                break;
        }
    }

    @Override
    protected void onAnimationComplete(String animName) {
        if ("process".equals(animName)) {
            // The processing animation is complete
            // Restart if there is still fuel
            if (hasFuel()) {
                playAnimation("process");
            }
        }
    }

    @Override
    protected void onAnimationEvent(AnimationEvent event) {
        if (event.getType() == AnimationEvent.EventType.PARTICLE) {
            // Spawn smoke particles server-side
            // Particles will be visible to all nearby players
            if (world instanceof WorldServer) {
                ((WorldServer) world).spawnParticle(
                    EnumParticleTypes.SMOKE_NORMAL,
                    pos.getX() + 0.5, pos.getY() + 1.0, pos.getZ() + 0.5,
                    event.getCount(), event.getSpread(), 0.1, event.getSpread(),
                    0.01);
            }
        }
    }
}

Triggering an animation

This section groups the most common practical cases for triggering an animation: on right-click, every tick, in response to a Forge event, from a command, or based on entity behavior (idle, walk, attack). All examples are server-side — EriAPI automatically syncs the animation state to clients.

Block — Right-click (open a lootbox)

Use the onRightClick callback from the EriAnimBlock builder. Always check isAnimating() to avoid restarting an animation that is already running. playAnimation() automatically sends the network packets to clients.

Java — EriAnimBlock with onRightClick callback
// EriAnimBlock with onRightClick callback
EriAnimBlock.create("mymod", "lootbox")
    .animation("mymod:block/lootbox")
    .onRightClick(ctx -> {
        AnimatedBlockTileEntity te = (AnimatedBlockTileEntity)
            ctx.world.getTileEntity(ctx.pos);
        if (te != null && !te.isAnimating()) {
            te.playAnimation("open");  // Triggers the "open" animation
        }
    })
    .register();

Block — Continuous tick (spinning machine)

For a machine that should animate in a loop while active, subclass AnimatedBlockTileEntity and override update(). The animation is restarted every time it ends. resetAnimation() returns the model to its neutral pose.

Java — Subclass with update()
// AnimatedBlockTileEntity subclass with ITickable
public class TileMachine extends AnimatedBlockTileEntity {

    private boolean running = false;

    @Override
    public void update() {
        super.update();
        if (!world.isRemote && running && !isAnimating()) {
            // Restart the animation in a loop
            playAnimation("spin");
        }
    }

    public void setRunning(boolean running) {
        this.running = running;
        if (!running) resetAnimation();
    }
}

Block — React to external event (explosion)

You can trigger an animation from any Forge event via EriEvents. Here, all animated blocks within a 5-block radius of an explosion play the "shake" animation.

Java — Triggering via EriEvents
// Via EriEvents - shake animated blocks near an explosion
EriEvents.on(ExplosionEvent.Detonate.class)
    .handle(e -> {
        BlockPos center = e.getExplosion().getPosition().toBlockPos();
        // Search our blocks within a 5-block radius
        for (BlockPos pos : BlockPos.getAllInBoxMutable(
                center.add(-5,-5,-5), center.add(5,5,5))) {
            TileEntity te = e.world.getTileEntity(pos);
            if (te instanceof AnimatedBlockTileEntity) {
                ((AnimatedBlockTileEntity) te).playAnimation("shake");
            }
        }
    });

Block — Trigger from a command

Useful for debugging or for admin commands. The EriCommand builder already provides the PosArg argument with auto-completion.

Java — Custom command
// Inside an EriCommand
EriCommand.create("machine")
    .sub("start")
        .arg(PosArg.of("pos"))
        .execute(ctx -> {
            BlockPos pos = ctx.getPos("pos");
            TileEntity te = ctx.getSender().world.getTileEntity(pos);
            if (te instanceof AnimatedBlockTileEntity) {
                ((AnimatedBlockTileEntity) te).playAnimation("run");
                ctx.reply("Animation started.");
            }
        })
    .register();

Entity — Idle animation (always active)

The "idle" animation runs continuously. Other animations (walk, attack) temporarily interrupt it. Remember to set endBehavior: "loop" in the .erianim.json.

Where to place this code

These examples go in your entity Java class extending EntityLiving (or EntityCreature, EntityMob, etc.) and implementing IAnimatedEntity. EriEntity is a configuration builder — it does not provide behavioral callbacks in the current version.

Java — Entity with idle animation
// In your entity Java class (extends EntityLiving implements IAnimatedEntity)
public class EntityMonster extends EntityLiving implements IAnimatedEntity {

    @Override
    public void onUpdate() {
        super.onUpdate();
        if (!world.isRemote) {
            // Start idle if no animation is playing
            if (dataManager.get(ANIM_NAME).isEmpty()) {
                playAnimation("idle");
            }
        }
    }
}

Entity — Walk animation (movement-based)

Derive the animation state from the entity's speed. The walk ↔ idle transition happens automatically once a speed threshold is crossed.

Java — Entity with walk animation
// In your entity Java class (extends EntityLiving implements IAnimatedEntity)
public class EntityMonster extends EntityLiving implements IAnimatedEntity {

    @Override
    public void onUpdate() {
        super.onUpdate();
        if (!world.isRemote) {
            double speed = Math.sqrt(motionX * motionX + motionZ * motionZ);
            String current = dataManager.get(ANIM_NAME);
            if (speed > 0.01 && !"walk".equals(current)) {
                playAnimation("walk");       // Start walking
            } else if (speed <= 0.01 && "walk".equals(current)) {
                playAnimation("idle");       // Back to idle
            }
        }
    }
}

Entity — Attack animation (on hit)

attackEntityAsMob is called server-side when the entity hits. Trigger the animation after confirming that the hit landed.

Java — Entity with attack animation
// In your entity Java class (extends EntityMob implements IAnimatedEntity)
public class EntityMonster extends EntityMob implements IAnimatedEntity {

    @Override
    public boolean attackEntityAsMob(Entity target) {
        boolean result = super.attackEntityAsMob(target);
        if (result && !world.isRemote) {
            playAnimation("attack");         // Play the attack animation
        }
        return result;
    }
}

Animated Item — Animation playback v1.3.2

Animation playback is supported

AnimatedItemController plays .erianim.json animations on animated items in real time. Playback state is shared per modelId: all items rendered with the same model share the same animation.

Java — Play an animation on an item
// Declare the item with an animated model
EriItem.create("mymod", "sword")
    .animatedModel("mymod:item/sword")
    .onRightClick(ctx -> {
        if (ctx.world.isRemote) {
            AnimatedItemController.play("mymod:item/sword", "use");
        }
    })
    .register();

// From anywhere on the client side
AnimatedItemController.play("mymod:item/sword", "mymod:item/sword_anims", "idle"); // explicit animFileId
AnimatedItemController.play("mymod:item/sword", "swing");  // animFileId = modelId
AnimatedItemController.stop("mymod:item/sword");
boolean playing = AnimatedItemController.isPlaying("mymod:item/sword");
animFileId format

animFileId follows the same format as blocks: "modid:path" resolves to assets/modid/animations/path.erianim.json.

.erianim.json File Format

The .erianim.json file is a JSON file that describes all animations for a block. A single file can contain multiple animations (idle, open, close, process...).

Root structure

JSON — Root structure
{
  "format_version": 1,
  "model": "mymod:block/my_machine",
  "animations": {
    "idle_breathe": { ... },
    "open": { ... },
    "close": { ... },
    "process": { ... }
  }
}
Field Type Description
format_version int Format version. Currently 1.
model string Associated Blockbench model identifier (e.g., "mymod:block/machine"). Informational.
animations object An object where each key is the animation name and the value is its definition.

Animation structure

JSON — Animation definition
{
  "duration": 40,
  "endBehavior": "freeze",
  "rollbackDuration": 10,
  "blendIn": 5,
  "blendOut": 0,
  "loopCount": 3,
  "loopPause": 20,
  "tracks": { ... },
  "events": [ ... ]
}
Field Type Default Description
duration int 20 Total duration in ticks (20 ticks = 1 second).
endBehavior string "freeze" What to do when the animation ends. See EndBehaviors.
rollbackDuration int 10 Return animation duration in ticks (for endBehavior: "rollback").
blendIn int 0 Blend-in duration (in ticks) from the previous pose. 0 = no blending.
blendOut int 0 Blend-out duration (in ticks). 0 = no blending.
loopCount int 0 Number of loops for endBehavior: "loop_count".
loopPause int 0 Pause in ticks between each loop for endBehavior: "loop_pause".
tracks object {} Animation tracks for each model group. Key = group name.
elementTracks v1.5.1 object {} Animation tracks targeting individual elements inside a group. Key = "groupName:elementIndex" (e.g. "body:2"). Same structure as a group track. See Element-level animation.
events array [] List of events triggered at specific ticks during the animation.

Track Types

Each group of your Blockbench model can have one or more tracks (animation channels). Each track contains a list of keyframes — points in time where you define a value. The system automatically interpolates between keyframes using the chosen easing function.

Here are the 7 available track types:

rotation
Pose — absolute orientation
Keyframe [x, y, z]

Defines the absolute rotation of the group in degrees (0-359) on each axis. This is the fixed orientation of the group at a given instant. Values are interpolated between keyframes.

JSON — Rotation track
"rotation": [
  { "tick": 0,  "value": [0, 0, 0],     "easing": "linear" },
  { "tick": 20, "value": [-90, 0, 0],   "easing": "ease_out_back" },
  { "tick": 40, "value": [-90, 45, 0],  "easing": "ease_in_out" }
]
rotate
Cumulative rotation — multi-turn
Keyframe [x, y, z]

Cumulative rotation in degrees since the beginning of the animation. Unlike rotation, values are not limited to 0-359: you can go beyond 360 to make multiple turns. For example, [0, 720, 0] = 2 full turns on the Y axis. Adds on top of the pose rotation (rotation).

JSON — Rotate track (2 full turns on Y)
"rotate": [
  { "tick": 0,  "value": [0, 0, 0],     "easing": "linear" },
  { "tick": 60, "value": [0, 720, 0],   "easing": "ease_in_out" }
]
translation
Position — displacement
Keyframe [x, y, z]

Moves the group in Blockbench pixels (1 Blockbench pixel = 1/16 of a Minecraft block). [0, 16, 0] = move 1 block upward. [0, -4, 0] = move 0.25 blocks downward.

JSON — Translation track (the group goes down then back up)
"translation": [
  { "tick": 0,  "value": [0, 0, 0],    "easing": "linear" },
  { "tick": 10, "value": [0, -3, 0],   "easing": "ease_out" },
  { "tick": 20, "value": [0, 0, 0],    "easing": "ease_in" }
]
scale
Scale — size
Keyframe [x, y, z]

Changes the size of the group on each axis. [1, 1, 1] = normal size. [2, 2, 2] = double size. [0, 0, 0] = invisible (crushed to zero).

JSON — Scale track (the group grows then returns)
"scale": [
  { "tick": 0,  "value": [1, 1, 1],      "easing": "linear" },
  { "tick": 10, "value": [1.5, 1.5, 1.5], "easing": "ease_out_back" },
  { "tick": 20, "value": [1, 1, 1],      "easing": "ease_in" }
]
visible
Visibility — show/hide
Instant

Shows or hides a group at a specific tick. No interpolation — it is an on/off switch. The value field is a boolean: true = visible, false = invisible.

JSON — Visible track (the padlock disappears at tick 8)
"visible": [
  { "tick": 0, "value": true },
  { "tick": 8, "value": false }
]
spin
Continuous rotation — degrees/tick
SpinKeyframe

Rotates the group continuously at a given speed (in degrees per tick). Unlike rotation which defines a fixed angle, spin defines a rotation speed that accumulates frame after frame. Ideal for gears, fans, etc.

You can change the spin speed during the animation. The easing controls the transition between the old and new speed.

JSON — Spin track (accelerates then decelerates)
"spin": [
  { "tick": 0,  "axis": "y", "speed": 0,   "easing": "linear" },
  { "tick": 10, "axis": "y", "speed": 18,  "easing": "ease_in" },
  { "tick": 30, "axis": "y", "speed": 18,  "easing": "linear" },
  { "tick": 40, "axis": "y", "speed": 0,   "easing": "ease_out" }
]
Field Type Description
tick int Tick when this speed takes effect.
axis string Rotation axis: "x", "y", or "z". Default: "y".
speed float Rotation speed in degrees per tick. 18 = 1 full turn in 20 ticks (1 second). 0 = stop.
easing string Easing function for the transition to this speed. Default: "linear".
texture
Texture change — instant swap
Instant

Changes the texture of a face or element of the model at a specific tick. No interpolation — the change is instant. Replacement textures must be declared in the Blockbench model's texture map.

JSON — Texture track (the screen turns on at tick 10)
"texture": [
  { "tick": 0,  "target": "screen", "value": "screen_off" },
  { "tick": 10, "target": "screen", "value": "screen_on" }
]
Field Type Description
tick int Tick when the texture change applies.
target string Texture key to replace (name in the model's texture map, e.g., "screen").
value string Replacement texture key (e.g., "screen_on").

Standard keyframe format

The rotation, rotate, translation, and scale tracks all use the same keyframe format:

Field Type Description
tick int Position in time (in ticks). 0 = start of the animation.
value float[3] Value [x, y, z]. Degrees for rotation/rotate, BB pixels for translation, factor for scale.
easing string Interpolation function. Default: "linear". See Easings.

Element-level animation v1.5.1

By default, an animation track applies to the entire Blockbench group: every cube attached to the group rotates / translates / scales together around the group's pivot. Since v1.5.1, you can also animate individual elements inside a group. Each element (Blockbench cube) owns a rotation.origin (pivot) defined in the model and becomes independently animatable.

When to use elementTracks?

  • A button, a trigger, a mechanical part inside a group that must move independently of other cubes in the same group.
  • Multiple elements of the same group that must rotate around different pivots (e.g. turbine blades sharing a "turbine" group).
  • Cases where you don't want (or can't) restructure the Blockbench model into sub-groups.

If each moving part already deserves its own Blockbench group, just use tracks — it's more readable and cheaper at runtime.

JSON format

The elementTracks field sits next to tracks in the animation definition. Keys follow the "groupName:elementIndex" format, where elementIndex is the zero-based position of the element in its Blockbench group's elements array. The value has the same structure as a group track (rotation, rotate, translation, scale, visible, spin — see Track Types).

JSON — element animation with endless rotation
{
  "formatVersion": 1,
  "modelId": "mymod:block/turret",
  "animations": {
    "idle": {
      "duration": 40,
      "endBehavior": "loop",
      "tracks": {
        "base": {
          "rotation": [
            { "tick": 0,  "value": [0, 0, 0] },
            { "tick": 40, "value": [0, 360, 0] }
          ]
        }
      },
      "elementTracks": {
        "body:2": {
          "translation": [
            { "tick": 0,  "value": [0, 0, 0] },
            { "tick": 20, "value": [0, 2, 0] },
            { "tick": 40, "value": [0, 0, 0] }
          ]
        }
      }
    }
  }
}
Fully backward compatible. If elementTracks is absent, behavior is identical to previous versions: only group tracks apply and no extra OpenGL matrix is pushed per element.

Java API

The entire animation stack (AnimationDef, AnimationPose, AnimationController, AnimatedBlockTESR, AnimatedEntityRenderer) supports element poses. You can read an element pose with AnimationPose.getElement(String groupName, int elementIndex).

Java — AnimationDef
// Check for element tracks before iterating
if (animDef.hasElementTracks()) {
    for (Map.Entry<String, GroupTrack> entry : animDef.getElementTracks().entrySet()) {
        // entry.getKey() = "body:2"
        // entry.getValue() = GroupTrack
    }
}

// Direct access via group + index
GroupTrack t = animDef.getElementTrack("body", 2);

// Canonical key (static helper)
String key = AnimationDef.elementTrackKey("body", 2); // -> "body:2"
Java — AnimationPose
AnimationPose pose = controller.getCurrentPose(partialTicks);

// Group pose
AnimationPose.GroupPose bodyPose = pose.getGroup("body");

// Element pose (null if no track exists for that element)
AnimationPose.GroupPose elemPose = pose.getElement("body", 2);
if (elemPose != null) {
    if (!elemPose.visible) return; // element is hidden
    float rotY = elemPose.rotation[1];
    // ...
}

Rendering — transform order

In AnimatedBlockTESR and AnimatedEntityRenderer, for each animated element, the OpenGL matrix is pushed around the element's rotationOrigin in this order:

  1. translate(rotationOrigin)
  2. Element's static JSON rotation (if rotation is set in the Blockbench model).
  3. Animated translation (elemPose.translation / 16, converted from Blockbench pixels).
  4. Animated rotations (rotation Z/Y/X then cumulativeRotation Y/X/Z — the spin part).
  5. Animated scale (elemPose.scale).
  6. translate(-rotationOrigin).
  7. Render the element's faces.

The element's static JSON rotation is preserved — the animation applies on top, in the local pivot space. If the element has neither an animated pose nor a static rotation, no extra matrix is pushed (optimization: the zero-cost path of previous versions).

Events in .erianim.json

Events are actions triggered at specific ticks during the animation. They are listed in the "events" array of each animation. Each event has a tick and a type.

The 6 event types

Type Description Additional fields
sound Plays a Minecraft sound at the block's position. The sound is audible to all nearby players. value (sound ResourceLocation), volume (default: 1.0), pitch (default: 1.0)
particle Triggers the onAnimationEvent() callback in the TileEntity. You handle particle spawning in your Java code. value (particle name), count (default: 1), spread (default: 0.5)
callback Calls onAnimationCallback(value) in the TileEntity. This is the primary way to synchronize server logic with the animation. value (callback name)
hide_group Hides a model group. The event is dispatched server-side for information. The renderer uses the visible track client-side. value (group name)
show_group Shows a model group. Same behavior as hide_group but in reverse. value (group name)
cut Immediately stops the animation and resets the block to the IDLE state. Equivalent to calling resetAnimationInstant(). value (ignored)
JSON — Event examples
"events": [
  {
    "tick": 0,
    "type": "sound",
    "value": "minecraft:block.anvil.use",
    "volume": 0.8,
    "pitch": 1.5
  },
  {
    "tick": 10,
    "type": "particle",
    "value": "smoke",
    "count": 5,
    "spread": 0.3
  },
  {
    "tick": 20,
    "type": "callback",
    "value": "halfway_done"
  },
  {
    "tick": 5,
    "type": "hide_group",
    "value": "lock"
  },
  {
    "tick": 30,
    "type": "show_group",
    "value": "reward"
  },
  {
    "tick": 40,
    "type": "cut"
  }
]

EndBehaviors

The EndBehavior determines what happens when an animation reaches its last tick. It is defined in the .erianim.json file ("endBehavior" field) or can be overridden in Java via te.playAnimation("open", EndBehavior.LOOP).

EndBehavior
fr.eri.eriapi.anim.EndBehavior
Enum
Value JSON Description Use case
FREEZE "freeze" Stays frozen on the last frame indefinitely. Open door, open chest, pulled lever.
ROLLBACK "rollback" Replays the animation in reverse over rollbackDuration ticks to return to frame 0. Button that springs back up, machine that gradually powers down.
ROLLBACK_INSTANT "rollback_instant" Returns instantly to frame 0 without a return animation. Flash animation, quick visual impact.
WAIT_SERVER "wait_server" Stays frozen on the last frame and waits for a server call (te.resetAnimation()) to return. Machine processing, chest awaiting confirmation. The server decides when to reset.
LOOP "loop" Infinite loop. The animation automatically restarts from the beginning at each end. Spinning gear, pulsing flame, looping idle.
LOOP_COUNT "loop_count" Loops N times (configured by loopCount) then freezes on the last frame. Charge animation (3 blinks), repeated light signal.
LOOP_PAUSE "loop_pause" Infinite loop with a pause of loopPause ticks between each cycle. Machine that processes in batches (cycle, pause, cycle, pause...).
Java — Override the EndBehavior in code
AnimatedBlockTileEntity te = (AnimatedBlockTileEntity) world.getTileEntity(pos);

// Use the default EndBehavior from the .erianim.json file
te.playAnimation("process");

// Force a different EndBehavior
te.playAnimation("process", EndBehavior.LOOP);
te.playAnimation("open", EndBehavior.WAIT_SERVER);
te.playAnimation("flash", EndBehavior.ROLLBACK_INSTANT);

// Pass null = use the file's default
te.playAnimation("idle", null);

Easings

Easing functions control how a value transitions from one keyframe to another. Instead of a linear (constant) movement, you can have acceleration, deceleration, bounce, etc. The easing is defined in each keyframe by the "easing" field.

Each function takes a normalized time t between 0 and 1 and returns an interpolated value. Some functions (ELASTIC, BOUNCE, BACK, SPRING) can return values outside 0-1, which produces overshoot effects.

Easing
fr.eri.eriapi.gui.anim.Easing
Enum — 13 functions
JSON Name Java Enum Description Typical usage
"linear" LINEAR Constant movement, no acceleration. f(t) = t Mechanical rotations, machine movements.
"ease_in" EASE_IN Starts slow, accelerates at the end. f(t) = t^2 Object starting to move, vehicle starting up.
"ease_out" EASE_OUT Starts fast, slows down at the end. f(t) = t(2-t) Door closing, object settling down.
"ease_in_out" EASE_IN_OUT Slow at start and end, fast in the middle. Natural movements, lids, levers.
"ease_in_cubic" EASE_IN_CUBIC Like ease_in but more pronounced. f(t) = t^3 Falls, strong accelerations.
"ease_out_cubic" EASE_OUT_CUBIC Like ease_out but more pronounced. Faster braking. Heavy object stopping, dampened bounce.
"bounce" BOUNCE Bounce on arrival, like a ball hitting the ground. Object falling and bouncing, button clicking.
"elastic" ELASTIC Elastic oscillation on arrival, like a spring. Overshoots the target then returns. Pop-up, element appearing with punch.
"ease_in_back" EASE_IN_BACK Pulls back slightly before moving forward (overshoot at entry). Strike preparation, wind-up before a jump.
"ease_out_back" EASE_OUT_BACK Overshoots the target then returns (overshoot at exit). Very satisfying "pop" effect. Chest lid opening with punch, item growing.
"ease_in_expo" EASE_IN_EXPO Exponential start — very slow then explosion. f(t) = 2^(10(t-1)) Energy charge, power build-up.
"ease_out_expo" EASE_OUT_EXPO Exponential deceleration — brakes much harder than cubic. f(t) = 1 - 2^(-10t) Emergency braking, abrupt stop.
"spring" SPRING Physical spring simulation — dampened oscillations. f(t) = 1 - cos(t*4.5*PI) * e^(-6t) Antennas, flexible elements, hit feedback.
Notation in JSON

In the .erianim.json file, the easing is written in lowercase with underscores or hyphens. Both notations are accepted: "ease_out_back" and "ease-out-back" are equivalent. Uppercase is also accepted. If the name is not recognized, LINEAR is used by default.

AnimStates

The TileEntity's animation state is accessible via te.getAnimState(). It allows you to know exactly what the block is doing at a given moment.

AnimState
fr.eri.eriapi.anim.AnimState
Enum
State Description Server tick?
IDLE No animation in progress. The block is at rest. The TESR displays the model's default pose. No
PLAYING The animation is currently playing. Events are triggered and tracks are interpolated. Yes
PAUSED The animation is paused at a specific tick. The block is frozen at the position where it was paused. No
FROZEN The animation is finished and frozen on the last frame (endBehavior = FREEZE or completed LOOP_COUNT). No
ROLLING_BACK The animation is playing in reverse, returning to frame 0 (endBehavior = ROLLBACK). Yes
WAITING_SERVER The animation is finished and waiting for a server signal (te.resetAnimation()) to return to IDLE. No
Java — Check the animation state
AnimatedBlockTileEntity te = (AnimatedBlockTileEntity) world.getTileEntity(pos);

// Check if an animation is in progress
if (te.isAnimating()) {
    // PLAYING or ROLLING_BACK
}

// Check a specific state
if (te.getAnimState() == AnimState.WAITING_SERVER) {
    // The animation is finished, the server must act
    te.resetAnimation();
}

// Check the progress
float progress = te.getAnimationProgress(); // 0.0 to 1.0
if (progress > 0.5f) {
    // The animation has passed the halfway point
}

Animated Items v1.3.1

Since version 1.3.1, you can use the .animatedModel() method on an EriItem to render the item with a 3D Blockbench model instead of the standard 2D texture. The item will be displayed in 3D in the player's hand and in the inventory.

Rendering is delegated to an internal TileEntityItemStackRenderer (AnimatedBlockItemRenderer) that uses the same rendering pipeline as AnimatedBlockTESR — groups, elements, faces, UV, textures.

Usage

Simply call .animatedModel("modid:item/name") in the builder chain. The Blockbench JSON model must be located at assets/{modid}/models/item/{name}.json.

Java — Item with 3D Blockbench model
EriItem.create("mymod", "fancy_sword")
    .maxStackSize(1)
    .rarity(EnumRarity.EPIC)
    .animatedModel("mymod:item/fancy_sword")  // 3D Blockbench model
    .register();
Textures are in the Blockbench JSON

The item's textures are defined directly in the Blockbench model JSON file, not in Java code. The renderer automatically reads the textures referenced by the model.

Model file location

The Blockbench JSON model must be placed at assets/{modid}/models/item/{name}.json. The ID passed to animatedModel() uses the format "modid:item/name".

Animated Entities v1.3.0

The animated entity system allows you to create custom Minecraft entities rendered with 3D Blockbench models and .erianim.json animations. It relies on three components: the EriEntity builder, the AnimatedEntityRenderer, and the IAnimatedEntity interface.

EriEntity — Fluent builder

EriEntity is the builder for defining a custom entity. It stores the configuration (model, textures, animations, hitbox, seats) in an EntityDefinition registered in the ContentRegistry.

Manual registration

Unlike EriItem and EriBlock, EriEntity does not handle automatic Forge registration. The mod must register EntityEntry entries itself in RegistryEvent.Register<EntityEntry> using ContentRegistry.getEntityDefs() to retrieve the definitions.

Builder methods

MethodDescription
create(String modId, String registryName)Creates a new entity builder. Static method.
model(String modelId)Sets the Blockbench model (e.g. "mymod:entity/monster").
texture(String key, String path)Maps a model texture key to a ResourceLocation path.
animation(String name, String animId)Maps an animation name (e.g. "idle", "walk") to an .erianim ID.
javaAnimation(String name, EriAnimJava anim) v1.6.5Registers a Java animation directly on the builder. Stored in EntityDefinition.javaAnimations, retrievable via ContentRegistry.getEntityDef(modId, name).getJavaAnimation("idle"). Coexists with .animation().
hitbox(float width, float height)Sets the hitbox dimensions.
seat(float x, float y, float z)Adds a simple passenger seat (Blockbench pixel coordinates).
seat(float x, float y, float z, String attachedGroup)Adds a seat attached to a model group.
seat(float x, float y, float z, float yawOffset, String attachedGroup, boolean lockCamera)Full seat: yaw rotation, attached group, camera lock.
spawnEgg(int primary, int secondary)Enables a spawn egg (colors in 0xRRGGBB).
creativeTab(String tabName)Creative tab for the spawn egg.
register()Registers the definition in the ContentRegistry.

IAnimatedEntity — Entity interface

The Java entity must implement the IAnimatedEntity interface to provide animation poses to the renderer. Two methods to implement:

MethodDescription
AnimationPose getCurrentPose(float partialTicks)Returns the current interpolated pose, or null if no animation is active (IDLE).
AnimState getAnimState()Returns the current animation state of the entity.

AnimatedEntityRenderer — Renderer

The entity renderer that displays Blockbench models with animations. The rendering pipeline is identical to AnimatedBlockTESR. Register via RenderingRegistry.registerEntityRenderingHandler().

Static methodDescription
factory(String modelId)Creates an IRenderFactory for the given model (default shadow: 0.5).
factory(String modelId, float shadowSize)Factory with custom shadow size.

EntitySeat — Seat system

Each entity can have one or more passenger seats. Coordinates are in Blockbench pixels (same coordinate space as the model).

FieldTypeDescription
x, y, zfloatSeat position in Blockbench pixels (local to entity).
yawOffsetfloatPassenger yaw rotation in degrees (default: 0).
attachedGroupStringModel group name the seat is attached to (may be null). The seat follows the group's animations.
lockCamerabooleanIf true, the seated player's camera is locked (default: false).

Full example

Java — Complete animated entity
// 1. Define the entity via EriEntity builder (in preInit)
EriEntity.create("mymod", "monster")
    .model("mymod:entity/monster")
    .texture("body", "mymod:textures/entity/monster/body")
    .animation("idle", "mymod:entity/monster_idle")
    .animation("walk", "mymod:entity/monster_walk")
    .hitbox(0.8f, 1.8f)
    .seat(0.0f, 24.0f, 0.0f)
    .register();

// 2. Entity class — animation state managed via DataParameters
public class EntityMonster extends EntityLiving implements IAnimatedEntity {

    // DataParameters for server -> client sync
    private static final DataParameter<String> ANIM_NAME =
        EntityDataManager.createKey(EntityMonster.class, DataSerializers.STRING);
    private static final DataParameter<Integer> ANIM_TICK =
        EntityDataManager.createKey(EntityMonster.class, DataSerializers.VARINT);

    private final AnimationController controller = new AnimationController();

    public EntityMonster(World world) {
        super(world);
        setSize(0.8f, 1.8f);
    }

    @Override
    protected void entityInit() {
        super.entityInit();
        dataManager.register(ANIM_NAME, "");
        dataManager.register(ANIM_TICK, 0);
    }

    // Call server-side to start an animation
    public void playAnimation(String name) {
        dataManager.set(ANIM_NAME, name);
        dataManager.set(ANIM_TICK, 0);
    }

    public void stopAnimation() {
        dataManager.set(ANIM_NAME, "");
    }

    @Override
    public void onUpdate() {
        super.onUpdate();
        if (!world.isRemote) {
            String name = dataManager.get(ANIM_NAME);
            if (!name.isEmpty()) {
                dataManager.set(ANIM_TICK, dataManager.get(ANIM_TICK) + 1);
            }
        }
    }

    // --- IAnimatedEntity ---

    @Override
    public AnimationPose getCurrentPose(float partialTicks) {
        String animName = dataManager.get(ANIM_NAME);
        if (animName.isEmpty()) return null;

        // Load animation file from cache
        AnimationFile animFile = EriAnimCache.get("mymod:entity/monster_" + animName);
        if (animFile == null) return null;

        AnimationDef animDef = animFile.getAnimations().values().iterator().next();
        if (animDef == null) return null;

        float tickFloat = dataManager.get(ANIM_TICK) + partialTicks;
        return controller.computePose(
            animDef,
            AnimState.PLAYING,
            tickFloat,
            animDef.getDuration(),
            10,    // rollbackDuration (unused here)
            null,  // blendPose
            1.0f   // blendProgress
        );
    }

    @Override
    public AnimState getAnimState() {
        return dataManager.get(ANIM_NAME).isEmpty() ? AnimState.IDLE : AnimState.PLAYING;
    }
}

// 3. Register the renderer (ClientProxy.preInit)
RenderingRegistry.registerEntityRenderingHandler(
    EntityMonster.class,
    AnimatedEntityRenderer.factory("mymod:entity/monster")
);

// 4. Register with Forge (RegistryEvent.Register<EntityEntry>)
for (EntityDefinition def : ContentRegistry.getEntityDefs()) {
    // Register EntityEntry with EntityRegistry.registerModEntity(...)
    // Attach AnimatedEntityRenderer via RenderingRegistry (client only)
}

Entity animations — all approaches v1.6.5

Since v1.6.5, EriEntity supports two kinds of animations on the builder: .animation(name, animId) for .erianim JSON files (original approach) and .javaAnimation(name, anim) for Java-coded animations. Both coexist and can be mixed on the same entity.

Java — With .erianim files (original approach)
// Requires .erianim.json files in assets/
EriEntity.create("mymod", "monster")
    .model("mymod:entity/monster")
    .texture("body", "mymod:textures/entity/monster")
    .hitbox(0.8f, 1.8f)
    .animation("idle", "mymod:entity/monster_idle")   // ← .erianim file
    .animation("walk", "mymod:entity/monster_walk")   // ← .erianim file
    .register();
Java — With Java animations v1.6.5
// Java anims declared directly on the builder (since v1.6.5)
EriEntity.create("mymod", "monster")
    .model("mymod:entity/monster")
    .texture("body", "mymod:textures/entity/monster")
    .hitbox(0.8f, 1.8f)
    .javaAnimation("idle", new IdleAnim())
    .javaAnimation("walk", new WalkAnim())
    .register();
Java — Mix .erianim + Java on the same entity
// Mix: .erianim for long/complex animations + Java for procedural effects
EriEntity.create("mymod", "monster")
    .model("mymod:entity/monster")
    .animation("walk", "mymod:entity/monster_walk")   // .erianim
    .javaAnimation("headBob", new HeadBobAnim())       // Java
    .register();
Java — Entity class: retrieve anims via ContentRegistry
import fr.eri.eriapi.anim.AnimationController;
import fr.eri.eriapi.anim.AnimationPose;
import fr.eri.eriapi.anim.AnimState;
import fr.eri.eriapi.anim.EndBehavior;
import fr.eri.eriapi.anim.EriAnimJava;
import fr.eri.eriapi.anim.IAnimatedEntity;
import fr.eri.eriapi.content.ContentRegistry;
import fr.eri.eriapi.content.EntityDefinition;

public class EntityMonster extends EntityLiving implements IAnimatedEntity {

    private final AnimationController controller = new AnimationController();
    private EriAnimJava currentAnim;
    private float animTick = 0;

    public EntityMonster(World world) {
        super(world);
        setSize(0.8f, 1.8f);
        // Anims retrieved from the EntityDefinition created by EriEntity
        EntityDefinition def = ContentRegistry.getEntityDef("mymod", "monster");
        this.currentAnim = def != null ? def.getJavaAnimation("idle") : null;
    }

    @Override
    public void onUpdate() {
        super.onUpdate();
        if (currentAnim != null) {
            animTick += 1.0f;
            float dur = currentAnim.duration();
            if (currentAnim.endBehavior() == EndBehavior.LOOP && animTick >= dur) {
                animTick -= dur;   // loop
            }
        }
    }

    /** Switch animation by name (resolved from the EntityDefinition). */
    public void setAnimation(String name) {
        EntityDefinition def = ContentRegistry.getEntityDef("mymod", "monster");
        EriAnimJava anim = def != null ? def.getJavaAnimation(name) : null;
        if (anim != null) {
            this.currentAnim = anim;
            this.animTick = 0;
        }
    }

    // --- IAnimatedEntity ---

    @Override
    public AnimationPose getCurrentPose(float partialTicks) {
        if (currentAnim == null) return null;
        long worldTick = world != null ? world.getTotalWorldTime() : 0;
        return controller.computeJavaPose(
            currentAnim,             // current EriAnimJava
            AnimState.PLAYING,
            animTick + partialTicks, // interpolated tickFloat
            partialTicks,
            worldTick,
            this,                    // target accessible via ctx.asEntity()
            null,                    // blendPose
            1.0f                     // blendProgress
        );
    }

    @Override
    public AnimState getAnimState() {
        return currentAnim != null ? AnimState.PLAYING : AnimState.IDLE;
    }
}
Why .javaAnimation() instead of .animation()? .animation() maps a name to a .erianim file (resource pack, disk loading). .javaAnimation() stores an EriAnimJava instance directly in the EntityDefinition, retrievable via ContentRegistry.getEntityDef(modId, name).getJavaAnimation("idle") — no need to instantiate anims inside the entity class.
Multiplayer synchronization: in this example, currentAnim and animTick are not synchronized server→client. For an entity visible to other players, you must expose an animation identifier (for example an int) through a DataParameter and rebuild currentAnim client-side accordingly. See the DataParameters pattern in the full example above.
Supported EndBehavior: Java animations support FREEZE, WAIT_SERVER, LOOP, LOOP_COUNT and LOOP_PAUSE. ROLLBACK is treated as an instant reset. The example above handles looping manually inside onUpdate() so it can use a local tick without any network sync.
Manual animation state management

Unlike animated blocks which have a server-side AnimatedBlockTileEntity, entities manage their animation state manually via Forge DataParameters. Only the animation name and current tick are synchronized server→client. Pose calculation (AnimationController) is purely client-side, executed each frame.

ContentRegistry.getEntityDefs()

Use ContentRegistry.getEntityDefs() in your RegistryEvent.Register<EntityEntry> handler to retrieve all entity definitions registered via EriEntity. Each EntityDefinition contains the modId, registryName, modelId, textures, animations, hitbox and seats.

Forge Events

The animation module publishes 4 Forge events on the MinecraftForge.EVENT_BUS. They are all cancelable (@Cancelable) and fired server-side only. You can listen to them with the EriAPI event system or with @SubscribeEvent.

AnimationStartEvent
fr.eri.eriapi.anim.event.AnimationStartEvent
@Cancelable

Fired before an animation starts. If canceled, the animation is not played.

  • World getWorld()
    The world where the block is located.
    Getter
  • BlockPos getPos()
    The block's position.
    Getter
  • AnimatedBlockTileEntity getTileEntity()
    The animated block's TileEntity.
    Getter
  • String getAnimName()
    The name of the animation that is about to play.
    Getter
  • EndBehavior getEndBehavior()
    The effective EndBehavior (override or file default).
    Getter
AnimationEndEvent
fr.eri.eriapi.anim.event.AnimationEndEvent
@Cancelable

Fired when an animation reaches its last tick, before the EndBehavior is applied. If canceled, the animation is frozen on the last frame (FROZEN).

  • World getWorld()
    The world where the block is located.
    Getter
  • BlockPos getPos()
    The block's position.
    Getter
  • AnimatedBlockTileEntity getTileEntity()
    The animated block's TileEntity.
    Getter
  • String getAnimName()
    The name of the animation that is ending.
    Getter
  • EndBehavior getEndBehavior()
    The EndBehavior that is about to be applied.
    Getter
AnimationResetEvent
fr.eri.eriapi.anim.event.AnimationResetEvent
@Cancelable

Fired before an animation is reset. If canceled, the reset is not applied.

  • World getWorld()
    The world where the block is located.
    Getter
  • BlockPos getPos()
    The block's position.
    Getter
  • AnimatedBlockTileEntity getTileEntity()
    The animated block's TileEntity.
    Getter
  • String getPreviousAnim()
    The name of the animation that was in progress before the reset.
    Getter
  • boolean isInstant()
    true if the reset is instant, false if a rollback is used.
    Getter
AnimationLoopEvent
fr.eri.eriapi.anim.event.AnimationLoopEvent
@Cancelable

Fired when a looping animation is about to restart (LOOP, LOOP_COUNT, LOOP_PAUSE). If canceled, the animation stops looping and freezes (FROZEN).

  • World getWorld()
    The world where the block is located.
    Getter
  • BlockPos getPos()
    The block's position.
    Getter
  • AnimatedBlockTileEntity getTileEntity()
    The animated block's TileEntity.
    Getter
  • String getAnimName()
    The name of the looping animation.
    Getter
  • int getCurrentLoop()
    Current iteration number (starts at 0). The first time the animation ends = 0.
    Getter
  • int getMaxLoops()
    Maximum number of loops for LOOP_COUNT, or -1 for infinite loops (LOOP and LOOP_PAUSE).
    Getter

Listener examples

Java — Listen to events with EriEvents
import fr.eri.eriapi.anim.event.*;
import fr.eri.eriapi.event.EriEvents;

// Prevent certain animations from playing
EriEvents.on(AnimationStartEvent.class, e -> {
    if ("forbidden_anim".equals(e.getAnimName())) {
        e.setCanceled(true); // The animation will not play
    }
});

// React when an animation finishes
EriEvents.on(AnimationEndEvent.class, e -> {
    if ("charge".equals(e.getAnimName())) {
        // Give a buff to the nearest player
        giveBuffToNearestPlayer(e.getWorld(), e.getPos());
    }
});

// Stop a loop after a condition
EriEvents.on(AnimationLoopEvent.class, e -> {
    if ("production".equals(e.getAnimName())) {
        TileMachine te = (TileMachine) e.getTileEntity();
        if (!te.hasFuel()) {
            e.setCanceled(true); // Stops the loop, freezes the block
        }
    }
});

// Prevent a reset
EriEvents.on(AnimationResetEvent.class, e -> {
    if (isBlockLocked(e.getPos())) {
        e.setCanceled(true); // The reset is not applied
    }
});

Dynamic Textures

You can change the textures of an animated block in real-time, in two ways:

  • Via the .erianim.json file — with the texture track, textures change automatically during the animation.
  • Via the Java API — with te.setTexture(), you change textures from server code at any time.

Texture priority

When the TESR looks for which texture to display, it checks in this order:

  1. Animation override (texture track from .erianim.json) — highest priority.
  2. TileEntity override (te.setTexture()) — medium priority.
  3. Original model texture — lowest priority (default).
Java — Change textures via the API
AnimatedBlockTileEntity te = (AnimatedBlockTileEntity) world.getTileEntity(pos);

// Change a specific texture
te.setTexture("screen", "screen_active");

// The model's "screen" texture will be replaced by "screen_active"
// The change is automatically synchronized to all clients

// Restore the original texture
te.clearTexture("screen");

// Restore ALL original textures
te.clearAllTextures();

// Check current overrides
Map<String, String> overrides = te.getTextureOverrides();
// overrides = {"screen" -> "screen_active", ...}
Java — Use callbacks for server logic + textures
public class TileReactor extends AnimatedBlockTileEntity {

    public TileReactor() {
        super("mymod:block/reactor", "mymod:block/reactor");
    }

    @Override
    protected void onAnimationCallback(String callbackName) {
        switch (callbackName) {
            case "activate_core":
                // The animation reached the point where the core should light up
                setTexture("core", "core_glowing");
                setTexture("panel", "panel_active");
                break;
            case "deactivate_core":
                clearAllTextures();
                break;
        }
    }
}

Debug Commands

EriAPI automatically registers the /erianim command for testing and debugging animations in-game. All subcommands require OP level 2, except reload which requires OP level 3.

CommandEriAnim
fr.eri.eriapi.anim.command.CommandEriAnim
OP 2+
Command Description
/erianim play <x> <y> <z> <anim> [endBehavior] Plays an animation on the animated block at the given position. The EndBehavior is optional (uses the .erianim file's default).
/erianim reset <x> <y> <z> [true|false] Resets the animation. true = instant, false (default) = uses rollback if applicable.
/erianim stop <x> <y> <z> Immediately stops the animation (no rollback, no callback).
/erianim list <x> <y> <z> Lists all available animations in the block's .erianim file, with their duration, EndBehavior, number of tracks and events.
/erianim state <x> <y> <z> Displays the block's full state: model, anim file, state, current animation, progress, texture overrides.
/erianim perf Enables performance tracking and displays stats: number of blocks rendered, average render time, cache sizes. Run a second time to view data.
/erianim reload (OP 3) Clears model and animation caches. Files will be reloaded on the next render. Ideal for testing modifications without restarting the server.

File Structure

Here is where to place each file in your mod for the animation system to work correctly.

File structure within the mod
src/main/resources/assets/mymod/
  models/
    block/
      my_machine.json                     ← Blockbench model (export "Java Block/Item")
  animations/
    block/
      my_machine.erianim.json             ← EriAPI animation file
  textures/
    blocks/
      my_machine/                         ← Model texture folder
        base.png                          ← Main texture
        panel.png                         ← Panel texture
        screen_off.png                    ← Screen off
        screen_on.png                     ← Screen on (for texture swap)
  blockstates/
    my_machine.json                       ← Blockstate (auto-generated by EriAnimBlock)
Path resolution

The modelId "mymod:block/my_machine" resolves to assets/mymod/models/block/my_machine.json.

The animFileId "mymod:block/my_machine" resolves to assets/mymod/animations/block/my_machine.erianim.json.

Textures are resolved via the texture map in the Blockbench model ("textures" section of the JSON).

Performance

The animation system is designed to support 500 to 1000 concurrent players. Here are the built-in optimizations and best practices to maintain good performance.

Built-in optimizations

  • Fast exit in update() — The ITickable.update() method returns immediately for IDLE, FROZEN, PAUSED, and WAITING_SERVER states. Only blocks in PLAYING or ROLLING_BACK state consume server CPU time.
  • IDLE render cache — When the block is IDLE and nothing has changed, the TESR skips pose calculation and reuses the last render. A idleRenderDirty flag controls re-evaluation.
  • Model and animation caches — Blockbench models and .erianim files are parsed once then cached (AnimModelCache and EriAnimCache). Reload is available via /erianim reload.
  • Sorted events — Animation events are sorted by tick, allowing O(n) dispatch with early exit when the tick exceeds the current tick.

Best practices

  • Reduce renderDistance — For decorative or non-critical blocks, use a renderDistance of 32 or 48 instead of 64.
  • Avoid unnecessary infinite loops — A block in LOOP consumes server CPU every tick to dispatch events. If you have no events, prefer an IDLE animation without events using a spin track.
  • Use WAIT_SERVER for long processes — Instead of looping, play an animation once then wait for the server signal. This avoids continuous ticking.
  • Fewer groups = fewer calculations — Each animated group requires transformation calculations. Merge groups that move together in Blockbench.
  • Monitor with /erianim perf — Use the in-game command to check render time and identify problematic blocks.

EriAnim Editor

The EriAnim editor is a desktop tool (Electron application) that allows you to create and visually edit .erianim.json files without writing JSON by hand.

Features

  • Visual timeline — Displays tracks and keyframes on an interactive timeline. Drag and drop to move keyframes.
  • 3D Preview — Preview the animation in real-time with the Blockbench model loaded in the editor.
  • Keyframe editor — Form to edit values, easing, and tick of each keyframe.
  • Direct export — Generates the .erianim.json file ready to be integrated into your mod.
Where to find the editor

The EriAnim editor is an internal tool of EriniumGroup. If you are working on a mod that uses EriAPI, contact the team to get access. In the meantime, you can always create .erianim.json files by hand following the documentation on this page.