Skip to content

Maru32768/ConfigLib

Repository files navigation

ConfigLib - A Type-Safe Configuration API for Bukkit and Forge

ConfigLib is an advanced, type-safe Configuration API designed to simplify configuration management for Bukkit and Forge developers.

Features

  1. Type-Safety Configuration Handling
    Ensures configuration values are used in a type-safe manner directly within your code, reducing potential runtime errors and improving maintainability.
  2. Automatic JSON Mapping
    Automatically maps configuration data to Java objects using JSON, eliminating the need for manual parsing and data transformation.
  3. Automatic Configuration Reloading
    Monitors configuration files and reloads them automatically when changes are detected, ensuring your application always works with the latest settings.
  4. Automatic Configuration Saving
    Automatically saves updated configuration values to disk, preventing data loss and ensuring persistence.
  5. Command Generation for Configuration Management
    Seamlessly integrates with CommandLib to generate commands for managing configurations via the command line.
  6. Schema Migration
    Built-in versioned migration system allows safe evolution of configuration structure across releases — handling field renames, type changes, and validation constraint changes without breaking existing user data.
  7. Change History & Undo
    Every configuration change is automatically recorded with a timestamp. You can browse the history via command and revert to any previous state.

Requirements

  • Java 11 or later

Installation

To ensure stability, we recommend replacing latest.release with a specific version such as 0.16.0.
You can find the latest version on the CommandLib Release Page and ConfigLib Release Page.

Bukkit (Groovy DSL)
plugins {
    id "com.gradleup.shadow" version "8.3.5"
}

repositories {
    maven { url 'https://jitpack.io' }
}

dependencies {
    implementation "com.github.Maru32768.CommandLib:bukkit:latest.release"
    implementation 'com.github.Maru32768.ConfigLib:bukkit:latest.release'
}

shadowJar {
    archiveFileName = "${rootProject.name}-${project.version}.jar"
    relocate "net.kunmc.lab.commandlib", "${project.group}.${project.name.toLowerCase()}.commandlib"
    relocate "net.kunmc.lab.configlib", "${project.group}.${project.name.toLowerCase()}.configlib"
}
tasks.build.dependsOn tasks.shadowJar
Bukkit (Kotlin DSL)
plugins {
    id("com.gradleup.shadow") version "8.3.5"
}

repositories {
    maven { url = uri("https://jitpack.io") }
}

dependencies {
    implementation("com.github.Maru32768.CommandLib:bukkit:latest.release")
    implementation("com.github.Maru32768.ConfigLib:bukkit:latest.release")
}

tasks.named<ShadowJar>("shadowJar") {
    archiveFileName.set("${rootProject.name}-${project.version}.jar")
    relocate("net.kunmc.lab.commandlib", "${project.group}.${project.name.lowercase()}.commandlib")
    relocate("net.kunmc.lab.configlib", "${project.group}.${project.name.lowercase()}.configlib")
}
tasks.named("build") { dependsOn(tasks.named("shadowJar")) }
Forge (Groovy DSL)
plugins {
    id "com.gradleup.shadow" version "8.3.5"
}

repositories {
    maven { url 'https://jitpack.io' }
}

dependencies {
    implementation "com.github.Maru32768.CommandLib:forge:latest.release"
    implementation "com.github.Maru32768.ConfigLib:forge:latest.release"
}

shadowJar {
    archiveFileName = "${rootProject.name}-${project.version}.jar"
    dependencies {
        include(dependency("com.github.Maru32768.CommandLib:forge:.*"))
        include(dependency("com.github.Maru32768.ConfigLib:forge:.*"))
    }
    relocate "net.kunmc.lab.commandlib", "${project.group}.${project.name.toLowerCase()}.commandlib"
    relocate "net.kunmc.lab.configlib", "${project.group}.${project.name.toLowerCase()}.configlib"
    finalizedBy("reobfShadowJar")
}

reobf {
    shadowJar {
    }
}
Forge (Kotlin DSL)
plugins {
    id("com.gradleup.shadow") version "8.3.5"
}

repositories {
    maven { url = uri("https://jitpack.io") }
}

dependencies {
    implementation("com.github.Maru32768.CommandLib:forge:latest.release")
    implementation("com.github.Maru32768.ConfigLib:forge:latest.release")
}

