Skip to main content

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​

ProjectWhat it isWhere it runs
VersatiaAPIa libraryin your build: the API you compile against and a compile-time code generator
VersatiaCorea server pluginon 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.

  1. 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.
  2. 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 @Value property set from them and re-read on reload, every Listener is registered and every BackgroundTask runs, 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.

Dependency injection →

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.

Auto-invoked functions →

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.

Tasks and coroutines →

Utilities

One static entry point, VersatiaEngine, for server-wide calls; strict configuration getters, deep-merging maps, scheduler and logging shorthands, reflection helpers and more.

Utilities →

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:

For server admins

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.

For plugin developers

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​

ComponentVersion
Versatia0.0.1
Paper1.21.4
Java21
Kotlin2.4.20
KSP2.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.