Skip to main content
The Oyasai Server Platform uses a sophisticated plugin system built on Gradle, Kotlin, and Nix. This document covers plugin development, the build process, and integration with the server.

Plugin Architecture

Plugins are:
  • Kotlin-based: Written in Kotlin 2.3.0 for modern JVM development
  • Gradle-managed: Multi-project Gradle build with shared configuration
  • Nix-packaged: Reproducible builds via gradle2nix
  • Purpur-compatible: Target Purpur 1.21.8 API

Plugin Structure

Each plugin follows a standard layout:
plugins/PluginName/
├── build.gradle.kts          # Plugin-specific dependencies
└── src/
    └── main/
        ├── kotlin/           # Kotlin source code
        │   └── com/oyasai/pluginname/
        │       └── PluginNamePlugin.kt
        └── resources/
            └── plugin.yml    # Plugin metadata

plugin.yml

The plugin descriptor defines metadata and entry points:
name: PluginName
version: ${version}
main: com.oyasai.pluginname.PluginNamePlugin
api-version: '1.21'
author: Oyasai Team
description: Plugin description

commands:
  commandname:
    description: Command description
    usage: /<command> [args]
    permission: pluginname.command

permissions:
  pluginname.command:
    description: Permission description
    default: op
The ${version} placeholder is automatically expanded during build from the project version property.

Build Configuration

The root build.gradle.kts applies common configuration to all plugins:
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar

buildscript {
  dependencies {
    classpath(libs.kotlin.plugin)
    classpath(libs.shadow.plugin)
  }
  repositories { mavenCentral() }
}

allprojects {
  repositories {
    mavenCentral()
    maven("https://repo.purpurmc.org/snapshots")
    maven("https://nexus.frengor.com/repository/public/")
    maven("https://nexus.scarsz.me/content/groups/public/")
  }
}

subprojects {
  if (!project.path.startsWith(":plugins:")) {
    return@subprojects
  }
  
  apply(plugin = "org.jetbrains.kotlin.jvm")
  apply(plugin = "com.gradleup.shadow")
  apply(plugin = "java-library")
  
  afterEvaluate {
    tasks.withType<Jar>().configureEach {
      if (name == "jar") {
        enabled = false
      }
    }
    
    tasks.withType<ShadowJar>().configureEach {
      archiveBaseName = project.name
      archiveClassifier = ""
      duplicatesStrategy = DuplicatesStrategy.EXCLUDE
    }
    
    tasks.withType<ProcessResources>().configureEach {
      val version: String by project
      val properties = mapOf("version" to version)
      inputs.properties(properties)
      filteringCharset = Charsets.UTF_8.name()
      filesMatching("plugin.yml") { expand(properties) }
    }
    
    tasks.named("build") { dependsOn("shadowJar") }
  }
}

Configuration Highlights

