Common config gui

This commit is contained in:
nea
2023-05-24 02:29:20 +02:00
parent 5ff50799b6
commit 5984383d2c
19 changed files with 262 additions and 295 deletions

View File

@@ -14,10 +14,7 @@
- Recipe Viewer for Forge Recipes
- Fishing Helper for Fishing particles (currently not working if you sneak because of 1.19 messing up positioning)
- Fairy soul highlighter
Not working features:
- A config gui, lol. All features are sort of on all the time. There is a config gui, but it is quite barren and not well documented.
- Basic Config Gui (/firm config). Still needs improvement, but for the basics it's enough.
### Building your own

View File

@@ -1,17 +1,20 @@
package moe.nea.firmament.commands
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.firmament.features.world.FairySouls
import moe.nea.firmament.gui.repoGui
import moe.nea.firmament.gui.config.AllConfigsGui
import moe.nea.firmament.repo.RepoManager
import moe.nea.firmament.util.SBData
import moe.nea.firmament.util.ScreenUtil.setScreenLater
fun firmamentCommand() = literal("firmament") {
thenLiteral("config") {
thenExecute {
AllConfigsGui.showAllGuis()
}
}
thenLiteral("repo") {
thenLiteral("reload") {
thenLiteral("fetch") {
@@ -25,9 +28,6 @@ fun firmamentCommand() = literal("firmament") {
RepoManager.reload()
}
}
thenExecute {
setScreenLater(CottonClientScreen(repoGui()))
}
}
thenLiteral("dev") {
thenLiteral("config") {

View File

@@ -16,6 +16,8 @@ object FeatureManager : DataHolder<FeatureManager.Config>(serializer(), "feature
private val features = mutableMapOf<String, FirmamentFeature>()
val allFeatures: Collection<FirmamentFeature> get() = features.values
private var hasAutoloaded = false
init {

View File

@@ -1,6 +1,6 @@
package moe.nea.firmament.features
import moe.nea.firmament.util.config.ManagedConfig
import moe.nea.firmament.gui.config.ManagedConfig
interface FirmamentFeature {
val name: String

View File

@@ -16,7 +16,7 @@ import moe.nea.firmament.events.WorldRenderLastEvent
import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.TimeMark
import moe.nea.firmament.util.config.ManagedConfig
import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.util.render.RenderBlockContext.Companion.renderBlocks
object FishingWarning : FirmamentFeature {

View File

@@ -11,7 +11,7 @@ import moe.nea.firmament.repo.RepoManager
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.SBData
import moe.nea.firmament.util.blockPos
import moe.nea.firmament.util.config.ManagedConfig
import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.util.data.ProfileSpecificDataHolder
import moe.nea.firmament.util.render.RenderBlockContext.Companion.renderBlocks
import moe.nea.firmament.util.unformattedString

View File

@@ -1,94 +0,0 @@
package moe.nea.firmament.gui
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.WTextField
import io.github.cottonmc.cotton.gui.widget.WToggleButton
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.firmament.Firmament
import moe.nea.firmament.util.data.DataHolder
import net.minecraft.text.Text
import kotlin.reflect.KMutableProperty1
class ConfigGui<K>(val holder: DataHolder<K>, val build: ConfigGui<K>.() -> Unit) : LightweightGuiDescription() {
private val root = WGridPanelWithPadding(verticalPadding = 4)
private val reloadables = mutableListOf<(() -> Unit)>()
init {
setRootPanel(root)
root.insets = Insets.ROOT_PANEL
build()
reload()
}
fun title(text: Text) {
if (col != 0) {
Firmament.logger.warn("Set title not at the top of the ConfigGui")
}
val label = WLabel(text)
label.verticalAlignment = VerticalAlignment.TOP
label.horizontalAlignment = HorizontalAlignment.CENTER
root.add(label, 0, col, 11, 1)
col++
}
private fun label(text: Text) {
val label = WLabel(text)
label.verticalAlignment = VerticalAlignment.CENTER
root.add(label, 0, col, 5, 1)
}
fun toggle(text: Text, prop: KMutableProperty1<K, Boolean>) {
val toggle = WToggleButton(text)
reloadables.add { toggle.toggle = prop.get(holder.data) }
toggle.setOnToggle {
prop.set(holder.data, true)
holder.markDirty()
}
root.add(toggle, 5, col, 6, 1)
label(text)
col++
}
fun button(text: Text, buttonText: Text, runnable: () -> Unit) {
val button = WButton(buttonText)
button.setOnClick {
runnable.invoke()
}
root.add(button, 5, col, 6, 1)
label(text)
col++
}
fun textfield(
text: Text,
background: Text,
prop: KMutableProperty1<K, String>,
maxLength: Int = 255
) {
val textfield = WTextField(background)
textfield.isEditable = true
reloadables.add {
textfield.text = prop.get(holder.data)
}
textfield.maxLength = maxLength
textfield.setChangedListener {
prop.set(holder.data, it)
holder.markDirty()
}
root.add(textfield, 5, col, 6, 11)
label(text)
col++
}
fun reload() {
reloadables.forEach { it.invoke() }
}
private var col = 0
}

View File

@@ -1,33 +0,0 @@
package moe.nea.firmament.gui
import io.github.cottonmc.cotton.gui.widget.WPanelWithInsets
import io.github.cottonmc.cotton.gui.widget.WWidget
import io.github.cottonmc.cotton.gui.widget.data.Insets
class WGridPanelWithPadding(
val grid: Int = 18,
val verticalPadding: Int = 0,
val horizontalPadding: Int = 0,
) : WPanelWithInsets() {
private inline val vertOffset get() = grid + verticalPadding
private inline val horiOffset get() = grid + horizontalPadding
fun add(w: WWidget, x: Int, y: Int, width: Int = 1, height: Int = 1) {
children.add(w)
w.parent = this
w.setLocation(x * horiOffset + insets.left, y * vertOffset + insets.top)
if (w.canResize())
w.setSize(
grid + (horiOffset * (width - 1)),
grid + (vertOffset * (height - 1)),
)
expandToFit(w, insets)
}
override fun setInsets(insets: Insets): WGridPanelWithPadding {
super.setInsets(insets)
return this
}
}

View File

@@ -0,0 +1,42 @@
package moe.nea.firmament.gui.config
import io.github.cottonmc.cotton.gui.client.BackgroundPainter
import io.github.cottonmc.cotton.gui.client.CottonClientScreen
import io.github.cottonmc.cotton.gui.client.LightweightGuiDescription
import io.github.cottonmc.cotton.gui.widget.WButton
import io.github.cottonmc.cotton.gui.widget.WGridPanel
import io.github.cottonmc.cotton.gui.widget.WLabel
import io.github.cottonmc.cotton.gui.widget.WListPanel
import io.github.cottonmc.cotton.gui.widget.data.Insets
import net.minecraft.text.Text
import moe.nea.firmament.features.FeatureManager
import moe.nea.firmament.repo.RepoManager
import moe.nea.firmament.util.ScreenUtil.setScreenLater
object AllConfigsGui {
fun showAllGuis() {
val lwgd = LightweightGuiDescription()
var screen: CottonClientScreen? = null
lwgd.setRootPanel(WListPanel(
listOf(
RepoManager.Config
) + FeatureManager.allFeatures.mapNotNull { it.config }, ::WGridPanel
) { config, panel ->
panel.insets = Insets.ROOT_PANEL
panel.backgroundPainter = BackgroundPainter.VANILLA
panel.add(WLabel(Text.translatable("firmament.config.${config.name}")), 0, 0, 10, 1)
panel.add(WButton(Text.translatable("firmanent.config.edit")).also {
it.setOnClick {
config.showConfigEditor(screen)
}
}, 0, 1, 10, 1)
println("Panel size: ${panel.width} ${panel.height}")
}.also {
it.setListItemHeight(52)
it.setSize(10 * 18 + 14 + 16, 300)
})
screen = CottonClientScreen(lwgd)
setScreenLater(screen)
}
}

View File

@@ -0,0 +1,33 @@
package moe.nea.firmament.gui.config
import io.github.cottonmc.cotton.gui.widget.WLabel
import io.github.cottonmc.cotton.gui.widget.WToggleButton
import io.github.cottonmc.cotton.gui.widget.data.VerticalAlignment
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.boolean
import kotlinx.serialization.json.jsonPrimitive
import net.minecraft.text.Text
class BooleanHandler(val config: ManagedConfig) : 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: ManagedConfig.Option<Boolean>, guiAppender: GuiAppender) {
guiAppender.appendLabeledRow(
opt.labelText,
WToggleButton(opt.labelText).apply {
guiAppender.onReload { toggle = opt.value }
setOnToggle {
opt.value = it
config.save()
}
}
)
}
}

View File

@@ -0,0 +1,25 @@
package moe.nea.firmament.gui.config
import io.github.cottonmc.cotton.gui.widget.WButton
import io.github.cottonmc.cotton.gui.widget.WLabel
import kotlinx.serialization.json.JsonElement
import net.minecraft.text.Text
class ClickHandler(val config: ManagedConfig, val runnable: () -> Unit) : ManagedConfig.OptionHandler<Unit> {
override fun toJson(element: Unit): JsonElement? {
return null
}
override fun fromJson(element: JsonElement) {}
override fun emitGuiElements(opt: ManagedConfig.Option<Unit>, guiAppender: GuiAppender) {
guiAppender.appendLabeledRow(
Text.translatable("firmament.config.${config.name}.${opt.propertyName}"),
WButton(Text.translatable("firmament.config.${config.name}.${opt.propertyName}")).apply {
setOnClick {
runnable()
}
},
)
}
}

View File

@@ -0,0 +1,44 @@
package moe.nea.firmament.gui.config
import io.github.cottonmc.cotton.gui.widget.WGridPanel
import io.github.cottonmc.cotton.gui.widget.WLabel
import io.github.cottonmc.cotton.gui.widget.WWidget
import io.github.cottonmc.cotton.gui.widget.data.VerticalAlignment
import net.minecraft.text.Text
class GuiAppender(val width: Int) {
private var row = 0
internal val panel = WGridPanel().also { it.setGaps(4, 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 appendLabeledRow(label: Text, right: WWidget) {
appendSplitRow(
WLabel(label).setVerticalAlignment(VerticalAlignment.CENTER),
right
)
}
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)
}
}

View File

@@ -1,61 +1,25 @@
package moe.nea.firmament.util.config
package moe.nea.firmament.gui.config
import io.github.cottonmc.cotton.gui.client.CottonClientScreen
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.properties.ReadWriteProperty
import kotlin.reflect.KProperty
import net.minecraft.client.gui.screen.Screen
import net.minecraft.text.Text
import moe.nea.firmament.Firmament
import moe.nea.firmament.gui.WGridPanelWithPadding
import moe.nea.firmament.util.MC
import moe.nea.firmament.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
@@ -63,10 +27,14 @@ abstract class ManagedConfig(val name: String) {
}
inner class Option<T : Any> internal constructor(
val config: ManagedConfig,
val propertyName: String,
val default: () -> T,
val handler: OptionHandler<T>
) : ReadOnlyProperty<Any?, T> {
) : ReadWriteProperty<Any?, T> {
val rawLabelText = "firmament.config.${config.name}.${propertyName}"
val labelText = Text.translatable(rawLabelText)
private lateinit var _value: T
private var loaded = false
@@ -81,6 +49,10 @@ abstract class ManagedConfig(val name: String) {
_value = value
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
this.value = value
}
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
return value
}
@@ -134,71 +106,48 @@ abstract class ManagedConfig(val name: String) {
val allOptions = mutableMapOf<String, Option<*>>()
val sortedOptions = mutableListOf<Option<*>>()
private var latestGuiAppender: GuiAppender? = null
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 {
return Option(this, 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("firmament.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("firmament.config.${config.name}.${opt.propertyName}")),
WButton(Text.translatable("firmament.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))
}
protected fun string(propertyName: String, default: () -> String): Option<String> {
return option(propertyName, default, StringHandler(this))
}
fun reloadGui() {
latestGuiAppender?.reloadables?.forEach {it() }
}
fun showConfigEditor(parent: Screen? = null) {
val lwgd = LightweightGuiDescription()
val guiapp = GuiAppender(20)
latestGuiAppender = guiapp
guiapp.panel.insets = Insets.ROOT_PANEL
sortedOptions.forEach { it.appendToGui(guiapp) }
guiapp.reloadables.forEach { it() }
lwgd.setRootPanel(guiapp.panel)
setScreenLater(object : CottonClientScreen(lwgd) {
override fun close() {
latestGuiAppender = null
MC.screen = parent
}
})
}
}

View File

@@ -0,0 +1,32 @@
package moe.nea.firmament.gui.config
import io.github.cottonmc.cotton.gui.widget.WLabel
import io.github.cottonmc.cotton.gui.widget.WTextField
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.jsonPrimitive
import net.minecraft.text.Text
class StringHandler(val config: ManagedConfig) : ManagedConfig.OptionHandler<String> {
override fun toJson(element: String): JsonElement? {
return JsonPrimitive(element)
}
override fun fromJson(element: JsonElement): String {
return element.jsonPrimitive.toString()
}
override fun emitGuiElements(opt: ManagedConfig.Option<String>, guiAppender: GuiAppender) {
guiAppender.appendLabeledRow(
opt.labelText,
WTextField(opt.labelText).apply {
suggestion = Text.translatableWithFallback(opt.rawLabelText + ".hint", "")
guiAppender.onReload { text = opt.value }
setChangedListener {
opt.value = it
config.save()
}
}
)
}
}

View File

@@ -1,36 +0,0 @@
package moe.nea.firmament.gui
import net.minecraft.text.Text
import moe.nea.firmament.repo.RepoManager
fun repoGui(): ConfigGui<RepoManager.Config> {
return ConfigGui(RepoManager) {
title(Text.translatable("firmament.gui.repo.title"))
toggle(Text.translatable("firmament.gui.repo.autoupdate"), RepoManager.Config::autoUpdate)
textfield(
Text.translatable("firmament.gui.repo.username"),
Text.translatable("firmament.gui.repo.hint.username"),
RepoManager.Config::user,
maxLength = 255
)
textfield(
Text.translatable("firmament.gui.repo.reponame"),
Text.translatable("firmament.gui.repo.hint.reponame"),
RepoManager.Config::repo
)
textfield(
Text.translatable("firmament.gui.repo.branch"),
Text.translatable("firmament.gui.repo.hint.branch"),
RepoManager.Config::branch
)
button(
Text.translatable("firmament.gui.repo.reset.label"),
Text.translatable("firmament.gui.repo.reset"),
) {
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 =
Firmament.httpClient.get("https://api.github.com/repos/${RepoManager.data.user}/${RepoManager.data.repo}/commits/${RepoManager.data.branch}")
Firmament.httpClient.get("https://api.github.com/repos/${RepoManager.Config.username}/${RepoManager.Config.reponame}/commits/${RepoManager.Config.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.data.user}/${RepoManager.data.repo}/archive/$latestSha.zip"
val requestUrl = "https://github.com/${RepoManager.Config.username}/${RepoManager.Config.reponame}/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

@@ -8,8 +8,6 @@ import io.github.moulberry.repo.data.NEUItem
import io.github.moulberry.repo.data.NEURecipe
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import kotlinx.serialization.serializer
import net.minecraft.client.MinecraftClient
import net.minecraft.network.packet.s2c.play.SynchronizeRecipesS2CPacket
import net.minecraft.text.Text
@@ -17,16 +15,21 @@ import moe.nea.firmament.Firmament
import moe.nea.firmament.Firmament.logger
import moe.nea.firmament.hud.ProgressBar
import moe.nea.firmament.util.SkyblockId
import moe.nea.firmament.util.data.DataHolder
import moe.nea.firmament.gui.config.ManagedConfig
object RepoManager : DataHolder<RepoManager.Config>(serializer(), "repo", ::Config) {
@Serializable
data class Config(
var user: String = "NotEnoughUpdates",
var repo: String = "NotEnoughUpdates-REPO",
var autoUpdate: Boolean = true,
var branch: String = "dangerous",
)
object RepoManager {
object Config : ManagedConfig("repo") {
var username by string("username") { "NotEnoughUpdates" }
var reponame by string("reponame") { "NotEnoughUpdates-REPO" }
var branch by string("branch") { "prerelease" }
val autoUpdate by toggle("autoUpdate") { true }
val reset by button("reset") {
username = "NotEnoughUpdates"
reponame = "NotEnoughUpdates-REPO"
branch = "prerelease"
save()
}
}
val currentDownloadedSha by RepoDownloadManager::latestSavedVersionHash
@@ -93,7 +96,7 @@ object RepoManager : DataHolder<RepoManager.Config>(serializer(), "repo", ::Conf
}
fun initialize() {
if (data.autoUpdate) {
if (Config.autoUpdate) {
launchAsyncUpdate()
} else {
reload()

View File

@@ -4,13 +4,14 @@ import io.github.moulberry.repo.data.Coordinate
import net.minecraft.client.MinecraftClient
import net.minecraft.client.gui.screen.ingame.HandledScreen
import net.minecraft.util.math.BlockPos
import moe.nea.firmament.mixins.accessor.AccessorHandledScreen
object MC {
inline val soundManager get() = MinecraftClient.getInstance().soundManager
inline val player get() = MinecraftClient.getInstance().player
inline val world get() = MinecraftClient.getInstance().world
inline val screen get() = MinecraftClient.getInstance().currentScreen
inline var screen
get() = MinecraftClient.getInstance().currentScreen
set(value) = MinecraftClient.getInstance().setScreen(value)
inline val handledScreen: HandledScreen<*>? get() = MinecraftClient.getInstance().currentScreen as? HandledScreen<*>
}

View File

@@ -3,24 +3,26 @@
"firmament.repo.reload.disk": "Reloading repository from disk. This may lag a bit.",
"firmament.repo.cache": "Recaching items",
"firmament.repo.brokenitem": "Failed to render item: %s",
"firmament.gui.repo.title": "firmament Repo Settings",
"firmament.gui.repo.autoupdate": "Auto Update",
"firmament.gui.repo.username": "Repo Username",
"firmament.gui.repo.hint.username": "NotEnoughUpdates",
"firmament.gui.repo.reponame": "Repo Name",
"firmament.gui.repo.hint.reponame": "NotEnoughUpdates-REPO",
"firmament.gui.repo.branch": "Repo Branch",
"firmament.gui.repo.hint.branch": "dangerous",
"firmament.gui.repo.reset": "Reset",
"firmament.gui.repo.reset.label": "Reset to Defaults",
"firmanent.config.edit": "Edit",
"firmament.config.repo": "Firmament Repo Settings",
"firmament.config.repo.autoUpdate": "Auto Update",
"firmament.config.repo.username": "Repo Username",
"firmament.config.repo.username.hint": "NotEnoughUpdates",
"firmament.config.repo.reponame": "Repo Name",
"firmament.config.repo.reponame.hint": "NotEnoughUpdates-REPO",
"firmament.config.repo.branch": "Repo Branch",
"firmament.config.repo.branch.hint": "dangerous",
"firmament.config.repo.reset": "Reset",
"firmament.sbinfo.nolocraw": "No locraw data available",
"firmament.sbinfo.profile": "Current profile cutename: %s",
"firmament.sbinfo.server": "Locraw Server: %s",
"firmament.sbinfo.gametype": "Locraw Gametype: %s",
"firmament.sbinfo.mode": "Locraw Mode: %s",
"firmament.sbinfo.map": "Locraw Map: %s",
"firmament.config.fairy-souls": "Fairy Souls",
"firmament.config.fairy-souls.show": "Show Fairy Soul Waypoints",
"firmament.config.fairy-souls.reset": "Reset Collected Fairy Souls",
"firmament.config.fishing-warning": "Fishing Warning",
"firmament.config.fishing-warning.display-warning": "Display a warning when you are about to hook a fish",
"firmament.config.fishing-warning.highlight-wake-chain": "Highlight fishing particles",
"firmament.key.slotlocking": "Lock Slot / Slot Binding",