Skip to content
Merged
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
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ dependencies {
testImplementation(libs.paper.api)
testImplementation(libs.junit4)
testImplementation(libs.syntaxdb)
testImplementation(libs.syntaxcore)
// Direct JDBC dependencies remain test-only for low-level persistence contract tests.
testImplementation(libs.sqlite)
testRuntimeOnly(libs.mariadb)
Expand Down
54 changes: 54 additions & 0 deletions docs/wiki/en/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,60 @@ For rank-based limits and multi-world configuration, see:

Settings are stored in `plugins/PlotsX/config.yml`. A value of `true` means “yes” and `false` means “no”. Preserve indentation and use spaces instead of tabs.

## Versioned loading and reload

PlotsX initializes SyntaxCore first and reuses `SyntaxCore.logger`. Its configuration
is then loaded by SyntaxCore's ConfigManager before plot/database services start:

```kotlin
SyntaxCore.init(this, versionType = "paper")
logger = SyntaxCore.logger
// PluginInitializer.setupConfig():
configManager = ConfigManager(
plugin = this,
logger = logger,
initialVersion = 100,
targetVersion = 103
).registerMigrations(ConfigMigrations.steps())
configManager.load()
reloadConfig() // refresh Bukkit's separate view used by PlotsX services
pluginConfig = config
```

The current bundled schema is **103**. Existing unversioned files are treated as 100.
Steps 100 → 101 → 102 → 103 preserve user settings and add missing template fields;
101/102 were documentation changes, so there is no automatic database conversion or
backend switch. Keep your actual database selection and credentials. Unsupported
backends fail through normal database validation.

Before migration, ConfigManager creates a backup such as `config.yml.102.bak`.
It validates the staged YAML before atomically replacing the live file. Failed migration
leaves the original file intact. Files already at schema 103 or a newer schema are
read without rewriting or downgrading. New installations use the current template.
Do not manually raise `config-version` to skip a required migration.

`/ptx reload` uses the same loader and refreshes Bukkit's view before rebuilding
runtime caches. Changes to `plots.claiming.mode` still require a restart. A failed
configuration load keeps existing runtime caches; correct the YAML and try again.
Startup-only services, including the database connection and SyntaxCore's own startup
settings, require a restart to apply changes. Core services start before PlotsX's
migration; configuration reload does not automatically reinitialize them.

Backup is per migration attempt/chain, not per individual version step. Retries create
unique backups and retain previous copies. If backup creation fails, migration callbacks
do not run. Parse validation happens before backup/mutation: malformed YAML is rejected
without replacing it with a default file. Current/future schemas do not trigger a backup
because no migration occurs. Missing fields in a current/future file are not automatically
written back; add a new versioned step when evolving the schema.

Language files and the reload prevalidation use strict Bukkit YAML parsing as well:
syntax errors propagate and leave the original bytes intact. They cannot be converted
into an empty document and overwritten with defaults after a parse failure.

Language-file completion remains separate from the versioned main configuration.
For adding a field or renaming a path in a future release, raise the template and
`targetVersion` together and add the corresponding step in `ConfigMigrations`.

## Full default configuration

```yaml
Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ run-paper = "3.1.0"
plugin-deployer = "1.0.6-R0.2-SNAPSHOT"
foojay-resolver = "1.0.0"
paper = "1.21.11-R0.1-SNAPSHOT"
syntaxcore = "1.4.1-R0.1-SNAPSHOT"
syntaxcore = "2.0.0-R0.1-SNAPSHOT"
syntaxdb = "0.1.0-R0.1-SNAPSHOT"
messagehandler = "1.3.0-R0.2-SNAPSHOT"
cleanerx = "1.5.8"
Expand Down
7 changes: 5 additions & 2 deletions src/main/kotlin/pl/syntaxdevteam/plotsx/PlotsX.kt
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ class PlotsX : JavaPlugin() {
lateinit var pluginsManager: PluginManagerX

lateinit var uuidManager: UUIDManager
lateinit var configManager: pl.syntaxdevteam.core.config.ConfigManager
lateinit var configHandler: ConfigHandler
lateinit var pluginConfig: FileConfiguration
lateinit var statsCollector: StatsCollector
Expand Down Expand Up @@ -105,6 +106,7 @@ class PlotsX : JavaPlugin() {
ModrinthSource("")
)
SyntaxCore.init(this, versionType = "paper")
logger = SyntaxCore.logger
borderVisualizer = pl.syntaxdevteam.plotsx.visualization.PlotBorderVisualizer(this)
server.pluginManager.registerEvents(borderVisualizer, this)
pluginInitializer = PluginInitializer(this)
Expand Down Expand Up @@ -153,6 +155,7 @@ class PlotsX : JavaPlugin() {

if (::databaseHandler.isInitialized) databaseHandler.closeConnection()
if (::pluginInitializer.isInitialized) pluginInitializer.onDisable()
SyntaxCore.close()
}

/**
Expand All @@ -161,10 +164,10 @@ class PlotsX : JavaPlugin() {
*/
@JvmOverloads
fun onReload(completion: (Exception?) -> Unit = {}) {
val candidate = org.bukkit.configuration.file.YamlConfiguration.loadConfiguration(File(dataFolder, "config.yml"))
val candidate = pl.syntaxdevteam.plotsx.common.YamlFiles.read(File(dataFolder, "config.yml"))
require(validateClaimConfig(candidate) == claimMode) { "Changing plots.claiming.mode requires a server restart" }
configHandler.verifyAndUpdateConfig()
if (::borderVisualizer.isInitialized) borderVisualizer.close()
super.reloadConfig()

schedulerAdapter.runAsync(Runnable {
val failure = try {
Expand Down
3 changes: 3 additions & 0 deletions src/main/kotlin/pl/syntaxdevteam/plotsx/commands/PlotsXCMD.kt
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ class PlotsXCMD(private val plugin: PlotsX) : BasicCommand {
stack.sender.sendMessage("Reload rejected: ${failure.message}")
} catch (failure: IllegalStateException) {
stack.sender.sendMessage("Reload unavailable: ${failure.message}")
} catch (failure: Exception) {
plugin.logger.err("Config reload failed: ${failure.message}")
stack.sender.sendMessage("Reload failed; check the server log and configuration file.")
}
}

Expand Down
63 changes: 11 additions & 52 deletions src/main/kotlin/pl/syntaxdevteam/plotsx/common/ConfigHandler.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package pl.syntaxdevteam.plotsx.common

import org.bukkit.configuration.ConfigurationSection
import org.bukkit.configuration.file.YamlConfiguration
import pl.syntaxdevteam.plotsx.PlotsX
import java.io.File

Expand All @@ -10,21 +8,10 @@ class ConfigHandler(private val plugin: PlotsX) {
private val bundledLanguages = listOf("pl", "en")

fun verifyAndUpdateConfig() {
val confFile = File(plugin.dataFolder, "config.yml")
val defaultConfStream = plugin.getResource("config.yml")

if (defaultConfStream == null) {
plugin.logger.err("Default $confFile file not found in plugin resources!")
return
}

val defaultConfig = YamlConfiguration.loadConfiguration(defaultConfStream.reader())
val currentConfig = YamlConfiguration.loadConfiguration(confFile)

if (synchronizeSections(defaultConfig, currentConfig)) {
plugin.logger.success("Updating $confFile file with missing entries.")
currentConfig.save(confFile)
}
plugin.configManager.load()
// PlotsX services consume Bukkit's view, not the separate BoostedYAML document.
plugin.reloadConfig()
plugin.pluginConfig = plugin.config
}

/**
Expand All @@ -43,43 +30,15 @@ class ConfigHandler(private val plugin: PlotsX) {
return@forEach
}

if (!messageFile.exists()) {
messageFile.parentFile.mkdirs()
plugin.saveResource(resourcePath, false)
return@forEach
}

val defaultMessages = defaultStream.reader().use { reader ->
YamlConfiguration.loadConfiguration(reader)
}
val currentMessages = YamlConfiguration.loadConfiguration(messageFile)

if (synchronizeSections(defaultMessages, currentMessages)) {
plugin.logger.success("Updating $messageFile file with missing entries.")
currentMessages.save(messageFile)
}
}
}

private fun synchronizeSections(
defaultSection: ConfigurationSection,
currentSection: ConfigurationSection
): Boolean {
var updated = false
for (key in defaultSection.getKeys(false)) {
if (!currentSection.contains(key)) {
currentSection[key] = defaultSection[key]
updated = true
} else if (defaultSection.isConfigurationSection(key)) {
require(currentSection.isConfigurationSection(key)) {
"${currentSection.currentPath}.$key must be a configuration section"
defaultStream.reader().use { reader ->
if (!messageFile.exists()) {
messageFile.parentFile.mkdirs()
plugin.saveResource(resourcePath, false)
} else if (YamlFiles.updateMissing(messageFile, reader)) {
plugin.logger.success("Updating $messageFile file with missing entries.")
}
updated = synchronizeSections(
defaultSection.getConfigurationSection(key)!!,
currentSection.getConfigurationSection(key)!!
) || updated
}
}
return updated
}

}
44 changes: 44 additions & 0 deletions src/main/kotlin/pl/syntaxdevteam/plotsx/common/ConfigMigrations.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package pl.syntaxdevteam.plotsx.common

import pl.syntaxdevteam.core.config.BaseConfigMigration
import pl.syntaxdevteam.core.config.ConfigMigration
import pl.syntaxdevteam.core.config.ConfigMigrationContext

/** Historical 101/102 releases changed backend documentation, not configuration paths.
* 103 adopts the versioned loader while retaining the old additive loader's custom keys.
* Database backend selections are preserved; migration never silently switches storage.
*/
internal object ConfigMigrations {
fun steps(): List<ConfigMigration> = listOf(
documentationStep(100, 101), documentationStep(101, 102),
object : BaseConfigMigration(102, 103) {
override fun migrate(context: ConfigMigrationContext) {
val user = context.userConfig ?: return
for (key in user.keys) {
val path = key.toString()
val section = context.readUserSectionMap(path)
context.config.set(path, if (section == null) user.get(path)
else merge(context.readSectionMap(path).orEmpty(), section))
}
}
}
)

// Merge defaults first and raw user settings last, preserving unknown nested fields.
private fun merge(defaults: Map<String, Any?>, user: Map<String, Any?>): Map<String, Any?> {
val result = defaults.toMutableMap()
for ((key, value) in user) {
val fallback = defaults[key]
result[key] = if (value is Map<*, *> && fallback is Map<*, *>) {
merge(stringMap(fallback), stringMap(value))
} else value
}
return result
}
private fun stringMap(value: Map<*, *>): Map<String, Any?> =
value.entries.associate { it.key.toString() to it.value }

private fun documentationStep(from: Int, to: Int) = object : BaseConfigMigration(from, to) {
override fun migrate(context: ConfigMigrationContext) = Unit
}
}
36 changes: 36 additions & 0 deletions src/main/kotlin/pl/syntaxdevteam/plotsx/common/YamlFiles.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package pl.syntaxdevteam.plotsx.common

import org.bukkit.configuration.ConfigurationSection
import org.bukkit.configuration.file.YamlConfiguration
import java.io.File
import java.io.Reader

/** Unlike loadConfiguration, strict load throws on malformed YAML instead of returning an empty document. */
internal object YamlFiles {
fun read(file: File): YamlConfiguration = YamlConfiguration().apply { load(file) }

fun updateMissing(file: File, template: Reader): Boolean {
val defaults = YamlConfiguration().apply { load(template) }
val current = read(file)
val changed = synchronizeSections(defaults, current)
if (changed) current.save(file)
return changed
}

private fun synchronizeSections(defaults: ConfigurationSection, current: ConfigurationSection): Boolean {
var updated = false
for (key in defaults.getKeys(false)) {
if (!current.contains(key)) {
current[key] = defaults[key]
updated = true
} else if (defaults.isConfigurationSection(key)) {
require(current.isConfigurationSection(key)) {
"${current.currentPath}.$key must be a configuration section"
}
updated = synchronizeSections(defaults.getConfigurationSection(key)!!,
current.getConfigurationSection(key)!!) || updated
}
}
return updated
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import pl.syntaxdevteam.plotsx.listener.RenamePlotChatListener
class PluginInitializer(private val plugin: PlotsX) {

fun onEnable() {
setUpLogger()
setupCompatibility()
setupConfig()
setupUUID()
Expand All @@ -42,19 +41,19 @@ class PluginInitializer(private val plugin: PlotsX) {
plugin.logger.err(plugin.pluginMeta.name + " " + plugin.pluginMeta.version + " has been disabled ☹️")
}

private fun setUpLogger() {
plugin.pluginConfig = plugin.config
plugin.logger = SyntaxCore.logger
}

private fun setupCompatibility() {
plugin.versionChecker = VersionChecker(plugin)
plugin.versionCompatibility = VersionCompatibility(plugin.versionChecker)
plugin.schedulerAdapter = BukkitSchedulerAdapter(plugin)
}

private fun setupConfig() {
plugin.saveDefaultConfig()
plugin.configManager = pl.syntaxdevteam.core.config.ConfigManager(
plugin = plugin,
logger = plugin.logger,
initialVersion = 100,
targetVersion = 103
).registerMigrations(pl.syntaxdevteam.plotsx.common.ConfigMigrations.steps())
plugin.configHandler = ConfigHandler(plugin)
plugin.configHandler.verifyAndUpdateConfig()
plugin.configHandler.verifyAndUpdateLanguageFiles()
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/config.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
config-version: 102
config-version: 103

# /********************************************/
# Global settings
Expand Down
Loading
Loading