diff --git a/doc/Config.md b/doc/Config.md
index f80966fc4..2abdfcd65 100644
--- a/doc/Config.md
+++ b/doc/Config.md
@@ -1,5 +1,21 @@
# Config file
+
There is a config file located in:
- * Windows: `%AppData%\Minosoft`
- * MacOS: `"~/Library/Application Support/Minosoft"`
- * Linux (and all others): `~\Minosoft`
+
+* Windows: `%AppData%\Minosoft`
+* MacOS: `"~/Library/Application Support/Minosoft"`
+* Linux (and all others): `~\Minosoft`
+
+
+- Profiles
+- Select profile per server
+- Config reloading
+ - From disk
+ - In eros
+ - Per key combination
+- Migration
+- Automatic saving (periodic, per event. maybe use delegates?)
+- Config editor
+- Config value checker
+- Change event, batch those, apply event
+ - Multiple config files (e.g. eros, key combinations, particles, accounts, log, audio, blocks, entities, hit boxes, world, network, assets, physics, hud, other)
diff --git a/pom.xml b/pom.xml
index 330eb12d2..9600538bc 100644
--- a/pom.xml
+++ b/pom.xml
@@ -429,5 +429,10 @@
commons-lang3
3.12.0
+
+ com.fasterxml.jackson.module
+ jackson-module-kotlin
+ 2.13.0
+
diff --git a/src/main/java/de/bixilon/minosoft/Minosoft.kt b/src/main/java/de/bixilon/minosoft/Minosoft.kt
index e998c9f02..4754fb1e1 100644
--- a/src/main/java/de/bixilon/minosoft/Minosoft.kt
+++ b/src/main/java/de/bixilon/minosoft/Minosoft.kt
@@ -14,6 +14,7 @@
package de.bixilon.minosoft
import de.bixilon.minosoft.config.Configuration
+import de.bixilon.minosoft.config.config2.GlobalProfileManager
import de.bixilon.minosoft.data.accounts.Account
import de.bixilon.minosoft.data.assets.JarAssetsManager
import de.bixilon.minosoft.data.assets.Resources
@@ -99,6 +100,12 @@ object Minosoft {
Log.log(LogMessageType.OTHER, LogLevels.VERBOSE) { "Config file loaded!" }
})
+ taskWorker += Task(identifier = StartupTasks.LOAD_CONFIG2, priority = ThreadPool.HIGH, dependencies = arrayOf(StartupTasks.LOAD_VERSIONS), executor = {
+ Log.log(LogMessageType.OTHER, LogLevels.VERBOSE) { "Loading config2 file..." }
+ GlobalProfileManager.load()
+ Log.log(LogMessageType.OTHER, LogLevels.VERBOSE) { "Config2 file loaded!" }
+ })
+
taskWorker += Task(identifier = StartupTasks.LOAD_LANGUAGE_FILES, dependencies = arrayOf(StartupTasks.LOAD_CONFIG), executor = {
Log.log(LogMessageType.OTHER, LogLevels.VERBOSE) { "Loading language files (${config.config.general.language})" }
LANGUAGE_MANAGER.translators[ProtocolDefinition.MINOSOFT_NAMESPACE] = load(config.config.general.language, null, ResourceLocation(ProtocolDefinition.MINOSOFT_NAMESPACE, "language/"))
diff --git a/src/main/java/de/bixilon/minosoft/config/config2/GlobalProfileManager.kt b/src/main/java/de/bixilon/minosoft/config/config2/GlobalProfileManager.kt
new file mode 100644
index 000000000..0a54da7a1
--- /dev/null
+++ b/src/main/java/de/bixilon/minosoft/config/config2/GlobalProfileManager.kt
@@ -0,0 +1,15 @@
+package de.bixilon.minosoft.config.config2
+
+import de.bixilon.minosoft.config.config2.config.eros.ErosProfileManager
+
+object GlobalProfileManager {
+ val DEFAULT_MANAGERS: List> = listOf(
+ ErosProfileManager,
+ )
+
+ fun load() {
+ for (manager in DEFAULT_MANAGERS) {
+ manager.load(null)
+ }
+ }
+}
diff --git a/src/main/java/de/bixilon/minosoft/config/config2/ProfileManager.kt b/src/main/java/de/bixilon/minosoft/config/config2/ProfileManager.kt
new file mode 100644
index 000000000..fe4c55844
--- /dev/null
+++ b/src/main/java/de/bixilon/minosoft/config/config2/ProfileManager.kt
@@ -0,0 +1,157 @@
+package de.bixilon.minosoft.config.config2
+
+import com.google.common.collect.HashBiMap
+import de.bixilon.minosoft.config.config2.config.Profile
+import de.bixilon.minosoft.config.config2.util.ConfigDelegate
+import de.bixilon.minosoft.data.registries.ResourceLocation
+import de.bixilon.minosoft.gui.eros.crash.ErosCrashReport.Companion.crash
+import de.bixilon.minosoft.terminal.RunConfiguration
+import de.bixilon.minosoft.util.KUtil
+import de.bixilon.minosoft.util.KUtil.toInt
+import de.bixilon.minosoft.util.Util
+import de.bixilon.minosoft.util.json.jackson.Jackson
+import de.bixilon.minosoft.util.logging.Log
+import de.bixilon.minosoft.util.logging.LogLevels
+import de.bixilon.minosoft.util.logging.LogMessageType
+import de.bixilon.minosoft.util.task.pool.DefaultThreadPool
+import java.io.File
+import java.io.FileNotFoundException
+import java.io.FileWriter
+import java.io.IOException
+import java.util.concurrent.locks.ReentrantLock
+
+
+interface ProfileManager {
+ val namespace: ResourceLocation
+ val latestVersion: Int
+ val saveLock: ReentrantLock
+
+ val profiles: HashBiMap
+ var selected: T
+
+ val baseDirectory: File
+ get() = File(RunConfiguration.HOME_DIRECTORY + "config/" + namespace.namespace + "/")
+
+ fun getPath(profileName: String, baseDirectory: File = this.baseDirectory): String {
+ return baseDirectory.path + "/" + profileName + "/" + namespace.path + ".json"
+ }
+
+
+ /**
+ * Migrates the config from 1 version to the next
+ * Does not convert to the latest version, just 1 version number higher
+ */
+ fun migrate(from: Int, data: MutableMap) = Unit
+ fun load(name: String, data: MutableMap?): T
+
+
+ fun delegate(value: V, checkEquals: Boolean = true): ConfigDelegate
+
+ fun selectDefault()
+ fun createDefaultProfile(): T
+
+ fun initDefaultProfile() {
+ val profile = createDefaultProfile()
+ this.selected = profile
+ save(profile)
+ }
+
+ fun serialize(profile: T): Map
+
+ fun save(profile: T) {
+ saveLock.lock()
+ DefaultThreadPool += {
+ try {
+ val data = serialize(profile)
+ val jsonString = Jackson.MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(data)
+
+ val profileFile = File(getPath(getName(profile)))
+ val parent = profileFile.parentFile
+ if (!parent.exists()) {
+ parent.mkdirs()
+ if (!parent.isDirectory) {
+ throw IOException("Could not create profile folder: ${parent.path}")
+ }
+ }
+
+ val tempFile = File("${profileFile.path}.tmp")
+ if (tempFile.exists()) {
+ if (!tempFile.delete()) {
+ throw IOException("Could not delete $tempFile!")
+ }
+ }
+ FileWriter(tempFile).apply {
+ write(jsonString)
+ close()
+ }
+ if (profileFile.exists() && !profileFile.delete()) {
+ throw IOException("Could not delete $profileFile!")
+ }
+ if (!tempFile.renameTo(profileFile)) {
+ throw IOException("Could not move $tempFile to $profileFile!")
+ }
+ } catch (exception: Exception) {
+ exception.crash()
+ } finally {
+ saveLock.unlock()
+ }
+ }
+ }
+
+ fun getName(profile: T): String {
+ return profiles.inverse()[profile] ?: "Unknown profile"
+ }
+
+ fun load(selected: String?) {
+ val baseDirectory = baseDirectory
+ if (!baseDirectory.exists()) {
+ baseDirectory.mkdirs()
+ // ToDo: Skip further processing
+ }
+ if (!baseDirectory.isDirectory) {
+ throw IOException("${baseDirectory.path} is not an directory!")
+ }
+ val profileNames = baseDirectory.list { current, name -> File(current, name).isDirectory } ?: throw IOException("Can not create a list of profiles in ${baseDirectory.path}")
+ if (selected == null || profileNames.isEmpty()) {
+ initDefaultProfile()
+ }
+ var migrated = false
+ for (profileName in profileNames) {
+ val path = getPath(profileName, baseDirectory)
+ val json: MutableMap?
+ val jsonString = KUtil.tryCatch(FileNotFoundException::class.java) { Util.readFile(path) }
+ if (jsonString != null) {
+ json = Jackson.MAPPER.readValue(jsonString, Jackson.JSON_MAP_TYPE)!!
+ val version = json["version"]?.toInt() ?: throw IllegalArgumentException("Can not find version attribute in profile: $path")
+ if (version > latestVersion) {
+ throw IllegalStateException("Your profile ($path) was created with a newer version of minosoft. Expected $version <= $latestVersion!")
+ }
+ if (version < latestVersion) {
+ for (toMigrate in version until latestVersion) {
+ migrate(toMigrate, json)
+ }
+ Log.log(LogMessageType.OTHER, LogLevels.VERBOSE) { "Migrated profile ($path) from $version to $latestVersion" }
+ json["version"] = latestVersion
+ migrated = true
+ }
+ } else {
+ json = null
+ }
+
+ val profile = load(profileName, json)
+ if (migrated) {
+ save(profile)
+ }
+ }
+
+ if (selected != null) {
+ profiles[selected]?.let { this.selected = it } ?: selectDefault()
+ }
+
+ Log.log(LogMessageType.OTHER, LogLevels.INFO) { "Loaded ${profiles.size} $namespace profiles!" }
+ }
+
+ companion object {
+ const val DEFAULT_PROFILE_NAME = "Default"
+ }
+}
diff --git a/src/main/java/de/bixilon/minosoft/config/config2/config/Profile.kt b/src/main/java/de/bixilon/minosoft/config/config2/config/Profile.kt
new file mode 100644
index 000000000..cdfd5674d
--- /dev/null
+++ b/src/main/java/de/bixilon/minosoft/config/config2/config/Profile.kt
@@ -0,0 +1,6 @@
+package de.bixilon.minosoft.config.config2.config
+
+interface Profile {
+ val version: Int
+ val description: String?
+}
diff --git a/src/main/java/de/bixilon/minosoft/config/config2/config/eros/ErosProfile.kt b/src/main/java/de/bixilon/minosoft/config/config2/config/eros/ErosProfile.kt
new file mode 100644
index 000000000..1aba35eb2
--- /dev/null
+++ b/src/main/java/de/bixilon/minosoft/config/config2/config/eros/ErosProfile.kt
@@ -0,0 +1,19 @@
+package de.bixilon.minosoft.config.config2.config.eros
+
+import de.bixilon.minosoft.config.config2.config.Profile
+import de.bixilon.minosoft.config.config2.config.eros.ErosProfileManager.delegate
+import de.bixilon.minosoft.config.config2.config.eros.ErosProfileManager.latestVersion
+import de.bixilon.minosoft.config.config2.config.eros.general.GeneralC2
+
+class ErosProfile(
+ description: String? = null,
+) : Profile {
+ override val version: Int = latestVersion
+ override val description by delegate(description ?: "")
+
+ val general: GeneralC2 = GeneralC2()
+
+ override fun toString(): String {
+ return ErosProfileManager.getName(this)
+ }
+}
diff --git a/src/main/java/de/bixilon/minosoft/config/config2/config/eros/ErosProfileManager.kt b/src/main/java/de/bixilon/minosoft/config/config2/config/eros/ErosProfileManager.kt
new file mode 100644
index 000000000..81a89ead1
--- /dev/null
+++ b/src/main/java/de/bixilon/minosoft/config/config2/config/eros/ErosProfileManager.kt
@@ -0,0 +1,59 @@
+package de.bixilon.minosoft.config.config2.config.eros
+
+import com.google.common.collect.HashBiMap
+import de.bixilon.minosoft.config.config2.ProfileManager
+import de.bixilon.minosoft.config.config2.util.ConfigDelegate
+import de.bixilon.minosoft.modding.event.master.GlobalEventMaster
+import de.bixilon.minosoft.util.KUtil.toResourceLocation
+import de.bixilon.minosoft.util.KUtil.unsafeCast
+import de.bixilon.minosoft.util.json.jackson.Jackson
+import java.util.concurrent.locks.ReentrantLock
+
+object ErosProfileManager : ProfileManager {
+ override val namespace = "minosoft:eros".toResourceLocation()
+ override val latestVersion = 1
+ override val saveLock = ReentrantLock()
+
+
+ private var currentLoadingPath: String? = null
+ override val profiles: HashBiMap = HashBiMap.create()
+
+ override var selected: ErosProfile = null.unsafeCast()
+ set(value) {
+ field = value
+ GlobalEventMaster.fireEvent(ErosProfileSelectEvent(value))
+ }
+
+ override fun selectDefault() {
+ selected = profiles[ProfileManager.DEFAULT_PROFILE_NAME] ?: createDefaultProfile()
+ }
+
+ override fun createDefaultProfile(): ErosProfile {
+ currentLoadingPath = ProfileManager.DEFAULT_PROFILE_NAME
+ val profile = ErosProfile("Default eros profile")
+ currentLoadingPath = null
+ profiles[ProfileManager.DEFAULT_PROFILE_NAME] = profile
+
+ return profile
+ }
+
+ override fun load(name: String, data: MutableMap?): ErosProfile {
+ currentLoadingPath = name
+ val profile: ErosProfile = if (data == null) {
+ ErosProfile()
+ } else {
+ Jackson.MAPPER.convertValue(data, ErosProfile::class.java)
+ }
+ profiles[name] = profile
+ currentLoadingPath = null
+ return profile
+ }
+
+ override fun serialize(profile: ErosProfile): Map {
+ return Jackson.MAPPER.convertValue(profile, Jackson.JSON_MAP_TYPE)
+ }
+
+ override fun delegate(value: V, checkEquals: Boolean): ConfigDelegate {
+ return ConfigDelegate(value, checkEquals, this, currentLoadingPath ?: throw IllegalAccessException("Delegate can only be created while loading or creating profiles!"))
+ }
+}
diff --git a/src/main/java/de/bixilon/minosoft/config/config2/config/eros/ErosProfileSelectEvent.kt b/src/main/java/de/bixilon/minosoft/config/config2/config/eros/ErosProfileSelectEvent.kt
new file mode 100644
index 000000000..656d2b20f
--- /dev/null
+++ b/src/main/java/de/bixilon/minosoft/config/config2/config/eros/ErosProfileSelectEvent.kt
@@ -0,0 +1,7 @@
+package de.bixilon.minosoft.config.config2.config.eros
+
+import de.bixilon.minosoft.modding.event.events.Event
+
+class ErosProfileSelectEvent(
+ val profile: ErosProfile,
+) : Event
diff --git a/src/main/java/de/bixilon/minosoft/config/config2/config/eros/general/GeneralC2.kt b/src/main/java/de/bixilon/minosoft/config/config2/config/eros/general/GeneralC2.kt
new file mode 100644
index 000000000..b3a920015
--- /dev/null
+++ b/src/main/java/de/bixilon/minosoft/config/config2/config/eros/general/GeneralC2.kt
@@ -0,0 +1,11 @@
+package de.bixilon.minosoft.config.config2.config.eros.general
+
+import de.bixilon.minosoft.config.config2.config.eros.ErosProfileManager.delegate
+import java.util.*
+
+class GeneralC2 {
+ /**
+ * Language to use for eros (and the fallback for the connection)
+ */
+ var language: Locale by delegate(Locale.getDefault())
+}
diff --git a/src/main/java/de/bixilon/minosoft/config/config2/migration/ConfigMigrator.kt b/src/main/java/de/bixilon/minosoft/config/config2/migration/ConfigMigrator.kt
new file mode 100644
index 000000000..2273efe59
--- /dev/null
+++ b/src/main/java/de/bixilon/minosoft/config/config2/migration/ConfigMigrator.kt
@@ -0,0 +1,6 @@
+package de.bixilon.minosoft.config.config2.migration
+
+interface ConfigMigrator {
+
+ fun migrate(data: MutableMap)
+}
diff --git a/src/main/java/de/bixilon/minosoft/config/config2/util/ConfigDelegate.kt b/src/main/java/de/bixilon/minosoft/config/config2/util/ConfigDelegate.kt
new file mode 100644
index 000000000..1208b2666
--- /dev/null
+++ b/src/main/java/de/bixilon/minosoft/config/config2/util/ConfigDelegate.kt
@@ -0,0 +1,32 @@
+package de.bixilon.minosoft.config.config2.util
+
+import de.bixilon.minosoft.config.config2.ProfileManager
+import de.bixilon.minosoft.util.KUtil.realName
+import de.bixilon.minosoft.util.logging.Log
+import de.bixilon.minosoft.util.logging.LogLevels
+import de.bixilon.minosoft.util.logging.LogMessageType
+import kotlin.properties.ReadWriteProperty
+import kotlin.reflect.KProperty
+
+open class ConfigDelegate(
+ private var value: V,
+ private val checkEquals: Boolean,
+ private val profileManager: ProfileManager<*>,
+ private val profileName: String,
+) : ReadWriteProperty {
+
+
+ override fun getValue(thisRef: Any, property: KProperty<*>): V {
+ return value
+ }
+
+ override fun setValue(thisRef: Any, property: KProperty<*>, value: V) {
+ if (checkEquals && this.value == value) {
+ return
+ }
+ Log.log(LogMessageType.OTHER, LogLevels.VERBOSE) { "Changed option $property in ${thisRef::class.java.realName} in profile $profileName from ${this.value} to $value" }
+
+ // ToDo: Fire event, save config
+ this.value = value
+ }
+}
diff --git a/src/main/java/de/bixilon/minosoft/data/language/Description.kt b/src/main/java/de/bixilon/minosoft/data/language/Description.kt
new file mode 100644
index 000000000..01962535b
--- /dev/null
+++ b/src/main/java/de/bixilon/minosoft/data/language/Description.kt
@@ -0,0 +1,6 @@
+package de.bixilon.minosoft.data.language
+
+annotation class Description(
+ val nameKey: String,
+ val translationKey: String,
+)
diff --git a/src/main/java/de/bixilon/minosoft/terminal/RunConfiguration.kt b/src/main/java/de/bixilon/minosoft/terminal/RunConfiguration.kt
index abe571efa..3cadf5423 100644
--- a/src/main/java/de/bixilon/minosoft/terminal/RunConfiguration.kt
+++ b/src/main/java/de/bixilon/minosoft/terminal/RunConfiguration.kt
@@ -21,6 +21,7 @@ import java.io.File
import java.lang.management.ManagementFactory
object RunConfiguration {
+ @Deprecated("Use profile manager")
var CONFIG_FILENAME = "minosoft.json" // Filename of minosoft's base configuration (located in AppData/Minosoft/config)
var LOG_COLOR_MESSAGE = true // The message (after all prefixes) should be colored with ANSI color codes
diff --git a/src/main/java/de/bixilon/minosoft/util/KUtil.kt b/src/main/java/de/bixilon/minosoft/util/KUtil.kt
index e18be778f..3cc22ab76 100644
--- a/src/main/java/de/bixilon/minosoft/util/KUtil.kt
+++ b/src/main/java/de/bixilon/minosoft/util/KUtil.kt
@@ -137,7 +137,7 @@ object KUtil {
return synchronizedCopy { Collections.synchronizedSet(this.toMutableSet()) }
}
- fun T.synchronizedDeepCopy(): T? {
+ fun T.synchronizedDeepCopy(): T {
return when (this) {
is Map<*, *> -> {
val map: MutableMap = synchronizedMapOf()
@@ -170,7 +170,7 @@ object KUtil {
is String -> this
is Number -> this
is Boolean -> this
- null -> null
+ null -> null.unsafeCast()
else -> TODO("Don't know how to copy ${(this as T)!!::class.java.name}")
}
}
diff --git a/src/main/java/de/bixilon/minosoft/util/json/JSONSerializer.kt b/src/main/java/de/bixilon/minosoft/util/json/JSONSerializer.kt
index 4dd60a534..30f882539 100644
--- a/src/main/java/de/bixilon/minosoft/util/json/JSONSerializer.kt
+++ b/src/main/java/de/bixilon/minosoft/util/json/JSONSerializer.kt
@@ -20,6 +20,7 @@ import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import de.bixilon.minosoft.config.config.Config
import de.bixilon.minosoft.gui.rendering.textures.properties.ImageProperties
+@Deprecated("Moshi is deprecated, use klaxon or mbf instead")
object JSONSerializer {
val MOSHI = Moshi.Builder()
.add(RGBColorSerializer)
diff --git a/src/main/java/de/bixilon/minosoft/util/json/jackson/Jackson.kt b/src/main/java/de/bixilon/minosoft/util/json/jackson/Jackson.kt
new file mode 100644
index 000000000..4eecd6deb
--- /dev/null
+++ b/src/main/java/de/bixilon/minosoft/util/json/jackson/Jackson.kt
@@ -0,0 +1,19 @@
+package de.bixilon.minosoft.util.json.jackson
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.databind.PropertyNamingStrategies
+import com.fasterxml.jackson.databind.type.MapType
+import com.fasterxml.jackson.module.kotlin.KotlinModule
+
+object Jackson {
+ val MAPPER = ObjectMapper()
+ .registerModule(KotlinModule())
+
+
+ val JSON_MAP_TYPE: MapType = MAPPER.typeFactory.constructMapType(HashMap::class.java, String::class.java, Any::class.java)
+
+ init {
+ MAPPER.propertyNamingStrategy = PropertyNamingStrategies.SNAKE_CASE
+ }
+
+}
diff --git a/src/main/java/de/bixilon/minosoft/util/task/worker/StartupTasks.kt b/src/main/java/de/bixilon/minosoft/util/task/worker/StartupTasks.kt
index 5b5d01fd7..f2f7de594 100644
--- a/src/main/java/de/bixilon/minosoft/util/task/worker/StartupTasks.kt
+++ b/src/main/java/de/bixilon/minosoft/util/task/worker/StartupTasks.kt
@@ -14,7 +14,9 @@
package de.bixilon.minosoft.util.task.worker
enum class StartupTasks {
+ @Deprecated("Will be replaced with LOAD_CONFIG2")
LOAD_CONFIG,
+ LOAD_CONFIG2,
LOAD_LANGUAGE_FILES,
LOAD_VERSIONS,
LOAD_DEFAULT_REGISTRIES,