wip: profiles

This commit is contained in:
Bixilon 2021-12-01 18:46:48 +01:00
parent 334ee2f536
commit ca26e2711e
No known key found for this signature in database
GPG Key ID: 5CAD791931B09AC4
18 changed files with 374 additions and 5 deletions

View File

@ -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)

View File

@ -429,5 +429,10 @@
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-kotlin</artifactId>
<version>2.13.0</version>
</dependency>
</dependencies>
</project>

View File

@ -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/"))

View File

@ -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<ProfileManager<*>> = listOf(
ErosProfileManager,
)
fun load() {
for (manager in DEFAULT_MANAGERS) {
manager.load(null)
}
}
}

View File

@ -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<T : Profile> {
val namespace: ResourceLocation
val latestVersion: Int
val saveLock: ReentrantLock
val profiles: HashBiMap<String, T>
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<String, Any?>) = Unit
fun load(name: String, data: MutableMap<String, Any?>?): T
fun <V> delegate(value: V, checkEquals: Boolean = true): ConfigDelegate<V>
fun selectDefault()
fun createDefaultProfile(): T
fun initDefaultProfile() {
val profile = createDefaultProfile()
this.selected = profile
save(profile)
}
fun serialize(profile: T): Map<String, Any?>
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<String, Any?>?
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"
}
}

View File

@ -0,0 +1,6 @@
package de.bixilon.minosoft.config.config2.config
interface Profile {
val version: Int
val description: String?
}

View File

@ -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)
}
}

View File

@ -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<ErosProfile> {
override val namespace = "minosoft:eros".toResourceLocation()
override val latestVersion = 1
override val saveLock = ReentrantLock()
private var currentLoadingPath: String? = null
override val profiles: HashBiMap<String, ErosProfile> = 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<String, Any?>?): 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<String, Any?> {
return Jackson.MAPPER.convertValue(profile, Jackson.JSON_MAP_TYPE)
}
override fun <V> delegate(value: V, checkEquals: Boolean): ConfigDelegate<V> {
return ConfigDelegate(value, checkEquals, this, currentLoadingPath ?: throw IllegalAccessException("Delegate can only be created while loading or creating profiles!"))
}
}

View File

@ -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

View File

@ -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())
}

View File

@ -0,0 +1,6 @@
package de.bixilon.minosoft.config.config2.migration
interface ConfigMigrator {
fun migrate(data: MutableMap<String, Any>)
}

View File

@ -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<V>(
private var value: V,
private val checkEquals: Boolean,
private val profileManager: ProfileManager<*>,
private val profileName: String,
) : ReadWriteProperty<Any, V> {
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
}
}

View File

@ -0,0 +1,6 @@
package de.bixilon.minosoft.data.language
annotation class Description(
val nameKey: String,
val translationKey: String,
)

View File

@ -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

View File

@ -137,7 +137,7 @@ object KUtil {
return synchronizedCopy { Collections.synchronizedSet(this.toMutableSet()) }
}
fun <T> T.synchronizedDeepCopy(): T? {
fun <T> T.synchronizedDeepCopy(): T {
return when (this) {
is Map<*, *> -> {
val map: MutableMap<Any?, Any?> = 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}")
}
}

View File

@ -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)

View File

@ -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
}
}

View File

@ -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,