Best practices
The rules that keep a plugin built on Versatia, or an extension of the framework, working with it rather than around it. Each links to the page with the details.
Writing a plugin
Beans
- Inject through the primary constructor, and keep beans, constructors and members as private as you like: the generated code reaches them without wider visibility.
- Inject
loggerwhen you only log, not the plugin; aComponentLoggeriscomponentLogger. See Beans every plugin has. - An optional dependency is a default value,
class PriceList(format: PriceFormat = PlainFormat), never a nullable parameter or a lookup. See Optional dependencies. - An integration is a conditional bean:
@ConditionalOnPlugin("Vault"), with@Primaryover a fallback, never anisPluginEnabledcheck. See Conditional beans.
Configuration and reload
- Configuration is
@Valueproperties; the default is in the shipped file, not in the code. - What derives from a value is never computed in a constructor: read it in a getter, rebuild it
in a
PLUGIN_RELOAD_OR_STARTfunction, or react withPreviousValueTracking. The same holds for what derives from another plugin's beans. See Designing for reload. - Listeners and coroutines read the beans' current state instead of copying values when they start; a reload does not restart them.
Reloadableis for module prototypes only; a plugin's own bean gets its values from@Value.
Secrets
A password, a token or an API key is a Secret, never a String;
reveal() only in the call that hands it over. The shipped file holds '', which switches the
feature off.
@Value("discord.token")
lateinit var token: String
@Value("discord.token")
var token = Secret.EMPTY
@AutoInvoke(Phase.PLUGIN_RELOAD_OR_START)
fun connect() {
if (token.isEmpty()) return logger.info("Discord bridge off: discord.token is empty")
client.login(token.reveal())
}
Tasks and threads
- Work as long as the plugin runs is a
BackgroundTask, never a timer started in a start or reload function. - Work something starts is launched from the injected
scope; one job per player goes intoscope.keyedJobs<K>(). Cancel your jobs, never the scope. - Waiting for a state is a
Condition, never a polling loop. One made for a single task iscondition(...)inside its coroutine; ascope.condition(...)made for a player or a request is closed withclose()when that ends. - The server thread is never blocked: blocking I/O goes into
withContext(Dispatchers.IO) { }. See Threads. - Periods from configuration are
Durations (30s,5m), not numbers of ticks.
fun teleportHome(player: Player, home: Location) {
val start = player.location.block
val standsStill = scope.condition(recheckOn<PlayerMoveEvent>(player)) { player.location.block == start }
scope.launch { if (runOnceWhile(standsStill) { delay(5.seconds) }) player.teleport(home) }
}
fun teleportHome(player: Player, home: Location) {
val start = player.location.block
scope.launch {
val standsStill = condition(recheckOn<PlayerMoveEvent>(player)) { player.location.block == start }
if (runOnceWhile(standsStill) { delay(5.seconds) }) player.teleport(home)
}
}
Naming
| What | Convention | Example |
|---|---|---|
a BackgroundTask | what it does plus Task, in the package of the feature it serves | AutoBroadcastTask |
a Condition | the state it describes, without is | standsStill, anyoneOnline |
a KeyedJobs | the plural of one job | teleports, combatTags |
an injected PluginScope | scope | class Teleports(scope: PluginScope) |
Commands
Commands are written with the CommandAPI Kotlin DSL in a command
subpackage, registered at PLUGIN_START_ONLY under @ConditionalOnPlugin("CommandAPI"), and
guarded by versatia.<plugin>.command.<command>. See Commands.
Server-wide calls
Work with no receiver starts at VersatiaEngine (VersatiaEngine.playerOrNull(name),
loadService<Economy>(), reload("shop")); work on an object you hold is a call on that object
(plugin.runTaskLater(20) { }). Never ServicesManager, a cast or a reflective lookup for
everyday work. See Utilities.
Plugin builds
A plugin depends on VersatiaStarter and VersatiaAPI-ksp, and its JAR carries nothing the
server provides: no VersatiaAPI, Kotlin, kotlinx.coroutines, Paper API or module API, and no
coroutine library for Bukkit. A second copy breaks class loading. See
plugin.yml.
Extending Versatia
For authors of scopes, enhancements and modules.
Definitions and processors
- A feature is annotations in the API plus an enhancement processor; the loader and the descriptor stay untouched. What a feature needs is recorded as an attribute and interpreted in its processor.
- A feature every plugin needs is an annotation on
VersatiaPlugin, switched off through@DisableVersatiaFeatures, never a special case in the build or the runtime. - A processor is stateless: a no-argument class or an
object; what it remembers goes intocontext.state(per plugin) orcontext.runtimeState, and resources there implementAutoCloseable, closed on disable. - It depends on another feature through that feature's API contract (
context.state[BeanResolver.KEY]), defers whole-plugin work withcontext.onPluginEnhanced { }, reads facts from the element rather than by reflection, and reports withcontext.failure(message). - Names:
<Feature>EnhancementProcessor, its class name held in a<Feature>Processorsobject; an enhancement annotation says what it does (@LocalBean), an attribute the fact it records (@Qualifier).
Internal plumbing
Declarations marked @VersatiaInternalApi (an opt-in warning) are public only so that the
compile-time step, the descriptor and VersatiaCore can reach them. A plugin or a custom processor
never opts in: the warning means the code reaches into wiring that may change without notice.
Modules
- The entry point is an
@ExposePrototypebehind an interface, so each consumer gets its own instance; one that reads the consumer's files implementsReloadable. - Everything belongs to the consumer: the module keeps nothing in its own plugin and takes the
consumer's plugin, logger and
PluginScopeas constructor parameters. - API and implementation are separate:
VersatiaModule-<Name>(public, compiled against) andVersatiaModule-<Name>Impl(internal, its JAR carries the API classes).
See Modules.