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
.cfgfiles viaConfigParser. - 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.
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.
@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.
@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.
@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:
// 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(
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("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(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("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.
@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.
@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. |
ConfigParser and do not appear in the generated GUI.
Enum example
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(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(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(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
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
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
boolean registered = ConfigParser.isRegistered(Class<?> configClass);
Returns true if the class has already been registered. Safe to call at any
time.
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
// 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
@Categoryvalue, 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 callsConfigSyncHandler.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
// 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(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(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(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.
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
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
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
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
// 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));
})
);