rudimentary config gui (again)

This commit is contained in:
nea
2022-10-22 00:34:22 +02:00
parent c98d4693f1
commit f85c449ed5
13 changed files with 277 additions and 53 deletions

View File

@@ -1,4 +1,3 @@
- translations
- recipes
- fairy souls
- easy config gui builder

View File

@@ -26,7 +26,7 @@ import moe.nea.notenoughupdates.dbus.NEUDbusObject
import moe.nea.notenoughupdates.features.FeatureManager
import moe.nea.notenoughupdates.repo.RepoManager
import moe.nea.notenoughupdates.util.SBData
import moe.nea.notenoughupdates.util.config.IConfigHolder
import moe.nea.notenoughupdates.util.data.IDataHolder
object NotEnoughUpdates : ModInitializer, ClientModInitializer {
const val MOD_ID = "notenoughupdates"
@@ -72,7 +72,7 @@ object NotEnoughUpdates : ModInitializer, ClientModInitializer {
override fun onInitialize() {
dbusConnection.requestBusName("moe.nea.notenoughupdates")
dbusConnection.exportObject(NEUDbusObject)
IConfigHolder.registerEvents()
IDataHolder.registerEvents()
RepoManager.initialize()
SBData.init()
FeatureManager.autoload()

View File

@@ -4,6 +4,7 @@ import com.mojang.brigadier.CommandDispatcher
import io.github.cottonmc.cotton.gui.client.CottonClientScreen
import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource
import net.minecraft.text.Text
import moe.nea.notenoughupdates.features.world.FairySouls
import moe.nea.notenoughupdates.gui.repoGui
import moe.nea.notenoughupdates.repo.RepoManager
import moe.nea.notenoughupdates.util.SBData
@@ -29,6 +30,11 @@ fun neuCommand() = literal("neu") {
}
}
thenLiteral("dev") {
thenLiteral("config") {
thenExecute {
FairySouls.TConfig.showConfigEditor()
}
}
thenLiteral("sbdata") {
thenExecute {
source.sendFeedback(Text.translatable("notenoughupdates.sbinfo.profile", SBData.profileCuteName))

View File

@@ -4,9 +4,9 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.serializer
import moe.nea.notenoughupdates.NotEnoughUpdates
import moe.nea.notenoughupdates.features.world.FairySouls
import moe.nea.notenoughupdates.util.config.ConfigHolder
import moe.nea.notenoughupdates.util.data.DataHolder
object FeatureManager : ConfigHolder<FeatureManager.Config>(serializer(), "features", ::Config) {
object FeatureManager : DataHolder<FeatureManager.Config>(serializer(), "features", ::Config) {
@Serializable
data class Config(
val enabledFeatures: MutableMap<String, Boolean> = mutableMapOf()
@@ -40,11 +40,11 @@ object FeatureManager : ConfigHolder<FeatureManager.Config>(serializer(), "featu
}
fun isEnabled(identifier: String): Boolean? =
config.enabledFeatures[identifier]
data.enabledFeatures[identifier]
fun setEnabled(identifier: String, value: Boolean) {
config.enabledFeatures[identifier] = value
data.enabledFeatures[identifier] = value
markDirty()
}

View File

@@ -3,6 +3,7 @@ package moe.nea.notenoughupdates.features.world
import io.github.moulberry.repo.data.Coordinate
import kotlinx.serialization.Serializable
import kotlinx.serialization.serializer
import net.minecraft.client.MinecraftClient
import net.minecraft.util.math.BlockPos
import moe.nea.notenoughupdates.events.ServerChatLineReceivedEvent
import moe.nea.notenoughupdates.events.SkyblockServerUpdateEvent
@@ -11,7 +12,8 @@ import moe.nea.notenoughupdates.features.NEUFeature
import moe.nea.notenoughupdates.repo.RepoManager
import moe.nea.notenoughupdates.util.MC
import moe.nea.notenoughupdates.util.SBData
import moe.nea.notenoughupdates.util.config.ProfileSpecificConfigHolder
import moe.nea.notenoughupdates.util.config.ManagedConfig
import moe.nea.notenoughupdates.util.data.ProfileSpecificDataHolder
import moe.nea.notenoughupdates.util.render.RenderBlockContext.Companion.renderBlocks
import moe.nea.notenoughupdates.util.unformattedString
@@ -19,12 +21,22 @@ val Coordinate.blockPos: BlockPos
get() = BlockPos(x, y, z)
object FairySouls : NEUFeature,
ProfileSpecificConfigHolder<FairySouls.Config>(serializer(), "fairy-souls.json", ::Config) {
ProfileSpecificDataHolder<FairySouls.Config>(serializer(), "found-fairysouls", ::Config) {
@Serializable
data class Config(
val foundSouls: MutableMap<String, MutableSet<Int>> = mutableMapOf()
)
object TConfig : ManagedConfig("fairysouls") {
val displaySouls by toggle("show") { false }
val resetSouls by button("reset") {
FairySouls.data?.foundSouls?.clear() != null
updateMissingSouls()
}
}
override val name: String get() = "Fairy Souls"
override val identifier: String get() = "fairy-souls"
@@ -37,7 +49,7 @@ object FairySouls : NEUFeature,
fun updateMissingSouls() {
currentMissingSouls = emptyList()
val c = config ?: return
val c = data ?: return
val fi = c.foundSouls[currentLocationName] ?: setOf()
val cms = currentLocationSouls.toMutableList()
fi.asSequence().sortedDescending().filter { it in cms.indices }.forEach { cms.removeAt(it) }
@@ -65,7 +77,7 @@ object FairySouls : NEUFeature,
private fun markNearestSoul() {
val nearestSoul = findNearestClickableSoul() ?: return
val c = config ?: return
val c = data ?: return
val loc = currentLocationName ?: return
val idx = currentLocationSouls.indexOf(nearestSoul)
c.foundSouls.computeIfAbsent(loc) { mutableSetOf() }.add(idx)
@@ -92,6 +104,7 @@ object FairySouls : NEUFeature,
}
}
WorldRenderLastEvent.subscribe {
if (!TConfig.displaySouls) return@subscribe
renderBlocks(it.camera) {
color(1F, 1F, 0F, 0.8F)
currentMissingSouls.forEach {

View File

@@ -9,11 +9,11 @@ import io.github.cottonmc.cotton.gui.widget.data.HorizontalAlignment
import io.github.cottonmc.cotton.gui.widget.data.Insets
import io.github.cottonmc.cotton.gui.widget.data.VerticalAlignment
import moe.nea.notenoughupdates.NotEnoughUpdates
import moe.nea.notenoughupdates.util.config.ConfigHolder
import moe.nea.notenoughupdates.util.data.DataHolder
import net.minecraft.text.Text
import kotlin.reflect.KMutableProperty1
class ConfigGui<K>(val holder: ConfigHolder<K>, val build: ConfigGui<K>.() -> Unit) : LightweightGuiDescription() {
class ConfigGui<K>(val holder: DataHolder<K>, val build: ConfigGui<K>.() -> Unit) : LightweightGuiDescription() {
private val root = WGridPanelWithPadding(verticalPadding = 4)
private val reloadables = mutableListOf<(() -> Unit)>()
@@ -43,9 +43,9 @@ class ConfigGui<K>(val holder: ConfigHolder<K>, val build: ConfigGui<K>.() -> Un
fun toggle(text: Text, prop: KMutableProperty1<K, Boolean>) {
val toggle = WToggleButton(text)
reloadables.add { toggle.toggle = prop.get(holder.config) }
reloadables.add { toggle.toggle = prop.get(holder.data) }
toggle.setOnToggle {
prop.set(holder.config, true)
prop.set(holder.data, true)
holder.markDirty()
}
root.add(toggle, 5, col, 6, 1)
@@ -72,11 +72,11 @@ class ConfigGui<K>(val holder: ConfigHolder<K>, val build: ConfigGui<K>.() -> Un
val textfield = WTextField(background)
textfield.isEditable = true
reloadables.add {
textfield.text = prop.get(holder.config)
textfield.text = prop.get(holder.data)
}
textfield.maxLength = maxLength
textfield.setChangedListener {
prop.set(holder.config, it)
prop.set(holder.data, it)
holder.markDirty()
}
root.add(textfield, 5, col, 6, 11)

View File

@@ -27,9 +27,9 @@ fun repoGui(): ConfigGui<RepoManager.Config> {
Text.translatable("notenoughupdates.gui.repo.reset.label"),
Text.translatable("notenoughupdates.gui.repo.reset"),
) {
RepoManager.config.user = "NotEnoughUpdates"
RepoManager.config.repo = "NotEnoughUpdates-REPO"
RepoManager.config.branch = "dangerous"
RepoManager.data.user = "NotEnoughUpdates"
RepoManager.data.repo = "NotEnoughUpdates-REPO"
RepoManager.data.branch = "dangerous"
reload()
}
}

View File

@@ -50,7 +50,7 @@ object RepoDownloadManager {
private suspend fun requestLatestGithubSha(): String? {
val response =
NotEnoughUpdates.httpClient.get("https://api.github.com/repos/${RepoManager.config.user}/${RepoManager.config.repo}/commits/${RepoManager.config.branch}")
NotEnoughUpdates.httpClient.get("https://api.github.com/repos/${RepoManager.data.user}/${RepoManager.data.repo}/commits/${RepoManager.data.branch}")
if (response.status.value != 200) {
return null
}
@@ -77,7 +77,7 @@ object RepoDownloadManager {
}
val currentSha = loadSavedVersionHash()
if (latestSha != currentSha || force) {
val requestUrl = "https://github.com/${RepoManager.config.user}/${RepoManager.config.repo}/archive/$latestSha.zip"
val requestUrl = "https://github.com/${RepoManager.data.user}/${RepoManager.data.repo}/archive/$latestSha.zip"
logger.info("Planning to upgrade repository from $currentSha to $latestSha from $requestUrl")
val zipFile = downloadGithubArchive(requestUrl)
logger.info("Download repository zip file to $zipFile. Deleting old repository")

View File

@@ -9,13 +9,13 @@ import kotlinx.serialization.serializer
import moe.nea.notenoughupdates.NotEnoughUpdates
import moe.nea.notenoughupdates.NotEnoughUpdates.logger
import moe.nea.notenoughupdates.hud.ProgressBar
import moe.nea.notenoughupdates.util.config.ConfigHolder
import moe.nea.notenoughupdates.util.data.DataHolder
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents
import net.minecraft.client.MinecraftClient
import net.minecraft.network.packet.s2c.play.SynchronizeRecipesS2CPacket
import net.minecraft.text.Text
object RepoManager : ConfigHolder<RepoManager.Config>(serializer(), "repo", ::Config) {
object RepoManager : DataHolder<RepoManager.Config>(serializer(), "repo", ::Config) {
@Serializable
data class Config(
var user: String = "NotEnoughUpdates",
@@ -80,7 +80,7 @@ object RepoManager : ConfigHolder<RepoManager.Config>(serializer(), "repo", ::Co
}
fun initialize() {
if (config.autoUpdate) {
if (data.autoUpdate) {
launchAsyncUpdate()
} else {
reload()

View File

@@ -0,0 +1,206 @@
package moe.nea.notenoughupdates.util.config
import io.github.cottonmc.cotton.gui.client.CottonClientScreen
import io.github.cottonmc.cotton.gui.client.LibGui
import io.github.cottonmc.cotton.gui.client.LightweightGuiDescription
import io.github.cottonmc.cotton.gui.widget.WButton
import io.github.cottonmc.cotton.gui.widget.WLabel
import io.github.cottonmc.cotton.gui.widget.WToggleButton
import io.github.cottonmc.cotton.gui.widget.WWidget
import io.github.cottonmc.cotton.gui.widget.data.Insets
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.boolean
import kotlinx.serialization.json.jsonPrimitive
import kotlin.io.path.createDirectories
import kotlin.io.path.readText
import kotlin.io.path.writeText
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
import net.minecraft.client.MinecraftClient
import net.minecraft.text.Text
import moe.nea.notenoughupdates.NotEnoughUpdates
import moe.nea.notenoughupdates.gui.WGridPanelWithPadding
import moe.nea.notenoughupdates.util.ScreenUtil.setScreenLater
abstract class ManagedConfig(val name: String) {
class GuiAppender(val width: Int) {
private var row = 0
internal val panel = WGridPanelWithPadding(verticalPadding = 4, horizontalPadding = 4)
internal val reloadables = mutableListOf<(() -> Unit)>()
fun set(x: Int, y: Int, w: Int, h: Int, widget: WWidget) {
panel.add(widget, x, y, w, h)
}
fun onReload(reloadable: () -> Unit) {
reloadables.add(reloadable)
}
fun skipRows(r: Int) {
row += r
}
fun appendSplitRow(left: WWidget, right: WWidget) {
val lw = width / 2
set(0, row, lw, 1, left)
set(lw, row, width - lw, 1, right)
skipRows(1)
}
fun appendFullRow(widget: WWidget) {
set(0, row, width, 1, widget)
skipRows(1)
}
}
interface OptionHandler<T : Any> {
fun toJson(element: T): JsonElement?
fun fromJson(element: JsonElement): T
fun emitGuiElements(opt: Option<T>, guiAppender: GuiAppender)
}
inner class Option<T : Any> internal constructor(
val propertyName: String,
val default: () -> T,
val handler: OptionHandler<T>
) : ReadOnlyProperty<Any?, T> {
private lateinit var _value: T
private var loaded = false
var value: T
get() {
if (!loaded)
load()
return _value
}
set(value) {
loaded = true
_value = value
}
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
return value
}
private fun load() {
if (data.containsKey(propertyName)) {
try {
value = handler.fromJson(data[propertyName]!!)
} catch (e: Exception) {
NotEnoughUpdates.logger.error(
"Exception during loading of config file $name. This will reset this config.",
e
)
}
}
value = default()
}
internal fun toJson(): JsonElement? {
return handler.toJson(value)
}
fun appendToGui(guiapp: GuiAppender) {
handler.emitGuiElements(this, guiapp)
}
}
val file = NotEnoughUpdates.CONFIG_DIR.resolve("$name.json")
val data: JsonObject by lazy {
try {
NotEnoughUpdates.json.decodeFromString(
file.readText()
)
} catch (e: Exception) {
NotEnoughUpdates.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(NotEnoughUpdates.json.encodeToString(data))
}
val allOptions = mutableMapOf<String, Option<*>>()
val sortedOptions = mutableListOf<Option<*>>()
protected fun <T : Any> option(propertyName: String, default: () -> T, handler: OptionHandler<T>): Option<T> {
if (propertyName in allOptions) error("Cannot register the same name twice")
return Option(propertyName, default, handler).also {
allOptions[propertyName] = it
sortedOptions.add(it)
}
}
class BooleanHandler(val config: ManagedConfig) : OptionHandler<Boolean> {
override fun toJson(element: Boolean): JsonElement? {
return JsonPrimitive(element)
}
override fun fromJson(element: JsonElement): Boolean {
return element.jsonPrimitive.boolean
}
override fun emitGuiElements(opt: Option<Boolean>, guiAppender: GuiAppender) {
guiAppender.appendFullRow(
WToggleButton(Text.translatable("neu.config.${config.name}.${opt.propertyName}")).apply {
guiAppender.onReload { toggle = opt.value }
setOnToggle {
opt.value = it
config.save()
}
}
)
}
}
class ClickHandler(val config: ManagedConfig, val runnable: () -> Unit) : OptionHandler<Unit> {
override fun toJson(element: Unit): JsonElement? {
return null
}
override fun fromJson(element: JsonElement) {}
override fun emitGuiElements(opt: Option<Unit>, guiAppender: GuiAppender) {
guiAppender.appendSplitRow(
WLabel(Text.translatable("neu.config.${config.name}.${opt.propertyName}")),
WButton(Text.translatable("neu.config.${config.name}.${opt.propertyName}")).apply {
setOnClick {
runnable()
}
},
)
}
}
protected fun toggle(propertyName: String, default: () -> Boolean): Option<Boolean> {
return option(propertyName, default, BooleanHandler(this))
}
fun showConfigEditor() {
val lwgd = LightweightGuiDescription()
val guiapp = GuiAppender(20)
guiapp.panel.insets = Insets.ROOT_PANEL
sortedOptions.forEach { it.appendToGui(guiapp) }
guiapp.reloadables.forEach { it() }
lwgd.setRootPanel(guiapp.panel)
setScreenLater(CottonClientScreen(lwgd))
}
protected fun button(propertyName: String, runnable: () -> Unit): Option<Unit> {
return option(propertyName, { }, ClickHandler(this, runnable))
}
}

View File

@@ -1,4 +1,4 @@
package moe.nea.notenoughupdates.util.config
package moe.nea.notenoughupdates.util.data
import java.nio.file.Path
import kotlinx.serialization.KSerializer
@@ -7,19 +7,19 @@ import kotlin.io.path.readText
import kotlin.io.path.writeText
import moe.nea.notenoughupdates.NotEnoughUpdates
abstract class ConfigHolder<T>(
abstract class DataHolder<T>(
val serializer: KSerializer<T>,
val name: String,
val default: () -> T
) : IConfigHolder<T> {
) : IDataHolder<T> {
final override var config: T
final override var data: T
private set
init {
config = readValueOrDefault()
IConfigHolder.putConfig(this::class, this)
data = readValueOrDefault()
IDataHolder.putDataHolder(this::class, this)
}
private val file: Path get() = NotEnoughUpdates.CONFIG_DIR.resolve("$name.json")
@@ -32,7 +32,7 @@ abstract class ConfigHolder<T>(
file.readText()
)
} catch (e: Exception) {/* Expecting IOException and SerializationException, but Kotlin doesn't allow multi catches*/
IConfigHolder.badLoads.add(name)
IDataHolder.badLoads.add(name)
NotEnoughUpdates.logger.error(
"Exception during loading of config file $name. This will reset this config.",
e
@@ -46,15 +46,15 @@ abstract class ConfigHolder<T>(
}
override fun save() {
writeValue(config)
writeValue(data)
}
override fun load() {
config = readValueOrDefault()
data = readValueOrDefault()
}
override fun markDirty() {
IConfigHolder.markDirty(this::class)
IDataHolder.markDirty(this::class)
}
}

View File

@@ -1,4 +1,4 @@
package moe.nea.notenoughupdates.util.config
package moe.nea.notenoughupdates.util.data
import java.util.concurrent.CopyOnWriteArrayList
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents
@@ -9,17 +9,17 @@ import net.minecraft.text.Text
import moe.nea.notenoughupdates.NotEnoughUpdates
import moe.nea.notenoughupdates.events.ScreenOpenEvent
interface IConfigHolder<T> {
interface IDataHolder<T> {
companion object {
internal var badLoads: MutableList<String> = CopyOnWriteArrayList()
private val allConfigs: MutableMap<KClass<out IConfigHolder<*>>, IConfigHolder<*>> = mutableMapOf()
private val dirty: MutableSet<KClass<out IConfigHolder<*>>> = mutableSetOf()
private val allConfigs: MutableMap<KClass<out IDataHolder<*>>, IDataHolder<*>> = mutableMapOf()
private val dirty: MutableSet<KClass<out IDataHolder<*>>> = mutableSetOf()
internal fun <T : IConfigHolder<K>, K> putConfig(kClass: KClass<T>, inst: IConfigHolder<K>) {
internal fun <T : IDataHolder<K>, K> putDataHolder(kClass: KClass<T>, inst: IDataHolder<K>) {
allConfigs[kClass] = inst
}
fun <T : IConfigHolder<K>, K> markDirty(kClass: KClass<T>) {
fun <T : IDataHolder<K>, K> markDirty(kClass: KClass<T>) {
if (kClass !in allConfigs) {
NotEnoughUpdates.logger.error("Tried to markDirty '${kClass.qualifiedName}', which isn't registered as 'IConfigHolder'")
return
@@ -68,7 +68,7 @@ interface IConfigHolder<T> {
}
val config: T
val data: T
fun save()
fun markDirty()
fun load()

View File

@@ -1,4 +1,4 @@
package moe.nea.notenoughupdates.util.config
package moe.nea.notenoughupdates.util.data
import java.nio.file.Path
import kotlinx.serialization.KSerializer
@@ -13,15 +13,15 @@ import kotlin.io.path.writeText
import moe.nea.notenoughupdates.NotEnoughUpdates
import moe.nea.notenoughupdates.util.SBData
abstract class ProfileSpecificConfigHolder<S>(
private val configSerializer: KSerializer<S>,
abstract class ProfileSpecificDataHolder<S>(
private val dataSerializer: KSerializer<S>,
val configName: String,
private val configDefault: () -> S
) : IConfigHolder<S?> {
) : IDataHolder<S?> {
var allConfigs: MutableMap<String, S>
override val config: S?
override val data: S?
get() = SBData.profileCuteName?.let {
allConfigs.computeIfAbsent(it) { configDefault() }
}
@@ -29,10 +29,10 @@ abstract class ProfileSpecificConfigHolder<S>(
init {
allConfigs = readValues()
readValues()
IConfigHolder.putConfig(this::class, this)
IDataHolder.putDataHolder(this::class, this)
}
private val configDirectory: Path get() = NotEnoughUpdates.CONFIG_DIR.resolve("profiles")
private val configDirectory: Path get() = NotEnoughUpdates.CONFIG_DIR.resolve("profiles").resolve(configName)
private fun readValues(): MutableMap<String, S> {
if (!configDirectory.exists()) {
@@ -43,9 +43,9 @@ abstract class ProfileSpecificConfigHolder<S>(
.filter { it.extension == "json" }
.mapNotNull {
try {
it.nameWithoutExtension to NotEnoughUpdates.json.decodeFromString(configSerializer, it.readText())
it.nameWithoutExtension to NotEnoughUpdates.json.decodeFromString(dataSerializer, it.readText())
} catch (e: Exception) { /* Expecting IOException and SerializationException, but Kotlin doesn't allow multi catches*/
IConfigHolder.badLoads.add(configName)
IDataHolder.badLoads.add(configName)
NotEnoughUpdates.logger.error(
"Exception during loading of profile specific config file $it ($configName). This will reset that profiles config.",
e
@@ -67,12 +67,12 @@ abstract class ProfileSpecificConfigHolder<S>(
}
c.forEach { (name, value) ->
val f = configDirectory.resolve("$name.json")
f.writeText(NotEnoughUpdates.json.encodeToString(configSerializer, value))
f.writeText(NotEnoughUpdates.json.encodeToString(dataSerializer, value))
}
}
override fun markDirty() {
IConfigHolder.markDirty(this::class)
IDataHolder.markDirty(this::class)
}
override fun load() {