Overlay HUD
Permanently display information on the player's HUD — health bars, faction stats, visual effects — without interrupting gameplay.
What is an overlay?
An overlay is a graphical element displayed permanently on the player's screen,
on top of the game and Minecraft's vanilla HUD. Unlike a GUI (EriGuiScreen)
which takes control of the screen, an overlay stays visible at all times while the player is playing.
Typical use cases:
- Resource bars — mana, stamina, energy, heat
- Faction HUD — faction name, rank, territory points
- Status indicators — active effects, buffs/debuffs
- Custom minimap or compass
- Decorative effects — particles, aurora, ambient fog
Overlay vs GUI — what is the difference?
| Characteristic | EriGuiScreen (GUI) | EriOverlay (HUD) |
|---|---|---|
| Opening | On player action (key, command) | Permanent (registered once) |
| Blocks gameplay | Yes — the player cannot move | No — the player plays normally |
| Interactions | Keyboard, mouse, clicks | None (render only) |
| Coordinates | 1920x1080 design pixels | 1920x1080 design pixels (identical) |
| Components | All EriAPI components | Visual components only (no input) |
| Lifecycle | Open / close | Register / unregister + show / hide |
Overlays use exactly the same components, the same 1920x1080 coordinate system
and the same animations as a regular GUI. If you know how to create an EriGuiScreen,
you already know how to create an overlay.
EriOverlay
EriOverlay is the abstract base class for all HUD overlays.
You create a subclass, define the components in buildOverlay(),
and register it with OverlayManager.
Creating an overlay
import fr.eri.eriapi.overlay.*;
import fr.eri.eriapi.gui.components.*;
public class HealthOverlay extends EriOverlay {
public HealthOverlay() {
// unique id, width and height in design pixels
super("health_hud", 200, 60);
setAnchor(Anchor.BOTTOM_LEFT); // bottom-left corner
setOffset(10, -10); // offset from the anchor
}
@Override
protected void buildOverlay() {
getRoot().add(
new Rectangle(0, 0, 200, 60)
.fillColor(0x80000000)
.cornerRadius(8),
new Label(10, 20, 180, 20, "HP : 20")
.color(0xFFFF5555)
);
}
}
// Registration (once, in ClientProxy.init() for example):
OverlayManager.getInstance().register(new HealthOverlay());
Lifecycle
The buildOverlay() method is called only once on the first render
(lazy initialization). Subsequent calls to render() reuse the already-built components.
If you want to force a rebuild (data that changes), call rebuild().
-
ConstructorEriOverlay(String id, int designWidth, int designHeight)Constructor.
idis the unique identifier used byOverlayManager.designWidthanddesignHeightdefine the overlay area in design pixels (1920x1080). -
To implementvoid buildOverlay()Abstract method to implement. Call
getRoot().add(...)to add components. Called only once on first render. -
GetterContainerComponent getRoot()Returns the root container. Use from
buildOverlay()to add components. -
Methodvoid rebuild()Forces the overlay to be rebuilt on the next render. Useful to refresh the display after a significant data change.
-
FluentEriOverlay setAnchor(Anchor anchor)Sets the anchor point of the overlay on screen. See the Anchor section. Default:
TOP_LEFT. -
FluentEriOverlay setOffset(int offsetX, int offsetY)Offset in design pixels from the anchor. Positive = down/right, negative = up/left.
-
FluentEriOverlay setLayer(OverlayLayer layer)Sets the render layer (moment in the HUD pipeline). Default:
POST_ALL. -
FluentEriOverlay hideInGui(boolean hide)If
true(default), the overlay disappears when a GUI is open. Set tofalseto always display the overlay even in menus. -
FluentEriOverlay hideOnF1(boolean hide)If
true(default), the overlay disappears when the player presses F1 (hide HUD). Set tofalseto ignore F1. -
Methodvoid show() / hide() / toggle()Direct visibility control for this overlay. Equivalent to
OverlayManager.getInstance().show("id").
OverlayManager
OverlayManager is the singleton that centralizes all registered overlays.
It automatically handles rendering and ticks via Forge events — you have nothing to configure
beyond the initial registration.
import fr.eri.eriapi.overlay.OverlayManager;
OverlayManager om = OverlayManager.getInstance();
// Register overlays (once, during init)
om.register(new HealthOverlay());
om.register(new ManaOverlay());
om.register(new FactionHudOverlay());
// Visibility control by ID
om.show("health_hud");
om.hide("health_hud");
om.toggle("health_hud");
// Global control
om.hideAll();
om.showAll();
// Retrieve an overlay to modify it
EriOverlay overlay = om.get("health_hud");
if (overlay != null) {
overlay.rebuild(); // Force a rebuild
}
// Unregister an overlay
om.unregister("health_hud");
-
Staticstatic OverlayManager getInstance()Returns the unique manager instance.
-
Methodvoid register(EriOverlay overlay)Registers an overlay. If an overlay with the same ID already exists, it is replaced. Registration order determines render order.
-
Methodvoid unregister(String id)Removes a registered overlay. No effect if the ID is unknown.
-
GetterEriOverlay get(String id)Returns the overlay matching the ID, or
nullif it does not exist. -
Methodvoid show(String id) / hide(String id) / toggle(String id)Controls the visibility of an overlay by its ID. No effect if the ID is unknown.
-
Methodvoid showAll() / hideAll()Shows or hides all registered overlays.
Register your overlays in ClientProxy.init() (or postInit() if
you need items/blocks to be loaded first). Do not register them in a GUI constructor or in an
event that fires frequently — registration is done only once.
Anchor
The Anchor enum defines the nine possible anchor points for an overlay.
The anchor is the corner or edge of the screen from which the overlay is positioned.
Combine with setOffset(x, y) to fine-tune the positioning.
| Value | Position on screen | Typical use case |
|---|---|---|
TOP_LEFT |
Top-left corner (0, 0) | Minimap, mod logo |
TOP_CENTER |
Center of the top edge | Zone title, boss bar |
TOP_RIGHT |
Top-right corner | Clock, coordinates, ping |
CENTER_LEFT |
Center of the left edge | Side status bar |
CENTER |
Absolute center of the screen | Custom crosshair, central alert |
CENTER_RIGHT |
Center of the right edge | Buff/debuff list |
BOTTOM_LEFT |
Bottom-left corner | Mana/stamina bar |
BOTTOM_CENTER |
Center of the bottom edge | Action bar, shortcuts |
BOTTOM_RIGHT |
Bottom-right corner | Faction stats, faction name |
// Overlay in bottom-right with 15px margin
new MonOverlay()
.setAnchor(Anchor.BOTTOM_RIGHT)
.setOffset(-15, -15); // negative = toward top/left
// Overlay centered at top with 20px margin from edge
new BossBarOverlay()
.setAnchor(Anchor.TOP_CENTER)
.setOffset(0, 20); // positive = toward bottom
Offsets are in design pixels (1920x1080 space), just like GUI components.
EriAPI automatically converts them to the player's actual resolution. An offset of
(-15, -15) will represent the same visual margin on a 1080p or 4K screen.
OverlayLayer
OverlayLayer defines at which point in Minecraft's HUD render
pipeline your overlay is drawn. This determines what is displayed above or below it.
| Value | Render moment | Result |
|---|---|---|
PRE_ALL |
Before all vanilla HUD elements | Your overlay is drawn beneath the health bar, hotbar, etc. |
POST_HOTBAR |
After the item bar (hotbar) | Drawn above the hotbar but below the health bar |
POST_EXPERIENCE |
After the experience bar | Drawn after the XP bar, before other elements |
POST_ALL |
After all vanilla HUD elements (default) | Your overlay is drawn on top of everything in the vanilla HUD |
// Display the overlay on top of the entire HUD (default behavior)
overlay.setLayer(OverlayLayer.POST_ALL);
// Display just after the hotbar
overlay.setLayer(OverlayLayer.POST_HOTBAR);
// Ambient background beneath the entire HUD
overlay.setLayer(OverlayLayer.PRE_ALL);
Components available in overlays
Overlays reuse exactly the same components as GUIs. The only difference: interactive components (TextField, ScrollList, TabView, etc.) serve no purpose in an overlay since there is no keyboard/mouse interaction with the HUD.
Shapes and backgrounds
Solid rectangle with optional rounded corners and border. Perfect for HUD panel backgrounds.
new Rectangle(0, 0, 200, 60)
.fillColor(0x80000000) // semi-transparent background
.borderColor(0x40FFFFFF) // subtle border
.cornerRadius(8);
Rectangle with a linear gradient (vertical or horizontal). Ideal for ambient bars or gradient backgrounds.
// Vertical gradient purple -> blue
new GradientRectangle()
.originalPos(0, 0).originalSize(200, 60)
.vertical(0xFF6B2FA0, 0xFF00E5FF)
.cornerRadius(8);
// Horizontal gradient
new GradientRectangle()
.originalPos(0, 0).originalSize(300, 8)
.horizontal(0xFF00E5FF, 0x00000000); // fade to transparent
Filled circle or triangle (directional indicator). Useful for icons or navigation arrows.
Text
Text display with scale, alignment and shadow. The basic text component for all HUDs.
new Label(10, 10, 180, 20, "Faction: Erinium")
.color(0xFFFFD700)
.scale(1.2f)
.shadow(true);
Progress controls
Progress bar with animated fill, colors and rounded corners. The ideal component for displaying resources (mana, health, exp).
ProgressBar manaBar = new ProgressBar(0, 0, 180, 14)
.progress(0.75f) // 75% mana
.fillColor(0xFF6B2FA0) // fill color
.backgroundColor(0x40000000) // background
.cornerRadius(7)
.showText(false); // no text in overlay
Containers
Container for nesting components. Allows grouping elements and applying a collective
opacity or animations. The getRoot() method of EriOverlay
already returns a root ContainerComponent — use sub-containers to
organize your HUD into logical sections.
Decorative visual effects
These components are particularly suited to overlays: they animate continuously,
consume few resources and provide visual ambiance without requiring any
AnimationManager configuration.
Undulating northern-lights bands with a vertical gradient. Up to 10 stackable bands,
each with its own color, frequency, amplitude and speed. Animated continuously via
System.currentTimeMillis() — no EriAPI animation needed.
new Aurora()
.originalPos(0, 0).originalSize(1920, 400)
.addBand(0x406B2FA0, 0.3f, 1.2f, 80, 50) // purple
.addBand(0x3000E5FF, 0.5f, 0.8f, 60, 120) // cyan
.addBand(0x20FF6B6B, 0.7f, 0.6f, 50, 200) // subtle pink
.speed(0.02f);
// addBand parameters: color, frequency, amplitude, height, offsetY
Star field with sinusoidal twinkle and random shooting stars. Up to 3 simultaneous shooting stars.
new Starfield()
.originalPos(0, 0).originalSize(600, 400)
.starCount(80)
.starColor(0xFFF0F2FF)
.shootingStarChance(0.003f); // 0 = never, 0.01 = frequent
Soft particle system (circles with radial gradient) that spawn, drift and fade out. Additive blending for a glowing effect. Perfect for a magical ambiance.
new ParticleSystem()
.originalPos(0, 0).originalSize(300, 100)
.maxParticles(20)
.spawnRate(0.3f)
.particleLife(2000, 4000)
.particleSize(20, 60)
.particleColor(0x306B2FA0)
.drift(0, -0.008f) // rises gently
.spread(0.01f)
.fadeIn(0.2f).fadeOut(0.4f);
Organic fog based on fractal Simplex noise (FBM). Animated continuously, the fog flows and deforms naturally. Very subtle at low intensity.
new SmokeFog()
.originalPos(0, 0).originalSize(400, 200)
.color(0x6B2FA0) // RGB color
.intensity(0.08f) // max alpha (very subtle)
.scale(0.006f) // wisp size
.speed(0.0003f)
.octaves(3)
.cellSize(12);
Animations and the "overlay" scope
All EriAPI animations work in overlays. However, there is an important subtlety:
when a GUI is closed, EriGuiScreen.onGuiClosed() calls
AnimationManager.getInstance().stopGuiAnimations() — this stops all
animations that have no scope.
To protect overlay animations from being stopped, use .scope("overlay"):
public class ManaOverlay extends EriOverlay {
private Label manaLabel;
private ProgressBar manaBar;
public ManaOverlay() {
super("mana_hud", 200, 40);
setAnchor(Anchor.BOTTOM_LEFT);
setOffset(10, -70); // above the vanilla health bar
}
@Override
protected void buildOverlay() {
manaLabel = new Label(10, 5, 100, 15, "Mana")
.color(0xFF9B59B6);
manaBar = new ProgressBar(10, 22, 180, 10)
.progress(1f)
.fillColor(0xFF6B2FA0)
.backgroundColor(0x40000000)
.cornerRadius(5);
getRoot().add(manaLabel, manaBar);
// Breathing animation — scope "overlay" so it is not stopped
// when the player opens/closes a GUI
Animation anim = Animation.create(0.6f, 1f, 30)
.pingPong()
.scope("overlay")
.easing(Easing.EASE_IN_OUT)
.onUpdate(v -> manaLabel.setOpacity(v));
AnimationManager.getInstance().play(anim);
}
// Public method to update the mana value
public void setMana(float ratio) {
if (manaBar != null) {
manaBar.progress(ratio);
}
}
}
Without this scope, your pingPong() or loop() animations will
be interrupted every time the player closes a GUI (inventory, etc.). With
.scope("overlay"), they keep running independently.
Component animation helpers
The pre-built methods on Component (fadeIn, breathe, pulse, etc.)
also work in overlays, but they do not automatically define a scope.
For looping animations in an overlay, create the animation manually with .scope("overlay").
// ❌ Will be stopped when the player closes a GUI
label.breathe(40); // internal helper, no scope
// ✅ Continues even when a GUI is opened then closed
Animation.create(0.4f, 1f, 20)
.pingPong()
.scope("overlay")
.easing(Easing.EASE_IN_OUT)
.onUpdate(v -> label.setOpacity(v));
AnimationManager.getInstance().play(breatheAnim);
Example 1 — Mana bar
A simple overlay with a progress bar and a label, anchored to the bottom-left. The bar can be updated from anywhere in the code.
import fr.eri.eriapi.overlay.*;
import fr.eri.eriapi.gui.components.*;
public class ManaOverlay extends EriOverlay {
private ProgressBar manaBar;
private Label manaLabel;
public ManaOverlay() {
super("mana_overlay", 210, 50);
setAnchor(Anchor.BOTTOM_LEFT);
setOffset(10, -80); // just above the health bar
hideInGui(true); // hide when GUI is open
hideOnF1(true); // hide when F1 is pressed
}
@Override
protected void buildOverlay() {
// Background
getRoot().add(new Rectangle(0, 0, 210, 50)
.fillColor(0x90000000)
.cornerRadius(6));
// "Mana" label
manaLabel = new Label(8, 6, 60, 12, "MANA")
.color(0xFFBB86FC)
.scale(0.8f)
.shadow(true);
// Mana bar
manaBar = new ProgressBar(8, 24, 194, 12)
.progress(1f)
.fillColor(0xFF6B2FA0)
.backgroundColor(0x50000000)
.cornerRadius(6);
// Value label
Label valueLabel = new Label(8, 38, 194, 10, "100 / 100")
.color(0x80FFFFFF)
.scale(0.75f)
.align(Label.Align.CENTER);
getRoot().add(manaLabel, manaBar, valueLabel);
}
/** Called from your mana system to update the bar. */
public void setMana(int current, int max) {
if (manaBar != null) {
manaBar.progress((float) current / max);
}
}
}
// In ClientProxy.init():
ManaOverlay manaOverlay = new ManaOverlay();
OverlayManager.getInstance().register(manaOverlay);
// From anywhere in your mod (client side):
ManaOverlay overlay = (ManaOverlay) OverlayManager.getInstance().get("mana_overlay");
if (overlay != null) overlay.setMana(75, 100);
Example 2 — Faction HUD
A faction HUD in the bottom-right corner showing the faction name, the player's rank and the number of members online.
import fr.eri.eriapi.overlay.*;
import fr.eri.eriapi.gui.components.*;
public class FactionHudOverlay extends EriOverlay {
private Label factionNameLabel;
private Label rankLabel;
private Label membersLabel;
public FactionHudOverlay() {
super("faction_hud", 220, 70);
setAnchor(Anchor.BOTTOM_RIGHT);
setOffset(-10, -10);
}
@Override
protected void buildOverlay() {
// Gradient background
getRoot().add(new GradientRectangle()
.originalPos(0, 0).originalSize(220, 70)
.vertical(0x80150A2A, 0x801A0B35)
.cornerRadius(8));
// Top border
getRoot().add(new GradientRectangle()
.originalPos(0, 0).originalSize(220, 2)
.horizontal(0x00000000, 0xFF6B2FA0)
.cornerRadius(0));
// Icon / prefix
getRoot().add(new Label(8, 10, 20, 16, "\u2726")
.color(0xFFFFD700)
.scale(1.0f));
// Faction name
factionNameLabel = new Label(28, 8, 180, 18, "No faction")
.color(0xFFE0E0E0)
.scale(1.0f)
.shadow(true);
// Rank
rankLabel = new Label(8, 30, 204, 14, "Rank: ---")
.color(0xFF9B8BCC)
.scale(0.85f);
// Members online
membersLabel = new Label(8, 48, 204, 14, "Members online: 0")
.color(0xFF6BCB77)
.scale(0.85f);
getRoot().add(factionNameLabel, rankLabel, membersLabel);
}
public void update(String faction, String rank, int onlineMembers) {
if (factionNameLabel != null) factionNameLabel.text(faction);
if (rankLabel != null) rankLabel.text("Rank: " + rank);
if (membersLabel != null) membersLabel.text("Members online: " + onlineMembers);
}
}
Example 3 — Overlay with animated visual effects
An ambient overlay with a star field background and an aurora, displayed at the top of the screen. This type of overlay is used for permanent decorative effects.
import fr.eri.eriapi.overlay.*;
import fr.eri.eriapi.gui.components.*;
import fr.eri.eriapi.gui.anim.*;
public class AmbientOverlay extends EriOverlay {
public AmbientOverlay() {
super("ambient_overlay", 1920, 300);
setAnchor(Anchor.TOP_LEFT);
setLayer(OverlayLayer.PRE_ALL); // beneath the vanilla HUD
hideInGui(false); // stays visible even in menus
hideOnF1(true);
}
@Override
protected void buildOverlay() {
// Star field background
getRoot().add(new Starfield()
.originalPos(0, 0).originalSize(1920, 300)
.starCount(60)
.starColor(0xFFF0F2FF)
.shootingStarChance(0.002f));
// Aurora on top
getRoot().add(new Aurora()
.originalPos(0, 0).originalSize(1920, 300)
.addBand(0x306B2FA0, 0.4f, 1.0f, 100, 80)
.addBand(0x2000E5FF, 0.6f, 0.7f, 80, 160)
.speed(0.015f));
}
}
Technical notes
Animation scope
When a GUI is closed (EriGuiScreen.onGuiClosed()), the framework calls
AnimationManager.getInstance().stopGuiAnimations(). This method stops
all animations without a scope (scope == null).
Animations with .scope("overlay") (or any other non-null scope) are preserved.
Rule to remember: in an overlay, every looping animation (loop(),
pingPong()) must have .scope("overlay").
One-shot animations (fade-in, slide) do not need a scope.
hideInGui
By default, hideInGui(true) automatically hides the overlay when
Minecraft.getMinecraft().currentScreen != null (a GUI is open).
Set to false if your overlay must remain visible in menus
(decorative effect, persistent information).
hideOnF1
By default, hideOnF1(true) hides the overlay when
mc.gameSettings.hideGUI == true (player presses F1 to hide the HUD).
Set to false only for overlays that have a strong reason to always
display (debug info, etc.).
Thread safety
Overlays are entirely rendered on the client render thread. Do not modify
components from an async thread (EriScheduler.async or similar). Use
EriScheduler.delay(0, () -> overlay.setMana(...)) to dispatch back to
the main thread if necessary.
Performance
Overlays are rendered every frame (60 FPS+). Avoid heavy computations in
buildOverlay() or in onUpdate lambdas. Decorative components
(SmokeFog, ParticleSystem) have built-in limits
(cellSize, maxParticles) to control GPU cost.
Store references to components as private fields of your overlay class.
Expose public methods (setMana(), update()) to
update values. Never create new components on every tick —
only modify the properties of existing components (.text(),
.progress(), etc.).
OverlayMod — Modular System
OverlayMod is EriAPI's new modular overlay system. It replaces the
manual approach with EriOverlay for complex overlays that need
user configuration, drag & drop positioning and automatic persistence
between game sessions.
Each OverlayMod is a self-contained module that includes:
- Its own position — stored in design pixels (1920x1080 space), converted at render time by the ScaleManager
- Its own zoom factor — each mod has its own configurable scale
- Configurable settings — via
ModSettings, exposed in the editor - Drag & drop positioning — the built-in editor lets the user freely move each mod
- Automatic JSON persistence — position, scale and settings are saved and restored
- Per-frame rendering with nanotime precision — the
onFrame()callback receives the delta in nanoseconds
Creating an OverlayMod
import fr.eri.eriapi.overlay.OverlayMod;
import fr.eri.eriapi.gui.components.*;
import net.minecraft.client.Minecraft;
import net.minecraft.entity.player.EntityPlayer;
public class HealthOverlayMod extends OverlayMod {
private Label healthLabel;
private Rectangle background;
public HealthOverlayMod() {
super("my_health", "Health Display", 200, 40);
setCategory("Combat");
setDescription("Displays the player's health points.");
setDefaultPosition(0.01f, 0.9f); // bottom-left (x and y as screen fraction)
setMinScale(0.5f);
setMaxScale(2.0f);
}
@Override
protected void buildOverlay() {
background = new Rectangle();
background.originalPos(0, 0).originalSize(200, 40);
background.fillColor(0xAA000000).cornerRadius(6);
getRoot().add(background);
healthLabel = new Label("HP: 20/20");
healthLabel.originalPos(8, 8).originalSize(184, 24);
healthLabel.color(0xFFFF5555).scale(1.2f);
getRoot().add(healthLabel);
}
@Override
protected void onFrame(float partialTicks, long deltaNanos) {
EntityPlayer player = Minecraft.getMinecraft().player;
if (player == null) return;
int hp = (int) player.getHealth();
int maxHp = (int) player.getMaxHealth();
healthLabel.text("HP: " + hp + "/" + maxHp);
}
}
Registration
Register your mods in ClientProxy.init() via OverlayModManager.
As with OverlayManager, registration is done only once.
import fr.eri.eriapi.overlay.OverlayModManager;
// In ClientProxy.init():
OverlayModManager.getInstance().register(new HealthOverlayMod());
OverlayModManager.getInstance().register(new DebugOverlayMod());
OverlayModManager.getInstance().register(new FactionInfoMod());
-
ConstructorOverlayMod(String id, String name, int designWidth, int designHeight)Constructor.
idis the unique identifier (used for JSON persistence).nameis the display name shown in the editor.designWidthanddesignHeightare the render area dimensions in design pixels (1920x1080 space). -
Configurationvoid setCategory(String category)Sets the category shown in the manager (e.g. "Combat", "Information", "Debug"). Allows grouping mods by theme in the management interface.
-
Configurationvoid setDescription(String description)Short description shown in the overlay manager. Should explain in one sentence what the mod displays.
-
Configurationvoid setDefaultPosition(float x, float y)Default position as a screen fraction (0.0 to 1.0 for x and y). Compatible with the legacy format — internally converts to design pixels (
x * 1920,y * 1080). Example:(0.01f, 0.9f)= approximately bottom-left corner. -
Configurationvoid setDefaultPositionDesign(float designX, float designY)Default position directly in design pixels (1920x1080 space). Preferred over
setDefaultPosition()for precise positioning. Example:(20, 950)= 20 design pixels from the left edge, 950 from the top. -
Configurationvoid setMinScale(float min) / setMaxScale(float max)Sets the user zoom limits in the editor. Typical values: min
0.5f, max2.0f. Default scale is1.0f. -
To implementvoid buildOverlay()Abstract method to implement. Build all visual components here with
getRoot().add(...). Called once on first render (and on everyrebuild()call). -
Optionalvoid buildSettings(ModSettings settings)Optional override. Declare the mod's configurable settings here via the
ModSettingsbuilder. See the ModSettings section. -
Optionalvoid onFrame(float partialTicks, long deltaNanos)Optional override. Called every render frame (60-144+ times per second).
partialTicksis the fraction of a tick elapsed since the last tick (0.0 to 1.0).deltaNanosis the time in nanoseconds since the last call — useful for internal timers. This is where you update labels, bars, etc. -
Optionalvoid onTick()Optional override. Called 20 times per second (every server tick). Use for heavy data updates or server synchronizations. See onFrame vs onTick.
-
Optionalvoid onSettingChanged(String key, Object value)Optional override. Called automatically when the user modifies a setting in the editor.
keycorresponds to the key declared inbuildSettings().valueis the new value (Integer, Float, Boolean or String depending on the setting type). -
Getter<T> T getSetting(String key, T defaultValue)Reads the current value of a setting. If the setting has not yet been loaded (first session), returns
defaultValue. The typeTis inferred fromdefaultValue— pass the same type as declared inbuildSettings(). -
Methodvoid rebuild()Forces a complete rebuild of the visual components on the next render. Calls
buildOverlay()again. Useful when a setting changes the layout (number of rows, compact mode, etc.). -
GetterContainerComponent getRoot()Returns the root container to which components are added. Available from
buildOverlay().
Use EriOverlay for simple, static overlays (mana bar,
faction HUD) where you manage updates yourself via public methods.
Use OverlayMod when you want the user to be able to configure,
move and enable/disable the overlay from a built-in editor, with
automatic persistence. Both systems coexist and can be used together.
ModSettings — Configurable settings
Each OverlayMod can declare configurable settings by implementing
buildSettings(ModSettings settings). These settings are automatically
exposed in the GuiOverlayHub editor and their values are persisted as JSON.
The ModSettings builder uses a fluent syntax — each call returns
the same builder to chain declarations.
@Override
protected void buildSettings(ModSettings settings) {
settings
.separator("Visible sections")
.toggle("show_fps", "FPS", true)
.toggle("show_coords", "Coordinates", true)
.separator("Appearance")
.colorPicker("text_color", "Text color", 0xFFCCCCCC)
.slider("bg_opacity", "Background opacity", 0f, 1f, 0.67f)
.toggle("compact", "Compact mode", false);
}
@Override
protected void onSettingChanged(String key, Object value) {
if ("text_color".equals(key)) {
int color = (Integer) value;
myLabel.color(color);
} else if ("compact".equals(key)) {
rebuild(); // The layout changes completely in compact mode
}
}
// Reading a value in onFrame() or onTick():
boolean showFps = getSetting("show_fps", true);
float opacity = getSetting("bg_opacity", 0.67f);
Available setting types
| Method | Return type | Description |
|---|---|---|
toggle(key, label, default) |
Boolean | On/off switch. Displays a toggle switch in the editor. |
slider(key, label, min, max, default) |
Float | Floating-point slider between min and max. Ideal for opacities, sizes, speeds. |
sliderInt(key, label, min, max, default) |
Integer | Integer slider. For counters, line limits, etc. |
colorPicker(key, label, defaultColor) |
Integer (ARGB) | Full color picker (wheel, brightness, alpha). The value is an ARGB integer 0xAARRGGBB. |
dropdown(key, label, default, options...) |
String | Dropdown menu with a list of choices. Pass options as String varargs. |
textField(key, label, default) |
String | Free text field. Useful for prefixes, suffixes or custom names. |
keybind(key, label, defaultKey) |
Integer (LWJGL) | Key selector. The value is an LWJGL key code (Keyboard.KEY_*). |
separator(title) |
— | Visual separator with title. Does not create a setting, used only to organize the interface. |
info(text) |
— | Non-interactive informational text. Useful for notes or warnings in the editor. |
The getSetting(key, defaultValue) method always returns a valid value:
either the saved value, or the default value if the setting has never been modified.
Use the same default value in buildSettings()
and in getSetting() to guarantee consistency.
GuiOverlayHub — The built-in editor
GuiOverlayHub is the central graphical interface of the modular system.
It allows users to position, configure and enable/disable their overlays
without leaving the game.
Editor features
The base layer of the editor displays all active overlays overlaid on the game screen. Each overlay can be:
- Moved — click + drag to reposition freely
- Zoomed — mouse wheel to change the scale
- Selected — right-click to open the context menu (settings, reset, toggle)
A Snap to grid toggle enables magnetic alignment to a grid. The Reset All button resets all overlays to their default position.
Opens a popup listing all registered OverlayMods. For each mod:
- Quick enable/disable toggle
- Category and description
- Search bar by name
- Filter by category
Available for each selected mod (via the button in the editor or the
right-click context menu). Displays all settings declared in
buildSettings() with their corresponding components
(toggle, slider, colorPicker, dropdown...). Changes are applied
in real time via onSettingChanged().
Opening the editor
The editor opens by default with the ] key (configurable via EriAPI keybindings). It can also be opened programmatically:
// Open the editor from code (e.g. from a keybinding or a command)
OverlayModManager.getInstance().openEditor();
Saving
The Save and close button writes the configuration as JSON (see JSON Persistence) and closes the editor. Until the user saves, changes are applied visually in real time but not yet persisted to disk.
onFrame vs onTick
OverlayMod provides two update callbacks with very different frequencies.
Choosing the right one directly impacts the smoothness and
performance of your overlay.
onFrame() for everything visual that needs to be smooth. onTick() for heavy computations, world accesses or server-synchronized data.
| Callback | Frequency | When to use it |
|---|---|---|
onFrame(partialTicks, deltaNanos) |
60 to 144+ times/s (every render frame) | Updating labels, coordinates, FPS, smooth animations, value interpolation |
onTick() |
20 times/s (every 50ms) | Heavy computations, biome access, world queries, server-synchronized data |
The deltaNanos parameter of onFrame() gives the exact time
elapsed since the last call in nanoseconds. It allows creating internal timers
to execute semi-heavy code at precise intervals without using onTick().
private long timer = 0L;
private static final long INTERVAL = 500_000_000L; // 500ms in nanoseconds
@Override
protected void onFrame(float partialTicks, long deltaNanos) {
timer += deltaNanos;
// Light update every frame (smooth)
fpsLabel.text("FPS: " + Minecraft.getDebugFPS());
// Heavy update only every 500ms
if (timer >= INTERVAL) {
timer = 0L;
biomeLabel.text("Biome: " + getBiomeName()); // world access, more costly
}
}
Player coordinates change between ticks (movement is interpolated
with partialTicks). If you update coordinates in
onTick(), the text jumps in discrete increments. With
onFrame(), the text follows the player's actual movement
completely smoothly.
Design-pixel positioning
Since the modular system update, OverlayMod positions are stored in
design pixels (1920x1080 space), identically to all EriAPI components.
The ScaleManager converts these coordinates to actual screen pixels at
render time.
Why this change?
The old system stored positions as screen fractions (0.0 to 1.0). Seemingly resolution-independent, this system caused a subtle problem: the visual gap between two adjacent overlays changed depending on the player's resolution and guiScale, because positions and sizes were not in the same coordinate space.
| Criterion | Old system (fractions) | New system (design pixels) |
|---|---|---|
| Position unit | Screen fraction (0.0 – 1.0) | Design pixels (0 – 1920 / 0 – 1080) |
| Component unit | Design pixels (ScaleManager) | Design pixels (ScaleManager) — identical |
| Gap between overlays | Varies with resolution | Constant regardless of resolution |
| Conversion at render | fraction * displayWidth / guiScale |
ScaleManager.scaleXf(posXDesign) |
Backward compatibility
The public API remains 100% compatible. setDefaultPosition(float x, float y) still
accepts fractions and converts them internally:
// Legacy method — fractions (0.0 to 1.0)
// Still valid, converted internally: x * 1920, y * 1080
setDefaultPosition(0.01f, 0.9f); // ≈ (19, 972) in design pixels
// New method — direct design pixels (recommended)
// More precise and consistent with the rest of the EriAPI system
setDefaultPositionDesign(20f, 960f); // exactly 20px from the left, 960px from the top
Example — two adjacent overlays
// Mana bar at the bottom left
public class ManaBarMod extends OverlayMod {
public ManaBarMod() {
super("mana_bar", "Mana", 240, 20);
setDefaultPositionDesign(20f, 1010f); // 20px from the left, 1010px from the top
}
}
// Stamina bar just below, 6 design pixels apart
public class StaminaMod extends OverlayMod {
public StaminaMod() {
super("stamina_bar", "Stamina", 240, 20);
// 1010 (mana) + 20 (mana height) + 6 (gap) = 1036
setDefaultPositionDesign(20f, 1036f);
}
}
// The 6 design-pixel gap is identical at every resolution.
getPosXDesign() and getPosYDesign() return the position in design
pixels. The legacy getters getPosXPercent() and getPosYPercent()
are still available for backward compatibility (they convert by dividing by 1920/1080).
JSON Persistence
The modular system automatically saves the configuration of each
OverlayMod to a JSON file. You have nothing to implement:
persistence is entirely managed by OverlayModManager.
File location
.minecraft/config/eriapi/overlay_mods.json
Data persisted per mod
| Data | JSON type | Description |
|---|---|---|
| enabled | boolean | Whether the mod is active or disabled in the manager |
| posX / posY | float | Position as a screen fraction (0.0 to 1.0), independent of resolution |
| scale | float | Zoom factor applied by the user in the editor |
| settings.* | mixed | Values of all settings declared in buildSettings() (boolean, float, int, String) |
Save and load cycle
- Loading — automatic on the first render frame, after all mods have been registered in
ClientProxy.init() - Saving — triggered when the user clicks "Save and close" in
GuiOverlayHub
The overlay_mods.json file is generated and read by EriAPI.
Manually modifying its structure can cause loading errors.
If the file is corrupted, delete it — EriAPI will recreate default values
on the next game launch.
Full example — DebugOverlayMod
Here is a real, complete example: the DebugOverlayMod from the EriniumFaction mod.
It displays FPS, coordinates, chunk, biome, direction, light and memory in real time.
It is a good model to understand how to combine onFrame(), a nanotime timer
for heavy data, and buildSettings().
Mod architecture
- Dimensions: 380 x 160 px (1920x1080 design)
- Default position: top-left corner (0.5%, 0.5%)
- Light data (FPS, coords, chunk, direction) — updated in
onFrame()every frame - Heavy data (biome, light, memory) — updated every 500ms via a nanotime timer in
onFrame() - 7 configurable settings: 7 visibility toggles, a colorPicker, a slider and a compact toggle
- Colored FPS: green ≥60, yellow ≥30, red <30
package fr.eriniumgroup.eriniumfaction.overlay;
import fr.eri.eriapi.gui.components.Label;
import fr.eri.eriapi.gui.components.Rectangle;
import fr.eri.eriapi.overlay.OverlayMod;
import fr.eri.eriapi.overlay.settings.ModSettings;
import net.minecraft.client.Minecraft;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.MathHelper;
import net.minecraft.world.biome.Biome;
import net.minecraft.world.chunk.Chunk;
public class DebugOverlayMod extends OverlayMod {
// Labels updated every frame
private Label fpsLabel;
private Label coordsLabel;
private Label chunkLabel;
private Label biomeLabel;
private Label directionLabel;
private Label lightLabel;
private Label memoryLabel;
// Background
private Rectangle background;
// nanotime timer for heavy updates (500ms)
private long heavyUpdateTimer = 0L;
private static final long HEAVY_UPDATE_INTERVAL = 500_000_000L;
// Cache for heavy values
private String cachedBiome = "";
private String cachedLight = "";
private String cachedMemory = "";
public DebugOverlayMod() {
super("erinium_debug", "Debug Info", 380, 160);
setCategory("Information");
setDescription("Displays FPS, coordinates, biome, direction and memory.");
setDefaultPosition(0.005f, 0.005f); // top-left corner
setMinScale(0.5f);
setMaxScale(2.0f);
}
@Override
protected void buildOverlay() {
int w = 380;
// Semi-transparent background with rounded corners
background = new Rectangle();
background.originalPos(0, 0).originalSize(w, 160);
background.fillColor(0xAA1A1A2E).cornerRadius(6);
getRoot().add(background);
// Creating labels on each row (20px spacing)
int x = 8, y = 6, lineH = 20;
fpsLabel = createLine(x, y, w - 16); y += lineH;
coordsLabel = createLine(x, y, w - 16); y += lineH;
chunkLabel = createLine(x, y, w - 16); y += lineH;
biomeLabel = createLine(x, y, w - 16); y += lineH;
directionLabel = createLine(x, y, w - 16); y += lineH;
lightLabel = createLine(x, y, w - 16); y += lineH;
memoryLabel = createLine(x, y, w - 16);
getRoot().add(fpsLabel, coordsLabel, chunkLabel, biomeLabel,
directionLabel, lightLabel, memoryLabel);
}
private Label createLine(int x, int y, int w) {
Label label = new Label("...");
label.originalPos(x, y).originalSize(w, 18);
label.color(0xFFCCCCCC).scale(0.9f);
return label;
}
@Override
protected void buildSettings(ModSettings settings) {
settings
.separator("Visible sections")
.toggle("show_fps", "FPS", true)
.toggle("show_coords", "Coordinates", true)
.toggle("show_chunk", "Chunk", true)
.toggle("show_biome", "Biome", true)
.toggle("show_direction", "Direction", true)
.toggle("show_light", "Light", true)
.toggle("show_memory", "Memory", true)
.separator("Appearance")
.colorPicker("text_color", "Text color", 0xFFCCCCCC)
.slider("bg_opacity", "Background opacity", 0f, 1f, 0.67f)
.toggle("compact", "Compact mode", false);
}
@Override
protected void onSettingChanged(String key, Object value) {
if ("text_color".equals(key)) {
applyTextColor((Integer) value);
} else if ("bg_opacity".equals(key)) {
float opacity = (Float) value;
int alpha = (int) (opacity * 255);
background.fillColor((alpha << 24) | 0x1A1A2E);
} else if ("compact".equals(key) || key.startsWith("show_")) {
rebuild(); // Layout changes (hidden rows in compact mode)
}
}
private void applyTextColor(int color) {
if (fpsLabel != null) fpsLabel.color(color);
if (coordsLabel != null) coordsLabel.color(color);
if (chunkLabel != null) chunkLabel.color(color);
if (biomeLabel != null) biomeLabel.color(color);
if (directionLabel != null) directionLabel.color(color);
if (lightLabel != null) lightLabel.color(color);
if (memoryLabel != null) memoryLabel.color(color);
}
@Override
protected void onFrame(float partialTicks, long deltaNanos) {
Minecraft mc = Minecraft.getMinecraft();
EntityPlayer player = mc.player;
if (player == null || mc.world == null) return;
heavyUpdateTimer += deltaNanos;
// FPS — every frame, with adaptive color
if (fpsLabel != null && getSetting("show_fps", true)) {
int fps = Minecraft.getDebugFPS();
fpsLabel.text("FPS: " + fps);
fpsLabel.color(fps >= 60 ? 0xFF55FF55 : fps >= 30 ? 0xFFFFFF55 : 0xFFFF5555);
}
// Coordinates — every frame for maximum smoothness
if (coordsLabel != null && getSetting("show_coords", true)) {
boolean compact = getSetting("compact", false);
coordsLabel.text(compact
? String.format("XYZ: %.0f / %.0f / %.0f", player.posX, player.posY, player.posZ)
: String.format("X: %.1f Y: %.1f Z: %.1f", player.posX, player.posY, player.posZ));
}
// Chunk — every frame
if (chunkLabel != null && getSetting("show_chunk", true)) {
int cx = MathHelper.floor(player.posX) >> 4;
int cz = MathHelper.floor(player.posZ) >> 4;
chunkLabel.text("Chunk: " + cx + ", " + cz);
}
// Direction — every frame (interpolated rotation)
if (directionLabel != null && getSetting("show_direction", true)) {
float yaw = MathHelper.wrapDegrees(player.rotationYaw);
directionLabel.text("Direction: " + getCardinalDirection(yaw)
+ " (" + String.format("%.0f", yaw) + ")");
}
// Heavy data — every 500ms via nanotime timer
if (heavyUpdateTimer >= HEAVY_UPDATE_INTERVAL) {
heavyUpdateTimer = 0L;
BlockPos pos = player.getPosition();
if (getSetting("show_biome", true)) {
cachedBiome = mc.world.getBiome(pos).getBiomeName();
}
if (getSetting("show_light", true)) {
Chunk chunk = mc.world.getChunk(pos);
cachedLight = "Block: "
+ chunk.getLightFor(net.minecraft.world.EnumSkyBlock.BLOCK, pos)
+ " Sky: "
+ chunk.getLightFor(net.minecraft.world.EnumSkyBlock.SKY, pos);
}
if (getSetting("show_memory", true)) {
Runtime rt = Runtime.getRuntime();
long used = (rt.totalMemory() - rt.freeMemory()) / 1048576L;
long max = rt.maxMemory() / 1048576L;
cachedMemory = used + "MB / " + max + "MB (" + (used * 100 / max) + "%)";
}
}
// Apply cached values for heavy data
if (biomeLabel != null) biomeLabel.text("Biome: " + cachedBiome);
if (lightLabel != null) lightLabel.text("Light " + cachedLight);
if (memoryLabel != null) memoryLabel.text("Mem: " + cachedMemory);
// Visibility per setting
if (fpsLabel != null) fpsLabel.visible(getSetting("show_fps", true));
if (coordsLabel != null) coordsLabel.visible(getSetting("show_coords", true));
if (chunkLabel != null) chunkLabel.visible(getSetting("show_chunk", true));
if (biomeLabel != null) biomeLabel.visible(getSetting("show_biome", true));
if (directionLabel != null) directionLabel.visible(getSetting("show_direction", true));
if (lightLabel != null) lightLabel.visible(getSetting("show_light", true));
if (memoryLabel != null) memoryLabel.visible(getSetting("show_memory", true));
}
@Override
protected void onTick() {
// All update logic is in onFrame() for smoothness.
// onTick() is not used here but could serve for network calls.
}
private static String getCardinalDirection(float yaw) {
if (yaw < 0) yaw += 360f;
if (yaw >= 337.5f || yaw < 22.5f) return "S";
if (yaw < 67.5f) return "SW";
if (yaw < 112.5f) return "W";
if (yaw < 157.5f) return "NW";
if (yaw < 202.5f) return "N";
if (yaw < 247.5f) return "NE";
if (yaw < 292.5f) return "E";
return "SE";
}
}
Key takeaways from this example
Accessing the biome (world.getBiome()), chunk light and
JVM memory are non-trivial operations. Executing them every frame
(60+ times/s) would waste CPU. The heavyUpdateTimer limits them
to once every 500ms, while labels are updated from the cache every frame —
rendering stays smooth without overhead.
When the compact setting or a show_* toggle changes,
we call rebuild() rather than simply changing label visibility.
In compact mode, the text format itself changes
("XYZ: 10 / 64 / -30" instead of "X: 10.0 Y: 64.0 Z: -30.0"),
which requires a complete component rebuild.
If only visibility changed, a simple label.visible(false)
would suffice without rebuild().
Overlay editor: auto-snap alignment system v1.2.0
When you open the in-game editor (GuiOverlayHub) and drag an overlay, the auto-snap system activates. The overlay being moved automatically snaps to the edges and centers of other overlays and to fixed screen reference points, enabling precise alignment without entering coordinates manually.
All coordinates are expressed in 1920x1080 design pixels, consistent with the rest of the EriAPI positioning system.
Snap types
The system detects 10 distinct snap types. A snap triggers when the distance between two reference points is less than or equal to 5 design pixels.
| Type | Description | Source reference | Target reference |
|---|---|---|---|
LEFT_TO_LEFT |
Left edge aligned to the left edge of another overlay | x of the dragged overlay | x of a neighbor overlay |
LEFT_TO_RIGHT |
Left edge aligned to the right edge of another overlay | x of the dragged overlay | x + width of a neighbor overlay |
RIGHT_TO_RIGHT |
Right edge aligned to the right edge of another overlay | x + width of the dragged overlay | x + width of a neighbor overlay |
RIGHT_TO_LEFT |
Right edge aligned to the left edge of another overlay | x + width of the dragged overlay | x of a neighbor overlay |
TOP_TO_TOP |
Top edge aligned to the top edge of another overlay | y of the dragged overlay | y of a neighbor overlay |
TOP_TO_BOTTOM |
Top edge aligned to the bottom edge of another overlay | y of the dragged overlay | y + height of a neighbor overlay |
BOTTOM_TO_BOTTOM |
Bottom edge aligned to the bottom edge of another overlay | y + height of the dragged overlay | y + height of a neighbor overlay |
BOTTOM_TO_TOP |
Bottom edge aligned to the top edge of another overlay | y + height of the dragged overlay | y of a neighbor overlay |
CENTER_H |
Horizontal center aligned to the horizontal center of another overlay or the screen center (960) | x + width/2 of the dragged overlay | horizontal center of the target |
CENTER_V |
Vertical center aligned to the vertical center of another overlay or the screen center (540) | y + height/2 of the dragged overlay | vertical center of the target |
Screen edge snapping
In addition to other overlays, all four screen borders are snap targets:
- Left border — x = 0
- Right border — x = 1920
- Top border — y = 0
- Bottom border — y = 1080
The screen center (960, 540) is also a target for CENTER_H
and CENTER_V snaps.
Visual feedback
Two visual indicators appear during dragging to guide positioning:
Guide lines (cyan)
When at least one snap is active, a semi-transparent cyan guide line appears on the snap axis:
- A vertical line for a snap on the X axis (left/right edges or horizontal center).
- A horizontal line for a snap on the Y axis (top/bottom edges or vertical center).
These lines span the full width or height of the screen to remain visible regardless of the overlay position. They disappear immediately when the overlay is released or when the snap is no longer active.
Overlap indicator (red)
If the dragged overlay overlaps (at least partially) another overlay during the drag, it takes on a semi-transparent red tint. This signals that an overlap is occurring. The overlay can still be dropped at that position — this is a visual warning, not a block.
Bypassing snap (Shift key)
To move an overlay freely without any snapping, hold the Shift key during the drag. The overlay will then follow the mouse movement exactly, pixel by pixel, with no snapping or position correction applied.
Use normal drag (without Shift) to align overlays with each other or with screen borders. Use Shift only when you need a very specific position that does not correspond to any grid reference point.