Nexus is a Kotlin-first application framework providing automatic dependency injection with classpath scanning, YAML configuration management, command auto-discovery, and coroutine infrastructure backed by Java 21 virtual threads.
Nexus ships as two modules:
nexus-core— DI container, config system, coroutine infrastructure, and Hytale command adaptersnexus-paper— Paper Brigadier command system,BukkitDispatcher, and Paper-specific extensions
- Automatic component discovery —
@Component,@Service,@Repositoryfound at startup via ClassGraph - Constructor injection — dependencies resolved automatically through primary constructors
- Lifecycle management —
@PostConstructand@PreDestroyhooks (supports suspend functions) - Scopes — Singleton (default) and Prototype via
@Scope - Polymorphic resolution — beans resolved by interface or superclass type
- Qualifier support —
@Qualifier("name")to disambiguate multiple beans of the same type - External beans — pre-built instances registered via
externalBeansmap before scanning - Thread-safe — concurrent access with double-check locking for singletons
- Virtual thread dispatchers — Java 21 virtual threads with automatic classloader propagation
- Per-plugin scopes — each plugin gets its own
CoroutineScopewithSupervisorJob - Injectable — scope and dispatchers auto-registered as beans
- Lifecycle-managed — scopes cancelled automatically on context shutdown
- Shared utilities —
withIOandwithDefaultdispatcher helpers
- Auto-discovery —
@ConfigFileclasses found by classpath scanning, loaded, and registered as injectable beans - YAML format — human-friendly config files with comment preservation
- Annotation-based —
@ConfigFile,@ConfigName,@Comment,@Transient - Type-safe loading — automatic type conversion for primitives, collections, nested objects
- Hot reload — reload configs at runtime without restarting
- Centralized management —
ConfigManagerfor loading, saving, and caching all configs
- Annotation-based —
@Commandclasses with@Argparameters auto-discovered - Type-safe arguments — map Kotlin types to Hytale arguments via
ArgumentResolver - Adapters — AsyncCommand, PlayerCommand, TargetPlayerCommand, TargetEntityCommand
- Suspend support — command execute methods can be suspend functions
- Context injection —
@Contextparameters for CommandContext, World, Store, etc.
@Command+@Subcommand— single command class with multiple subcommand methods- Paper Brigadier integration — builds Mojang
LiteralCommandNodetrees automatically - Multi-arg support — bottom-up Brigadier node construction (handles
then()build semantics correctly) @Argtype resolution — built-in resolvers for String, Int, Double, Float, Boolean, Player@Contextinjection — Player, CommandSender, CommandSourceStack, Server@Permission— per-subcommand permission checks@PlayerOnly— restrict subcommands to player senders@Async— run subcommands on coroutine scope instead of main threadBukkitDispatcher— coroutine dispatcher that runs on Paper's main thread (skips dispatch when already on main thread)
dependencies {
// Core DI + config + coroutines
implementation("net.badgersmc:nexus-core:1.5.3")
// Paper command system + BukkitDispatcher (optional)
implementation("net.badgersmc:nexus-paper:1.5.3")
}@Repository
class PlayerRepository(private val database: Database) {
suspend fun findPlayer(id: UUID): PlayerStats? = withIO {
// database query
}
}
@Service
class PlayerService(private val repository: PlayerRepository) {
@PostConstruct
fun init() {
println("PlayerService initialized!")
}
suspend fun getPlayer(id: UUID): PlayerStats? {
return repository.findPlayer(id)
}
}val nexus = NexusContext.create(
basePackage = "net.example.myplugin",
classLoader = this::class.java.classLoader,
configDirectory = dataDirectory,
contextName = "MyPlugin",
externalBeans = mapOf(
"plugin" to this,
"database" to database
)
)
// Retrieve auto-discovered beans
val playerService = nexus.getBean<PlayerService>()
val config = nexus.getBean<MyPluginConfig>()
// Cleanup on disable
nexus.close()// In onEnable(), after creating the NexusContext:
nexus.registerPaperCommands(
basePackage = "net.example.myplugin",
classLoader = this::class.java.classLoader,
plugin = this
)Define a single @Command class with @Subcommand methods:
@Command(name = "sg", description = "Survival Games", permission = "sg.use")
class SGCommand(
private val gameManager: GameManager, // DI works!
private val config: GameConfig
) {
@Subcommand("join")
@PlayerOnly
fun join(@Context player: Player, @Arg("arena") arena: String) {
gameManager.joinGame(player, arena)
}
@Subcommand("create")
@Permission("sg.admin")
fun create(
@Context player: Player,
@Arg("name") name: String,
@Arg("radius") radius: Int // multi-arg commands work correctly
) {
gameManager.createArena(player, name, radius)
}
@Subcommand("stats")
@Async
suspend fun stats(@Context player: Player, @Arg("target") target: Player) {
val stats = statsService.getStats(target)
player.sendMessage(stats.format())
}
}| Type | Description |
|---|---|
Player |
The player who ran the command |
CommandSender |
Generic sender (player or console) |
CommandSourceStack |
Paper's raw command source |
Server |
The server instance |
Run coroutines on the Paper main thread:
@Service
class MyService(private val bukkitDispatcher: BukkitDispatcher) {
suspend fun doMainThreadWork() {
withContext(bukkitDispatcher) {
// Safe to call Bukkit API here
player.teleport(location)
}
}
}BukkitDispatcher skips dispatch when already on the main thread — no scheduling overhead.
For Hytale mods, pass commandRegistry to NexusContext.create():
val context = NexusContext.create(
basePackage = "net.example.mymod",
classLoader = this::class.java.classLoader,
commandRegistry = this.commandRegistry // Hytale's CommandRegistry
)@Command(
name = "heal",
description = "Heal a player",
permission = "admin.heal",
type = CommandType.PLAYER
)
class HealCommand(private val healthService: HealthService) {
fun execute(
@Context context: CommandContext,
@Context world: World,
@Context store: Store<EntityStore>,
@Context ref: Ref<EntityStore>,
@Arg("amount", "Amount of health", required = false, defaultValue = "20") amount: Int
) {
healthService.heal(store, ref, amount)
context.sendMessage(Message.raw("Healed for $amount HP"))
}
}| Type | Thread | Context Parameters |
|---|---|---|
ASYNC |
Background | CommandContext |
PLAYER |
World thread | CommandContext, World, Store<EntityStore>, PlayerRef, Ref<EntityStore> |
TARGET_PLAYER |
World thread | Same as PLAYER (target player's ref) |
TARGET_ENTITY |
World thread | CommandContext, World, Store<EntityStore>, ObjectList<Ref<EntityStore>> |
@ConfigFile("mymod")
@Comment("My Mod Configuration")
class MyModConfig {
@Comment("Enable debug mode")
var debug: Boolean = false
@ConfigName("max-players")
@Comment("Maximum players allowed")
var maxPlayers: Int = 100
@Comment("Database settings")
var database: DatabaseSettings = DatabaseSettings()
class DatabaseSettings {
var host: String = "localhost"
var port: Int = 3306
}
}# My Mod Configuration
# Enable debug mode
debug: false
# Maximum players allowed
max-players: 100
# Database settings
database:
host: "localhost"
port: 3306@Service
class GameService(private val config: MyModConfig) {
fun getMaxPlayers() = config.maxPlayers
}
@Service
class AdminService(private val configManager: ConfigManager) {
fun reloadAll() = configManager.reloadAll()
}Nexus provides centralized coroutine support backed by Java 21 virtual threads. When you pass a classLoader to NexusContext.create(), Nexus automatically creates a virtual thread executor, coroutine dispatcher, and plugin-scoped CoroutineScope.
Java 21 virtual threads inherit the system classloader, not the plugin's. When a coroutine continuation tries to load a plugin class on a virtual thread, it fails. Nexus wraps every virtual thread task to propagate the correct classloader automatically.
@Service
class MyService(private val scope: CoroutineScope) {
fun doAsyncWork() {
scope.launch {
// runs on virtual threads with correct classloader
}
}
}@PostConstruct and @PreDestroy methods can be suspend functions:
@Service
class CacheService {
@PostConstruct
suspend fun warmUp() { /* async initialization */ }
@PreDestroy
suspend fun flush() { /* async cleanup */ }
}When context.close() is called:
- Cancel the coroutine scope (stops all running coroutines)
- Invoke
@PreDestroyon all singletons - Shutdown the virtual thread executor
- Clear the bean registry
| Annotation | Description |
|---|---|
@Component |
Generic managed component |
@Service |
Service layer component |
@Repository |
Data access layer component |
| Annotation | Description |
|---|---|
@Inject |
Mark injection points (optional for constructors) |
@Qualifier("name") |
Disambiguate between multiple beans of same type |
| Annotation | Description |
|---|---|
@PostConstruct |
Called after DI (supports suspend) |
@PreDestroy |
Called before shutdown (supports suspend) |
@Scope(ScopeType) |
SINGLETON (default) or PROTOTYPE |
| Annotation | Description |
|---|---|
@ConfigFile("name") |
Maps class to name.yaml |
@ConfigName("key") |
Custom YAML key name |
@Comment("text") |
YAML comment above the field |
@Transient |
Excluded from save/load |
| Annotation | Description |
|---|---|
@Command(...) |
Command class with metadata (name, description, permission, aliases, type) |
@Arg("name", ...) |
User-provided argument |
@Context |
Runtime injection (CommandContext, World, Store, etc.) |
| Annotation | Description |
|---|---|
@Command(...) |
Root command with name, description, permission |
@Subcommand("path") |
Subcommand method (space-separated path, e.g. "admin setup") |
@Arg("name") |
Brigadier argument with auto-resolved type |
@Context |
Runtime injection (Player, CommandSender, CommandSourceStack, Server) |
@Permission("node") |
Per-subcommand permission |
@PlayerOnly |
Restrict to player senders |
@Async |
Run on coroutine scope instead of main thread |
nexus-core/
├── core/
│ ├── NexusContext Main container — lifecycle, bean management
│ ├── ComponentRegistry Bean definitions + singleton cache
│ ├── BeanFactory Constructor injection, lifecycle hooks
│ └── BeanDefinition Bean metadata (name, type, scope, factory)
├── scanning/
│ └── ComponentScanner ClassGraph-based classpath scanning
├── annotations/ @Component, @Service, @Repository, @Inject, etc.
├── coroutines/
│ ├── NexusDispatchers Virtual thread executor + classloader propagation
│ ├── NexusScope Per-plugin CoroutineScope with SupervisorJob
│ └── CoroutineExtensions withIO, withDefault helpers
├── config/
│ ├── ConfigManager Centralized config loading, saving, caching
│ ├── ConfigLoader YAML serialization with reflection
│ └── Annotations @ConfigFile, @ConfigName, @Comment, @Transient
└── commands/ Hytale command adapters
├── CommandScanner ClassGraph-based command discovery
├── CommandRegistry Bridges to Hytale's CommandRegistry
└── adapters/ Async, Player, TargetPlayer, TargetEntity
nexus-paper/
├── BukkitDispatcher Main-thread coroutine dispatcher
├── PaperNexusExtensions registerPaperCommands() extension
└── commands/
├── PaperCommandScanner Scans @Command + @Subcommand methods
├── PaperCommandRegistry Builds Brigadier trees, registers with Paper
├── PaperCommandDefinition Metadata for scanned commands
├── annotations/ @Subcommand, @Permission, @PlayerOnly, @Async
└── arguments/
├── PaperArgumentResolver Interface for type → Brigadier arg mapping
└── PaperArgumentResolvers Registry (String, Int, Double, Float, Boolean, Player)
- Java 21+ (for virtual threads)
- Kotlin 2.0+
- Hytale commands: Hytale Server API
- Paper commands: Paper 1.21.1+
When shading Nexus into your plugin, relocate ClassGraph as well:
tasks.shadowJar {
relocate("net.badgersmc.nexus", "com.example.mymod.shaded.nexus")
relocate("io.github.classgraph", "com.example.mymod.shaded.classgraph")
relocate("nonapi.io.github.classgraph", "com.example.mymod.shaded.nonapi.classgraph")
}MIT License — see LICENSE file for details.