Config System

The EriAPI Config System is an annotation-based configuration framework built on top of Forge's standard .cfg file format. Instead of manually creating Configuration objects and writing boilerplate load/save logic, you declare a plain Java class, annotate its fields, and let the system handle the rest.

The system provides four integrated features out of the box:

  • Parsing — reads and writes standard Forge .cfg files via ConfigParser.
  • Validation — enforces ranges, patterns and type constraints at load time.
  • Auto-GUI — generates a fully interactive in-game configuration screen via ConfigGuiFactory, with no manual UI code required.
  • Network sync — sends config values from server to clients (or client to server) automatically via ConfigSyncHandler.
The Config System uses the same EriAPI GUI components as the rest of the framework. The generated screen is a proper EriGuiScreen with Slider, TextField, Checkbox, ColorPicker and other components wired up automatically.

Quick Start

Four steps are enough to get a working config with auto-GUI and network sync.

Step 1 — Declare your config class

Create a plain Java class. Annotate it with @EriConfig and annotate each field you want to expose.

MyConfig.java
@EriConfig(modid = "mymod", filename = "mymod")
public class MyConfig {

    @Category("general")
    @Comment("Maximum number of players allowed.")
    @Range(min = 1, max = 100)
    public static int maxPlayers = 20;

    @Category("general")
    @Comment("Enable debug logging.")
    public static boolean debugMode = false;

    @Category("display")
    @Comment("Primary accent color (ARGB).")
    @ColorField
    public static int accentColor = 0xFF4A90E2;

    @Category("display")
    @Comment("Server address (hostname or IP).")
    @Pattern("^[\\w.-]+(:\\d+)?$")
    public static String serverAddress = "localhost";
}

Step 2 — Register with ConfigParser

Call ConfigParser.register() during your mod's pre-initialization event, passing the config class and the mod configuration directory.

YourMod.java — FMLPreInitializationEvent
@EventHandler
public void preInit(FMLPreInitializationEvent event) {
    ConfigParser.register(MyConfig.class, event.getModConfigurationDirectory());
}
ConfigParser.register() immediately loads the file from disk (creating it with defaults if it does not exist) and populates the static fields of your class.

Step 3 — Initialise network sync

Call ConfigSyncHandler.init() after registering your network channel. This registers the sync packet and hooks into the player login event so values are pushed to newly connected clients automatically.

YourMod.java — FMLPreInitializationEvent
@EventHandler
public void preInit(FMLPreInitializationEvent event) {
    ConfigParser.register(MyConfig.class, event.getModConfigurationDirectory());
    ConfigSyncHandler.init(); // registers packet + login hook
}

Step 4 — Open the config GUI

From anywhere on the client (a button, a command, a GuiFactory), open the generated screen:

Opening the auto-generated config screen
// From a button onClick callback:
.onClick(() -> {
    GuiScreen configScreen = ConfigGuiFactory.createGui(MyConfig.class);
    Minecraft.getMinecraft().displayGuiScreen(configScreen);
})

// Or with an explicit parent screen (Back button returns to it):
GuiScreen configScreen = ConfigGuiFactory.createGui(MyConfig.class, currentScreen);
Minecraft.getMinecraft().displayGuiScreen(configScreen);

Annotations

All annotations live in the fr.eri.eriapi.config.annotation package. They are all retained at runtime (@Retention(RUNTIME)) and target fields (@Target(FIELD)) unless stated otherwise.

@EriConfig

Marks a class as a config class. Must be present on the class itself (not a field). ConfigParser.register() will refuse to process a class that does not carry this annotation.

@EriConfig — parameters
@EriConfig(
    modid    = "mymod",    // used as the config file header and for namespacing
    filename = "mymod"     // filename without extension → mymod.cfg
)
public class MyConfig { ... }
@Target(TYPE) — this annotation goes on the class declaration, not on fields.

@Category

Groups fields into named sections inside the .cfg file. Fields that share the same category string are written under the same [category] header. In the auto-generated GUI, each category becomes a separate tab.

@Category — usage
@Category("general")
public static int maxPlayers = 20;

@Category("display")
public static int accentColor = 0xFF4A90E2;

// Fields with no @Category go into a default "general" section.

@Range

Constrains the value of a numeric field (int, float, double) to a minimum and maximum. The parser clamps loaded values to the declared range. In the GUI, a Slider component is generated instead of a plain text field.

