Modules
VersatiaCore is a runtime, not a toolkit: it reads descriptors and runs processors, and the features it knows are beans, lifecycle phases, configuration values, resources, listeners, tasks and messages. What not every server needs comes as a module: a plugin of its own, written against VersatiaAPI like any other, that exposes a bean. A server installs the modules it needs next to VersatiaCore; a plugin lists the ones it uses.
| Module | Repository | What it gives you |
|---|---|---|
| Database | VersatiaModule-Database | a database per plugin, injected and ready: SQLite with no setup, H2, MySQL, MariaDB or PostgreSQL by configuration, with migrations, background work and write-behind caches of rows |
Using a module
Every module follows the same three steps, shown here for Database. With VersatiaStarter the first one is already done: the starter exports the API of every module.
- build.gradle.kts
- plugin.yml
- Kotlin
dependencies {
implementation("dev.versatia:VersatiaStarter:$versatiaVersion")
ksp("dev.versatia:VersatiaAPI-ksp:$versatiaVersion")
}
The starter brings the module's API. A build that depends on VersatiaAPI alone adds the module
as compileOnly("dev.versatia:VersatiaModule-Database:0.0.1"). Either way the API
is on the compile classpath only: on the server the classes come from the module's plugin, and
nothing the server provides goes into your JAR; see
In your build. The artifact holds the module's
API only; the implementation lives in the plugin JAR.
depend:
- VersatiaCore
- VersatiaModule-Database
The module's plugin name is the repository name. Listing it guarantees the module is up before your plugin and lets your class loader see its classes.
@LocalBean
class Coins(private val database: Database) {
fun richest(limit: Int) = database.async {
sql("SELECT name FROM {prefix}profiles ORDER BY coins DESC LIMIT :limit", "limit" to limit).list<String>()
}
}
Nothing marks the type as foreign: it is a dependency like any other.
On the server, drop the module's JAR (VersatiaModule-Database-0.0.1.jar) into plugins/ next to
VersatiaCore. Like VersatiaCore, modules are not on a Maven repository yet: build the module and
run ./gradlew publishToMavenLocal in its checkout to get the artifact.
The server admin installs every module a plugin lists under depend:; see
Modules in the server admin guide.
One instance per plugin
A module exposes its entry point as an @ExposePrototype,
so every plugin that injects it gets an instance of its own: what your plugin registers on its
Text is invisible to other plugins, the instance disappears with your plugin, and a module that
keeps state for you, such as a bank of messages read from your data folder, is reloaded by
/versatia reload <your plugin> like your own beans, because it belongs to your plugin's bean
session. A prototype built on another one, from the same module or another (Messages takes
Text), receives, inside your instance, the instances made for your plugin as well.
Writing a module
A module is an ordinary Versatia plugin. What makes it a module is a handful of rules; follow them and the module fits the ecosystem.
- The entry point is an
@ExposePrototypeimplementing an interface the consumer injects. Per-plugin state and registrations live in that instance. - Keep nothing in the module's own plugin. No data folder, no logger, no reference to the
main class: everything belongs to the consumer, reached through a parameter typed
JavaPlugin(orPlugin), which a prototype receives as the consuming plugin. A prototype that only logs takes aLoggerorComponentLoggerinstead, and gets the consumer's; see Beans every plugin has. - Implement
Reloadableon the prototype when it reads anything from the consumer's data folder, and read it inreload(); see Reloadable beans. This is what the hook exists for: a plugin's own files are@Valueproperties, a module's prototype has no other way to follow the consumer's reloads. - Two repositories, like VersatiaAPI and VersatiaCore.
VersatiaModule-<Name>is the public API, published asdev.versatia:VersatiaModule-<Name>: the interfaces, value types and exceptions a consumer sees.VersatiaModule-<Name>Implis the implementation, private, allinternal; it builds the pluginVersatiaModule-<Name>-<version>.jar, which carries the API classes as VersatiaCore carries VersatiaAPI. Consumers compile against the API alone, so the implementation can change without touching them. - Run coroutines in the consumer's scope. A prototype that launches coroutines takes a
PluginScopein its constructor and receives the consuming plugin's, so the consumer's stop cancels them and its logger reports their failures; see Your own coroutines. - Report misuse with exceptions declared in
api, named after what went wrong and saying what was available, as the framework's own errors do.
The conventions are collected in Best practices.