aboutsummaryrefslogtreecommitdiff
path: root/src/main/kotlin/gui/config
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/kotlin/gui/config')
-rw-r--r--src/main/kotlin/gui/config/AllConfigsGui.kt39
-rw-r--r--src/main/kotlin/gui/config/BooleanHandler.kt3
-rw-r--r--src/main/kotlin/gui/config/BuiltInConfigScreenProvider.kt4
-rw-r--r--src/main/kotlin/gui/config/ChoiceHandler.kt7
-rw-r--r--src/main/kotlin/gui/config/ClickHandler.kt1
-rw-r--r--src/main/kotlin/gui/config/ColourHandler.kt83
-rw-r--r--src/main/kotlin/gui/config/DurationHandler.kt7
-rw-r--r--src/main/kotlin/gui/config/EnumRenderer.kt8
-rw-r--r--src/main/kotlin/gui/config/FirmamentConfigScreenProvider.kt4
-rw-r--r--src/main/kotlin/gui/config/GuiAppender.kt6
-rw-r--r--src/main/kotlin/gui/config/HudMetaHandler.kt21
-rw-r--r--src/main/kotlin/gui/config/IntegerHandler.kt6
-rw-r--r--src/main/kotlin/gui/config/JAnyHud.kt67
-rw-r--r--src/main/kotlin/gui/config/KeyBindingHandler.kt38
-rw-r--r--src/main/kotlin/gui/config/KeyBindingStateManager.kt117
-rw-r--r--src/main/kotlin/gui/config/ManagedConfig.kt252
-rw-r--r--src/main/kotlin/gui/config/ManagedConfigElement.kt8
-rw-r--r--src/main/kotlin/gui/config/ManagedOption.kt15
-rw-r--r--src/main/kotlin/gui/config/StringHandler.kt7
-rw-r--r--src/main/kotlin/gui/config/storage/ConfigLoadContext.kt98
-rw-r--r--src/main/kotlin/gui/config/storage/ConfigStorageClass.kt8
-rw-r--r--src/main/kotlin/gui/config/storage/FirmamentConfigLoader.kt252
-rw-r--r--src/main/kotlin/gui/config/storage/FirstLevelSplitJsonFolder.kt109
-rw-r--r--src/main/kotlin/gui/config/storage/LegacyImporter.kt68
-rw-r--r--src/main/kotlin/gui/config/storage/README.md68
25 files changed, 890 insertions, 406 deletions
diff --git a/src/main/kotlin/gui/config/AllConfigsGui.kt b/src/main/kotlin/gui/config/AllConfigsGui.kt
index 73ff444..60711ca 100644
--- a/src/main/kotlin/gui/config/AllConfigsGui.kt
+++ b/src/main/kotlin/gui/config/AllConfigsGui.kt
@@ -2,11 +2,19 @@ package moe.nea.firmament.gui.config
import io.github.notenoughupdates.moulconfig.observer.ObservableList
import io.github.notenoughupdates.moulconfig.xml.Bind
-import net.minecraft.client.gui.screen.Screen
-import net.minecraft.text.Text
+import net.minecraft.client.gui.screens.Screen
+import net.minecraft.network.chat.Component
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.commands.RestArgumentType
+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.MC
import moe.nea.firmament.util.MoulConfigUtils
import moe.nea.firmament.util.ScreenUtil.setScreenLater
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
object AllConfigsGui {
//
@@ -15,9 +23,11 @@ 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 }
+ val enableWideMC by toggle("wide-moulconfig") { false }
}
fun <T> List<T>.toObservableList(): ObservableList<T> = ObservableList(this)
@@ -27,16 +37,16 @@ object AllConfigsGui {
val configs = category.configs.map { EntryMapping(it) }.toObservableList()
@Bind
- fun name() = category.labelText.string
+ fun name() = category.labelText
@Bind
fun close() {
- MC.screen?.close()
+ MC.screen?.onClose()
}
class EntryMapping(val config: ManagedConfig) {
@Bind
- fun name() = Text.translatable("firmament.config.${config.name}").string
+ fun name() = Component.translatable("firmament.config.${config.name}")
@Bind
fun openEditor() {
@@ -53,7 +63,7 @@ object AllConfigsGui {
class CategoryEntry(val category: ManagedConfig.Category) {
@Bind
- fun name() = category.labelText.string
+ fun name() = category.labelText
@Bind
fun open() {
@@ -66,7 +76,7 @@ object AllConfigsGui {
return MoulConfigUtils.loadScreen("config/main", CategoryView(), parent)
}
- fun makeScreen(parent: Screen? = null): Screen {
+ fun makeScreen(search: String? = null, parent: Screen? = null): Screen {
val wantedKey = when {
ConfigConfig.enableMoulConfig -> "moulconfig"
ConfigConfig.enableYacl -> "yacl"
@@ -74,10 +84,23 @@ object AllConfigsGui {
}
val provider = FirmamentConfigScreenProvider.providers.find { it.key == wantedKey }
?: FirmamentConfigScreenProvider.providers.first()
- return provider.open(parent)
+ return provider.open(search, parent)
}
fun showAllGuis() {
setScreenLater(makeScreen())
}
+
+ @Subscribe
+ fun registerCommands(event: CommandEvent.SubCommand) {
+ event.subcommand("search") {
+ thenArgument("search", RestArgumentType) { search ->
+ thenExecute {
+ val search = this[search]
+ setScreenLater(makeScreen(search = search))
+ }
+ }
+ }
+ }
+
}
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/BuiltInConfigScreenProvider.kt b/src/main/kotlin/gui/config/BuiltInConfigScreenProvider.kt
index 19e7383..6495e68 100644
--- a/src/main/kotlin/gui/config/BuiltInConfigScreenProvider.kt
+++ b/src/main/kotlin/gui/config/BuiltInConfigScreenProvider.kt
@@ -1,14 +1,14 @@
package moe.nea.firmament.gui.config
import com.google.auto.service.AutoService
-import net.minecraft.client.gui.screen.Screen
+import net.minecraft.client.gui.screens.Screen
@AutoService(FirmamentConfigScreenProvider::class)
class BuiltInConfigScreenProvider : FirmamentConfigScreenProvider {
override val key: String
get() = "builtin"
- override fun open(parent: Screen?): Screen {
+ override fun open(search: String?, parent: Screen?): Screen {
return AllConfigsGui.makeBuiltInScreen(parent)
}
}
diff --git a/src/main/kotlin/gui/config/ChoiceHandler.kt b/src/main/kotlin/gui/config/ChoiceHandler.kt
index 2ea3efc..494d08a 100644
--- a/src/main/kotlin/gui/config/ChoiceHandler.kt
+++ b/src/main/kotlin/gui/config/ChoiceHandler.kt
@@ -7,16 +7,17 @@ import io.github.notenoughupdates.moulconfig.gui.component.RowComponent
import io.github.notenoughupdates.moulconfig.gui.component.TextComponent
import kotlinx.serialization.json.JsonElement
import kotlin.jvm.optionals.getOrNull
-import net.minecraft.util.StringIdentifiable
+import net.minecraft.util.StringRepresentable
import moe.nea.firmament.gui.CheckboxComponent
import moe.nea.firmament.util.ErrorUtil
+import moe.nea.firmament.util.data.ManagedConfig
import moe.nea.firmament.util.json.KJsonOps
class ChoiceHandler<E>(
val enumClass: Class<E>,
val universe: List<E>,
-) : ManagedConfig.OptionHandler<E> where E : Enum<E>, E : StringIdentifiable {
- val codec = StringIdentifiable.createCodec {
+) : ManagedConfig.OptionHandler<E> where E : Enum<E>, E : StringRepresentable {
+ val codec = StringRepresentable.fromEnum {
@Suppress("UNCHECKED_CAST", "PLATFORM_CLASS_MAPPED_TO_KOTLIN")
(universe as java.util.List<*>).toArray(arrayOfNulls<Enum<E>>(0)) as Array<E>
}
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
new file mode 100644
index 0000000..33daa6d
--- /dev/null
+++ b/src/main/kotlin/gui/config/ColourHandler.kt
@@ -0,0 +1,83 @@
+package moe.nea.firmament.gui.config
+
+import io.github.notenoughupdates.moulconfig.ChromaColour
+import io.github.notenoughupdates.moulconfig.gui.component.ColorSelectComponent
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.descriptors.SerialDescriptor
+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> {
+ @Serializable
+ data class ChromaDelegate(
+ @SerialName("h")
+ val hue: Float,
+ @SerialName("s")
+ val saturation: Float,
+ @SerialName("b")
+ val brightness: Float,
+ @SerialName("a")
+ val alpha: Int,
+ @SerialName("c")
+ val timeForFullRotationInMillis: Int,
+ ) {
+ constructor(delegate: ChromaColour) : this(
+ delegate.hue,
+ delegate.saturation,
+ delegate.brightness,
+ delegate.alpha,
+ delegate.timeForFullRotationInMillis
+ )
+
+ fun into(): ChromaColour = ChromaColour(hue, saturation, brightness, timeForFullRotationInMillis, alpha)
+ }
+
+ object ChromaSerializer : KSerializer<ChromaColour> {
+ override val descriptor: SerialDescriptor
+ get() = SerialDescriptor("FirmChromaColour", ChromaDelegate.serializer().descriptor)
+
+ override fun serialize(
+ encoder: Encoder,
+ value: ChromaColour
+ ) {
+ encoder.encodeSerializableValue(ChromaDelegate.serializer(), ChromaDelegate(value))
+ }
+
+ override fun deserialize(decoder: Decoder): ChromaColour {
+ return decoder.decodeSerializableValue(ChromaDelegate.serializer()).into()
+ }
+ }
+
+ override fun toJson(element: ChromaColour): JsonElement? {
+ return Json.encodeToJsonElement(ChromaSerializer, element)
+ }
+
+ override fun fromJson(element: JsonElement): ChromaColour {
+ return Json.decodeFromJsonElement(ChromaSerializer, element)
+ }
+
+ override fun emitGuiElements(
+ opt: ManagedOption<ChromaColour>,
+ guiAppender: GuiAppender
+ ) {
+ guiAppender.appendLabeledRow(
+ opt.labelText,
+ ColorSelectComponent(
+ 0,
+ 0,
+ opt.value.toLegacyString(),
+ {
+ opt.value = ChromaColour.forLegacyString(it)
+ config.markDirty()
+ },
+ { }
+ )
+ )
+ }
+}
diff --git a/src/main/kotlin/gui/config/DurationHandler.kt b/src/main/kotlin/gui/config/DurationHandler.kt
index 8d485b1..0fc945f 100644
--- a/src/main/kotlin/gui/config/DurationHandler.kt
+++ b/src/main/kotlin/gui/config/DurationHandler.kt
@@ -3,6 +3,7 @@
package moe.nea.firmament.gui.config
import io.github.notenoughupdates.moulconfig.common.IMinecraft
+import io.github.notenoughupdates.moulconfig.common.text.StructuredText
import io.github.notenoughupdates.moulconfig.gui.component.RowComponent
import io.github.notenoughupdates.moulconfig.gui.component.SliderComponent
import io.github.notenoughupdates.moulconfig.gui.component.TextComponent
@@ -14,8 +15,8 @@ import kotlinx.serialization.json.long
import kotlin.time.Duration
import kotlin.time.DurationUnit
import kotlin.time.toDuration
-import net.minecraft.text.Text
import moe.nea.firmament.util.FirmFormatters
+import moe.nea.firmament.util.data.ManagedConfig
class DurationHandler(val config: ManagedConfig, val min: Duration, val max: Duration) :
ManagedConfig.OptionHandler<Duration> {
@@ -31,8 +32,8 @@ class DurationHandler(val config: ManagedConfig, val min: Duration, val max: Dur
guiAppender.appendLabeledRow(
opt.labelText,
RowComponent(
- TextComponent(IMinecraft.instance.defaultFontRenderer,
- { FirmFormatters.formatTimespan(opt.value) },
+ TextComponent(IMinecraft.INSTANCE.defaultFontRenderer,
+ { StructuredText.of(FirmFormatters.formatTimespan(opt.value)) },
40,
TextComponent.TextAlignment.CENTER,
true,
diff --git a/src/main/kotlin/gui/config/EnumRenderer.kt b/src/main/kotlin/gui/config/EnumRenderer.kt
index 3b80b7e..a2dee69 100644
--- a/src/main/kotlin/gui/config/EnumRenderer.kt
+++ b/src/main/kotlin/gui/config/EnumRenderer.kt
@@ -1,14 +1,14 @@
package moe.nea.firmament.gui.config
-import net.minecraft.text.Text
+import net.minecraft.network.chat.Component
interface EnumRenderer<E : Any> {
- fun getName(option: ManagedOption<E>, value: E): Text
+ fun getName(option: ManagedOption<E>, value: E): Component
companion object {
fun <E : Enum<E>> default() = object : EnumRenderer<E> {
- override fun getName(option: ManagedOption<E>, value: E): Text {
- return Text.translatable(option.rawLabelText + ".choice." + value.name.lowercase())
+ override fun getName(option: ManagedOption<E>, value: E): Component {
+ return Component.translatable(option.rawLabelText + ".choice." + value.name.lowercase())
}
}
}
diff --git a/src/main/kotlin/gui/config/FirmamentConfigScreenProvider.kt b/src/main/kotlin/gui/config/FirmamentConfigScreenProvider.kt
index faad1cc..d2a8ab6 100644
--- a/src/main/kotlin/gui/config/FirmamentConfigScreenProvider.kt
+++ b/src/main/kotlin/gui/config/FirmamentConfigScreenProvider.kt
@@ -1,13 +1,13 @@
package moe.nea.firmament.gui.config
-import net.minecraft.client.gui.screen.Screen
+import net.minecraft.client.gui.screens.Screen
import moe.nea.firmament.util.compatloader.CompatLoader
interface FirmamentConfigScreenProvider {
val key: String
val isEnabled: Boolean get() = true
- fun open(parent: Screen?): Screen
+ fun open(search: String?, parent: Screen?): Screen
companion object : CompatLoader<FirmamentConfigScreenProvider>(FirmamentConfigScreenProvider::class) {
val providers by lazy {
diff --git a/src/main/kotlin/gui/config/GuiAppender.kt b/src/main/kotlin/gui/config/GuiAppender.kt
index 329319d..ba28400 100644
--- a/src/main/kotlin/gui/config/GuiAppender.kt
+++ b/src/main/kotlin/gui/config/GuiAppender.kt
@@ -6,8 +6,8 @@ import io.github.notenoughupdates.moulconfig.gui.GuiComponent
import io.github.notenoughupdates.moulconfig.gui.component.RowComponent
import io.github.notenoughupdates.moulconfig.gui.component.TextComponent
import io.github.notenoughupdates.moulconfig.observer.GetSetter
-import net.minecraft.client.gui.screen.Screen
-import net.minecraft.text.Text
+import net.minecraft.client.gui.screens.Screen
+import net.minecraft.network.chat.Component
import moe.nea.firmament.gui.FixedComponent
class GuiAppender(val width: Int, val screenAccessor: () -> Screen) {
@@ -18,7 +18,7 @@ class GuiAppender(val width: Int, val screenAccessor: () -> Screen) {
reloadables.add(reloadable)
}
- fun appendLabeledRow(label: Text, right: GuiComponent) {
+ fun appendLabeledRow(label: Component, right: GuiComponent) {
appendSplitRow(
TextComponent(label.string),
right
diff --git a/src/main/kotlin/gui/config/HudMetaHandler.kt b/src/main/kotlin/gui/config/HudMetaHandler.kt
index a9659ee..915dcf3 100644
--- a/src/main/kotlin/gui/config/HudMetaHandler.kt
+++ b/src/main/kotlin/gui/config/HudMetaHandler.kt
@@ -5,21 +5,29 @@ import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.decodeFromJsonElement
import kotlinx.serialization.json.encodeToJsonElement
-import net.minecraft.client.gui.screen.Screen
-import net.minecraft.text.MutableText
-import net.minecraft.text.Text
+import net.minecraft.client.gui.screens.Screen
+import net.minecraft.network.chat.MutableComponent
+import net.minecraft.network.chat.Component
+import moe.nea.firmament.Firmament
import moe.nea.firmament.gui.FirmButtonComponent
import moe.nea.firmament.jarvis.JarvisIntegration
import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.data.ManagedConfig
-class HudMetaHandler(val config: ManagedConfig, val label: MutableText, val width: Int, val height: Int) :
+class HudMetaHandler(
+ val config: ManagedConfig,
+ val propertyName: String,
+ val label: MutableComponent,
+ val width: Int,
+ val height: Int
+) :
ManagedConfig.OptionHandler<HudMeta> {
override fun toJson(element: HudMeta): JsonElement? {
return Json.encodeToJsonElement(element.position)
}
override fun fromJson(element: JsonElement): HudMeta {
- return HudMeta(Json.decodeFromJsonElement(element), label, width, height)
+ return HudMeta(Json.decodeFromJsonElement(element), Firmament.identifier(propertyName), label, width, height)
}
fun openEditor(option: ManagedOption<HudMeta>, oldScreen: Screen) {
@@ -34,7 +42,8 @@ class HudMetaHandler(val config: ManagedConfig, val label: MutableText, val widt
opt.labelText,
FirmButtonComponent(
TextComponent(
- Text.stringifiedTranslatable("firmament.hud.edit", label).string),
+ Component.translatableEscape("firmament.hud.edit", label).string
+ ),
) {
openEditor(opt, guiAppender.screenAccessor())
})
diff --git a/src/main/kotlin/gui/config/IntegerHandler.kt b/src/main/kotlin/gui/config/IntegerHandler.kt
index 31ce90f..ab0237a 100644
--- a/src/main/kotlin/gui/config/IntegerHandler.kt
+++ b/src/main/kotlin/gui/config/IntegerHandler.kt
@@ -3,6 +3,7 @@
package moe.nea.firmament.gui.config
import io.github.notenoughupdates.moulconfig.common.IMinecraft
+import io.github.notenoughupdates.moulconfig.common.text.StructuredText
import io.github.notenoughupdates.moulconfig.gui.component.RowComponent
import io.github.notenoughupdates.moulconfig.gui.component.SliderComponent
import io.github.notenoughupdates.moulconfig.gui.component.TextComponent
@@ -12,6 +13,7 @@ import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonPrimitive
import moe.nea.firmament.util.FirmFormatters
+import moe.nea.firmament.util.data.ManagedConfig
class IntegerHandler(val config: ManagedConfig, val min: Int, val max: Int) : ManagedConfig.OptionHandler<Int> {
override fun toJson(element: Int): JsonElement? {
@@ -26,8 +28,8 @@ class IntegerHandler(val config: ManagedConfig, val min: Int, val max: Int) : Ma
guiAppender.appendLabeledRow(
opt.labelText,
RowComponent(
- TextComponent(IMinecraft.instance.defaultFontRenderer,
- { FirmFormatters.formatCommas(opt.value, 0) },
+ TextComponent(IMinecraft.INSTANCE.defaultFontRenderer,
+ { StructuredText.of(FirmFormatters.formatCommas(opt.value, 0)) },
40,
TextComponent.TextAlignment.CENTER,
true,
diff --git a/src/main/kotlin/gui/config/JAnyHud.kt b/src/main/kotlin/gui/config/JAnyHud.kt
index 35c4eb2..63975c6 100644
--- a/src/main/kotlin/gui/config/JAnyHud.kt
+++ b/src/main/kotlin/gui/config/JAnyHud.kt
@@ -1,48 +1,67 @@
-
-
package moe.nea.firmament.gui.config
import moe.nea.jarvis.api.JarvisHud
-import moe.nea.jarvis.api.JarvisScalable
+import org.joml.Matrix3x2f
+import org.joml.Vector2i
+import org.joml.Vector2ic
import kotlinx.serialization.Serializable
-import net.minecraft.text.Text
+import net.minecraft.network.chat.Component
+import net.minecraft.resources.ResourceLocation
+import moe.nea.firmament.jarvis.JarvisIntegration
@Serializable
data class HudPosition(
- var x: Double,
- var y: Double,
- var scale: Float,
+ var x: Int,
+ var y: Int,
+ var scale: Float,
)
data class HudMeta(
val position: HudPosition,
- private val label: Text,
+ private val id: ResourceLocation,
+ private val label: Component,
private val width: Int,
private val height: Int,
-) : JarvisScalable, JarvisHud {
- override fun getX(): Double = position.x
+) : JarvisHud, JarvisHud.Scalable {
+ override fun getLabel(): Component = label
+ override fun getUnscaledWidth(): Int {
+ return width
+ }
+
+ override fun getUnscaledHeight(): Int {
+ return height
+ }
- override fun setX(newX: Double) {
- position.x = newX
- }
+ override fun getHudId(): ResourceLocation {
+ return id
+ }
- override fun getY(): Double = position.y
+ override fun getPosition(): Vector2ic {
+ return Vector2i(position.x, position.y)
+ }
- override fun setY(newY: Double) {
- position.y = newY
- }
+ override fun setPosition(p0: Vector2ic) {
+ position.x = p0.x()
+ position.y = p0.y()
+ }
- override fun getLabel(): Text = label
+ override fun isEnabled(): Boolean {
+ return true // TODO: this should be actually truthful, if possible
+ }
- override fun getWidth(): Int = width
+ override fun isVisible(): Boolean {
+ return true // TODO: this should be actually truthful, if possible
+ }
- override fun getHeight(): Int = height
+ override fun getScale(): Float = position.scale
- override fun getScale(): Float = position.scale
+ override fun setScale(newScale: Float) {
+ position.scale = newScale
+ }
- override fun setScale(newScale: Float) {
- position.scale = newScale
- }
+ fun applyTransformations(matrix4f: Matrix3x2f) {
+ applyTransformations(JarvisIntegration.jarvis, matrix4f)
+ }
}
diff --git a/src/main/kotlin/gui/config/KeyBindingHandler.kt b/src/main/kotlin/gui/config/KeyBindingHandler.kt
index d7d0b47..3c08da2 100644
--- a/src/main/kotlin/gui/config/KeyBindingHandler.kt
+++ b/src/main/kotlin/gui/config/KeyBindingHandler.kt
@@ -1,11 +1,5 @@
package moe.nea.firmament.gui.config
-import io.github.notenoughupdates.moulconfig.common.IMinecraft
-import io.github.notenoughupdates.moulconfig.common.MyResourceLocation
-import io.github.notenoughupdates.moulconfig.deps.libninepatch.NinePatch
-import io.github.notenoughupdates.moulconfig.gui.GuiImmediateContext
-import io.github.notenoughupdates.moulconfig.gui.KeyboardEvent
-import io.github.notenoughupdates.moulconfig.gui.component.TextComponent
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.decodeFromJsonElement
@@ -13,6 +7,7 @@ import kotlinx.serialization.json.encodeToJsonElement
import moe.nea.firmament.gui.FirmButtonComponent
import moe.nea.firmament.keybindings.FirmamentKeyBindings
import moe.nea.firmament.keybindings.SavedKeyBinding
+import moe.nea.firmament.util.data.ManagedConfig
class KeyBindingHandler(val name: String, val managedConfig: ManagedConfig) :
ManagedConfig.OptionHandler<SavedKeyBinding> {
@@ -35,39 +30,12 @@ class KeyBindingHandler(val name: String, val managedConfig: ManagedConfig) :
{ opt.value },
{
opt.value = it
- opt.element.save()
+ opt.element.markDirty()
},
{ button.blur() },
{ button.requestFocus() }
)
- button = object : FirmButtonComponent(
- TextComponent(
- IMinecraft.instance.defaultFontRenderer,
- { sm.label.string },
- 130,
- TextComponent.TextAlignment.LEFT,
- false,
- false
- ), action = {
- sm.onClick()
- }) {
- override fun keyboardEvent(event: KeyboardEvent, context: GuiImmediateContext): Boolean {
- if (event is KeyboardEvent.KeyPressed) {
- return sm.keyboardEvent(event.keycode, event.pressed)
- }
- return super.keyboardEvent(event, context)
- }
-
- override fun getBackground(context: GuiImmediateContext): NinePatch<MyResourceLocation> {
- if (sm.editing) return activeBg
- return super.getBackground(context)
- }
-
-
- override fun onLostFocus() {
- sm.onLostFocus()
- }
- }
+ button = sm.createButton()
sm.updateLabel()
return button
}
diff --git a/src/main/kotlin/gui/config/KeyBindingStateManager.kt b/src/main/kotlin/gui/config/KeyBindingStateManager.kt
index cc8178d..9cf2771 100644
--- a/src/main/kotlin/gui/config/KeyBindingStateManager.kt
+++ b/src/main/kotlin/gui/config/KeyBindingStateManager.kt
@@ -1,8 +1,18 @@
package moe.nea.firmament.gui.config
+import io.github.notenoughupdates.moulconfig.common.IMinecraft
+import io.github.notenoughupdates.moulconfig.common.MyResourceLocation
+import io.github.notenoughupdates.moulconfig.deps.libninepatch.NinePatch
+import io.github.notenoughupdates.moulconfig.gui.GuiImmediateContext
+import io.github.notenoughupdates.moulconfig.gui.KeyboardEvent
+import io.github.notenoughupdates.moulconfig.gui.component.TextComponent
+import io.github.notenoughupdates.moulconfig.platform.MoulConfigPlatform
import org.lwjgl.glfw.GLFW
-import net.minecraft.text.Text
-import net.minecraft.util.Formatting
+import net.minecraft.network.chat.Component
+import net.minecraft.ChatFormatting
+import moe.nea.firmament.gui.FirmButtonComponent
+import moe.nea.firmament.keybindings.GenericInputButton
+import moe.nea.firmament.keybindings.InputModifiers
import moe.nea.firmament.keybindings.SavedKeyBinding
class KeyBindingStateManager(
@@ -12,73 +22,65 @@ class KeyBindingStateManager(
val requestFocus: () -> Unit,
) {
var editing = false
- var lastPressed = 0
- var lastPressedNonModifier = 0
- var label: Text = Text.literal("")
+ var lastPressed: GenericInputButton? = null
+ var label: Component = Component.literal("")
- fun onClick() {
+ fun onClick(mouseButton: Int) {
if (editing) {
- editing = false
- blur()
- } else {
+ keyboardEvent(GenericInputButton.mouse(mouseButton), true)
+ } else if (mouseButton == GLFW.GLFW_MOUSE_BUTTON_LEFT) {
editing = true
requestFocus()
}
updateLabel()
}
- fun keyboardEvent(keyCode: Int, pressed: Boolean): Boolean {
- return if (pressed) onKeyPressed(keyCode, SavedKeyBinding.getModInt())
- else onKeyReleased(keyCode, SavedKeyBinding.getModInt())
+ fun keyboardEvent(keyCode: GenericInputButton, pressed: Boolean): Boolean {
+ return if (pressed) onKeyPressed(keyCode, InputModifiers.current())
+ else onKeyReleased(keyCode, InputModifiers.current())
}
- fun onKeyPressed(ch: Int, modifiers: Int): Boolean {
+ fun onKeyPressed(
+ ch: GenericInputButton,
+ modifiers: InputModifiers
+ ): Boolean { // TODO !!!!!: genericify this method to allow for other inputs
if (!editing) {
return false
}
- if (ch == GLFW.GLFW_KEY_ESCAPE) {
- lastPressedNonModifier = 0
+ if (ch == GenericInputButton.escape()) {
editing = false
- lastPressed = 0
- setValue(SavedKeyBinding(GLFW.GLFW_KEY_UNKNOWN))
+ lastPressed = null
+ setValue(SavedKeyBinding.unbound())
updateLabel()
blur()
return true
}
- if (ch == GLFW.GLFW_KEY_LEFT_SHIFT || ch == GLFW.GLFW_KEY_RIGHT_SHIFT
- || ch == GLFW.GLFW_KEY_LEFT_ALT || ch == GLFW.GLFW_KEY_RIGHT_ALT
- || ch == GLFW.GLFW_KEY_LEFT_CONTROL || ch == GLFW.GLFW_KEY_RIGHT_CONTROL
- ) {
+ if (ch.isModifier()) {
lastPressed = ch
} else {
- setValue(SavedKeyBinding(
- ch, modifiers
- ))
+ setValue(SavedKeyBinding(ch, modifiers))
editing = false
blur()
- lastPressed = 0
- lastPressedNonModifier = 0
+ lastPressed = null
}
updateLabel()
return true
}
fun onLostFocus() {
- lastPressedNonModifier = 0
editing = false
- lastPressed = 0
+ lastPressed = null
updateLabel()
}
- fun onKeyReleased(ch: Int, modifiers: Int): Boolean {
+ fun onKeyReleased(ch: GenericInputButton, modifiers: InputModifiers): Boolean {
if (!editing)
return false
- if (lastPressedNonModifier == ch || (lastPressedNonModifier == 0 && ch == lastPressed)) {
+ if (ch == lastPressed) { // TODO: check modifiers dont duplicate (CTRL+CTRL)
setValue(SavedKeyBinding(ch, modifiers))
editing = false
blur()
- lastPressed = 0
- lastPressedNonModifier = 0
+ lastPressed = null
}
updateLabel()
return true
@@ -87,22 +89,51 @@ class KeyBindingStateManager(
fun updateLabel() {
var stroke = value().format()
if (editing) {
- stroke = Text.literal("")
- val (shift, ctrl, alt) = SavedKeyBinding.getMods(SavedKeyBinding.getModInt())
- if (shift) {
- stroke.append("SHIFT + ")
- }
- if (alt) {
- stroke.append("ALT + ")
- }
- if (ctrl) {
- stroke.append("CTRL + ")
+ stroke = Component.empty()
+ val modifiers = InputModifiers.current()
+ if (!modifiers.isEmpty()) {
+ stroke.append(modifiers.format())
+ stroke.append(" + ")
}
stroke.append("???")
- stroke.styled { it.withColor(Formatting.YELLOW) }
+ stroke.withStyle { it.withColor(ChatFormatting.YELLOW) }
}
label = stroke
}
+ fun createButton(): FirmButtonComponent {
+ return object : FirmButtonComponent(
+ TextComponent(
+ IMinecraft.INSTANCE.defaultFontRenderer,
+ { MoulConfigPlatform.wrap(this@KeyBindingStateManager.label) },
+ 130,
+ TextComponent.TextAlignment.LEFT,
+ false,
+ false
+ ), action = {
+ this@KeyBindingStateManager.onClick(it)
+ }) {
+ override fun keyboardEvent(event: KeyboardEvent, context: GuiImmediateContext): Boolean {
+ if (event is KeyboardEvent.KeyPressed) {
+ return this@KeyBindingStateManager.keyboardEvent(
+ GenericInputButton.ofKeyAndScan(
+ event.keycode,
+ event.scancode
+ ), event.pressed
+ )
+ }
+ return super.keyboardEvent(event, context)
+ }
+ override fun getBackground(context: GuiImmediateContext): NinePatch<MyResourceLocation> {
+ if (this@KeyBindingStateManager.editing) return activeBg
+ return super.getBackground(context)
+ }
+
+
+ override fun onLostFocus() {
+ this@KeyBindingStateManager.onLostFocus()
+ }
+ }
+ }
}
diff --git a/src/main/kotlin/gui/config/ManagedConfig.kt b/src/main/kotlin/gui/config/ManagedConfig.kt
deleted file mode 100644
index 7ddda9e..0000000
--- a/src/main/kotlin/gui/config/ManagedConfig.kt
+++ /dev/null
@@ -1,252 +0,0 @@
-package moe.nea.firmament.gui.config
-
-import com.mojang.serialization.Codec
-import io.github.notenoughupdates.moulconfig.gui.CloseEventListener
-import io.github.notenoughupdates.moulconfig.gui.GuiComponentWrapper
-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 moe.nea.jarvis.api.Point
-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.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,
- MINING,
- 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)
- }
-
- 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 <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: () -> Point,
- ): ManagedOption<HudMeta> {
- val label = Text.translatable("firmament.config.${name}.${propertyName}")
- return option(propertyName, {
- val p = default()
- HudMeta(HudPosition(p.x, p.y, 1F), label, width, height)
- }, HudMetaHandler(this, label, width, height))
- }
-
- protected fun keyBinding(
- propertyName: String,
- default: () -> Int,
- ): ManagedOption<SavedKeyBinding> = keyBindingWithOutDefaultModifiers(propertyName) { SavedKeyBinding(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(GLFW.GLFW_KEY_UNKNOWN) }
- }
-
- 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 : GuiComponentWrapper(GuiContext(component)) {
- override fun close() {
- if (context.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 383f392..4c228de 100644
--- a/src/main/kotlin/gui/config/ManagedOption.kt
+++ b/src/main/kotlin/gui/config/ManagedOption.kt
@@ -5,8 +5,9 @@ import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
-import net.minecraft.text.Text
+import net.minecraft.network.chat.Component
import moe.nea.firmament.util.ErrorUtil
+import moe.nea.firmament.util.data.ManagedConfig
class ManagedOption<T : Any>(
val element: ManagedConfig,
@@ -23,15 +24,15 @@ class ManagedOption<T : Any>(
}
val rawLabelText = "firmament.config.${element.name}.${propertyName}"
- val labelText: Text = Text.translatable(rawLabelText)
+ val labelText: Component = Component.translatable(rawLabelText)
val descriptionTranslationKey = "firmament.config.${element.name}.${propertyName}.description"
- val labelDescription: Text = Text.translatable(descriptionTranslationKey)
+ val labelDescription: Component = Component.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)
}
@@ -49,7 +50,7 @@ class ManagedOption<T : Any>(
value = handler.fromJson(root[propertyName]!!)
return
} catch (e: Exception) {
- ErrorUtil.softError(
+ ErrorUtil.logError(
"Exception during loading of config file ${element.name}. This will reset this config.",
e
)
diff --git a/src/main/kotlin/gui/config/StringHandler.kt b/src/main/kotlin/gui/config/StringHandler.kt
index a326abb..17bb981 100644
--- a/src/main/kotlin/gui/config/StringHandler.kt
+++ b/src/main/kotlin/gui/config/StringHandler.kt
@@ -7,7 +7,8 @@ import io.github.notenoughupdates.moulconfig.observer.GetSetter
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.jsonPrimitive
-import net.minecraft.text.Text
+import net.minecraft.network.chat.Component
+import moe.nea.firmament.util.data.ManagedConfig
class StringHandler(val config: ManagedConfig) : ManagedConfig.OptionHandler<String> {
override fun toJson(element: String): JsonElement? {
@@ -25,11 +26,11 @@ 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,
- suggestion = Text.translatableWithFallback(opt.rawLabelText + ".hint", "").string
+ suggestion = Component.translatableWithFallback(opt.rawLabelText + ".hint", "").string
),
)
}
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..4a06ec6
--- /dev/null
+++ b/src/main/kotlin/gui/config/storage/ConfigLoadContext.kt
@@ -0,0 +1,98 @@
+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.ExperimentalPathApi
+import kotlin.io.path.OnErrorResult
+import kotlin.io.path.Path
+import kotlin.io.path.copyToRecursively
+import kotlin.io.path.createParentDirectories
+import kotlin.io.path.writeText
+import moe.nea.firmament.Firmament
+
+data class ConfigLoadContext(
+ val loadId: String,
+) : AutoCloseable {
+ val backupPath = Path("backups").resolve(Firmament.MOD_ID)
+ .resolve("config-$loadId")
+ .toAbsolutePath()
+ 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) {
+ if (Firmament.DEBUG)
+ Firmament.logger.info("[ConfigUpgrade] $message")
+ logBuffer.append("[INFO] ").append(message).appendLine()
+ }
+
+ fun logError(message: String, exception: Throwable) {
+ markShouldSaveLogBuffer()
+ if (Firmament.DEBUG)
+ 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()
+ }
+
+ fun use(block: (ConfigLoadContext) -> Unit) {
+ try {
+ block(this)
+ } catch (ex: Exception) {
+ logError("Caught exception on CLC", ex)
+ } finally {
+ close()
+ }
+ }
+
+ 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)
+ }
+ }
+ }
+
+ @OptIn(ExperimentalPathApi::class)
+ fun createBackup(folder: Path, string: String) {
+ val backupDestination = backupPath.resolve("$string-${System.currentTimeMillis()}")
+ logError("Creating backup of $folder in $backupDestination")
+ folder.copyToRecursively(
+ backupDestination.createParentDirectories(),
+ onError = { source: Path, target: Path, exception: Exception ->
+ logError("Failed to copy subtree $source to $target", exception)
+ OnErrorResult.SKIP_SUBTREE
+ },
+ followLinks = false,
+ overwrite = false
+ )
+ }
+}
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..0292721
--- /dev/null
+++ b/src/main/kotlin/gui/config/storage/FirmamentConfigLoader.kt
@@ -0,0 +1,252 @@
+package moe.nea.firmament.gui.config.storage
+
+import java.util.UUID
+import java.util.concurrent.CompletableFuture
+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 kotlin.time.Duration.Companion.seconds
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.TickEvent
+import moe.nea.firmament.features.debug.DebugLogger
+import moe.nea.firmament.util.SBData.NULL_UUID
+import moe.nea.firmament.util.TimeMark
+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)
+ var profileData =
+ profilePath.takeIf { it.exists() }
+ ?.listDirectoryEntries()
+ ?.filter { it.isDirectory() }
+ ?.mapNotNull {
+ val uuid= runCatching { UUID.fromString(it.name) }.getOrNull() ?: return@mapNotNull null
+ uuid to FirstLevelSplitJsonFolder(loadContext, it).load()
+ }
+ ?.toMap()
+ if (profileData.isNullOrEmpty())
+ profileData = mapOf(NULL_UUID to JsonObject(mapOf()))
+ 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) {
+ val h = (holder as IDataHolder<T>)
+ if (key == null) {
+ h.explicitDefaultLoad()
+ } else {
+ h.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("save-${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()))
+ )
+ }
+ writeConfigVersion()
+ }
+ }
+
+ 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)
+ )
+ }
+ writeConfigVersion()
+ }
+ }
+
+ fun writeConfigVersion() {
+ configVersionFile.writeText("$currentConfigVersion ${tagLines.random()}")
+ }
+
+ private fun updateOneConfig(
+ loadContext: ConfigLoadContext,
+ startVersion: Int,
+ storageClass: ConfigStorageClass,
+ firstLevelSplitJsonFolder: FirstLevelSplitJsonFolder
+ ) {
+ if (startVersion == currentConfigVersion) {
+ loadContext.logDebug("Skipping upgrade to ")
+ return
+ }
+ 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
+ }
+
+ @Subscribe
+ fun onTick(event: TickEvent) {
+ val config = configPromise ?: return
+ val passedTime = saveDebounceStart.passedTime()
+ if (passedTime < 1.seconds)
+ return
+ if (!config.isDone && passedTime < 3.seconds)
+ return
+ debugLogger.log("Performing config save")
+ configPromise = null
+ saveAll()
+ }
+
+ val debugLogger = DebugLogger("config")
+
+ var configPromise: CompletableFuture<Void?>? = null
+ var saveDebounceStart: TimeMark = TimeMark.farPast()
+ fun markDirty(
+ holder: IDataHolder<*>,
+ timeoutPromise: CompletableFuture<Void?>? = null
+ ) {
+ debugLogger.log("Config marked dirty")
+ this.saveDebounceStart = TimeMark.now()
+ this.configPromise = timeoutPromise ?: CompletableFuture.completedFuture(null)
+ }
+
+}
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..b92488a
--- /dev/null
+++ b/src/main/kotlin/gui/config/storage/FirstLevelSplitJsonFolder.kt
@@ -0,0 +1,109 @@
+@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.createDirectories
+import kotlin.io.path.deleteExisting
+import kotlin.io.path.exists
+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
+
+// TODO: make this class write / read async
+class FirstLevelSplitJsonFolder(
+ val context: ConfigLoadContext,
+ val folder: Path
+) {
+
+ var hasCreatedBackup = false
+
+ fun backup(cause: String) {
+ if (hasCreatedBackup) return
+ hasCreatedBackup = true
+ context.createBackup(folder, cause)
+ }
+
+ fun load(): JsonObject {
+ context.logInfo("Loading FLSJF from $folder")
+ if (!folder.exists())
+ return JsonObject(mapOf())
+ return try {
+ folder.listDirectoryEntries("*.json")
+ .mapNotNull(::loadIndividualFile)
+ .toMap()
+ .let(::JsonObject)
+ .also { context.logInfo("FLSJF from $folder - Voller Erfolg!") }
+ } catch (ex: Exception) {
+ context.logError("Could not load files from $folder", ex)
+ backup("failed-load")
+ JsonObject(mapOf())
+ }
+ }
+
+ fun loadIndividualFile(path: Path): Pair<String, JsonElement>? {
+ context.logDebug("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)
+ backup("failed-load")
+ null
+ }
+ }
+
+ fun save(value: JsonObject) {
+ context.logInfo("Saving FLSJF to $folder")
+ context.logDebug("Current value:\n$value")
+ if (!folder.exists()) {
+ context.logInfo("Creating folder $folder")
+ folder.createDirectories()
+ }
+ 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")
+ backup("save-deletion")
+ 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.logDebug("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)
+ backup("failed-save")
+ 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..c1f8b90
--- /dev/null
+++ b/src/main/kotlin/gui/config/storage/LegacyImporter.kt
@@ -0,0 +1,68 @@
+package moe.nea.firmament.gui.config.storage
+
+import java.nio.file.Path
+import kotlin.io.path.copyTo
+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)
+ }
+ }
+
+ val legacyStorage = listOf(
+ "inventory-buttons",
+ "macros",
+ )
+
+ fun importFromLegacy() {
+ if (!configFolder.exists()) return
+ configFolder.moveTo(backupPath)
+ configFolder.createDirectories()
+
+ legacyStorage.forEach {
+ copyIf(
+ backupPath.resolve("$it.json"),
+ storageFolder.resolve("$it.json")
+ )
+ }
+
+ backupPath.listDirectoryEntries("*.json")
+ .filter { it.nameWithoutExtension !in legacyStorage }
+ .forEach { path ->
+ val name = path.name
+ path.copyTo(configFolder.resolve(name))
+ }
+
+ backupPath.resolve("profiles")
+ .takeIf { it.exists() }
+ ?.forEachDirectoryEntry { category ->
+ category.forEachDirectoryEntry { profile ->
+ copyIf(
+ profile,
+ FirmamentConfigLoader.profilePath
+ .resolve(profile.nameWithoutExtension)
+ .resolve(category.name + ".json")
+ )
+ }
+ }
+
+ configVersionFile.writeText("$legacyConfigVersion LEGACY")
+ }
+}
diff --git a/src/main/kotlin/gui/config/storage/README.md b/src/main/kotlin/gui/config/storage/README.md
new file mode 100644
index 0000000..aad4afe
--- /dev/null
+++ b/src/main/kotlin/gui/config/storage/README.md
@@ -0,0 +1,68 @@
+<!--
+SPDX-FileCopyrightText: 2025 Linnea Gräf <nea@nea.moe>
+
+SPDX-License-Identifier: CC0-1.0
+-->
+
+# Plan for the 2026 Config Renewal of Firmament
+
+The current config system in Firmament is not growing at a reasonable pace. Here is a list of my grievances with it:
+
+- the config files are split, resulting in making migrations between different config files (which might load in
+ different order) difficult
+- it is difficult to detect extraneous properties / files, because not all files are loaded and consumed at once
+- profile specific data should be in a different hierarchy. the current hierarchy of `profiles/topic/<uuid>.json` orders
+ data from different profiles to be closer than data from the same profile. this also contributes to the two former
+ problems.
+
+## Goals
+
+- i want to retain having multiple different files for different topics, as well as a folder structure that makes sense
+ for profiles.
+- i want to split up "storage" type data, with "config" type data
+- i want to support partial loads with some broken files (resetting the files that are broken)
+- i want to support backups on any detected error (or simply at will)
+ - notably i do not care about the structure of the backups much. even just a all json files merged backup is fine
+ for me, for now.
+
+## Implementation
+
+### FirstLevelSplitJsonFolder
+
+One of the basic components of this new config folder is a `FirstLevelSplitJsonFolder`. A `FLSJF` takes in a folder
+containing multiple JSON-files and loads all of them unconditionally. Each file is then inserted side by side into a
+json object, to be processed further by other mechanisms.
+
+In essence the `FLSJF` takes a folder structure like this:
+
+```
+file-1.json
+file-2.json
+file-3.json
+```
+
+and turns it into a single merged json object:
+
+```json
+{
+ "file-1": "the json content of file-1.json",
+ "file-2": "the json content of file-2.json",
+ "file-3": "the json content of file-3.json"
+}
+```
+
+As with any stage of the implementation, any unparsable files shall be copied over to a backup spot and discarded.
+
+Nota bene: Folders are wholesale ignored.
+
+### Config folders
+
+Firmament stores all configs and data in the root config folder `./config/firmament`.
+
+- Any config data is stored as an [`FLSJF`](#firstlevelsplitjsonfolder) in the root config folder
+- Any generic storage data is stored as an [`FLSJF`](#firstlevelsplitjsonfolder) in `${rootConfigFolder}/storage/`.
+- Any profile specific storage data is stored as an [`FLSJF`](#firstlevelsplitjsonfolder) for each profile in `${rootConfigFolder}/profileStorage/${profileUuid}/`.
+- Any backup data is stored in `${rootConfigFolder}/backups/${launchId}/${loadId}/${fileName}`.
+ - Where `launchId` is `${currentLaunchTimestamp}-${random()}` to avoid collisions.
+ - Where `loadId` depends on which stage of the config load we are doing (`merge`/`upgrade`/etc.) and what type of config we are loading (`profileSpecific`/`config`/etc.).
+ - And where `fileName` may be a relative filename of where this data was originally found or some internal descriptor for the merged data stage we are on.