Running a server? This topic in the server admin guide
Installation
Versatia has two halves. The server needs VersatiaCore; your build needs VersatiaAPI, which comes with VersatiaStarter.
On the server Server admin
The server needs Paper 1.21.4 on Java 21, VersatiaCore in plugins/, and the
modules your plugin uses. Installing, configuring and updating them is
covered by the server admin guide, which is also the page to send to the
people who run your plugin.
What matters for your build: VersatiaCore is the only plugin that ships the Kotlin runtime. Its
plugin.yml lists kotlin-stdlib, kotlin-reflect and kotlinx-coroutines-core-jvm under
libraries:, and every Versatia plugin reaches Kotlin and kotlinx.coroutines through
VersatiaCore, which is why your plugin must not list either itself (see below).
A plugin built against a VersatiaAPI needs the VersatiaCore of the same release; any other refuses
it with the
descriptor format message.
Versatia is not published to a Maven repository yet. VersatiaAPI and VersatiaCore are open source: build VersatiaAPI and publish it to your local Maven repository, then build the VersatiaCore plugin JAR; the steps are in Building the framework.
In your build Plugin developer
A Versatia plugin is an ordinary Kotlin/JVM Gradle project with the KSP plugin applied. The
repositories are declared once, in settings.gradle.kts.
- build.gradle.kts
- settings.gradle.kts
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
kotlin("jvm") version "2.4.20"
id("com.google.devtools.ksp") version "2.3.12"
}
group = "com.example"
version = "1.0.0"
val versatiaVersion = "0.0.1"
dependencies {
implementation("dev.versatia:VersatiaStarter:$versatiaVersion")
ksp("dev.versatia:VersatiaAPI-ksp:$versatiaVersion")
}
java {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
kotlin {
compilerOptions {
jvmTarget = JvmTarget.JVM_21
}
}
rootProject.name = "MyPlugin"
pluginManagement {
repositories {
gradlePluginPortal()
mavenCentral()
}
}
dependencyResolutionManagement {
repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
repositories {
mavenLocal { content { includeGroup("dev.versatia") } }
mavenCentral()
maven("https://repo.papermc.io/repository/maven-public/")
}
}
FAIL_ON_PROJECT_REPOSmakes this the only place repositories are declared; arepositories { }block inbuild.gradle.ktsfails the build.- The Versatia artifacts come from the local Maven repository until they are published remotely. The filter takes nothing else from there, so a stale local copy of another library never wins.
- VersatiaAPI exports the Paper API, but Gradle never inherits repositories, so the Paper repository has to be declared here.
Two dependencies, nothing else:
| Dependency | Configuration | Role |
|---|---|---|
VersatiaStarter | implementation | the starter, in the spirit of Spring Boot's: no code, it exports VersatiaAPI (annotations, VersatiaPlugin, utilities, and with it the Paper API, kotlin-stdlib, kotlin-reflect and kotlinx-coroutines-core, pinned to the version VersatiaCore loads) and the API of every module, so you declare none of those |
VersatiaAPI-ksp | ksp | the code generator. Lives on the annotation-processing classpath only and never reaches the server; it cannot come through the starter, because Gradle does not fill the ksp configuration transitively |
Keep the version in one versatiaVersion value; both lines use it. A plugin that uses no module
may depend on dev.versatia:VersatiaAPI instead of the starter.
Versions from VersatiaCatalog
Instead of writing the Kotlin, KSP and Versatia versions yourself, you can import the Gradle
version catalog the framework is built with, dev.versatia:VersatiaCatalog
(published by VersatiaAPI). The examples and the template do this. The Versatia version moves to
gradle.properties, the one place both scripts read it from; settings.gradle.kts imports the
catalog inside dependencyResolutionManagement:
versatiaVersion=0.0.1
The catalog matches the framework release: Kotlin and KSP at the versions Versatia is compiled
with, plus libs.paper.api (for testImplementation), libs.versatia.api,
libs.versatia.database, libs.commandapi.*, libs.snakeyaml and libs.kotlinx.coroutines.test
(for testImplementation; kotlinx-coroutines-core itself comes with VersatiaAPI). Your own
libraries stay in your build, or in a second catalog of your own.
There is no configuration block for the processor. It finds your plugin's main class on its own and places the generated descriptor next to it.
VersatiaAPI, Kotlin, the Paper API and the module APIs are on the server already: VersatiaCore,
Paper and the module plugins provide them, and your plugin reaches them through depend:. A
second copy in your JAR is a second class of the same name and breaks at runtime. The starter
exports everything with compileOnlyApi, so it is on the compile classpath only: a plain jar
does not pack it, and neither does Shadow when you shade a third-party library. A test that needs
these classes at runtime adds them with testImplementation. The same rule covers another
plugin you inject beans from, which you add as
compileOnly.
plugin.yml Plugin developer
name: MyPlugin
version: '1.0.0'
main: com.example.myplugin.MyPlugin
api-version: '1.21'
depend:
- VersatiaCore
Two rules:
-
VersatiaCoregoes underdepend:. It guarantees that the runtime is up before your plugin enables and lets your plugin load the Kotlin and VersatiaAPI classes from VersatiaCore. -
No
libraries:entry for Kotlin, nor for a library built on it. Paper loads each plugin's libraries into a separate class loader. A plugin with its own copy of Kotlin would exchangekotlin.jvm.functions.Function1objects with VersatiaCore across two differentFunction1classes, and the JVM rejects that with aLinkageError. A Kotlin library such askotlinx-serialization-jsonbringskotlin-stdlibalong, so listing it underlibraries:has the same effect. Shade such a library into your JAR instead and leave Kotlin out:Wrong: plugin.yml loads a second Kotlindepend:- VersatiaCorelibraries:- org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0Right: build.gradle.kts shades the library without Kotlinplugins {id("com.gradleup.shadow") version "8.3.5"}dependencies {implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0")}tasks.shadowJar {dependencies {exclude(dependency("org.jetbrains.kotlin:.*"))exclude(dependency("org.jetbrains.kotlinx:kotlinx-coroutines-.*"))}} -
No copy of
kotlinx.coroutinesat all, neither underlibraries:nor shaded, nor through a coroutine library for Bukkit such as MCCoroutine. VersatiaCore loads it, VersatiaAPI exports it to your build at that same version, and aPluginScopeor aConditionis akotlinx.coroutinestype crossing between your plugin and VersatiaCore, so a second copy fails with aLinkageErroror aClassCastException.See Troubleshooting.
A Versatia plugin is described by plugin.yml. A paper-plugin.yml makes Paper load the JAR as a
Paper plugin, even next to a plugin.yml: its class loader is isolated, and it has no depend:
through which VersatiaCore shares Kotlin and VersatiaAPI the way it does with a Bukkit plugin. That
loading is not supported, and the checkPluginYml task of the template fails the build when
src/main/resources/paper-plugin.yml exists. See
Coming from Bukkit.
Check the setup
Run ./gradlew build. On success the build directory contains
build/generated/ksp/main/kotlin/com/example/myplugin/VersatiaDescriptor.kt
in the package of your main class. It already lists the elements every plugin inherits from
VersatiaPlugin (the main class as a bean, resource extraction, the binding of the plugin's
coroutine scope to its start and stop); beans and auto-invoked functions add theirs, which is the
subject of Your first plugin. The other
beans every plugin has, such as its
Logger and PluginScope, are not listed: VersatiaCore's own descriptor exposes them, so a
parameter of their type is recorded like any other dependency.