MultiLoader 1.21+ · Part 13

Config Files (MultiLoader 1.21+)

INTERMEDIATE MULTILOADER 1.21-1.21.1 18 min read · Aug 3, 2026
MultiLoader 1.21+ · 28 parts
1 Getting Started with MultiLoader 1.21+ 2 Setting Up RegistrationUtils 3 Creating Items (MultiLoader 1.21+) 4 Creating Blocks (MultiLoader 1.21+) 5 Data Generation: Block & Item Models (MultiLoader 1.21+) 6 Data Generation: Block Loot Tables (MultiLoader 1.21+) 7 Data Generation: Crafting Recipes (MultiLoader 1.21+) 8 Data Generation: Block & Item Tags (MultiLoader 1.21+) 9 Custom Food Items (MultiLoader 1.21+) 10 Custom Tools (MultiLoader 1.21+) 11 Custom Armour (MultiLoader 1.21+) 12 Block Entities (MultiLoader 1.21+) 13 Config Files (MultiLoader 1.21+) 14 Custom Sounds (MultiLoader 1.21+) 15 Events and Listeners (MultiLoader 1.21+) 16 Networking and Custom Packets (MultiLoader 1.21+) 17 Data Generation: Advancements (MultiLoader 1.21+) 18 Data Generation: Language Files (MultiLoader 1.21+) 19 Custom Entities (MultiLoader 1.21+) 20 Ore Generation (MultiLoader 1.21+) 21 Introduction to Mixins (MultiLoader 1.21+) 22 Custom Particles (MultiLoader 1.21+) 23 Menus & Screens (MultiLoader 1.21+) 24 Key Bindings (MultiLoader 1.21+) 25 Custom Potion Effects (MultiLoader 1.21+) 26 Custom Enchantments (MultiLoader 1.21+) 27 Custom Commands (MultiLoader 1.21+) 28 Custom Biomes (MultiLoader 1.21+)

NeoForge and Fabric each have their own configuration APIs, so multiloader mods need a small abstraction layer to keep shared code free of loader-specific imports. This tutorial defines a config interface in common, implements it separately for each loader, and shows how to read values from anywhere in your codebase.

NOTE
Complete the Getting Started tutorial before continuing. Config is loader-specific and is initialised in each loader's mod entry point.

Common Config Interface

Create a class called ExampleConfig in your common project under a config package. It holds a static reference to the active implementation and exposes typed getters:

java
public abstract class ExampleConfig {
private static ExampleConfig INSTANCE;
public static ExampleConfig get() {
if (INSTANCE == null) throw new IllegalStateException("Config not yet initialised");
return INSTANCE;
}
public static void setInstance(ExampleConfig config) {
INSTANCE = config;
}
// Config values: add your own fields here
public abstract boolean enableExampleFeature();
public abstract int maxStackOverride();
public abstract String greeting();
}

All shared code reads config values through ExampleConfig.get().enableExampleFeature() and so on, with no knowledge of which loader is running.

NeoForge Implementation

In your neoforge subproject, create NeoForgeExampleConfig in the same config package. Use ModConfigSpec to declare and validate each value:

java
public class NeoForgeExampleConfig extends ExampleConfig {
public static final ModConfigSpec SPEC;
private static final ModConfigSpec.BooleanValue ENABLE_EXAMPLE_FEATURE;
private static final ModConfigSpec.IntValue MAX_STACK_OVERRIDE;
private static final ModConfigSpec.ConfigValue<String> GREETING;
static {
ModConfigSpec.Builder builder = new ModConfigSpec.Builder();
builder.comment("Example Mod Configuration").push("general");
ENABLE_EXAMPLE_FEATURE = builder
.comment("Enables the example feature.")
.define("enableExampleFeature", true);
MAX_STACK_OVERRIDE = builder
.comment("Maximum stack size override for mod items (1-64).")
.defineInRange("maxStackOverride", 64, 1, 64);
GREETING = builder
.comment("Greeting message displayed on login.")
.define("greeting", "Welcome to Example Mod!");
builder.pop();
SPEC = builder.build();
}
@Override public boolean enableExampleFeature() { return ENABLE_EXAMPLE_FEATURE.get(); }
@Override public int maxStackOverride() { return MAX_STACK_OVERRIDE.get(); }
@Override public String greeting() { return GREETING.get(); }
}

Register the spec and set the common instance in your NeoForge mod constructor, before CommonClass.init() runs:

java
public ExampleMod(IEventBus eventBus, ModContainer container) {
ExampleConfig.setInstance(new NeoForgeExampleConfig());
ModLoadingContext.get().getActiveContainer().registerConfig(ModConfig.Type.COMMON, NeoForgeExampleConfig.SPEC);
// ... rest of constructor
CommonClass.init();
}

NeoForge writes the config file to .minecraft/config/examplemod-common.toml and reloads it automatically when the file changes on disk.

TIP
NeoForge supports three config types: ModConfig.Type.COMMON (both sides), CLIENT (client only), and SERVER (per-world, synced to clients on join). Use COMMON for values that must match on both sides.

Fabric Implementation

Fabric has no built-in config system. The simplest cross-platform approach is a plain JSON file read at startup via Gson, which ships with Minecraft. In your fabric subproject, create FabricExampleConfig:

java
public class FabricExampleConfig extends ExampleConfig {
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
private static final Path CONFIG_PATH = FabricLoader.getInstance()
.getConfigDir().resolve("examplemod.json");
private boolean enableExampleFeature = true;
private int maxStackOverride = 64;
private String greeting = "Welcome to Example Mod!";
public static FabricExampleConfig load() {
FabricExampleConfig config;
if (Files.exists(CONFIG_PATH)) {
try (Reader reader = Files.newBufferedReader(CONFIG_PATH)) {
config = GSON.fromJson(reader, FabricExampleConfig.class);
} catch (IOException e) {
Constants.LOG.error("Failed to read config, using defaults", e);
config = new FabricExampleConfig();
}
} else {
config = new FabricExampleConfig();
}
config.save();
return config;
}
private void save() {
try {
Files.createDirectories(CONFIG_PATH.getParent());
try (Writer writer = Files.newBufferedWriter(CONFIG_PATH)) {
GSON.toJson(this, writer);
}
} catch (IOException e) {
Constants.LOG.error("Failed to write config", e);
}
}
@Override public boolean enableExampleFeature() { return enableExampleFeature; }
@Override public int maxStackOverride() { return maxStackOverride; }
@Override public String greeting() { return greeting; }
}

Load and register it in your Fabric mod initialiser:

java
public class ExampleModFabric implements ModInitializer {
@Override
public void onInitialize() {
ExampleConfig.setInstance(FabricExampleConfig.load());
CommonClass.init();
}
}
TIP
For a more feature-rich Fabric config experience (GUI in the mods screen, schema validation, reload support), consider the Cloth Config library. The manual JSON approach shown here keeps your mod dependency-free, which is preferable for library-style mods or mods that ship for both loaders without a shared config library.

Reading Config Values

Any class in common, fabric, or neoforge can now read config values without importing loader-specific types:

java
// In any common class
if (ExampleConfig.get().enableExampleFeature()) {
// feature logic
}
// In an event handler or tick method
String msg = ExampleConfig.get().greeting();
player.sendSystemMessage(Component.literal(msg));

Testing

Launch the NeoForge client. After the game loads, find .minecraft/config/examplemod-common.toml, change enableExampleFeature to false, save the file, and confirm the change is reflected without restarting (NeoForge reloads the config on file change).

On Fabric, the config file is at .minecraft/config/examplemod.json. Fabric does not hot-reload, so a restart is required for changes to take effect with the manual JSON approach.

You can find the source for this tutorial here:

View Source on GitHub
NEXT IN SERIES

Custom Sounds (MultiLoader 1.21+)

Register a SoundEvent with RegistrationProvider, provide an OGG audio file, generate sounds.json with SoundDefinitionsProvider, and play the sound server-side so it broadcasts to nearby clients.

Continue →