Skip to content

Builder API

Endor H edited this page Dec 17, 2022 · 5 revisions

If you're using Kotlin in your mod, it's highly recommended you use the Kotlin API instead, as it's easier and safer to use

TLDR: You can instead read the commented DemoConfigCategory included in Simple Config, if you just want to see how things work


The Simple Config API lets you define 3 config files per mod (one per config type). The first thing you should do is decide which types of config you're going to need (probably only client, server or both).

To define config files, Simple Config exposes a builder API, and lets you specify a class that will be bound automatically to your config file (in more detail below).

Config files consist of an ordered set of entries, which may be organized in categories (each with their own tab in the menu) and groups (which may contain subgroups without any limitation).

Declaring a Config File

It is recommended you create a class for each of your config files (for instance, ClientConfig or ServerConfig). Inside that class, create a static method named register (or however you wish), and make sure to call this method from the constructor of your mod.

In the case of the client config, you may need to make this call through a DistExecutor, to ensure you only declare the client config on the client side (The server config will need to be defined in both, as clients need to understand the copy they receive (see Config Types)).

You should add a static import to your source file for endorh.simpleconfig.api.ConfigBuilderFactoryProxy.*, which exposes all the factory methods necessary to create the builders you'll need.

Then, in your register method call the config method, passing in your mod ID, config type and containing class reference, like config("yourmodid", SimpleConfig.Type.CLIENT, ClientConfig.class). This method will return a SimpleConfigBuilder object.

Rather than saving this builder object in a variable, you'll want to call methods on it instead, as all of its methods will return itself modified, so you can keep chaining declarations. The most important methods you can call on config builders are add and n.

  • .add("entry_name", ...) will add an entry with name "entry_name" to the config
  • .n(category("category_name")...) will add a category with name "category_name" to the config
  • .n(group("group_name")...) will add a group with name "group_name" to the config

Both the category and group factory methods return each a specific builder object you may use to add entries/subgroups to the respective category/group (keep in mind that categories can only be added at root level, as they each have a tab in the menu).


After you've defined all your config entries/categories/groups, you should end the method chain with a call to buildAndRegister, which will finish the setup process to create your config file.

You should add the entries/subgroups for categories and groups within the parentheses of the call to the n method of the parent, and each method call should start a new line, for clarity. Some people prefer to place the dots of a method call split across lines at the end of the line, but I recommend you place them at the start. For example, your config declaration could be looking like this:

config("yourmodid", SimpleConfig.Type.CLIENT, ClientConfig.class)
  .add("add_sparkles", yesNo(true))
  .add("sparkly_sparkles", yesNo(true))
  .add("sparklier_sparkles", yesNo(true))
  .n(
    group("sparkles")
      .add("sparkliness", number(100).range(0, 100).slider())
  ).buildAndRegister();

It is highly recommended you configure your code formatter to have a small continuation indent, unless you want to suffer for no reason.

The recommendation to start a new line for each method call is for your sake. This API is intended to be used with this formatting style, and it won't be pretty otherwise (if you want an actually pretty API, use the Kotlin API).

Another style option is creating a method to declare categories/groups, and call these methods from the n call. That ultimately depends on you feeling more comfortable with the declarations being more or less scattered.

Config entries

As you may have seen in the last example, config entries are created using methods (coming from the ConfigBuilderFactoryProxy), and also return builder objects, as you can see in the call to number(100).range(0, 100).slider().

The different entry types provided by this API have their own section.

Config entry builders are actually pretty special builders, as they are immutable. This means all of their methods return modified copies of themselves, so you can reuse builders in any way or form, without fearing side effects. For example, you can use modified copies of the same base builder to define similar entries:

IntegerEntryBuilder slider1to10 = number(10).range(1, 10).slider();
config("yourmodid", SimpleConfig.Type.CLIENT, ClientConfig.class)
  .add("fuzzyness", slider1to10.withValue(9))
  .add("fuzzier_fuzzyness", slider1to10.withValue(10))
  .buildAndRegister();
Pro tip

In case you don't know, pressing Ctrl+Alt+V (by default) in IntelliJ IDEA will let you extract a variable from whatever is under your caret, even automatically replacing uses of the same expression repeated in the same scope (this action is technically named Introduce Variable, and you may find it in the context menu Refactor This that appears if you press Ctrl+Alt+Shift+T, along with some other useful actions).