Only subprojects under :plugins: have plugin configuration applied:
if (!project.path.startsWith(":plugins:")) {
  return@subprojects
}
All plugins produce uber JARs with dependencies shaded:
tasks.withType<ShadowJar>().configureEach {
  archiveBaseName = project.name
  archiveClassifier = ""
  duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
The plugin.yml file has version properties expanded at build time:
tasks.withType<ProcessResources>().configureEach {
  val version: String by project
  filesMatching("plugin.yml") { expand(mapOf("version" to version)) }
}
Only shadow JARs are produced to avoid confusion:
tasks.withType<Jar>().configureEach {
  if (name == "jar") {
    enabled = false
  }
}

Dependency Management

Dependencies are managed via the version catalog in gradle/libs.versions.toml:
[versions]
kotlin = "2.3.0"
purpur-api = "1.21.8-R0.1-SNAPSHOT"
vault = "1.7.1"
luckperms = "5.5"
placeholderapi = "2.11.6"

[libraries]
purpur-api = { module = "org.purpurmc.purpur:purpur-api", version.ref = "purpur-api" }
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
vault-api = { module = "com.github.MilkBowl:VaultAPI", version.ref = "vault" }
luckperms-api = { module = "net.luckperms:api", version.ref = "luckperms" }
placeholderapi = { module = "me.clip:placeholderapi", version.ref = "placeholderapi" }

Using Dependencies

In a plugin’s build.gradle.kts:
dependencies {
  compileOnly(libs.purpur.api)
  compileOnly(libs.vault.api)
  compileOnly(libs.luckperms.api)
  
  // Plugin dependencies are compileOnly
  // Shadow plugin handles embedding if needed
}
Use compileOnly for server-provided APIs (Purpur, Vault, LuckPerms) and implementation for libraries you want to shade into your plugin JAR.

Available Libraries

dependencies {
  compileOnly(libs.purpur.api)      // Purpur server API
  compileOnly(libs.vault.api)       // Vault economy/permissions
  compileOnly(libs.luckperms.api)   // LuckPerms permissions
  compileOnly(libs.placeholderapi)  // PlaceholderAPI
}

Plugin Development

Basic Plugin Class

A minimal plugin extends JavaPlugin:
package com.oyasai.pluginname

import org.bukkit.plugin.java.JavaPlugin

class PluginNamePlugin : JavaPlugin() {
    override fun onEnable() {
        logger.info("${description.name} v${description.version} enabled")
        
        // Register commands
        // Register event listeners
        // Load configuration
    }
    
    override fun onDisable() {
        logger.info("${description.name} disabled")
        
        // Save data
        // Cancel tasks
        // Cleanup resources
    }
}

Command Registration

class PluginNamePlugin : JavaPlugin() {
    override fun onEnable() {
        getCommand("commandname")?.setExecutor(MyCommandExecutor(this))
    }
}

class MyCommandExecutor(private val plugin: PluginNamePlugin) : CommandExecutor {
    override fun onCommand(
        sender: CommandSender,
        command: Command,
        label: String,
        args: Array<out String>
    ): Boolean {
        if (!sender.hasPermission("pluginname.command")) {
            sender.sendMessage("You don't have permission!")
            return true
        }
        
        // Command logic
        return true
    }
}

Event Listeners

class PlayerJoinListener(private val plugin: PluginNamePlugin) : Listener {
    @EventHandler
    fun onPlayerJoin(event: PlayerJoinEvent) {
        val player = event.player
        plugin.logger.info("${player.name} joined the server")
    }
}

// Register in onEnable
server.pluginManager.registerEvents(PlayerJoinListener(this), this)

Configuration

class PluginNamePlugin : JavaPlugin() {
    override fun onEnable() {
        saveDefaultConfig()
        
        val someValue = config.getString("some.key", "default")
        val enabled = config.getBoolean("enabled", true)
    }
    
    fun reloadPluginConfig() {
        reloadConfig()
        // Re-read values
    }
}

Nix Integration

Plugins are built via Nix using gradle2nix:

Batch Building

All plugins are compiled together for efficiency:
# From nix/oyasai-scope.nix
plugins-batch = scopeSelf.gradle2nix.buildGradlePackage {
  pname = "plugins";
  version = "0.0.0";
  src = with lib.fileset; toSource {
    root = ../.; 
    fileset = unions [
      ../build.gradle.kts
      ../gradle
      ../plugins
      ../settings.gradle.kts
    ];
  };
  inherit (scopeSelf) gradle;
  buildJdk = scopeSelf.jdk;
  lockFile = ../gradle.lock;
  gradleBuildFlags = [ "build" ];
  installPhase = ''
    runHook preInstall
    mkdir -p $out
    cp plugins/*/build/libs/*.jar $out
    runHook postInstall
  '';
};

Individual Plugin Derivations

Each plugin gets a lightweight derivation:
plugins = lib.mapAttrs' (
  name: _:
  lib.nameValuePair (lib.toLower name) (
    pkgs.runCommand name { } ''
      mkdir -p $out
      cp ${plugins-batch}/${name}.jar $out
    ''
  )
) (builtins.readDir ../plugins);
This creates:
  • oyasai-plugins.oyasaiutilities
  • oyasai-plugins.oyasaipets
  • oyasai-plugins.oyasaiadmintools
  • etc.

Building Plugins

# Build all plugins
nix build .#legacyPackages.x86_64-linux.plugins-batch

# Build specific plugin
nix build .#legacyPackages.x86_64-linux.oyasai-plugins.oyasaiutilities

# Development build with Gradle
nix develop
gradle :plugins:OyasaiUtilities:build

Plugin Registry

Third-party plugins are managed via the plugin registry:
# From plugin registry usage
plugins = with (oyasai-plugin-registry.forVersion version); [
  essentialsx
  fastasyncworldedit
  luckperms
  plugmanx
  protocollib
  vault
  nuvotifier
  vertex
];
The registry provides version-specific plugin JARs with verified hashes.

Server Integration

Plugins are integrated into server packages:
# packages/oyasai-minecraft-main.nix
oyasaiPurpur rec {
  name = "oyasai-minecraft-main";
  version = "1.21.8";
  
  plugins = with (oyasai-plugin-registry.forVersion version); [
    # Third-party plugins
    essentialsx
    luckperms
    vault
    
    # Custom plugins (from oyasaiScope.plugins)
    vertex
  ];
}
The oyasaiPurpur function:
  1. Fetches the Purpur server JAR
  2. Creates a setup script to copy plugins
  3. Wraps the server with the setup script
  4. Accepts EULA automatically

Development Workflow

1

Create Plugin Directory

mkdir -p plugins/MyPlugin/src/main/{kotlin,resources}
2

Create build.gradle.kts

dependencies {
  compileOnly(libs.purpur.api)
}
3

Create plugin.yml

name: MyPlugin
version: ${version}
main: com.oyasai.myplugin.MyPlugin
api-version: '1.21'
4

Implement Plugin Class

package com.oyasai.myplugin

import org.bukkit.plugin.java.JavaPlugin

class MyPlugin : JavaPlugin() {
    override fun onEnable() {
        logger.info("MyPlugin enabled!")
    }
}
5

Build and Test

# Build with Gradle (fast iteration)
gradle :plugins:MyPlugin:build

# Build with Nix (reproducible)
nix build .#legacyPackages.x86_64-linux.oyasai-plugins.myplugin

Testing Plugins

Local Testing

# Build plugin
gradle :plugins:MyPlugin:build

# Copy to test server
cp plugins/MyPlugin/build/libs/MyPlugin.jar ~/test-server/plugins/

# Restart server
cd ~/test-server
./start.sh

Nix Testing

Create a minimal server configuration:
# packages/oyasai-minecraft-test.nix
{ oyasaiPurpur, oyasai-plugin-registry }:

oyasaiPurpur rec {
  name = "oyasai-minecraft-test";
  version = "1.21.8";
  
  plugins = with (oyasai-plugin-registry.forVersion version); [
    # Minimal dependencies
    vault
  ] ++ [
    # Your plugin from the batch build
    oyasaiScope.plugins.myplugin
  ];
}
Build and run:
nix build .#oyasai-minecraft-test
./result/bin/minecraft-server

Existing Plugins

OyasaiUtilities

Core utility functions and helper commands for server administration

OyasaiPets

Pet management system with customization and companion features

OyasaiAdminTools

Administrative tools and moderation commands

DynamicProfile

Player profile customization and display options

EntityPose

Controls for entity positioning and rotation

PaintTools

Creative building tools and utilities

SocialLikes3

Social interaction system with likes and reactions

SocialVotes

Voting and polling system for community decisions

TPswitch

Teleportation utilities and waypoint management

Vertex

Custom functionality plugin

Best Practices

Always use compileOnly for server-provided APIs to avoid JAR bloat and class conflicts.
Use the version catalog for all dependencies to ensure consistency across plugins.
Plugin names in Nix are automatically lowercased (e.g., OyasaiUtilities becomes oyasaiutilities).
When developing, use Gradle for fast iteration and Nix for final reproducible builds.

Dependency Updates

Update Gradle dependencies:
# Enter dev shell
nix develop

# Update gradle.lock
gradle2nix > gradle.lock

# Commit changes
git add gradle.lock
git commit -m "Update Gradle dependencies"

Troubleshooting

Check:
  • plugin.yml has correct main class
  • Version in plugin.yml uses ${version} placeholder
  • Plugin JAR is in server’s plugins/ directory
  • Server console for error messages
Ensure dependencies are properly shaded:
dependencies {
  implementation(libs.some.library) // Will be shaded
  compileOnly(libs.purpur.api)     // Server provides
}
Regenerate the lock file:
nix develop
gradle2nix > gradle.lock
Check plugin.yml permissions:
permissions:
  myplugin.command:
    default: op  # or: true, false, op

Architecture Overview

Understand the overall system architecture

Monorepo Structure

Learn about project organization

Build docs developers (and LLMs) love