@Range — usage
@Range(min = 0.0, max = 1.0)
public static float masterVolume = 0.8f;

@Range(min = 1, max = 100)
public static int tickRate = 20;
@Range is ignored on non-numeric fields. Applying it to a String or boolean has no effect and does not cause an error.

@Comment

Attaches a human-readable description to a field. The comment is written as a #-prefixed line above the entry in the .cfg file, and is also displayed as a tooltip in the auto-generated GUI.

@Comment — usage
@Comment("Maximum render distance in chunks. Higher values impact performance.")
@Range(min = 2, max = 32)
public static int renderDistance = 12;

@ColorField

Marks an int field as an ARGB color value. The parser stores and loads it in 0xAARRGGBB hexadecimal notation. In the auto-generated GUI, a ColorPicker component is generated instead of a numeric field.

@ColorField — usage
@Category("display")
@Comment("Background color of the main panel.")
@ColorField
public static int backgroundColor = 0xCC1A1A2E;
@ColorField and @Range are mutually exclusive. If both are present on the same field, @ColorField takes priority and @Range is ignored.

@Pattern

Validates a String field against a regular expression. If the value loaded from the file does not match, the field is reset to its default value and a warning is logged. In the auto-generated GUI, the TextField displays a red border while the current input does not match the pattern.

@Pattern — usage
@Comment("Server address (hostname:port or bare hostname).")
@Pattern("^[\\w.-]+(:\\d{1,5})?$")
public static String serverAddress = "localhost";

@Comment("Hex color code without leading #.")
@Pattern("^[0-9A-Fa-f]{6}$")
public static String hexTheme = "4A90E2";

Supported Types

The following field types are recognised by ConfigParser. Both primitive and boxed forms are accepted. The table also shows which GUI component is generated by ConfigGuiFactory for each type.

Java Type Relevant Annotations Generated GUI Component Notes
int / Integer @Range Slider Without @Range, falls back to NumericField (integers only).
int / Integer @ColorField ColorPicker Stored as 0xAARRGGBB hex in the .cfg file.
float / Float @Range Slider Without @Range, falls back to NumericField (decimals allowed).
double / Double @Range Slider Without @Range, falls back to NumericField (decimals allowed).
boolean / Boolean Checkbox Stored as true / false in the file.
String @Pattern TextField Red border while input does not match the declared pattern.
Any enum RadioGroup All enum constants are offered as radio buttons. Stored by constant name.
Types not listed above (arrays, collections, nested objects) are silently ignored by ConfigParser and do not appear in the generated GUI.

Enum example

Enum config field
public enum Difficulty { EASY, NORMAL, HARD, HARDCORE }

@EriConfig(modid = "mymod", filename = "mymod")
public class MyConfig {

    @Category("gameplay")
    @Comment("Server difficulty level.")
    public static Difficulty difficulty = Difficulty.NORMAL;
}

The generated GUI renders a RadioGroup with four options: EASY, NORMAL, HARD, HARDCORE. The selected value is written to the file as the constant name string.

ConfigParser

ConfigParser is a static utility class. It manages the lifecycle of all registered config classes: loading from disk, validating values, populating static fields, and saving back to disk.

register

ConfigParser.register()
ConfigParser.register(Class<?> configClass, File configDir);

Registers a config class and immediately performs the initial load. If the file does not exist it is created with default values. If a field value is invalid (outside range, pattern mismatch) it is replaced with the default and the file is re-saved.

reload

ConfigParser.reload()
ConfigParser.reload(Class<?> configClass);

Re-reads the .cfg file from disk and updates all static fields. Useful if the file was edited externally while the game is running.

save

ConfigParser.save()
ConfigParser.save(Class<?> configClass);

Writes the current static field values back to the .cfg file. This is called automatically by the Save button in the auto-generated GUI but can be invoked manually at any time.

getFields

ConfigParser.getFields()
List<Field> fields = ConfigParser.getFields(Class<?> configClass);

Returns the list of annotated fields in declaration order. Useful for building custom UIs or exporting config metadata.

getForgeConfig

ConfigParser.getForgeConfig()
Configuration cfg = ConfigParser.getForgeConfig(Class<?> configClass);

Returns the underlying Forge Configuration object for advanced use cases. Avoid modifying it directly unless you know what you are doing — changes bypass validation.

isRegistered