tasks.named<ShadowJar>("shadowJar") {
    archiveFileName.set("${rootProject.name}-${project.version}.jar")
    dependencies {
        include(dependency("com.github.Maru32768.CommandLib:forge:.*"))
        include(dependency("com.github.Maru32768.ConfigLib:forge:.*"))
    }
    relocate("net.kunmc.lab.commandlib", "${project.group}.${project.name.lowercase()}.commandlib")
    relocate("net.kunmc.lab.configlib", "${project.group}.${project.name.lowercase()}.configlib")
    finalizedBy("reobfShadowJar")
}

reobf {
    create("shadowJar")
}

Code Examples

Defining Configuration Classes
public final class TestConfig extends BaseConfig {
    public final IntegerValue integerValue = new IntegerValue(10);
    public final StringValue stringValue = new StringValue("testValue");

    public TestConfig(Plugin plugin) {
        super(plugin);
        initialize();
    }
}
Generating and Registering Configuration Commands
public final class TestPlugin extends JavaPlugin {
    public void onEnable() {
        TestConfig testConfig = new TestConfig(this);
        Command root = new Command("test") {
        };

        // The following commands will be generated:
        // /test config get <key> - Gets a specific configuration value.
        // /test config list - Gets all configuration values.
        // /test config modify <key> <value> - Sets a specific configuration value.
        // /test config reload - Reloads the configuration file. You may not need it because there's automatic reloading.
        root.addChildren(new ConfigCommandBuilder(testConfig).build());

        CommandLib.register(this, root);
    }
}
Registering Multiple Configurations to Commands
public final class TestPlugin extends JavaPlugin {
    public void onEnable() {
        TestConfigA testConfigA = new TestConfigA(this);
        TestConfigB testConfigB = new TestConfigB(this);
        Command root = new Command("test") {
        };
        root.addChildren(new ConfigCommandBuilder(testConfigA).addConfig(testConfigB)
                                                              .build());

        CommandLib.register(this, root);
    }
}
Listening to Configuration Changes
public final class TestConfig extends BaseConfig {
    public final IntegerValue integerValue = new IntegerValue(10).onModify(x -> {
        System.out.println("Changed integerValue to " + x);
    });

    public TestConfig(Plugin plugin) {
        super(plugin);
        initialize();
    }
}
Defining Custom Value Classes

In this section, we explain how to implement a custom SingleValue class and a custom ListValue class, based on the following example class.

// Represents a custom data structure with an integer and a string.
// Used as a value in the configuration.
class TestClass {
    private final int n;
    private final String s;

    public TestClass(int n, String s) {
        this.n = n;
        this.s = s;
    }

    @Override
    public String toString() {
        return "TestClass{" + "n=" + n + ", s='" + s + '\'' + '}';
    }
}

SingleValue Implementation

import java.util.List;

// Custom SingleValue implementation for TestClass.
// Allows storing and manipulating a single instance of TestClass in configurations.
public final class TestClassValue extends SingleValue<TestClass, TestClassValue> {
    public TestClassValue(TestClass initialValue) {
        super(initialValue);
    }

    @Override
    protected List<ArgumentDefinition<TestClass>> argumentDefinitions() {
        List<ArgumentDefinition<TestClass>> res = new ArrayList<>();

        // Defines the arguments required to construct a TestClass instance.
        res.add(new ArgumentDefinition(new IntegerArgument("n"), new StringArgument("s"), (n, s, ctx) -> {
            // Converts command arguments to a TestClass instance.
            return new TestClass(n, s);
        }));

        return res;
    }

    // Converts a TestClass instance to its string representation.
    // This string will be used for command completion suggestions and for get/list command results.
    @Override
    protected String valueToString(TestClass testClass) {
        return testClass.toString();
    }
}

ListValue Implementation

// Custom ListValue implementation for TestClass.
// Allows managing a list of TestClass instances in configurations.
public final class TestClassListValue extends ListValue<TestClass, TestClassListValue> {
    public TestClassListValue(List<TestClass> initialValue) {
        super(initialValue);
    }

