-
Notifications
You must be signed in to change notification settings - Fork 1
Annotation API
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 DemoDeclarativeConfigCategory included in Simple Config
The easiest way to declare config menus in Java is with annotations on fields. This is straightforward since the entries they create are automatically bound to the fields.
- Declaring a Config File
- Creating Config Entries
- Creating Config Groups/Categories
- Customizing Entries
- Bean Entries (Create your own config types easily)
- Caption Entries
- Inner Default Values
- Custom Annotations (Applying multiple annotations at once)
- Sibling Methods (Baking/Errors/Tooltips)
-
The
@ConfigureAnnotation (Using the builder API with custom annotations) - Samples
The annotation API is just a fancy wrapper for the Builder API, eliminating the gap between config entries and their backing fields.
To declare a config file, all you need to do is create a class and annotate it with @ConfigClass.
The annotation should contain the mod ID and the config type of the file. Each mod can only have one config file per type.
@ConfigClass(modId=ExampleMod.MOD_ID, type=SimpleConfig.Type.CLIENT)
public class ClientConfig {
@Entry public static entry = "value";
}This class should be abstract/final and only contain static fields, which will be used to declare config entries.
Every config file/category/group can be associated with a class, in which it'll look for static fields to bind to entries.
However, it's also possible to create entries from the fields directly, by annotating them with @Entry.
Fields annotated with @Entry create entries of their type in their associated file/category/group, and become automatically bound to them.
The entries created preserve the order of the fields, and have the initial value of the fields as default value.
The supported field types include all built-in entry types. Primitive types (i.e., int instead of Integer) are also supported indistinctively.
Generic types, like List, Set, Map, Pair and Triple are supported, as long as their type parameters are also supported.
@ConfigClass(modId=ExampleMod.MOD_ID, type=SimpleConfig.Type.CLIENT)
public class ClientConfig {
@Entry public static boolean boolEntry = true;
@Entry public static int intEntry = 42;
@Entry public static String stringEntry = "Hello, World!";
@Entry public static Axis enumEntry = Axis.X;
@Entry public static List<Integer> intListEntry = Arrays.asList(0, 1);
@Entry public static Map<String, Direction> = Util.make(new HashMap<>(), m -> {
m.put("a", Direction.NORTH);
});
}You can easily create config categories or groups as inner classes of a config class, annotated with @Category or @Group respectively.
Since the Java language doesn't support retrieving the declaration order of inner classes relative to fields without bytecode analysis, by default all groups in a file/category/group are added after all of its sibling entries, and the order of the groups is random.
To specify where a group should be added explicitly, you can define a marker field, annotated with @Group, with Void type and the same name as the group, followed by $marker. This is also possible for categories, to explicitly define an order between categories.
@Group public static Void Group$marker; // Marker field
@Group public static class Group {
// The inner classes can then contain more entries/groups
@Entry public static innerEntry = "value";
}
// Categories get their own tab in the config menu
@Category public static Void Category$marker; // Marker field
@Category public static class Category {
@Entry public static entry = "value";
}Categories must be defined as inner classes of the root config class for the file. Groups can be declared within other categories and groups without limit.
Each category has its own tab in the menu. Categories can have a different background texture each, and an icon and color for their tab button.
You can configure the background and tab button color with arguments to the @Category annotation.
To specify an icon with the annotation API, you can create a method, annotated with @Bind named getIcon which takes no argument and returns an Icon. This method will only be called once, so you can't use it to create an animated icon (to do so just return an AnimatedIcon).
If you define any entries/groups not within any category, they'll be placed in the default category for the file.
To customize this default category, you may pass the arguments to the @ConfigClass annotation directly, and define the getIcon method within the config file class too.
@ConfigClass(
modId=ExampleMod.MOD_ID, type=SimpleConfig.Type.CLIENT,
background="textures/block/oak_planks.png", color=0x808080FF
) public static class ClientConfig {
@Category(background="textures/block/birch_planks.png", color=0x8080FF80)
public static class Category {
@Bind public static Icon getIcon() {
return SimpleConfigIcons.Actions.ADD
}
}
}Simple Config provides some built-in annotations that can modify the entries created by a field. These annotations can be applied to both fields, their types, or their type parameters, to configure the inner entries of composite entry types, such as lists.
@Category public static class ConfigCategory {
// A slider between 0 and 10
@Min(0) @Max(10) @Slider
@Entry public static int sliderInt = 5;
// A list of at most 4 integers, each of them between 0 and 10
@Entry public static @Size(max=4) List<
@Min(0) @Max(10) Integer
> intList; // defaults to an empty list
}List of basic built-in annotations
-
Marker annotations
-
@RequireRestartmarks the entry as requiring a restart for changes to be effective -
@NonPersistentmakes the entry only available in the menu, not in the file. Non persistent entries can be used to enable profiling/debugging features you wouldn't want to leave enabled between game restarts. -
@Experimentalmarks the entry as related to an experimental feature. Users will be warned that modifying it may result in bugs. -
@Advancedmarks the entry as an advanced option. Users will be encouraged to ensure they know what they're doing modifying it. -
@Operatormarks the entry as being only relevant for server operators.
-
-
Numeric entries
-
@Minand@Maxcan change the allowed range of the entry -
@Slidermarks an entry as a slider, and can set a different slider min and max -
@Bake.Scalecan be applied to float and double entries, multiplying their value by the given scale before being baked into the field
-
-
Color entry
-
@HasAlphaallow transparency
-
-
String entry
-
@Lengthlimit min and max length -
@Suggestsuggest values, either hardcoded or provided by a specified method
-
-
Collections (list/set/map)
-
@Sizelimit min and max size
-
-
Map entry
-
@Linkedpreserve order in map
-
In addition to built-in types, it's possible to use bean classes as @Entry types. For a class to be accepted as a valid bean class, it must be annotated with the @Bean annotation.
In addition, for its properties to be editable in files/menus, they must be fields annotated with @Entry, as if they were inside the class of a config file/category/group.
@Category public static class ConfigCategory {
@Entry public static DemoBean beanEntry = new DemoBean();
@Entry public static Map<String, DemoBean> beanMap;
}
// Bean class
@Bean public static class DemoBean {
// Field properties annotated as `@Entry`
@Entry public String name = "Name";
@Entry @Min(0) @Max(10) public int number = 0;
@Entry public Pair<String, @Min(0) @Max(10) @Slider Integer> pair = Pair.of("a", 0);
// It's possible to have sub bean properties, but only if
// they do not result in a cyclic reference from any of the
// beans involved to itself
// While bean entries in config classes can be left uninitialized,
// sub bean properties in bean classes must always be initialized
@Entry public SubBean subBean = new SubBean();
@Bean public static class SubBean {
@Group.Caption public String name = "";
@Entry public int value = 0;
}
}Since it's encouraged to create bean classes specifically to be used in configuration files, for convenience, it's not necessary to define an equals method in bean classes, although it's recommended you do so if you plan to use your bean for anything else.
Simple Config allows some entries, as well as config groups to have a caption entry.
Caption entries are displayed besides the title of their target. This is only intended for cases where the meaning of the caption is clear and unambiguous, such as a master volume slider as the caption of a group of volume sliders, or a boolean toggle to enable/disable a feature as the caption of a group of settings related to that feature.
To make an entry the caption of a group or a bean, annotate it with @Group.Caption. It's possible to omit the @Entry annotation in this case.
@Group public static class FuzzyFeature {
@Group.Caption public static boolean enabled = true;
@Entry public static int fuzziness = 1;
}To create a captioned list/set/map, you can simply create a list of type Pair<Caption, List/Set/Map> where Caption is the type of the caption entry.
This can be used when the captioned collection is an inner type of yet another entry.
@Group public static class ConfigGroup {
@Entry public static Pair<String, List<Integer>> captioned_list =
Pair.of("str", asList(2, 4));
}Alternatively, you can create two adjacent fields, annotating the first one with @Entry.Caption instead of @Entry, which will make it the caption of the second. This is convenient as it'll let you access the caption and its entry from two separate fields.
@Group public static class ConfigGroup {
@Entry.Caption public static String caption = "str";
@Entry public static List<Integer> list = asList(2, 4);
}Please refrain from abusing caption entries in cases where it'll only confuse players, since they can't have their own entry title in the menu.
You can specify the default value for inner entries of composite entries (such as the elements of a list) with the @Default annotation, which receives a default value in YAML notation.
@Group public static class ConfigGroup {
@Entry public static List<
@Default("[2, 4]") List<@Default("42") Integer>
> intListList = asList(asList(2, 4), asList(6, 8));
}You must make sure that the type of the default value matches the entry, or an exception will be thrown at load time. Inner default values are used when adding new elements to lists/maps.
The builder API makes it easy to reuse configuration for entries, by making builders immutable and reusable.
The annotation API offers a different feature to reuse common configuration for entries, composing annotations. When Simple Config creates an entry from a field, it looks for its annotations, as well as the annotations their annotations have.
This makes possible to declare custom annotations a set of common options. Only one instance of an annotation type can be applied to a single entry. When multiple are found, the ones closer to the entry are used. This means that annotations directly applied to the entry always have priority.
@Group public static class configGroup {
// We define an annotation `@Slider10` which applies `@Min(0) @Max(10) @Slider`
// To be able to use your own annotation in fields and generic parameters,
// you must specify `FIELD` and `TYPE_USE` as `@Target`
// To be able to use your own annotation in other annotations
// you must specify `ANNOTATION_TYPE` as `@Target`
// For Simple Config to be able to scan your annotation,
// you must specify the `@Retention` as `RUNTIME`
@Target({ElementType.FIELD, ElementType.TYPE_USE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
// We apply the annotations we want the entries to have to the @interface declaration
@Min(0) @Max(10) @Slider
public @interface Slider10 {}
@Entry @Slider10 public static int slider10 = 5;
// We can override the annotations inherited from `@Slider10`
// The order in which they are applied doesn't affect the result
@Entry @Max(20) @Slider10 public static int slider20 = 10;
}While it's a drag, you need to specify the @Target and @Retention for all annotation types you define. That's just how shitty Java is.
For even more powerful custom annotations, see the @Configure annotation shown later in this page.
Simple Config lets you define what we call sibling methods for your entries. A sibling method is simply a method with the same name as a field, followed by a suffix, separated with $.
Simple Config supports a few sibling methods:
-
$bakereceives and returns the same type as the entry. Is applied before saving a new entry in the field. (Useful for preprocessing) -
$errorreceives the same type as the entry, and it's expected to return either anOptional<Component>error message, a nullableComponenterror message, a nullableStringtranslation key for the error message, or abooleanwhich istrueif an unspecified error is detected. -
$tooltipreceives the same type as the entry or no parameters, and it's expected to return either aList<Component>of tooltip lines, a singleComponentline, or aStringtooltip translation key (which may contain line breaks)
In general it's not recommended to use $tooltip, as entries already get automatically mapped translation keys for their tooltips (see Translating Config Menus). In general you shouldn't need dynamic tooltips.
For the $bake and $error it's better to use the @Bake or @Error annotations, which receive instead a method reference that can then be used for multiple entries. The recommended way to use this annotations is by defining a custom annotation within a class so method their method references become relative to such class, and the logic can be kept all in one place.
Instead of using @Bake or @Error you may want to use @Configure directly, which offers the most flexibility.
The methods specified by the @Bake annotation can receive the instance of the annotation type they're applied to, to use its arguments.
// Class used to hold your custom annotations, along
// with their associated methods
public static class MyCustomAnnotations {
@Target({ElementType.FIELD, ElementType.TYPE_USE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
// Binds to methods named `map` in the `MyConfigAnnotations` class
@Bake(method="map")
public @interface Mapped {
// Arguments of the annotation
float inputMin() default 0;
float inputMax() default 1;
float outputMin() default 0;
float outputMax() default 10;
}
// Baking methods for entry types that support the @Mapped annotation
// It's possible to use primitive types in the method signature
// If the annotation doesn't have arguments, or they aren't required,
// the method can omit the second parameter
// If we'd used `@Configure` instead of `@Bake` we'd also be able to
// set the min and max entries of the entry based on the
// `inputMin` and `inputMax` arguments
public static float map(float value, Mapped s) {
return (value - s.inputMin()) / (s.inputMax() - s.inputMin())
* (s.outputMax() - s.outputMin()) + s.outputMin();
}
// This sample annotation only supports float and double entries
// If we applied the @Mapped annotation to any other type,
// an exception would be thrown at load time
public static double map(double value, Mapped s) {
return (value - s.inputMin()) / (s.inputMax() - s.inputMin())
* (s.outputMax() - s.outputMin()) + s.outputMin();
}
}
@Category public static class ConfigCategory {
// The entries generated for the below entries will be baked by the
// methods defined above
@Entry @Mapped public static float mapped_float;
@Entry @Mapped(outputMin=10, outputMax=5) public static double mapped_double;
}For more complex needs than the ones described above, the annotation API has a special annotation, @Configure, which, unlike any other, can be applied multiple times (from different source annotations) to combine their effects.
This annotation lets you specify a method reference that will use the builder API to modify the generated entry for a field.
These method references can be relative to the class where a custom annotation is defined, if the @Configure annotation is being applied to a custom annotation, which lets you group all your decorator methods together in an utility class.
As with the @Bake annotations, the methods specified by @Configure when the annotation is applied to a custom annotation can receive as a second parameter the specific instance of this annotation type that was applied to the entry being built, so you can use the arguments of your custom annotations.
When an entry is annotated with @Configure, Simple Config will search for the most specific decorator method that can be applied to the entry type.
For convenience, if none is found an error isn't thrown, so you can create your own annotation hierarchies without having to worry about making all related annotations support the same set of types.
// Class used to hold your custom annotations, along
// with their decorator methods
public static class MyCustomAnnotations {
// Configure the annotation to be usable in config fields
@Target({ElementType.FIELD, ElementType.TYPE_USE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
// Method reference relative to the `MyCustomAnnotations` class
@Configure("decExtraSlider")
public @interface ExtraSlider {
// Annotation arguments
int min() default 0;
int extraMin() default 0;
int max() default 100;
int extraMax() default 200;
}
// Decorator methods for entry types that support the @ExtraSlider
// IntegerEntryBuilder applies to `int` entries
// The method also receives the `ExtraSlider` in order to use its arguments
public static IntegerEntryBuilder decExtraSlider(
IntegerEntryBuilder builder, ExtraSlider s
) {
// We apply `extraMin` and `extraMax` as the `min` and `max` of the entry,
// and `min` and `max` as the `min` and `max` of the slider
return builder.range(s.extraMin(), s.extraMax()).sliderRange(s.min(), s.max());
}
// In this sample we only define decorators for `int` and `float` entries
// It'd also be possible to define a single decorator for `RangedEntryBuilder`,
// however, this can make the method harder to write in a generic way
public static FloatEntryBuilder decExtraSlider(
FloatEntryBuilder builder, ExtraSlider s
) {
return builder.range(s.extraMin(), s.extraMax()).sliderRange(s.min(), s.max());
}
}
@Category public static class ConfigCategory {
// The entries generated for the below entries will be decorated by the
// methods defined above
@Entry @ExtraSlider public static int int_slider;
@Entry @ExtraSlider(max=1, extraMax=2) public static float float_slider;
}If you think one of your config annotations should be part of the built-in annotations, feel free to submit an issue or pull request proposing it, or let me know directly in the Discord Server.
Simple Config includes a commented sample category built with the annotation API. You can find its source here at GitHub:
However, as this is only a category, which has to be joined with the others from Simple Config's own client config, it doesn't showcase how you can register an entire config file with the annotation API. There's also a small demo which does this:
If you have any doubts, feel free to drop by the official Discord Server.