mirror of
https://gitlab.bixilon.de/bixilon/minosoft.git
synced 2025-09-16 10:55:01 -04:00
wip: profiles
This commit is contained in:
parent
334ee2f536
commit
ca26e2711e
@ -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)
|
||||
|
5
pom.xml
5
pom.xml
@ -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>
|
||||
|
@ -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/"))
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
package de.bixilon.minosoft.config.config2.config
|
||||
|
||||
interface Profile {
|
||||
val version: Int
|
||||
val description: String?
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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!"))
|
||||
}
|
||||
}
|
@ -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
|
@ -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())
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
package de.bixilon.minosoft.config.config2.migration
|
||||
|
||||
interface ConfigMigrator {
|
||||
|
||||
fun migrate(data: MutableMap<String, Any>)
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
package de.bixilon.minosoft.data.language
|
||||
|
||||
annotation class Description(
|
||||
val nameKey: String,
|
||||
val translationKey: String,
|
||||
)
|
@ -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
|
||||
|
@ -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}")
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
}
|
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user