    @Override
    protected List<ArgumentDefinition<List<TestClass>>> argumentDefinitionsForAdd() {
        List<ArgumentDefinition<List<TestClass>>> res = new ArrayList<>();

        // Defines the arguments required for the Add command.
        // These arguments will be used to construct a new TestClass instance and add it to the list.
        res.add(new ArgumentDefinition(new IntegerArgument("n"), new StringArgument("s"), (n, s, ctx) -> {
            // Converts command arguments to a new TestClass instance and adds it to the list.
            return Collections.singletonList(new TestClass(n, s));
        }));

        return res;
    }

    @Override
    protected List<ArgumentDefinition<List<TestClass>>> argumentDefinitionsForRemove() {
        List<ArgumentDefinition<List<TestClass>>> res = new ArrayList<>();

        // Defines the arguments required for the Remove command.
        // These arguments will be used to identify which TestClass instance to remove from the list.
        res.add(new ArgumentDefinition(new StringArgument("target", opt -> {
            opt.suggestionAction(sb -> {
                for (TestClass v : value) {
                    sb.suggest(v.toString());
                }
            });
        }, StringArgument.Type.PHRASE_QUOTED), (input, ctx) -> {
            // Finds a TestClass instance to remove based on user input.
            TestClass target = value.stream()
                                    .filter(x -> x.toString()
                                                  .equals(input))
                                    .findFirst()
                                    .orElseThrow(() -> new InvalidArgumentException(input + " is invalid"));
            return Collections.singletonList(target);
        }));

        return res;
    }

    // Converts a TestClass instance to its string representation.
    // This string will be used for command completion suggestions and for get/list command results.
    @Override
    protected String elementToString(TestClass testClass) {
        return testClass.toString();
    }
}

These custom Value classes can be used in the same way as built-in Value classes.

public final class TestConfig extends BaseConfig {
    public final TestClassValue testClassValue = new TestClassValue(null);
    public final TestClassListValue testClassListValue = new TestClassListValue(new ArrayList<>());

    public TestConfig(Plugin plugin) {
        super(plugin);
        initialize();
    }
}
Schema Migration

ConfigLib provides a versioned migration system to evolve your configuration schema safely across releases. Migrations are registered via Option and run automatically when an older config file is loaded. The current version is stored as _version_ in the JSON file.

public final class MyConfig extends BaseConfig {
    public final StringValue message = new StringValue("hello");
    public final EnumSetValue<EntityType> spawnTypes = new EnumSetValue<>(EntityType.class);

    public MyConfig(Plugin plugin) {
        super(plugin, opt -> opt
                // Version 1: rename field
                .migration(1, ctx -> {
                    ctx.rename("msg", "message");
                })
                // Version 2: type change (IntegerValue -> StringValue)
                .migration(2, ctx -> {
                    if (ctx.has("cooldown")) {
                        ctx.setString("cooldown", String.valueOf(ctx.getInt("cooldown")));
                    }
                })
                // Version 3: remove value that no longer passes validation
                .migration(3, ctx -> {
                    EnumSetValue<EntityType> types = ctx.getObject("spawnTypes",
                                                                   new TypeToken<EnumSetValue<EntityType>>() {
                                                                   }.getType());
                    types.remove(EntityType.GIANT);
                    ctx.setObject("spawnTypes", types);
                }));
        initialize();
    }
}

MigrationContext provides the following operations:

Method Description
has(key) Check if a key exists
getString(key) / getInt(key) / getDouble(key) / getBoolean(key) Read primitive values
getObject(key, Class<T>) Read a complex value by class
getObject(key, Type) Read a generic complex value (e.g. with TypeToken)
setString(key, value) / setInt / setDouble / setBoolean Write primitive values
setObject(key, value) Write a complex value
rename(from, to) Rename a field
remove(key) Remove a field

Migrations only run on existing files that have an older version. New installations start at the latest version and skip all migrations.

Usage Notes

  1. Calling initialize() initialize() must be called at the end of the concrete config class constructor, after all fields have been assigned. This ensures that the initial config load and file watching start only after the subclass is fully constructed.
    public final class TestConfig extends BaseConfig {
        public final IntegerValue integerValue = new IntegerValue(10);
    
        public TestConfig(Plugin plugin) {
            super(plugin);
            initialize(); // Must be the last statement
        }
    }
  2. Asynchronous Change Detection Change detection, including when modifying values with the set method, is handled asynchronously. Keep this in mind to avoid race conditions in your application logic.