You may think this doesn't save you too much time, but considering you probably don't know the names of the builder types, and that some builder types can have up to 8 type parameters, guessing the variable type correctly won't be as easy as you think.

Backing fields

At the start of this page, we mentioned that you can bind a class to a config file. What this means is that Simple Config will automatically find static fields of the proper types within the config class you specified when you declared your config, and update them any time the config file is modified.

This strategy of loading the latest config values from the file into fields is called baking the config. Baking config values has many advantages, mainly being:

  • Accessing a field is faster than looking up a config entry by name.
  • Accessing a field is safer, as you cannot misspell the field name and compile without error.
  • Accessing a field is more concise than looking up a config entry by name.

And, since Simple Config automatically does the heavy lifting of updating all backing fields for you, the only additional effort you'll need to make to access baked fields, is declaring them.

Declaring backing fields

Simple Config will look for static fields with the same name as your config entries in the class you've passed as argument to the config method at the beginning of this page. Similarly, it will look for inner static classes with the same name of your config categories/groups, and match their fields with the entries of the respective category/group.

Declaring backing fields for a config is a delicate step, as they must match the names and types of their entries. The Simple Config API defines the @Bind annotation, which marks fields and inner static classes which are expected to match a config entry/category/group, so a detailed exception can be thrown at load time if they do not. Using @Bind annotations is completely optional, but advised.

Likewise, fields that do not match their entry types will also throw a load time exception that should help you locate the problem.

public class ClientConfig {
    static void build() {
        config("yourmodid", SimpleConfig.Type.CLIENT, ClientConfig.class)
          .add("sample_entry", string(""))
          .buildAndRegister();
    }
    
    // The @Bind annotation is optional
    @Bind public static String sample_entry;
}

Translations & Comments

You may notice there are no methods in the entry builder types to specify translations or comments for users to understand what the entry does. This is by design. Instead of letting you type the same twice, one as a string literal in your config file, and another in your translations JSON (if you even consider translating your menu), Simple Config automatically maps translation keys for your config entries.

For example, the config key for an entry named "multilingual" at the root of the client config of a mod with ID "<modid>" would use as translation keys:

  • "<modid>.config.client.multilingual" for the title of the entry
  • "<modid>.config.client.multilingual:help" for the tooltip of the entry

Simple Config is smart enough to simply use the entry name if you haven't defined these keys. The tooltip key is also optional to define.

The comment displayed in the config file is created by appending the tooltip to the title of each entry, so both the menu and the file have display all possible information to the user, translated if available.

This is explained in its own section, Translating Config Menus, and enables the community to translate config menus of mods to their own languages.

Errors & Tooltips

You may define specific filters, or provide tooltips dependent on the entry values of entries by using the error and tooltip methods on entry builders. These accept a lambda function where you can return an optional error message for a certain value for the entry.

This will prevent invalid values from being loaded (replacing them by their defaults), and also help players understand why they cannot set a certain value in the menu.

If you're too lazy, you can also use the check method in which you can simply pass a predicate, and Simple Config will display a generic error message for invalid values, but you're encouraged to display proper error messages to the players.

Keep in mind that entry builders are immutable, and you should reuse them if multiple of them have the same shared behavior, for example,

StringEntryBuilder lowerString = string("").maxLength(40).error(
  s -> !s.equals(s.toLowerCase())
       ? Optional.of(Component.translatable("simpleconfig.config.error.not_lowercase", s))
       : Optional.empty());
config("yourmodid", SimpleConfig.Type.CLIENT, ClientConfig.class)
  .add("name_1", lowerString.withValue("steve"))
  .add("name_2", lowerString.withValue("alex"))
  .buildAndRegister();

Samples

The Simple Config mod includes a working demo category, written with the Java API, which could come in handy as inspiration.

You may read the source for this demo category here on GitHub.

The demo also explains advanced features like baking methods and secondary/transformed backing fields, which can be very useful to access config values more efficiently. It also provides examples of all the different existing Entry Types.

You may also take inspiration from config files of existing mods using the Simple Config API, like Aerobatic Elytra's client and server configs.


Declarative API

There's also a declarative and extensible API based in annotations, which wraps the builder API in a way that makes it easier to use. Since it creates entries from their backing fields, the annotation API avoids entirely the problem of binding the fields.

For its simplicity, the annotation API is the recommended one. You should only need to use the builder API when extending the annotation API.