aboutsummaryrefslogtreecommitdiff
path: root/src/main/kotlin/gui/config
diff options
context:
space:
mode:
authorLinnea Gräf <nea@nea.moe>2025-09-14 16:37:57 +0200
committerLinnea Gräf <nea@nea.moe>2025-09-14 16:37:57 +0200
commit9abe9f46f04f188037687adb2740b32220ad21b2 (patch)
tree48dbd9cdf48c59853310c0b2e9bc59801522400e /src/main/kotlin/gui/config
parent2851c1d6834fafdaeb009dce2a3485df1388907e (diff)
downloadFirmament-9abe9f46f04f188037687adb2740b32220ad21b2.tar.gz
Firmament-9abe9f46f04f188037687adb2740b32220ad21b2.tar.bz2
Firmament-9abe9f46f04f188037687adb2740b32220ad21b2.zip
snapshot
Diffstat (limited to 'src/main/kotlin/gui/config')
-rw-r--r--src/main/kotlin/gui/config/AllConfigsGui.kt3
-rw-r--r--src/main/kotlin/gui/config/BooleanHandler.kt3
-rw-r--r--src/main/kotlin/gui/config/ChoiceHandler.kt1
-rw-r--r--src/main/kotlin/gui/config/ClickHandler.kt1
-rw-r--r--src/main/kotlin/gui/config/ColourHandler.kt3
-rw-r--r--src/main/kotlin/gui/config/DurationHandler.kt1
-rw-r--r--src/main/kotlin/gui/config/HudMetaHandler.kt1
-rw-r--r--src/main/kotlin/gui/config/IntegerHandler.kt1
-rw-r--r--src/main/kotlin/gui/config/KeyBindingHandler.kt3
-rw-r--r--src/main/kotlin/gui/config/ManagedConfig.kt270
-rw-r--r--src/main/kotlin/gui/config/ManagedConfigElement.kt8
-rw-r--r--src/main/kotlin/gui/config/ManagedOption.kt7
-rw-r--r--src/main/kotlin/gui/config/StringHandler.kt3
-rw-r--r--src/main/kotlin/gui/config/storage/ConfigLoadContext.kt65
-rw-r--r--src/main/kotlin/gui/config/storage/ConfigStorageClass.kt8
-rw-r--r--src/main/kotlin/gui/config/storage/FirmamentConfigLoader.kt204
-rw-r--r--src/main/kotlin/gui/config/storage/FirstLevelSplitJsonFolder.kt83
-rw-r--r--src/main/kotlin/gui/config/storage/LegacyImporter.kt63
18 files changed, 443 insertions, 285 deletions
diff --git a/src/main/kotlin/gui/config/AllConfigsGui.kt b/src/main/kotlin/gui/config/AllConfigsGui.kt
index f9ffd2d..0add10f 100644
--- a/src/main/kotlin/gui/config/AllConfigsGui.kt
+++ b/src/main/kotlin/gui/config/AllConfigsGui.kt
@@ -10,9 +10,11 @@ import moe.nea.firmament.commands.get
import moe.nea.firmament.commands.thenArgument
import moe.nea.firmament.commands.thenExecute
import moe.nea.firmament.events.CommandEvent
+import moe.nea.firmament.util.data.ManagedConfig
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.MoulConfigUtils
import moe.nea.firmament.util.ScreenUtil.setScreenLater
+import moe.nea.firmament.util.data.Config
object AllConfigsGui {
//
@@ -21,6 +23,7 @@ object AllConfigsGui {
// RepoManager.Config
// ) + FeatureManager.allFeatures.mapNotNull { it.config }
+ @Config
object ConfigConfig : ManagedConfig("configconfig", Category.META) {
val enableYacl by toggle("enable-yacl") { false }
val enableMoulConfig by toggle("enable-moulconfig") { true }
diff --git a/src/main/kotlin/gui/config/BooleanHandler.kt b/src/main/kotlin/gui/config/BooleanHandler.kt
index 8592777..b954401 100644
--- a/src/main/kotlin/gui/config/BooleanHandler.kt
+++ b/src/main/kotlin/gui/config/BooleanHandler.kt
@@ -9,6 +9,7 @@ import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.boolean
import kotlinx.serialization.json.jsonPrimitive
+import moe.nea.firmament.util.data.ManagedConfig
class BooleanHandler(val config: ManagedConfig) : ManagedConfig.OptionHandler<Boolean> {
override fun toJson(element: Boolean): JsonElement? {
@@ -29,7 +30,7 @@ class BooleanHandler(val config: ManagedConfig) : ManagedConfig.OptionHandler<Bo
override fun set(newValue: Boolean) {
opt.set(newValue)
- config.save()
+ config.markDirty()
}
}, 200)
))
diff --git a/src/main/kotlin/gui/config/ChoiceHandler.kt b/src/main/kotlin/gui/config/ChoiceHandler.kt
index 2ea3efc..9c3dda2 100644
--- a/src/main/kotlin/gui/config/ChoiceHandler.kt
+++ b/src/main/kotlin/gui/config/ChoiceHandler.kt
@@ -9,6 +9,7 @@ import kotlinx.serialization.json.JsonElement
import kotlin.jvm.optionals.getOrNull
import net.minecraft.util.StringIdentifiable
import moe.nea.firmament.gui.CheckboxComponent
+import moe.nea.firmament.util.data.ManagedConfig
import moe.nea.firmament.util.ErrorUtil
import moe.nea.firmament.util.json.KJsonOps
diff --git a/src/main/kotlin/gui/config/ClickHandler.kt b/src/main/kotlin/gui/config/ClickHandler.kt
index fa1c621..9ea83aa 100644
--- a/src/main/kotlin/gui/config/ClickHandler.kt
+++ b/src/main/kotlin/gui/config/ClickHandler.kt
@@ -5,6 +5,7 @@ package moe.nea.firmament.gui.config
import io.github.notenoughupdates.moulconfig.gui.component.TextComponent
import kotlinx.serialization.json.JsonElement
import moe.nea.firmament.gui.FirmButtonComponent
+import moe.nea.firmament.util.data.ManagedConfig
class ClickHandler(val config: ManagedConfig, val runnable: () -> Unit) : ManagedConfig.OptionHandler<Unit> {
override fun toJson(element: Unit): JsonElement? {
diff --git a/src/main/kotlin/gui/config/ColourHandler.kt b/src/main/kotlin/gui/config/ColourHandler.kt
index 83ce8ac..33daa6d 100644
--- a/src/main/kotlin/gui/config/ColourHandler.kt
+++ b/src/main/kotlin/gui/config/ColourHandler.kt
@@ -10,6 +10,7 @@ import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
+import moe.nea.firmament.util.data.ManagedConfig
class ColourHandler(val config: ManagedConfig) :
ManagedConfig.OptionHandler<ChromaColour> {
@@ -73,7 +74,7 @@ class ColourHandler(val config: ManagedConfig) :
opt.value.toLegacyString(),
{
opt.value = ChromaColour.forLegacyString(it)
- config.save()
+ config.markDirty()
},
{ }
)
diff --git a/src/main/kotlin/gui/config/DurationHandler.kt b/src/main/kotlin/gui/config/DurationHandler.kt
index 32bec25..4800bf6 100644
--- a/src/main/kotlin/gui/config/DurationHandler.kt
+++ b/src/main/kotlin/gui/config/DurationHandler.kt
@@ -12,6 +12,7 @@ import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.long
+import moe.nea.firmament.util.data.ManagedConfig
import kotlin.time.Duration
import kotlin.time.DurationUnit
import kotlin.time.toDuration
diff --git a/src/main/kotlin/gui/config/HudMetaHandler.kt b/src/main/kotlin/gui/config/HudMetaHandler.kt
index 0637351..dcf5d8f 100644
--- a/src/main/kotlin/gui/config/HudMetaHandler.kt
+++ b/src/main/kotlin/gui/config/HudMetaHandler.kt
@@ -10,6 +10,7 @@ import net.minecraft.text.MutableText
import net.minecraft.text.Text
import moe.nea.firmament.Firmament
import moe.nea.firmament.gui.FirmButtonComponent
+import moe.nea.firmament.util.data.ManagedConfig
import moe.nea.firmament.jarvis.JarvisIntegration
import moe.nea.firmament.util.MC
diff --git a/src/main/kotlin/gui/config/IntegerHandler.kt b/src/main/kotlin/gui/config/IntegerHandler.kt
index fd10447..3e7be57 100644
--- a/src/main/kotlin/gui/config/IntegerHandler.kt
+++ b/src/main/kotlin/gui/config/IntegerHandler.kt
@@ -12,6 +12,7 @@ import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonPrimitive
+import moe.nea.firmament.util.data.ManagedConfig
import moe.nea.firmament.util.FirmFormatters
class IntegerHandler(val config: ManagedConfig, val min: Int, val max: Int) : ManagedConfig.OptionHandler<Int> {
diff --git a/src/main/kotlin/gui/config/KeyBindingHandler.kt b/src/main/kotlin/gui/config/KeyBindingHandler.kt
index 14a4b32..a5e626d 100644
--- a/src/main/kotlin/gui/config/KeyBindingHandler.kt
+++ b/src/main/kotlin/gui/config/KeyBindingHandler.kt
@@ -11,6 +11,7 @@ import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.decodeFromJsonElement
import kotlinx.serialization.json.encodeToJsonElement
import moe.nea.firmament.gui.FirmButtonComponent
+import moe.nea.firmament.util.data.ManagedConfig
import moe.nea.firmament.keybindings.FirmamentKeyBindings
import moe.nea.firmament.keybindings.SavedKeyBinding
@@ -35,7 +36,7 @@ class KeyBindingHandler(val name: String, val managedConfig: ManagedConfig) :
{ opt.value },
{
opt.value = it
- opt.element.save()
+ opt.element.markDirty()
},
{ button.blur() },
{ button.requestFocus() }
diff --git a/src/main/kotlin/gui/config/ManagedConfig.kt b/src/main/kotlin/gui/config/ManagedConfig.kt
deleted file mode 100644
index 90e58d0..0000000
--- a/src/main/kotlin/gui/config/ManagedConfig.kt
+++ /dev/null
@@ -1,270 +0,0 @@
-package moe.nea.firmament.gui.config
-
-import com.mojang.serialization.Codec
-import io.github.notenoughupdates.moulconfig.ChromaColour
-import io.github.notenoughupdates.moulconfig.gui.CloseEventListener
-import io.github.notenoughupdates.moulconfig.gui.GuiContext
-import io.github.notenoughupdates.moulconfig.gui.component.CenterComponent
-import io.github.notenoughupdates.moulconfig.gui.component.ColumnComponent
-import io.github.notenoughupdates.moulconfig.gui.component.PanelComponent
-import io.github.notenoughupdates.moulconfig.gui.component.RowComponent
-import io.github.notenoughupdates.moulconfig.gui.component.ScrollPanelComponent
-import io.github.notenoughupdates.moulconfig.gui.component.TextComponent
-import io.github.notenoughupdates.moulconfig.platform.MoulConfigScreenComponent
-import moe.nea.jarvis.api.Point
-import org.joml.Vector2i
-import org.lwjgl.glfw.GLFW
-import kotlinx.serialization.encodeToString
-import kotlinx.serialization.json.JsonElement
-import kotlinx.serialization.json.JsonObject
-import kotlin.io.path.createDirectories
-import kotlin.io.path.readText
-import kotlin.io.path.writeText
-import kotlin.time.Duration
-import net.minecraft.client.gui.screen.Screen
-import net.minecraft.text.Text
-import net.minecraft.util.StringIdentifiable
-import moe.nea.firmament.Firmament
-import moe.nea.firmament.gui.FirmButtonComponent
-import moe.nea.firmament.keybindings.GenericInputButton
-import moe.nea.firmament.keybindings.InputModifiers
-import moe.nea.firmament.keybindings.SavedKeyBinding
-import moe.nea.firmament.util.ScreenUtil.setScreenLater
-import moe.nea.firmament.util.collections.InstanceList
-
-abstract class ManagedConfig(
- override val name: String,
- val category: Category,
- // TODO: allow vararg secondaryCategories: Category,
-) : ManagedConfigElement() {
- enum class Category {
- // Böse Kategorie, nicht benutzten lol
- MISC,
- CHAT,
- INVENTORY,
- ITEMS,
- MINING,
- GARDEN,
- EVENTS,
- INTEGRATIONS,
- META,
- DEV,
- ;
-
- val labelText: Text = Text.translatable("firmament.config.category.${name.lowercase()}")
- val description: Text = Text.translatable("firmament.config.category.${name.lowercase()}.description")
- val configs: MutableList<ManagedConfig> = mutableListOf()
- }
-
- companion object {
- val allManagedConfigs = InstanceList<ManagedConfig>("ManagedConfig")
- }
-
- interface OptionHandler<T : Any> {
- fun initOption(opt: ManagedOption<T>) {}
- fun toJson(element: T): JsonElement?
- fun fromJson(element: JsonElement): T
- fun emitGuiElements(opt: ManagedOption<T>, guiAppender: GuiAppender)
- }
-
- init {
- allManagedConfigs.getAll().forEach {
- require(it.name != name) { "Duplicate name '$name' used for config" }
- }
- allManagedConfigs.add(this)
- category.configs.add(this)
- }
-
- // TODO: warn if two files use the same config file name :(
- val file = Firmament.CONFIG_DIR.resolve("$name.json")
- val data: JsonObject by lazy {
- try {
- Firmament.json.decodeFromString(
- file.readText()
- )
- } catch (e: Exception) {
- Firmament.logger.info("Could not read config $name. Loading empty config.")
- JsonObject(mutableMapOf())
- }
- }
-
- fun save() {
- val data = JsonObject(allOptions.mapNotNull { (key, value) ->
- value.toJson()?.let {
- key to it
- }
- }.toMap())
- file.parent.createDirectories()
- file.writeText(Firmament.json.encodeToString(data))
- }
-
-
- val allOptions = mutableMapOf<String, ManagedOption<*>>()
- val sortedOptions = mutableListOf<ManagedOption<*>>()
-
- private var latestGuiAppender: GuiAppender? = null
-
- protected fun <T : Any> option(
- propertyName: String,
- default: () -> T,
- handler: OptionHandler<T>
- ): ManagedOption<T> {
- if (propertyName in allOptions) error("Cannot register the same name twice")
- return ManagedOption(this, propertyName, default, handler).also {
- it.handler.initOption(it)
- it.load(data)
- allOptions[propertyName] = it
- sortedOptions.add(it)
- }
- }
-
- protected fun toggle(propertyName: String, default: () -> Boolean): ManagedOption<Boolean> {
- return option(propertyName, default, BooleanHandler(this))
- }
-
- protected fun colour(propertyName: String, default: () -> ChromaColour): ManagedOption<ChromaColour> {
- return option(propertyName, default, ColourHandler(this))
- }
-
- protected fun <E> choice(
- propertyName: String,
- enumClass: Class<E>,
- default: () -> E
- ): ManagedOption<E> where E : Enum<E>, E : StringIdentifiable {
- return option(propertyName, default, ChoiceHandler(enumClass, enumClass.enumConstants.toList()))
- }
-
- protected inline fun <reified E> choice(
- propertyName: String,
- noinline default: () -> E
- ): ManagedOption<E> where E : Enum<E>, E : StringIdentifiable {
- return choice(propertyName, E::class.java, default)
- }
-
- private fun <E> createStringIdentifiable(x: () -> Array<out E>): Codec<E> where E : Enum<E>, E : StringIdentifiable {
- return StringIdentifiable.createCodec { x() }
- }
-
- // TODO: wait on https://youtrack.jetbrains.com/issue/KT-73434
-// protected inline fun <reified E> choice(
-// propertyName: String,
-// noinline default: () -> E
-// ): ManagedOption<E> where E : Enum<E>, E : StringIdentifiable {
-// return choice(
-// propertyName,
-// enumEntries<E>().toList(),
-// StringIdentifiable.createCodec { enumValues<E>() },
-// EnumRenderer.default(),
-// default
-// )
-// }
- open fun onChange(option: ManagedOption<*>) {
- }
-
- protected fun duration(
- propertyName: String,
- min: Duration,
- max: Duration,
- default: () -> Duration,
- ): ManagedOption<Duration> {
- return option(propertyName, default, DurationHandler(this, min, max))
- }
-
-
- protected fun position(
- propertyName: String,
- width: Int,
- height: Int,
- default: () -> Vector2i,
- ): ManagedOption<HudMeta> {
- val label = Text.translatable("firmament.config.${name}.${propertyName}")
- return option(propertyName, {
- val p = default()
- HudMeta(HudPosition(p.x(), p.y(), 1F), Firmament.identifier(propertyName), label, width, height)
- }, HudMetaHandler(this, propertyName, label, width, height))
- }
-
- protected fun keyBinding(
- propertyName: String,
- default: () -> Int,
- ): ManagedOption<SavedKeyBinding> = keyBindingWithOutDefaultModifiers(propertyName) {
- SavedKeyBinding.keyWithoutMods(default())
- }
-
- protected fun keyBindingWithOutDefaultModifiers(
- propertyName: String,
- default: () -> SavedKeyBinding,
- ): ManagedOption<SavedKeyBinding> {
- return option(propertyName, default, KeyBindingHandler("firmament.config.${name}.${propertyName}", this))
- }
-
- protected fun keyBindingWithDefaultUnbound(
- propertyName: String,
- ): ManagedOption<SavedKeyBinding> {
- return keyBindingWithOutDefaultModifiers(propertyName) { SavedKeyBinding.unbound() }
- }
-
- protected fun integer(
- propertyName: String,
- min: Int,
- max: Int,
- default: () -> Int,
- ): ManagedOption<Int> {
- return option(propertyName, default, IntegerHandler(this, min, max))
- }
-
- protected fun button(propertyName: String, runnable: () -> Unit): ManagedOption<Unit> {
- return option(propertyName, { }, ClickHandler(this, runnable))
- }
-
- protected fun string(propertyName: String, default: () -> String): ManagedOption<String> {
- return option(propertyName, default, StringHandler(this))
- }
-
-
- fun reloadGui() {
- latestGuiAppender?.reloadables?.forEach { it() }
- }
-
- val translationKey get() = "firmament.config.${name}"
- val labelText: Text = Text.translatable(translationKey)
-
- fun getConfigEditor(parent: Screen? = null): Screen {
- var screen: Screen? = null
- val guiapp = GuiAppender(400) { requireNotNull(screen) { "Screen Accessor called too early" } }
- latestGuiAppender = guiapp
- guiapp.appendFullRow(
- RowComponent(
- FirmButtonComponent(TextComponent("←")) {
- if (parent != null) {
- save()
- setScreenLater(parent)
- } else {
- AllConfigsGui.showAllGuis()
- }
- }
- ))
- sortedOptions.forEach { it.appendToGui(guiapp) }
- guiapp.reloadables.forEach { it() }
- val component = CenterComponent(
- PanelComponent(
- ScrollPanelComponent(400, 300, ColumnComponent(guiapp.panel)),
- 10,
- PanelComponent.DefaultBackgroundRenderer.VANILLA
- )
- )
- screen = object : MoulConfigScreenComponent(Text.empty(), GuiContext(component), parent) {
- override fun close() {
- if (guiContext.onBeforeClose() == CloseEventListener.CloseAction.NO_OBJECTIONS_TO_CLOSE) {
- client!!.setScreen(parent)
- }
- }
- }
- return screen
- }
-
- fun showConfigEditor(parent: Screen? = null) {
- setScreenLater(getConfigEditor(parent))
- }
-
-}
diff --git a/src/main/kotlin/gui/config/ManagedConfigElement.kt b/src/main/kotlin/gui/config/ManagedConfigElement.kt
deleted file mode 100644
index 28cd6b8..0000000
--- a/src/main/kotlin/gui/config/ManagedConfigElement.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-package moe.nea.firmament.gui.config
-
-abstract class ManagedConfigElement {
- abstract val name: String
-
-}
diff --git a/src/main/kotlin/gui/config/ManagedOption.kt b/src/main/kotlin/gui/config/ManagedOption.kt
index 830086c..888b3f1 100644
--- a/src/main/kotlin/gui/config/ManagedOption.kt
+++ b/src/main/kotlin/gui/config/ManagedOption.kt
@@ -3,6 +3,7 @@ package moe.nea.firmament.gui.config
import io.github.notenoughupdates.moulconfig.observer.GetSetter
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
+import moe.nea.firmament.util.data.ManagedConfig
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
import net.minecraft.text.Text
@@ -27,11 +28,11 @@ class ManagedOption<T : Any>(
val descriptionTranslationKey = "firmament.config.${element.name}.${propertyName}.description"
val labelDescription: Text = Text.translatable(descriptionTranslationKey)
- private var actualValue: T? = null
+ var _actualValue: T? = null
var value: T
- get() = actualValue ?: error("Lateinit variable not initialized")
+ get() = _actualValue ?: error("Lateinit variable not initialized")
set(value) {
- actualValue = value
+ _actualValue = value
element.onChange(this)
}
diff --git a/src/main/kotlin/gui/config/StringHandler.kt b/src/main/kotlin/gui/config/StringHandler.kt
index a326abb..da14d4b 100644
--- a/src/main/kotlin/gui/config/StringHandler.kt
+++ b/src/main/kotlin/gui/config/StringHandler.kt
@@ -7,6 +7,7 @@ import io.github.notenoughupdates.moulconfig.observer.GetSetter
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.jsonPrimitive
+import moe.nea.firmament.util.data.ManagedConfig
import net.minecraft.text.Text
class StringHandler(val config: ManagedConfig) : ManagedConfig.OptionHandler<String> {
@@ -25,7 +26,7 @@ class StringHandler(val config: ManagedConfig) : ManagedConfig.OptionHandler<Str
object : GetSetter<String> by opt {
override fun set(newValue: String) {
opt.set(newValue)
- config.save()
+ config.markDirty()
}
},
130,
diff --git a/src/main/kotlin/gui/config/storage/ConfigLoadContext.kt b/src/main/kotlin/gui/config/storage/ConfigLoadContext.kt
new file mode 100644
index 0000000..59afaa1
--- /dev/null
+++ b/src/main/kotlin/gui/config/storage/ConfigLoadContext.kt
@@ -0,0 +1,65 @@
+package moe.nea.firmament.gui.config.storage
+
+import java.io.PrintWriter
+import java.nio.file.Path
+import org.apache.commons.io.output.StringBuilderWriter
+import kotlin.io.path.Path
+import kotlin.io.path.createParentDirectories
+import kotlin.io.path.writeText
+import moe.nea.firmament.Firmament
+
+data class ConfigLoadContext(
+ val loadId: String,
+) : AutoCloseable {
+ val logFile = Path("logs")
+ .resolve(Firmament.MOD_ID)
+ .resolve("config-$loadId.log")
+ .toAbsolutePath()
+ val logBuffer = StringBuilder()
+
+ var shouldSaveLogBuffer = false
+ fun markShouldSaveLogBuffer() {
+ shouldSaveLogBuffer = true
+ }
+
+ fun logDebug(message: String) {
+ logBuffer.append("[DEBUG] ").append(message).appendLine()
+ }
+
+ fun logInfo(message: String) {
+ Firmament.logger.info("[ConfigUpgrade] $message")
+ logBuffer.append("[INFO] ").append(message).appendLine()
+ }
+
+ fun logError(message: String, exception: Throwable) {
+ markShouldSaveLogBuffer()
+ Firmament.logger.error("[ConfigUpgrade] $message", exception)
+ logBuffer.append("[ERROR] ").append(message).appendLine()
+ PrintWriter(StringBuilderWriter(logBuffer)).use {
+ exception.printStackTrace(it)
+ }
+ logBuffer.appendLine()
+ }
+
+ fun logError(message: String) {
+ markShouldSaveLogBuffer()
+ Firmament.logger.error("[ConfigUpgrade] $message")
+ logBuffer.append("[ERROR] ").append(message).appendLine()
+ }
+
+ fun ensureWritable(path: Path) {
+ path.createParentDirectories()
+ }
+
+ override fun close() {
+ logInfo("Closing out config load.")
+ if (shouldSaveLogBuffer) {
+ try {
+ ensureWritable(logFile)
+ logFile.writeText(logBuffer.toString())
+ } catch (ex: Exception) {
+ logError("Could not save config load log", ex)
+ }
+ }
+ }
+}
diff --git a/src/main/kotlin/gui/config/storage/ConfigStorageClass.kt b/src/main/kotlin/gui/config/storage/ConfigStorageClass.kt
new file mode 100644
index 0000000..8258fe7
--- /dev/null
+++ b/src/main/kotlin/gui/config/storage/ConfigStorageClass.kt
@@ -0,0 +1,8 @@
+package moe.nea.firmament.gui.config.storage
+
+enum class ConfigStorageClass { // TODO: make this encode type info somehow
+ PROFILE,
+ STORAGE,
+ CONFIG,
+}
+
diff --git a/src/main/kotlin/gui/config/storage/FirmamentConfigLoader.kt b/src/main/kotlin/gui/config/storage/FirmamentConfigLoader.kt
new file mode 100644
index 0000000..22cba2c
--- /dev/null
+++ b/src/main/kotlin/gui/config/storage/FirmamentConfigLoader.kt
@@ -0,0 +1,204 @@
+package moe.nea.firmament.gui.config.storage
+
+import java.util.UUID
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.buildJsonObject
+import kotlinx.serialization.json.jsonObject
+import kotlin.io.path.Path
+import kotlin.io.path.exists
+import kotlin.io.path.forEachDirectoryEntry
+import kotlin.io.path.isDirectory
+import kotlin.io.path.listDirectoryEntries
+import kotlin.io.path.name
+import kotlin.io.path.readText
+import kotlin.io.path.writeText
+import moe.nea.firmament.util.data.IConfigProvider
+import moe.nea.firmament.util.data.IDataHolder
+import moe.nea.firmament.util.data.ProfileKeyedConfig
+import moe.nea.firmament.util.json.intoGson
+import moe.nea.firmament.util.json.intoKotlinJson
+
+object FirmamentConfigLoader {
+ val currentConfigVersion = 1000
+ val configFolder = Path("config/firmament")
+ .toAbsolutePath()
+ val storageFolder = configFolder.resolve("storage")
+ val profilePath = configFolder.resolve("profiles")
+ val tagLines = listOf(
+ "<- your config version here",
+ "I'm a teapot",
+ "mail.example.com ESMTP",
+ "Apples"
+ )
+ val configVersionFile = configFolder.resolve("config.version")
+
+ fun loadConfig() {
+ if (configFolder.exists()) {
+ if (!configVersionFile.exists()) {
+ LegacyImporter.importFromLegacy()
+ }
+ updateConfigs()
+ }
+
+ ConfigLoadContext("load-${System.currentTimeMillis()}").use { loadContext ->
+ val configData = FirstLevelSplitJsonFolder(loadContext, configFolder).load()
+ loadConfigFromData(configData, Unit, ConfigStorageClass.CONFIG)
+ val storageData = FirstLevelSplitJsonFolder(loadContext, storageFolder).load()
+ loadConfigFromData(storageData, Unit, ConfigStorageClass.STORAGE)
+ val profileData =
+ profilePath.listDirectoryEntries()
+ .filter { it.isDirectory() }
+ .associate {
+ UUID.fromString(it.name) to FirstLevelSplitJsonFolder(loadContext, it).load()
+ }
+ profileData.forEach { (key, value) ->
+ loadConfigFromData(value, key, ConfigStorageClass.PROFILE)
+ }
+ }
+ }
+
+ fun <T> loadConfigFromData(
+ configData: JsonObject,
+ key: T,
+ storageClass: ConfigStorageClass
+ ) {
+ for (holder in allConfigs) {
+ if (holder.storageClass == storageClass) {
+ (holder as IDataHolder<T>).loadFrom(key, configData)
+ }
+ }
+ }
+
+ fun <T> collectConfigFromData(
+ key: T,
+ storageClass: ConfigStorageClass,
+ ): JsonObject {
+ var json = JsonObject(mapOf())
+ for (holder in allConfigs) {
+ if (holder.storageClass == storageClass) {
+ json = mergeJson(json, (holder as IDataHolder<T>).saveTo(key))
+ }
+ }
+ return json
+ }
+
+ fun <T> saveStorage(
+ storageClass: ConfigStorageClass,
+ key: T,
+ firstLevelSplitJsonFolder: FirstLevelSplitJsonFolder,
+ ) {
+ firstLevelSplitJsonFolder.save(
+ collectConfigFromData(key, storageClass)
+ )
+ }
+
+ fun collectAllProfileIds(): Set<UUID> {
+ return allConfigs
+ .filter { it.storageClass == ConfigStorageClass.PROFILE }
+ .flatMapTo(mutableSetOf()) {
+ (it as ProfileKeyedConfig<*>).keys()
+ }
+ }
+
+ fun saveAll() {
+ ConfigLoadContext("load-${System.currentTimeMillis()}").use { context ->
+ saveStorage(
+ ConfigStorageClass.CONFIG,
+ Unit,
+ FirstLevelSplitJsonFolder(context, configFolder)
+ )
+ saveStorage(
+ ConfigStorageClass.STORAGE,
+ Unit,
+ FirstLevelSplitJsonFolder(context, storageFolder)
+ )
+ collectAllProfileIds().forEach { profileId ->
+ saveStorage(
+ ConfigStorageClass.PROFILE,
+ profileId,
+ FirstLevelSplitJsonFolder(context, profilePath.resolve(profileId.toString()))
+ )
+ }
+ }
+ }
+
+ fun mergeJson(a: JsonObject, b: JsonObject): JsonObject {
+ fun mergeInner(a: JsonElement?, b: JsonElement?): JsonElement {
+ if (a == null)
+ return b!!
+ if (b == null)
+ return a
+ a as JsonObject
+ b as JsonObject
+ return buildJsonObject {
+ (a.keys + b.keys)
+ .forEach {
+ put(it, mergeInner(a[it], b[it]))
+ }
+ }
+ }
+ return mergeInner(a, b) as JsonObject
+ }
+
+ val allConfigs: List<IDataHolder<*>> = IConfigProvider.providers.allValidInstances.flatMap { it.configs }
+
+ fun updateConfigs() {
+ val startVersion = configVersionFile.readText()
+ .substringBefore(' ')
+ .trim()
+ .toInt()
+ ConfigLoadContext("update-from-$startVersion-to-$currentConfigVersion-${System.currentTimeMillis()}")
+ .use { loadContext ->
+ updateOneConfig(
+ loadContext,
+ startVersion,
+ ConfigStorageClass.CONFIG,
+ FirstLevelSplitJsonFolder(loadContext, configFolder)
+ )
+ updateOneConfig(
+ loadContext,
+ startVersion,
+ ConfigStorageClass.STORAGE,
+ FirstLevelSplitJsonFolder(loadContext, storageFolder)
+ )
+ profilePath.forEachDirectoryEntry {
+ updateOneConfig(
+ loadContext,
+ startVersion,
+ ConfigStorageClass.PROFILE,
+ FirstLevelSplitJsonFolder(loadContext, it)
+ )
+ }
+ configVersionFile.writeText("$currentConfigVersion ${tagLines.random()}")
+ }
+ }
+
+ private fun updateOneConfig(
+ loadContext: ConfigLoadContext,
+ startVersion: Int,
+ storageClass: ConfigStorageClass,
+ firstLevelSplitJsonFolder: FirstLevelSplitJsonFolder
+ ) {
+ loadContext.logInfo("Starting upgrade from at ${firstLevelSplitJsonFolder.folder} ($storageClass) to $startVersion")
+ var data = firstLevelSplitJsonFolder.load()
+ for (nextVersion in (startVersion + 1)..currentConfigVersion) {
+ data = updateOneConfigOnce(nextVersion, storageClass, data)
+ }
+ firstLevelSplitJsonFolder.save(data)
+ }
+
+ private fun updateOneConfigOnce(
+ nextVersion: Int,
+ storageClass: ConfigStorageClass,
+ data: JsonObject
+ ): JsonObject {
+ return ConfigFixEvent.publish(ConfigFixEvent(storageClass, nextVersion, data.intoGson().asJsonObject))
+ .data.intoKotlinJson().jsonObject
+ }
+
+ fun markDirty(holder: IDataHolder<*>) {
+ TODO("Not yet implemented")
+ }
+
+}
diff --git a/src/main/kotlin/gui/config/storage/FirstLevelSplitJsonFolder.kt b/src/main/kotlin/gui/config/storage/FirstLevelSplitJsonFolder.kt
new file mode 100644
index 0000000..ff544d5
--- /dev/null
+++ b/src/main/kotlin/gui/config/storage/FirstLevelSplitJsonFolder.kt
@@ -0,0 +1,83 @@
+@file:OptIn(ExperimentalSerializationApi::class)
+
+package moe.nea.firmament.gui.config.storage
+
+import java.nio.file.Path
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.decodeFromStream
+import kotlinx.serialization.json.encodeToStream
+import kotlin.io.path.deleteExisting
+import kotlin.io.path.inputStream
+import kotlin.io.path.listDirectoryEntries
+import kotlin.io.path.nameWithoutExtension
+import kotlin.io.path.outputStream
+import moe.nea.firmament.Firmament
+
+class FirstLevelSplitJsonFolder(
+ val context: ConfigLoadContext,
+ val folder: Path
+) {
+ fun load(): JsonObject {
+ context.logInfo("Loading FLSJF from $folder")
+ return folder.listDirectoryEntries("*.json")
+ .mapNotNull(::loadIndividualFile)
+ .toMap()
+ .let(::JsonObject)
+ .also { context.logInfo("FLSJF from $folder - Voller Erfolg!") }
+ }
+
+ fun loadIndividualFile(path: Path): Pair<String, JsonElement>? {
+ context.logInfo("Loading partial file from $path")
+ return try {
+ path.inputStream().use {
+ path.nameWithoutExtension to Firmament.json.decodeFromStream(JsonElement.serializer(), it)
+ }
+ } catch (ex: Exception) {
+ context.logError("Could not load file from $path", ex)
+ null
+ }
+ }
+
+ fun save(value: JsonObject) {
+ context.logInfo("Saving FLSJF to $folder")
+ context.logDebug("Current value:\n$value")
+ val entries = folder.listDirectoryEntries("*.json")
+ .toMutableList()
+ for ((name, element) in value) {
+ val path = saveIndividualFile(name, element)
+ if (path != null) {
+ entries.remove(path)
+ }
+ }
+ if (entries.isNotEmpty()) {
+ context.logInfo("Deleting additional files.")
+ for (path in entries) {
+ context.logInfo("Deleting $path")
+// context.backup(path)
+ try {
+ path.deleteExisting()
+ } catch (ex: Exception) {
+ context.logError("Could not delete $path", ex)
+ }
+ }
+ }
+ context.logInfo("FLSJF to $folder - Voller Erfolg!")
+ }
+
+ fun saveIndividualFile(name: String, element: JsonElement): Path? {
+ try {
+ context.logInfo("Saving partial file with name $name")
+ val path = folder.resolve("$name.json")
+ context.ensureWritable(path)
+ path.outputStream().use {
+ Firmament.json.encodeToStream(JsonElement.serializer(), element, it)
+ }
+ return path
+ } catch (ex: Exception) {
+ context.logError("Could not save $name with value $element", ex)
+ return null
+ }
+ }
+}
diff --git a/src/main/kotlin/gui/config/storage/LegacyImporter.kt b/src/main/kotlin/gui/config/storage/LegacyImporter.kt
new file mode 100644
index 0000000..8915c17
--- /dev/null
+++ b/src/main/kotlin/gui/config/storage/LegacyImporter.kt
@@ -0,0 +1,63 @@
+package moe.nea.firmament.gui.config.storage
+
+import java.nio.file.Path
+import javax.xml.namespace.QName
+import kotlin.io.path.Path
+import kotlin.io.path.copyTo
+import kotlin.io.path.copyToRecursively
+import kotlin.io.path.createDirectories
+import kotlin.io.path.createParentDirectories
+import kotlin.io.path.exists
+import kotlin.io.path.forEachDirectoryEntry
+import kotlin.io.path.listDirectoryEntries
+import kotlin.io.path.moveTo
+import kotlin.io.path.name
+import kotlin.io.path.nameWithoutExtension
+import kotlin.io.path.writeText
+import moe.nea.firmament.gui.config.storage.FirmamentConfigLoader.configFolder
+import moe.nea.firmament.gui.config.storage.FirmamentConfigLoader.configVersionFile
+import moe.nea.firmament.gui.config.storage.FirmamentConfigLoader.storageFolder
+
+object LegacyImporter {
+ val legacyConfigVersion = 995
+ val backupPath = configFolder.resolveSibling("firmament-legacy-config-${System.currentTimeMillis()}")
+
+ fun copyIf(from: Path, to: Path) {
+ if (from.exists()) {
+ to.createParentDirectories()
+ from.copyTo(to)
+ }
+ }
+
+ fun importFromLegacy() {
+ configFolder.moveTo(backupPath)
+ configFolder.createDirectories()
+
+ copyIf(
+ backupPath.resolve("inventory-buttons.json"),
+ storageFolder.resolve("inventory-buttons.json")
+ )
+
+ backupPath.listDirectoryEntries("*.json")
+ .forEach { path ->
+ val name = path.name
+ if (name == "inventory-buttons.json")
+ return@forEach
+ path.copyTo(configFolder.resolve(name))
+ }
+
+ backupPath.resolve("profiles")
+ .forEachDirectoryEntry { category ->
+ category.forEachDirectoryEntry { profile ->
+ copyIf(
+ profile,
+ FirmamentConfigLoader.profilePath
+ .resolve(profile.nameWithoutExtension)
+ .resolve(category.name + ".json")
+ )
+ }
+ }
+
+ configVersionFile.writeText(legacyConfigVersion.toString())
+ }
+}