diff options
Diffstat (limited to 'src/main/kotlin/gui/config')
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. |
