Skip to content

Kotlin API

Endor H edited this page Nov 28, 2022 · 3 revisions

If you can't use Kotlin in your mod, I'm afraid you'll have to use the Java API

For mods written in Kotlin, Simple Config provides a much cleaner API using Kotlin features.

Declaring Config Files

Config files in Kotlin are associated with an object that inherits a special type, SimpleKonfig.

Unfortunately, defining an object is not enough to define a config file, as there's no guarantee that the object will be initialized at all if it's not reachable from the mod's constructor.

object ClientKonfig : SimpleKonfig(
    Type.CLIENT, background = "textures/block/warped_planks.png"
) { /* ... */ }

To register your config object as a config file, simply call SimpleKonfig.buildAndRegister(YourConfigObject) from your mod's constructor.

@Mod(MyMod.MOD_ID)
object MyMod {
    const val MOD_ID = "mymodid"
    
    init {
        SimpleKonfig.buildAndRegister(ClientKonfig)
    }
    
    // ...
}

Declaring Config Entries

Declaring config entries inside a config object is as simple as declaring delegated properties in it using the delegates provided by the factory methods from the package endorh.simpleconfig.konfig.builders.*.

object ClientKonfig : SimpleKonfig(Type.CLIENT) {
    val str by string("str").maxLength(10)
    val int by number(0)
    val bool by yesNo(true)
    // ...
}

The order in which entries/groups are declared is taken into account when displayed on the menu.

It's also possible to declare entries as var, if you want to be able to modify them from your mod, although that's not recommended in general, as it may be unintuitive for players. It's also possible to have non-delegated properties mixed with the others.

Declaring Categories/Groups

To declare a config category/group within your config object, simply create a nested object extending from category or group, (type aliases for KonfigCategory and KonfigGroup).

object ClientKonfig : SimpleKonfig(Type.CLIENT) {
    // ...
    object SubCategory : category(
        background = "textures/block/bookshelf.png",
        color = 0xAA8080FF,
        icon = SimpleConfigIcons.Status.INFO
    ) {
        // ...
        object SubGroup : group(expand = true) {
            val int by int(1)
            // ...
        }
        // ...
    }
    // ...
}

General Tips

  • You can define a typealias to access your config object with a single letter/short name, like C, if that's your style.
typealias C = ClientKonfig
  • You can define baked properties, computed from the others after each config file change, by using the baked delegate.
object ClientKonfig : SimpleKonfig(Type.CLIENT) {
    // Simple entry
    val time by double(1.0) // In seconds
    // Computed entry (updated on any config change)
    val timeInTicks by baked { (time * 20).toInt() }
}
  • Likewise, you may transform an entry into a different type for the uses of your property, by using baked as an extension method for entries, passing a lambda that transforms the value into the value you want to be accessible from the property on the code side.
object ClientKonfig : SimpleKonfig(Type.CLIENT) {
    // Transformed entry (stored/displayed as seconds, directly available as ticks)
    val timeInTicks by double(1.0) baked { (it * 20).toInt() }
}

Data Class Entries

The Kotlin API replaces bean entries with data class entries, which are much safer/easier to setup.

object ClientKonfig : SimpleKonfig(Type.CLIENT) {
    val sampleData by data(SampleDataClass("John", 20)) { bind {
        ::name caption string()
        ::number by int()
    }}
}

data class SampleDataClass(val name: String, val number: Int = 20)

Bound properties take their default value from the default value of the data class entry, so you should always omit it where possible.

To bind entries to properties, you call bind within the lambda argument of the data factory method, and perform a series of infix calls to by and caption (only one property can be caption) on property references. These calls are type-safe, and also make sure you can't misspell property names, as you could in the Java API.

Once multiple context receivers become a stable Kotlin feature, the bind call will be unnecessary, and you'll be able to access property references and the infix calls directly within the lambda.

Common features with the Java API

The entry builders used by the Kotlin API are the same as those of the Java API. As such, some features are identical:

Additionally, you may also be interested in the Entry Types section, which describes the different entry types provided by the API, as well as how to compose them into complex types or create custom ones.

Demo

Show demo
// Entry builders are immutable, that is, all their methods return modified
//   copies, so you can reuse them without fear
private val letter = string().restrict("a", "b", "c", "d")

typealias C = ClientKonfig
object ClientKonfig : SimpleKonfig(
    Type.CLIENT, background = "textures/block/warped_planks.png"
) {
    // Simple entries
    val str by string("str").maxLength(10)
    val int by number(0)
    val bool by yesNo(true)

    // Simple entry
    val time by double(1.0) // In seconds
    // Computed entry (updated on any config change)
    val timeInTicks by baked { (time * 20).toInt() }
    // Transformed entry (stored/displayed as seconds, directly available as ticks)
    val timeInTicks2 by double(1.0) baked { (it * 20).toInt() }

    // Complex entries
    val list by list(string(), listOf("a", "b", "c", "d"))
    val map by map(pair(string(), int()), triple(string(), int(), regex()))
    val pair by pair(string(), int())
    val pairList by pairList(string(), int()).caption(int())
    
    val data by data(Data("<unnamed>", Color.BLUE, 10)) { bind {
        // Type-safe, spell-safe property bindings
        ::name caption string()
        ::color by color()
        // Computed properties are also allowed in beans
        ::number by baked { color.rgb }
        ::variable by string()
    }}
    
    // Config group as nested object
    object SubGroup : group(expand = true) {
        val caption by caption(number(0))
        val bool by yesNo(true)
        
        // Two properties using the same base builder
        val letter1 by letter.withValue("b")
        val letter2 by letter.withValue("d")
        
        // Groups can be nested within other groups/categories
        object SubSubGroup : group() {
            val item by item(Items.GOLDEN_APPLE)
        }
    }
    
    // Config category as nested object
    object SubCategory : category(
        // Categories can have background, color and icon
        background = "textures/block/bookshelf.png",
        color = 0xAA8080FF,
        icon = SimpleConfigIcons.Status.INFO
    ) {
        val bool by yesNo(true)
    }
    
    // The order in which entries/groups are declared is respected
    val number by int()
}

// Sample data class used above
data class Data(
  val name: String, val color: Color, val number: Int
) {
    // While possible, you should avoid having non-identifying
    // properties in config data classes
    var variable = "$name+$number"
}

You may also check Simple Config's own test config file using the Kotlin API, or the example included in the documentation for SimpleKonfig.

Clone this wiki locally