Skip to main content

Your first plugin

This page walks through a minimal plugin: a main class, two beans, a configuration value and two lifecycle functions. The installation is assumed to be done.

The main class​

package com.example.myplugin

import dev.versatia.VersatiaPlugin

class MyPlugin : VersatiaPlugin()

VersatiaPlugin extends JavaPlugin and seals onEnable and onDisable. There is nothing to override: on enable it hands the plugin to the runtime, on disable it tells the runtime to clean up. Start-up code goes into auto-invoked functions instead.

Beans​

A bean is a class annotated with a scope. Its constructor parameters are its dependencies.

package com.example.myplugin

import dev.versatia.config.Value
import dev.versatia.di.LocalBean

@LocalBean
class ShopConfig {
@Value("currency")
lateinit var currency: String
}

@LocalBean
class PriceList(private val config: ShopConfig) {
fun describe(item: String) = "$item costs 10 ${config.currency}"
}

@LocalBean means: one instance, visible only inside this plugin. The other scopes, @ExposeSingleton and @ExposePrototype, share a bean with other plugins; see Scopes and lifetime.

Nothing has to be registered. The processor finds every class carrying a scope annotation and records it in the descriptor.

Configuration​

currency is a @Value property: the runtime sets it from config.yml in the plugin's data folder. Ship the file with its default under src/main/resources/resources/:

src/main/resources/resources/config.yml
currency: coins

At start the file is extracted to plugins/MyPlugin/config.yml, where the server owner edits it, and currency is set from it; at every reload it is set again. PriceList reads config.currency when it describes an item, so it always shows the current value.

Lifecycle functions​

package com.example.myplugin

import dev.versatia.Phase
import dev.versatia.VersatiaPlugin
import dev.versatia.lifecycle.AutoInvoke

class MyPlugin : VersatiaPlugin() {

@AutoInvoke(Phase.PLUGIN_RELOAD_OR_START)
private fun start(prices: PriceList) = logger.info(prices.describe("Diamond"))

@AutoInvoke(Phase.PLUGIN_STOP)
private fun stop() = logger.info("Bye")
}
  • start runs when the plugin enables (and on a Versatia reload). Its PriceList parameter is injected from the beans, exactly like a constructor dependency.
  • stop runs when the plugin disables.
  • Both functions are private; the generated code reaches them through reflection. Visibility is your choice, not the framework's.

Auto-invoked functions may also live in Kotlin objects and in beans; see Auto-invoked functions.

Reaching the server​

Server-wide calls start at one class, VersatiaEngine. Type VersatiaEngine. and the completion offers what a plugin needs every day; IntelliJ adds the import:

VersatiaEngine.broadcast(text("The shop is open"))
VersatiaEngine.playerOrNull("Notch")?.sendMessage(text("Welcome back"))

Work on an object you already hold stays on that object: plugin.runTaskLater(20) { }, player.sendMessage(...). The full list is in Utilities.

Build and look at the result​

./gradlew build

The processor generated build/generated/ksp/main/kotlin/com/example/myplugin/VersatiaDescriptor.kt. Shortened, it reads:

@file:OptIn(VersatiaInternalApi::class)

object VersatiaDescriptor : VersatiaPluginDescriptor {
override val formatMajor: Int = 1
override val formatMinor: Int = 0

override val elements: List<EnhancedElement>
get() = listOf(
EnhancedElement(
processorClass = "dev.versatia.di.PluginBeanEnhancementProcessor",
phase = Phase.PLUGIN_RELOAD_OR_START,
annotation = AnnotationOccurrence("dev.versatia.di.PluginBean"),
target = PluginTarget(
className = "com.example.myplugin.MyPlugin",
assignableTypes = listOf("com.example.myplugin.MyPlugin", "dev.versatia.VersatiaPlugin", /* ... */),
annotations = emptyList(),
),
),
EnhancedElement(
processorClass = "dev.versatia.resources.ExtractResourcesEnhancementProcessor",
phase = Phase.PLUGIN_RELOAD_OR_START,
annotation = AnnotationOccurrence("dev.versatia.resources.ExtractResources"),
target = PluginTarget(/* the same */),
),
EnhancedElement(
processorClass = "dev.versatia.di.LocalBeanEnhancementProcessor",
phase = Phase.PLUGIN_START_ONLY,
annotation = AnnotationOccurrence("dev.versatia.di.LocalBean"),
target = ClassTarget(
className = "com.example.myplugin.PriceList",
assignableTypes = listOf("com.example.myplugin.PriceList"),
annotations = emptyList(),
constructorParameters = listOf(Parameter("com.example.myplugin.ShopConfig")),
instantiator = Instantiator.Direct { args -> PriceList(args[0] as ShopConfig) },
),
),
EnhancedElement(
processorClass = "dev.versatia.lifecycle.AutoInvokeEnhancementProcessor",
phase = Phase.PLUGIN_RELOAD_OR_START,
annotation = AnnotationOccurrence("dev.versatia.lifecycle.AutoInvoke", mapOf("phase" to "PLUGIN_RELOAD_OR_START")),
target = FunctionTarget(
className = "com.example.myplugin.MyPlugin",
binaryClassName = "com.example.myplugin.MyPlugin",
functionName = "start",
owner = MemberOwner.PLUGIN,
annotations = emptyList(),
parameters = listOf(Parameter("com.example.myplugin.PriceList")),
invoker = Invoker.Reflective("com.example.myplugin.MyPlugin", "start", listOf("com.example.myplugin.PriceList")),
),
),
// ShopConfig, its @Value property, stop ...
)
}

One element per bean and per function, plus one per enhancement the main class inherits from VersatiaPlugin (@PluginBean and @ExtractResources above). MyPlugin is a local bean named plugin without any annotation, so beans and functions may ask for it (or for JavaPlugin) like for any other bean. Each element names the VersatiaCore class that interprets it. PriceList is public, so the descriptor calls its constructor directly; start is private, so it gets a reflective invoker. Nothing in the file says in which order to create the beans: the runtime derives that from the constructor parameters.

Run it​

Copy the JAR to the server's plugins/ directory next to VersatiaCore and start the server. The whole plugin, and what its log shows:

[MyPlugin] Diamond costs 10 coins

and, on shutdown, Bye.

Reload it​

Change currency to gems in plugins/MyPlugin/config.yml and run /versatia reload MyPlugin (the command needs CommandAPI on the server). The runtime sets currency again, then runs start, because its phase is PLUGIN_RELOAD_OR_START:

[MyPlugin] Diamond costs 10 gems

No restart and no reload code. What else a reload does is in Reloading plugins.

Break it on purpose​

Make ShopConfig depend on PriceList:

@LocalBean
class ShopConfig(prices: PriceList)

The build now fails with

Dependency cycle: ShopConfig -> PriceList -> ShopConfig

pointing at the file and line. No descriptor is generated, so the problem can never reach a server. The full list of what the processor checks is in Compile-time checks.

Next steps​