Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions .github/workflows/release-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@ name: Create Release

on:
workflow_dispatch:
inputs:
branch:
description: 'Branch to build and publish'
required: true
default: 'develop'
type: choice
options:
- develop
- feat/platform

jobs:
build:
Expand All @@ -12,7 +21,7 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v3
with:
ref: develop # Ensure it works from the develop branch
ref: ${{ inputs.branch }}

- name: Set up JDK 21
uses: actions/setup-java@v3
Expand Down Expand Up @@ -56,7 +65,7 @@ jobs:
release_name: v${{ env.GRADLE_VERSION }}-dev.${{ env.COMMIT_HASH }}
draft: false
prerelease: true
commitish: develop
commitish: ${{ inputs.branch }}
body: |
This release contains dev builds for all Gradle modules.
env:
Expand Down
5 changes: 1 addition & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@ build/
!**/src/test/**/build/

### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
.idea/
*.iws
*.iml
*.ipr
Expand Down
8 changes: 5 additions & 3 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ plugins {
val baseVersion = "0.0.1"
val commitHash = System.getenv("COMMIT_HASH")
val timestamp = System.currentTimeMillis() // Temporary to be able to build and publish directly out of fix branch with same commit hash
val snapshotVersion = "${baseVersion}-dev.${timestamp}-${commitHash}"
val snapshotVersion = "${baseVersion}-platform.${timestamp}-${commitHash}"

allprojects {
group = "app.simplecloud.plugin"
Expand All @@ -19,7 +19,6 @@ allprojects {
maven("https://libraries.minecraft.net")
maven("https://buf.build/gen/maven")
maven("https://repo.simplecloud.app/snapshots")
maven("https://buf.build/gen/maven")
maven("https://repo.papermc.io/repository/maven-public")
}
}
Expand All @@ -31,8 +30,11 @@ subprojects {
dependencies {
testImplementation(rootProject.libs.kotlin.test)
compileOnly(rootProject.libs.kotlin.jvm)
compileOnly(rootProject.libs.bundles.simpleCloudController)
compileOnly(rootProject.libs.kotlin.coroutines)
compileOnly(rootProject.libs.simplecloud.api)
compileOnly(rootProject.libs.bundles.adventure)
compileOnly(rootProject.libs.bundles.configurate)
compileOnly(rootProject.libs.slf4j.api)
}

kotlin {
Expand Down
152 changes: 152 additions & 0 deletions docs/config-migration-system.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# Config Migration System

The migration system lets you upgrade old YAML configs to the current structure before they are deserialized.
It is designed to plug directly into `ConfigurationFactory`, `YamlDirectoryRepository`, and `YamlFileConfigurator`.

## Main pieces

- `VersionedConfig`: marks a config as migration-capable
- `ConfigMigrator`: defines the current version and the upgrade steps
- root YAML key `version`: stores the config version on disk

## Defining a versioned config

Your config should implement `VersionedConfig` and expose the current version.

```kotlin
import app.simplecloud.plugin.api.shared.config.VersionedConfig
import org.spongepowered.configurate.objectmapping.ConfigSerializable

@ConfigSerializable
class WarpConfig : VersionedConfig {
override var version: Int = 2

var name: String = ""
var world: String = "world"
var enabled: Boolean = true
}
```

## Creating a migrator

`ConfigMigrator` upgrades a YAML node step by step.
Each migration receives the raw Configurate node, so you can rename fields, move sections, or add defaults.

```kotlin
import app.simplecloud.plugin.api.shared.config.ConfigMigrator

val warpMigrator = ConfigMigrator.builder(currentVersion = 2)
.migrate(fromVersion = 0, toVersion = 1) { node ->
val oldWorldName = node.node("location", "worldName").string ?: return@migrate
node.node("world").set(oldWorldName)
node.node("location").set(null)
}
.migrate(fromVersion = 1, toVersion = 2) { node ->
if (node.node("enabled").virtual()) {
node.node("enabled").set(true)
}
}
.build()
```

## Using migrations with a single config

```kotlin
import app.simplecloud.plugin.api.shared.config.ConfigurationFactory

val factory = ConfigurationFactory(
file = dataDirectory.resolve("config.yml").toFile(),
javaClass = WarpConfig::class.java,
configMigrator = warpMigrator,
)

val config = factory.loadOrCreate(WarpConfig())
```

When an older file is loaded:

1. the YAML is parsed
2. the migrator checks the stored `version`
3. all missing migration steps are applied in order
4. the updated YAML is written back to disk
5. the final structure is deserialized into your config class

## Using migrations with a directory repository

```kotlin
import app.simplecloud.plugin.api.shared.config.YamlDirectoryRepository
import java.nio.file.Path

class WarpRepository(
directory: Path,
) : YamlDirectoryRepository<WarpConfig, String>(
directory = directory,
javaClass = WarpConfig::class.java,
configMigrator = warpMigrator,
) {

override fun save(entity: WarpConfig) {
save("${entity.name}.yml", entity)
}

override fun find(identifier: String): WarpConfig? {
return findAll().firstOrNull { it.name == identifier }
}
}
```

Calling `load()` will migrate each YAML file as it is read.

## Legacy files without a version

Files that do not have a `version` key are treated as version `0` by default.
That makes it easy to introduce migrations for older configs that existed before versioning.

Example legacy file:

```yaml
name: spawn
location:
worldName: lobby
```

After migration:

```yaml
version: 2
name: spawn
world: lobby
enabled: true
```

## Changing the fallback version

If your oldest supported files should be treated as something other than version `0`, set a fallback version:

```kotlin
val migrator = ConfigMigrator.builder(currentVersion = 3)
.fallbackVersion(1)
.migrate(fromVersion = 1, toVersion = 2) { node ->
node.node("newField").set("default")
}
.migrate(fromVersion = 2, toVersion = 3) { node ->
node.node("anotherField").set(true)
}
.build()
```

## Important behavior

- migrations only work for configs that implement `VersionedConfig`
- the migration chain must be complete from the file version to `currentVersion`
- if a file version is newer than the library supports, loading fails
- if a migration step is missing, loading fails
- migrated files are persisted automatically after a successful load

## Best practices

- increase `version` only when the YAML structure changes
- keep each migration small and focused
- prefer additive migrations where possible
- remove old fields explicitly with `node.node("oldField").set(null)` when renaming
- test migrations with real old YAML examples
158 changes: 158 additions & 0 deletions docs/config-system.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# Config System

The config system in `plugin-shared` is built around Configurate YAML serialization.
Most configs should be regular Kotlin classes annotated with `@ConfigSerializable`.

## Main pieces

- `ConfigurationFactory<E>`: loads or creates one config file
- `YamlDirectoryRepository<E, I>`: manages many YAML files in one directory
- `YamlFileConfigurator<E>`: low-level file serializer if you need direct access
- `AbstractMessageConfig`: optional base class for MiniMessage-based message configs

## Single config file

Use `ConfigurationFactory` when you have one config file such as `config.yml`.

```kotlin
import app.simplecloud.plugin.api.shared.config.ConfigurationFactory
import org.spongepowered.configurate.objectmapping.ConfigSerializable
import java.io.File

@ConfigSerializable
class PluginConfig {
var prefix: String = "<gray>[<gold>Example<gray>]"
var enabled: Boolean = true
}

val factory = ConfigurationFactory(
File(dataDirectory.toFile(), "config.yml"),
PluginConfig::class.java,
)

val config = factory.loadOrCreate(PluginConfig())

if (config.enabled) {
println(config.prefix)
}

config.enabled = false
factory.save(config)
```

### Useful methods

- `loadOrCreate(defaultConfig)`: loads the file, or creates it from the default object
- `get()`: returns the cached config after it has been loaded
- `save(entry)`: writes the config back to disk
- `reload()`: reloads the file into the cache

## Directory-based configs

Use `YamlDirectoryRepository` when each entity should live in its own YAML file.
This works well for things like warps, templates, arenas, or message profiles.

```kotlin
import app.simplecloud.plugin.api.shared.config.YamlDirectoryRepository
import org.spongepowered.configurate.objectmapping.ConfigSerializable
import java.nio.file.Path

@ConfigSerializable
class WarpConfig {
var name: String = ""
var world: String = "world"
var x: Double = 0.0
var y: Double = 64.0
var z: Double = 0.0
}

class WarpRepository(
directory: Path,
) : YamlDirectoryRepository<WarpConfig, String>(
directory = directory,
javaClass = WarpConfig::class.java,
) {

override fun save(entity: WarpConfig) {
save("${entity.name}.yml", entity)
}

override fun find(identifier: String): WarpConfig? {
return findAll().firstOrNull { it.name.equals(identifier, ignoreCase = true) }
}
}
```

Usage:

```kotlin
val repository = WarpRepository(dataDirectory.resolve("warps"))

repository.load()

val lobby = WarpConfig().apply {
name = "lobby"
world = "world"
x = 10.0
y = 65.0
z = -5.0
}

repository.save(lobby)

val loadedLobby = repository.find("lobby")
val allWarps = repository.findAll()
```

### Repository flow

- implement your own repository class
- extend `YamlDirectoryRepository<E, I>`
- decide how files are named in `save(entity)`
- decide how lookups work in `find(identifier)`
- call `load()` during startup to populate the cache

## Direct YAML access

`YamlFileConfigurator` is the lower-level serializer used by both higher abstractions.
Use it if you want YAML serialization without repository or factory behavior.

```kotlin
import app.simplecloud.plugin.api.shared.config.YamlFileConfigurator

val configurator = YamlFileConfigurator(PluginConfig::class.java)
val file = dataDirectory.resolve("config.yml").toFile()

configurator.save(file, PluginConfig())
val loaded = configurator.load(file)
```

## Message configs

`AbstractMessageConfig` is helpful when your config stores MiniMessage strings and shared variables.

```kotlin
import app.simplecloud.plugin.api.shared.config.AbstractMessageConfig

class MessagesConfig : AbstractMessageConfig() {
override val variables = mapOf(
"prefix" to "<gray>[<gold>Example<gray>]"
)

val noPermission = "<prefix> <red>You do not have permission."
}
```

Then render a message with:

```kotlin
val component = messagesConfig.msg(messagesConfig.noPermission)
```

## Best practices

- annotate config classes with `@ConfigSerializable`
- prefer mutable `var` fields with default values for YAML-backed classes
- use `ConfigurationFactory` for one file and `YamlDirectoryRepository` for many files
- load repositories during startup so `findAll()` and `find()` work from cache
- keep config classes simple and serialization-friendly
Loading