ConfigParser.isRegistered()
boolean registered = ConfigParser.isRegistered(Class<?> configClass);

Returns true if the class has already been registered. Safe to call at any time.

All ConfigParser methods throw IllegalArgumentException if the given class is not annotated with @EriConfig, and IllegalStateException if called before register() (except register() and isRegistered() themselves).

ConfigGuiFactory

ConfigGuiFactory generates a complete, interactive EriGuiScreen from any registered config class. No manual UI code is required.

createGui

ConfigGuiFactory.createGui()
// Without parent — closing the screen returns to the main/game screen.
GuiScreen screen = ConfigGuiFactory.createGui(Class<?> configClass);

// With parent — the Back / Cancel button returns to parentScreen.
GuiScreen screen = ConfigGuiFactory.createGui(Class<?> configClass, GuiScreen parentScreen);

Minecraft.getMinecraft().displayGuiScreen(screen);

Generated screen layout

The generated screen has a fixed structure:

  • A title bar showing the mod ID and config filename.
  • A TabView with one tab per @Category value, plus a "general" tab for uncategorised fields.
  • Inside each tab, all fields for that category are listed vertically with their label on the left and their GUI component on the right.
  • A footer row with three buttons: Save, Cancel and Reset to defaults.

Component mapping

Field type + annotation Generated component Behaviour
int/float/double + @Range Slider Draggable; value snapped to type precision.
int + @ColorField ColorPicker Full HSB wheel + brightness slider + hex input.
int/float/double (no @Range) NumericField Keyboard input; rejects non-numeric characters.
boolean Checkbox Toggle on click.
String TextField Red border if @Pattern present and not matching.
Enum RadioGroup One RadioButton per constant; single selection.

Save / Cancel / Reset buttons

  • Save — validates all fields, writes values to the static fields of the config class, calls ConfigParser.save(), then calls ConfigSyncHandler.sendAllToClient() if on a server. Closes the screen.
  • Cancel — discards any in-progress changes and closes the screen without writing anything.
  • Reset to defaults — restores all visible fields to their Java default values (as declared in the class). Does not save automatically; the user must click Save.
ConfigGuiFactory.createGui() throws IllegalStateException if the config class has not been registered with ConfigParser first.

ConfigSyncHandler

ConfigSyncHandler handles synchronisation of config values over the network using EriAPI's existing GuiNetworkHandler infrastructure. It registers a dedicated packet and hooks into Forge events so that newly connecting clients automatically receive the server's config.

init

ConfigSyncHandler.init()
// Call once, after GuiNetworkHandler is initialised:
ConfigSyncHandler.init();

Registers the PacketConfigSync packet with GuiNetworkHandler and subscribes to PlayerEvent.PlayerLoggedInEvent so that each connecting player receives the current server config automatically.

sendToServer

ConfigSyncHandler.sendToServer()
ConfigSyncHandler.sendToServer(Class<?> configClass);

Sends the current static field values of the config class from the client to the server. The server validates, applies and re-saves the received values. Only operators (permission level 2+) are permitted to push config changes to the server; the packet is silently rejected otherwise.

sendToClient

ConfigSyncHandler.sendToClient()
ConfigSyncHandler.sendToClient(Class<?> configClass, EntityPlayerMP player);

Sends the current server-side config values to a specific player. Called automatically on login; can be called again if you need to push a mid-session update to one player.

sendAllToClient

ConfigSyncHandler.sendAllToClient()
ConfigSyncHandler.sendAllToClient(Class<?> configClass);

Broadcasts the current server-side config values to all connected players. Called automatically by the Save button in the auto-generated GUI when running on the server.

The sync payload is a compact NBT compound: one entry per field, keyed by category.fieldName. Fields not annotated with any recognised annotation are still included in the sync if they are a supported type.
ConfigSyncHandler only syncs values. It does not sync annotation metadata. Clients must have the same mod version (and therefore the same config class) as the server for the received values to be applied correctly.

Full Example

The following example shows a complete, self-contained setup: a config class, mod initialisation, a command that opens the config GUI, and a reload command.

1 — Config class

ServerConfig.java
package com.example.mymod.config;

import fr.eri.eriapi.config.annotation.*;

@EriConfig(modid = "mymod", filename = "mymod-server")
public class ServerConfig {

    // ---- General ----

    @Category("general")
    @Comment("Enable verbose logging to the server console.")
    public static boolean verboseLogging = false;

