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,