Introduction
Versatia is a framework for Paper 1.21.4 plugins written in Kotlin.
Its mission is to remove the Bukkit boilerplate from plugins: wiring singletons, passing
dependencies around by hand, registering things in onEnable and tearing them down in
onDisable. A plugin declares what it has; Versatia takes care of when and how it is hooked
into the server.
class ShopPlugin : VersatiaPlugin() {
@AutoInvoke(Phase.PLUGIN_RELOAD_OR_START)
private fun start(prices: PriceList) = logger.info("Loaded ${prices.size} prices")
}
@LocalBean
class PriceList(private val config: ShopConfig) {
val size get() = config.prices.size
}
@LocalBean
class ShopConfig {
@Value("prices")
var prices: Map<String, Int> = emptyMap()
}
Nothing is overridden, nothing is registered. The beans are created in dependency order when the
plugin enables, prices is read from config.yml, start runs once all of that is done, and
everything is dropped when the plugin disables. /versatia reload reads prices again and runs
start again.
Two projects
| Project | What it is | Where it runs |
|---|---|---|
| VersatiaAPI | a library | in your build: the API you compile against and a compile-time code generator |
| VersatiaCore | a server plugin | on the server, next to your plugin: the runtime that gives the API its behaviour; distributed as a JAR, its sources are open |
Your plugin depends on VersatiaAPI at compile time and lists VersatiaCore under depend: in
its plugin.yml. It never compiles against VersatiaCore.
Nothing is discovered at runtime
The central design rule: nothing is discovered while the server runs. There is no classpath scanning and no reflective search for annotated classes.
- At compile time, a KSP processor from VersatiaAPI reads your sources, validates them and generates a descriptor: a small Kotlin object listing everything the runtime has to act on.
- At runtime, VersatiaCore loads that descriptor by name at a few well-defined moments (plugin start, reload, plugin stop) and executes it.
This buys four things:
- Errors at build time. A dependency cycle, an ambiguous injection or a bean that cannot be constructed stops the build with a message pointing at the line. It never reaches a server.
- Fast, predictable start-up. The runtime walks a list; it does not search.
- Encapsulation kept. Beans, constructors and functions may be
private. Generated code calls visible members directly and uses reflection only where it has to. - No boilerplate. The main class is a bean, the files under
resources/in the JAR land in the data folder, a configuration value is a@Valueproperty set from them and re-read on reload, everyListeneris registered and everyBackgroundTaskruns, without a line of code. An integration with a plugin that may be absent is one annotation,@ConditionalOnPlugin.
What is in the box
Dependency injection
Constructor injection with three scopes: @LocalBean, @ExposeSingleton,
@ExposePrototype. Beans can be shared between plugins; @Qualifier and
@Primary resolve ambiguity.
Lifecycle hooks
@AutoInvoke(phase) runs a function at plugin start, reload or stop, with its
parameters injected. It replaces onEnable and onDisable;
/versatia reload runs the reload phase on demand.
Tasks and coroutines
A BackgroundTask runs as long as the plugin and restarts with every reload; an
injected PluginScope launches coroutines on the server thread, waiting in ticks or
real time and for Conditions rechecked on events, never polled.
Utilities
One static entry point, VersatiaEngine, for server-wide calls; strict
configuration getters, deep-merging maps, scheduler and logging shorthands, reflection
helpers and more.
Extensible core
Annotations are enhancements interpreted by processors. The descriptor format is the contract between the compile-time and runtime halves.
Under the hood →Beyond the runtime, modules add what plugins use every day, each as a plugin of its own that exposes a bean, such as a bank of messages per plugin, read from its data folder, reloaded with it and written in MiniMessage with runtime arguments in braces, or a database per plugin, migrated and ready, on SQLite or a database server. VersatiaStarter brings the API of all of them into a build with one dependency.
Who is this for
This documentation is for the plugin developer, who writes the plugin. The server admin, who installs it and edits its files on the server without touching its code, has a documentation of their own, the server admin guide, switched to with Server admins in the navbar. Where a developer page still has a part for the admin, two markers keep the parts apart:
The server admin's part: the files in plugins/<Name>/ to edit (and the ones to leave alone),
commands, permissions and what takes effect at a reload or only after a restart.
The plugin developer's part: the build, what goes under src/main/resources/, annotations and
code.
A section or a table row meant for one of them carries a label on its line: Server admin Plugin developer. Anything unmarked concerns the developer, or both.
Versions
| Component | Version |
|---|---|
| Versatia | 0.0.1 |
| Paper | 1.21.4 |
| Java | 21 |
| Kotlin | 2.4.20 |
| KSP | 2.3.12 |
Ready? Start with the installation, or browse the example plugins, one feature each. Coming from a Bukkit plugin? See Coming from Bukkit for its everyday code next to the same plugin with Versatia.