    @Category("general")
    @Comment("Maximum number of simultaneous connections.")
    @Range(min = 1, max = 500)
    public static int maxConnections = 50;

    // ---- Display ----

    @Category("display")
    @Comment("Primary accent color shown in the HUD (ARGB hex).")
    @ColorField
    public static int accentColor = 0xFF4A90E2;

    @Category("display")
    @Comment("HUD opacity, from 0.0 (transparent) to 1.0 (opaque).")
    @Range(min = 0.0, max = 1.0)
    public static float hudOpacity = 0.85f;

    // ---- Network ----

    @Category("network")
    @Comment("Remote API endpoint (hostname or IP, optional port).")
    @Pattern("^[\\w.-]+(:\\d{1,5})?$")
    public static String apiEndpoint = "api.example.com";

    @Category("network")
    @Comment("Request timeout in seconds.")
    @Range(min = 1, max = 60)
    public static int requestTimeout = 10;

    // ---- Gameplay ----

    @Category("gameplay")
    @Comment("Default server difficulty.")
    public static Difficulty difficulty = Difficulty.NORMAL;

    public enum Difficulty { EASY, NORMAL, HARD, HARDCORE }
}

2 — Mod main class

MyMod.java
package com.example.mymod;

import com.example.mymod.config.ServerConfig;
import fr.eri.eriapi.config.ConfigParser;
import fr.eri.eriapi.config.ConfigSyncHandler;
import fr.eri.eriapi.network.GuiNetworkHandler;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.Mod.EventHandler;
import net.minecraftforge.fml.common.event.FMLPreInitializationEvent;
import net.minecraftforge.fml.common.event.FMLServerStartingEvent;

@Mod(modid = "mymod", version = "1.0.0")
public class MyMod {

    @EventHandler
    public void preInit(FMLPreInitializationEvent event) {
        // 1. Register the config (loads from disk immediately).
        ConfigParser.register(ServerConfig.class, event.getModConfigurationDirectory());

        // 2. Init EriAPI networking (required before ConfigSyncHandler).
        GuiNetworkHandler.init("mymod");

        // 3. Register the sync packet and login hook.
        ConfigSyncHandler.init();
    }

    @EventHandler
    public void serverStarting(FMLServerStartingEvent event) {
        event.registerServerCommand(new CommandConfig());
    }
}

3 — Command to open config GUI

CommandConfig.java
package com.example.mymod;

import com.example.mymod.config.ServerConfig;
import fr.eri.eriapi.config.ConfigGuiFactory;
import fr.eri.eriapi.config.ConfigParser;
import net.minecraft.client.Minecraft;
import net.minecraft.command.CommandBase;
import net.minecraft.command.ICommandSender;
import net.minecraft.server.MinecraftServer;

public class CommandConfig extends CommandBase {

    @Override
    public String getName() { return "mymodconfig"; }

    @Override
    public String getUsage(ICommandSender sender) { return "/mymodconfig [reload]"; }

    @Override
    public void execute(MinecraftServer server, ICommandSender sender, String[] args) {
        if (args.length > 0 && args[0].equalsIgnoreCase("reload")) {
            // Reload from disk and push to all clients.
            ConfigParser.reload(ServerConfig.class);
            ConfigSyncHandler.sendAllToClient(ServerConfig.class);
            sender.sendMessage(new TextComponentString("Config reloaded and synced."));
        } else {
            // Open the auto-generated GUI on the client side.
            // (Use a scheduled task to switch to client thread.)
            Minecraft.getMinecraft().addScheduledTask(() -> {
                Minecraft.getMinecraft().displayGuiScreen(
                    ConfigGuiFactory.createGui(ServerConfig.class)
                );
            });
        }
    }
}

4 — Opening the GUI from a Button in an existing screen

Integrating into an existing EriGuiScreen
// Inside your EriGuiScreen subclass:
addComponent(
    new Button(10, 10, 160, 30)
        .text("Server Config")
        .colorScheme(Button.Style.PRIMARY)
        .onClick(() -> {
            // Pass 'this' as the parent so Cancel returns here.
            mc.displayGuiScreen(ConfigGuiFactory.createGui(ServerConfig.class, this));
        })
);
The generated config screen inherits all EriAPI GUI behaviour: keyboard navigation, ScaleManager auto-scaling, and the same visual style as the rest of your mod's UI. No additional styling is needed.