Generated Commands

ConfigCommandBuilder automatically generates a command tree from your config classes. The examples below use /config as the root command name (set via .name("config")).

Config-level subcommands

These subcommands operate on the config as a whole.

Command Description
/config list Show all field values
/config reload Reload from file
/config reset Reset all fields to their default values

Use .disableList() / .disableReload() / .disableReset() on ConfigCommandBuilder to suppress any of these.

With multiple configs, each subcommand also accepts a config name:

Command Description
/config list <configName> List fields for that config
/config reload <configName> Reload that config
/config reset <configName> Reset that config
/config <configName> List fields for that config (alias)
Per-field subcommands

These subcommands are generated for each Value field. <field> is the Java field name, or the name set via .entryName().

Get

Command Description
/config <field> Show the current value (hover for description)
/config <configName>.<field> Same, always available even with multiple configs

SingleValue — set

Command Description
/config <field> <value> Set the value (shorthand)
/config <field> set <value> Set the value
/config <field> reset Reset to the default value

NumericValue — arithmetic (IntegerValue, DoubleValue, FloatValue) — extends SingleValue, so set and reset also apply

Command Description
/config <field> inc Increment by 1
/config <field> inc <amount> Increment by amount (capped at max)
/config <field> dec Decrement by 1
/config <field> dec <amount> Decrement by amount (floored at min)

CollectionValue — add / remove / clear (ListValue, SetValue)

Command Description
/config <field> add <element> Add an element
/config <field> remove <element> Remove an element
/config <field> clear Remove all elements
/config <field> reset Reset to the default value

Disable individual operations with .disableAdd() / .disableRemove() / .disableClear().

MapValue — put / remove / clear

Command Description
/config <field> put <key> <value> Add or update an entry
/config <field> remove <key> Remove an entry
/config <field> clear Remove all entries
/config <field> reset Reset to the default value

Disable individual operations with .disablePut() / .disableRemove() / .disableClear().

Use .disableGet() / .disableModify() on ConfigCommandBuilder to suppress get or modify commands globally.

Change history, undo, and diff

Every time a configuration value is modified, the new state is automatically saved to a history file (<configName>.history.json) alongside the config file. The history persists across server restarts and is capped at 50 entries by default (override createConfigStore() to change this).

History uses 0-based indexing where [0] is the current (latest) state.

Single config

Command Description
/config history List all entries with timestamps (hover to preview values)
/config history <index> Show field values at that index
/config history diff <index> Diff current state vs history entry
/config history diff <index1> <index2> Diff between two history entries
/config history undo Revert to the previous state
/config history undo <N> Revert N steps back
/config undo Revert to the previous state
/config undo <N> Revert N steps back
/config diff <index> Diff current state vs history entry
/config diff <index1> <index2> Diff between two history entries

Multiple configs — prefix with the config name using either order:

Command Description
/config history <configName> List entries for that config
/config history <configName> <index> Show field values at that index
/config history <configName> diff <index> Diff for that config
/config history <configName> undo [N] Undo for that config
/config undo <configName> [N] Undo for that config
/config diff <configName> <index> Diff for that config
/config <configName> history [index] Alternative prefix order
/config <configName> undo [N] Alternative prefix order
/config <configName> diff <index> Alternative prefix order

diff shows only fields that differ, formatted as fieldName: <old> → <new>.

To hide history commands from the generated command tree:

public final class MyConfig extends BaseConfig {
    public MyConfig(Plugin plugin) {
        super(plugin);
        disableHistory();
        initialize();
    }
    // ...
}

Use .disableHistory() on ConfigCommandBuilder to suppress history commands globally.

Claude Code Integration

If you use Claude Code, a /configlib skill is available to help you write ConfigLib code. It loads the API into context and provides usage conventions tailored to library consumers.

Setup: Copy .claude/skills/configlib/ from this repository into your project's .claude/skills/ directory.

Usage:

/configlib How do I define a config with an IntegerValue and a StringValue?
/configlib Write a custom SingleValue for org.bukkit.World

Sample Projects

Bukkit
Forge

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors