Configuration
The configuration system lets mods define settings files that are automatically loaded and saved. Use the withConfig() API with BuilderCodec for type-safe config values.
Basic Setup
Section titled “Basic Setup”1. Define a Config Class
Section titled “1. Define a Config Class”Create a class with your settings as fields:
public class MyConfig { public static final BuilderCodec<MyConfig> CODEC = BuilderCodec.builder( MyConfig.class, MyConfig::new ) .append(new KeyedCodec<>("MaxPlayers", Codec.INTEGER), (cfg, val) -> cfg.maxPlayers = val, cfg -> cfg.maxPlayers) .add() .append(new KeyedCodec<>("WelcomeMessage", Codec.STRING), (cfg, val) -> cfg.welcomeMessage = val, cfg -> cfg.welcomeMessage) .add() .build();
private int maxPlayers = 100; // default value private String welcomeMessage = "Welcome!"; // default value
// getters public int getMaxPlayers() { return maxPlayers; } public String getWelcomeMessage() { return welcomeMessage; }}2. Register in Your Mod
Section titled “2. Register in Your Mod”Initialize the config as a field (must be done during field initialization, not in lifecycle methods):
public class MyMod extends JavaPlugin { private final Config<MyConfig> config = withConfig(MyConfig.CODEC);
public MyMod(JavaPluginInit init) { super(init); }
@Override protected void start() { // access config values MyConfig cfg = config.get(); int max = cfg.getMaxPlayers(); String msg = cfg.getWelcomeMessage(); }}3. Config File Location
Section titled “3. Config File Location”The config file is created at:
mods/{group}_{name}/config.jsonThe directory name uses your mod’s group and name from manifest.json, joined with underscore.
Example content:
{ "MaxPlayers": 100, "WelcomeMessage": "Welcome!"}Codec Types
Section titled “Codec Types”Use Codec.STRING, Codec.INTEGER, Codec.BOOLEAN, etc. for field types. See Codecs for the full list.
Validation
Section titled “Validation”Add validators between .append() and .add() to validate config values:
.append(new KeyedCodec<>("MaxPlayers", Codec.INTEGER), (cfg, val) -> cfg.maxPlayers = val, cfg -> cfg.maxPlayers).addValidator(Validators.nonNull()) // required field.addValidator(Validators.range(1, 1000)) // must be 1-1000.add()Common validators:
| Validator | Purpose |
|---|---|
nonNull() | Field is required |
range(min, max) | Value between min and max (inclusive) |
min(value) | Value >= minimum |
max(value) | Value <= maximum |
greaterThan(value) | Value > threshold |
lessThan(value) | Value < threshold |
equal(value) | Must equal specific value |
notEqual(value) | Must not equal value |
nonEmptyString() | String must not be empty |
nonEmptyArray() | Array must have elements |
nonEmptyMap() | Map must have entries |
arraySize(n) | Array must have exactly n elements |
arraySizeRange(min, max) | Array size in range |
uniqueInArray() | No duplicate elements |
or(v1, v2, ...) | Any validator passes |
Validation errors are logged during config loading.
Custom Validators
Section titled “Custom Validators”Implement Validator<T> for custom validation logic:
// inline lambda.addValidator((value, results) -> { if (value % 2 != 0) { results.fail("Must be an even number"); }})
// reusable validatorpublic static <T extends Comparable<T>> Validator<T> exclusiveRange(T min, T max) { return (value, results) -> { if (value != null && (value.compareTo(min) <= 0 || value.compareTo(max) >= 0)) { results.fail("Must be between " + min + " and " + max + " (exclusive)"); } };}Multiple Config Files
Section titled “Multiple Config Files”Use withConfig(name, codec) to register additional config files:
private final Config<GeneralConfig> general = withConfig(GeneralConfig.CODEC); // config.jsonprivate final Config<SpawnConfig> spawns = withConfig("spawns", SpawnConfig.CODEC); // spawns.jsonprivate final Config<RewardsConfig> rewards = withConfig("rewards", RewardsConfig.CODEC); // rewards.jsonAll registered configs are loaded in parallel during preLoad() and stored in mods/{group}_{name}/.
Dynamic Data Files
Section titled “Dynamic Data Files”For data that isn’t tied to a specific entity — world-level settings, leaderboards, arena configs — use the codec system directly with RawJsonReader and BsonUtil:
public class MyPlugin extends JavaPlugin { // static config for mod settings private final Config<MyConfig> config = withConfig(MyConfig.CODEC);
// per-world data loaded at runtime private final Map<String, WorldData> worldData = new HashMap<>();
@Override protected void start() { // load data for each world for (World world : Universe.get().getWorlds().values()) { Path path = getDataDirectory() .resolve("worlds/" + world.getName() + ".json"); WorldData data = RawJsonReader.readSyncWithBak( path, WorldData.CODEC, getLogger()); if (data == null) { data = new WorldData(); // defaults if file doesn't exist } worldData.put(world.getName(), data); } }
// save a specific world's data public void saveWorldData(String worldName) { WorldData data = worldData.get(worldName); if (data != null) { Path path = getDataDirectory() .resolve("worlds/" + worldName + ".json"); BsonUtil.writeSync(path, WorldData.CODEC, data, getLogger()); } }}BsonUtil.writeSync() and BsonUtil.writeDocument() automatically create parent directories, so you don’t need to create worlds/ manually.
This is the same pattern used by built-in plugins — TeleportPlugin saves warps with BsonUtil.writeDocument(), and BarterShopState persists shop state with BsonUtil.writeSync().
Reading and Writing Utilities
Section titled “Reading and Writing Utilities”| Method | Behavior |
|---|---|
RawJsonReader.readSync(path, codec, logger) | Read JSON file synchronously. Throws IOException if the file doesn’t exist |
RawJsonReader.readSyncWithBak(path, codec, logger) | Read with .bak fallback. Returns null if neither file exists |
BsonUtil.writeSync(path, codec, value, logger) | Write JSON synchronously, creates .bak backup |
BsonUtil.writeDocument(path, doc) | Write BsonDocument asynchronously, returns CompletableFuture |
BsonUtil.readDocument(path) | Read BsonDocument asynchronously, returns CompletableFuture |
For the async variants, encode/decode manually:
// async writeBsonDocument doc = MyData.CODEC.encode(data, new ExtraInfo());BsonUtil.writeDocument(path, doc);
// async readBsonDocument doc = BsonUtil.readDocument(path).join();MyData data = MyData.CODEC.decode(doc, ExtraInfo.THREAD_LOCAL.get());Data Directory
Section titled “Data Directory”Use getDataDirectory() for all mod file storage:
Path dataDir = getDataDirectory(); // mods/{group}_{name}/You can create any subdirectory structure under this path:
mods/mygroup_mymod/├── config.json # from withConfig()├── spawns.json # from withConfig("spawns", ...)└── worlds/ # from manual file I/O ├── overworld.json └── nether.jsonRuntime Operations
Section titled “Runtime Operations”The Config<T> object provides methods for runtime config management:
// get the loaded valueMyConfig cfg = config.get();
// save current state to disk (returns CompletableFuture)config.save();Calling config.load() again after the initial load completes will re-read the file from disk. During an in-flight load, duplicate load() calls return the same future to prevent concurrent reads.