Nexus is a Kotlin-first application framework for Hytale mods providing automatic dependency injection with classpath scanning, YAML configuration management, and coroutine infrastructure backed by Java 21 virtual threads.
- Automatic Component Discovery: Annotate classes with
@Component,@Service, or@Repositoryand they're found at startup via ClassGraph - no registration lists to maintain - Constructor Injection: Dependencies resolved automatically through primary constructors
- Lifecycle Management:
@PostConstructand@PreDestroyhooks (supports suspend functions) - Scopes: Singleton (default) and Prototype scopes via
@Scope - Polymorphic Resolution: Beans resolved by interface or superclass type
- Qualifier Support:
@Qualifier("name")to disambiguate multiple beans of the same type - 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 are 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
dependencies {
implementation("net.badgersmc:nexus:1.4.0")
}@Repository
class PlayerRepository(private val storage: Storage) {
suspend fun findPlayer(id: UUID): Player? = withIO {
// database query
}
}
@Service
class PlayerService(private val repository: PlayerRepository) {
@PostConstruct
fun init() {
println("PlayerService initialized!")
}
suspend fun getPlayer(id: UUID): Player? {
return repository.findPlayer(id)
}
}// Nexus scans net.example.mymod and all sub-packages for annotated classes.
// Passing configDirectory enables automatic @ConfigFile discovery and loading.
val context = NexusContext.create(
basePackage = "net.example.mymod",
classLoader = this::class.java.classLoader,
configDirectory = dataDirectory,
contextName = "MyPlugin"
)
// Register any beans created outside the container
context.registerBean("storage", Storage::class, storage)
// Retrieve auto-discovered beans — configs are injectable too
val playerService = context.getBean<PlayerService>()
val config = context.getBean<MyModConfig>()
// Cleanup when done
context.close()That's it. Add a new @Service, @Repository, or @ConfigFile class anywhere under your base package and it's automatically available for injection on next startup.
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.
// Launch coroutines on virtual threads with correct classloader
context.scope!!.launch {
val data = withIO { database.query("SELECT ...") }
processData(data)
}The CoroutineScope and NexusDispatchers are registered as beans, so any component can receive them via constructor injection:
@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, Nexus shuts down in order:
- Cancel the coroutine scope (stops all running coroutines)
- Invoke
@PreDestroyon all singletons - Shutdown the virtual thread executor
- Clear the bean registry
@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
}
}When you pass configDirectory to NexusContext.create(), Nexus scans for all @ConfigFile-annotated classes, loads them (creating YAML files with defaults if missing), and registers them as singleton beans. Services can then inject configs directly:
@Service
class GameService(private val config: MyModConfig) {
fun getMaxPlayers() = config.maxPlayers
}A ConfigManager bean is also registered automatically, so any service can inject it for runtime reloads:
@Service
class AdminService(private val configManager: ConfigManager) {
fun reloadAll() = configManager.reloadAll()
}If you need config management without classpath scanning:
val configManager = ConfigManager(dataDirectory)
// Load config (creates mymod.yaml with defaults if missing)
val config = configManager.load<MyModConfig>()
// Modify and save
config.debug = true
configManager.save(config)
// Reload from disk
configManager.reload<MyModConfig>()# My Mod Configuration
# Enable debug mode
debug: false
# Maximum players allowed
max-players: 100
# Database settings
database:
host: "localhost"
port: 3306| Annotation | Target | Description |
|---|---|---|
@Component |
Class | Generic managed component |
@Service |
Class | Service layer component |
@Repository |
Class | Data access layer component |
| Annotation | Target | Description |
|---|---|---|
@Inject |
Constructor, field, param | Mark injection points (optional for constructors) |
@Qualifier("name") |
Parameter | Disambiguate between multiple beans of same type |
| Annotation | Target | Description |
|---|---|---|
@PostConstruct |
Function | Called after dependency injection (supports suspend) |
@PreDestroy |
Function | Called before container shutdown (supports suspend) |
@Scope(ScopeType) |
Class | SINGLETON (default) or PROTOTYPE |
| Annotation | Target | Description |
|---|---|---|
@ConfigFile("name") |
Class | Maps class to name.yaml |
@ConfigName("key") |
Property | Custom YAML key name |
@Comment("text") |
Class, property | YAML comment above the field |
@Transient |
Property | Excluded from save/load |
For beans created outside the container (database connections, plugin instances):
val context = NexusContext.create(
basePackage = "net.example.mymod",
classLoader = this::class.java.classLoader,
configDirectory = dataDirectory
)
// These are available for injection into scanned components
context.registerBean("storage", Storage::class, storage)
context.registerBean("plugin", MyPlugin::class, this)Bean factories use lazy resolution, so manually registered beans are available even when registered after create(). Note that @ConfigFile classes no longer need manual registration — they're discovered and loaded automatically when configDirectory is provided.
If you don't need classpath scanning:
val context = NexusContext.create()
context.registerBean("myBean", MyBean::class, MyBean())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")
}nexus/
├── core/
│ ├── NexusContext Main container — creates context, manages lifecycle
│ ├── ComponentRegistry Bean definitions + singleton cache, polymorphic type indexing
│ ├── BeanFactory Constructor injection, PostConstruct/PreDestroy invocation
│ └── BeanDefinition Bean metadata (name, type, scope, factory)
├── scanning/
│ └── ComponentScanner ClassGraph-based classpath scanning
├── annotations/
│ ├── @Component, @Service, @Repository
│ ├── @Inject, @Qualifier, @Scope
│ └── @PostConstruct, @PreDestroy
├── 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
└── @ConfigFile, @ConfigName, @Comment, @Transient
- Java 21+ (for virtual threads)
- Kotlin 2.0+
MIT License - See LICENSE file for details