From b774daef5bd961f955d365ce07bd5aa4acb161f4 Mon Sep 17 00:00:00 2001 From: Linnea Gräf Date: Tue, 12 Nov 2024 17:02:08 +0100 Subject: feat: MoulConfig config gui --- .../moulconfig/java/MCConfigEditorIntegration.kt | 337 +++++++++++++++++++++ .../moulconfig/java/ProcessedCategoryFirm.kt | 47 +++ .../moulconfig/java/ProcessedEditableOptionFirm.kt | 27 ++ src/compat/moulconfig/java/ProcessedOptionFirm.kt | 39 +++ src/main/kotlin/gui/config/AllConfigsGui.kt | 8 +- .../gui/config/FirmamentConfigScreenProvider.kt | 17 +- src/main/kotlin/gui/config/KeyBindingHandler.kt | 8 +- src/main/kotlin/gui/config/ManagedConfig.kt | 2 +- src/main/kotlin/util/MoulConfigUtils.kt | 13 + src/test/kotlin/root.kt | 2 +- .../testdata/chat/sacks/gain-and-lose-regular.snbt | 101 ++++++ .../testdata/chat/sacks/gain-rotten-flesh.snbt | 101 ++++++ 12 files changed, 683 insertions(+), 19 deletions(-) create mode 100644 src/compat/moulconfig/java/MCConfigEditorIntegration.kt create mode 100644 src/compat/moulconfig/java/ProcessedCategoryFirm.kt create mode 100644 src/compat/moulconfig/java/ProcessedEditableOptionFirm.kt create mode 100644 src/compat/moulconfig/java/ProcessedOptionFirm.kt create mode 100644 src/test/resources/testdata/chat/sacks/gain-and-lose-regular.snbt create mode 100644 src/test/resources/testdata/chat/sacks/gain-rotten-flesh.snbt (limited to 'src') diff --git a/src/compat/moulconfig/java/MCConfigEditorIntegration.kt b/src/compat/moulconfig/java/MCConfigEditorIntegration.kt new file mode 100644 index 0000000..7686beb --- /dev/null +++ b/src/compat/moulconfig/java/MCConfigEditorIntegration.kt @@ -0,0 +1,337 @@ +package moe.nea.firmament.compat.moulconfig + +import com.google.auto.service.AutoService +import io.github.notenoughupdates.moulconfig.Config +import io.github.notenoughupdates.moulconfig.common.IMinecraft +import io.github.notenoughupdates.moulconfig.gui.GuiComponent +import io.github.notenoughupdates.moulconfig.gui.GuiElementWrapper +import io.github.notenoughupdates.moulconfig.gui.GuiOptionEditor +import io.github.notenoughupdates.moulconfig.gui.HorizontalAlign +import io.github.notenoughupdates.moulconfig.gui.MoulConfigEditor +import io.github.notenoughupdates.moulconfig.gui.VerticalAlign +import io.github.notenoughupdates.moulconfig.gui.component.AlignComponent +import io.github.notenoughupdates.moulconfig.gui.component.RowComponent +import io.github.notenoughupdates.moulconfig.gui.component.SliderComponent +import io.github.notenoughupdates.moulconfig.gui.component.TextComponent +import io.github.notenoughupdates.moulconfig.gui.editors.ComponentEditor +import io.github.notenoughupdates.moulconfig.gui.editors.GuiOptionEditorAccordion +import io.github.notenoughupdates.moulconfig.gui.editors.GuiOptionEditorBoolean +import io.github.notenoughupdates.moulconfig.gui.editors.GuiOptionEditorButton +import io.github.notenoughupdates.moulconfig.gui.editors.GuiOptionEditorText +import io.github.notenoughupdates.moulconfig.observer.GetSetter +import io.github.notenoughupdates.moulconfig.processor.ProcessedCategory +import io.github.notenoughupdates.moulconfig.processor.ProcessedOption +import java.lang.reflect.Type +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds +import kotlin.time.DurationUnit +import net.minecraft.client.gui.screen.Screen +import moe.nea.firmament.gui.config.BooleanHandler +import moe.nea.firmament.gui.config.ClickHandler +import moe.nea.firmament.gui.config.DurationHandler +import moe.nea.firmament.gui.config.FirmamentConfigScreenProvider +import moe.nea.firmament.gui.config.HudMeta +import moe.nea.firmament.gui.config.HudMetaHandler +import moe.nea.firmament.gui.config.IntegerHandler +import moe.nea.firmament.gui.config.KeyBindingHandler +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.gui.config.ManagedOption +import moe.nea.firmament.gui.config.StringHandler +import moe.nea.firmament.keybindings.SavedKeyBinding +import moe.nea.firmament.util.ErrorUtil +import moe.nea.firmament.util.FirmFormatters +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.MoulConfigUtils.xmap + +@AutoService(FirmamentConfigScreenProvider::class) +class MCConfigEditorIntegration : FirmamentConfigScreenProvider { + override val key: String + get() = "moulconfig" + + val handlers: MutableMap>, ((ManagedConfig.OptionHandler<*>, ManagedOption<*>, accordionId: Int, configObject: Config) -> ProcessedEditableOptionFirm<*>)> = + mutableMapOf() + + fun > register( + handlerClass: Class, + transform: (H, ManagedOption, accordionId: Int, configObject: Config) -> ProcessedEditableOptionFirm + ) { + handlers[handlerClass] = + transform as ((ManagedConfig.OptionHandler<*>, ManagedOption<*>, accordionId: Int, configObject: Config) -> ProcessedEditableOptionFirm<*>) + } + + fun getHandler( + option: ManagedOption, + accordionId: Int, + configObject: Config + ): ProcessedEditableOptionFirm<*> { + val transform = handlers[option.handler.javaClass] + ?: error("Could not transform ${option.handler}") // TODO: replace with soft error and an error config element + return transform.invoke(option.handler, option, accordionId, configObject) as ProcessedEditableOptionFirm + } + + class CustomSliderEditor( + option: ProcessedOption, + setter: GetSetter, + fromT: (T) -> Float, + toT: (Float) -> T, + minValue: T, maxValue: T, + minStep: Float, + formatter: (T) -> String, + ) : ComponentEditor(option) { + override fun getDelegate(): GuiComponent { + return delegateI + } + + val mappedSetter = setter.xmap(fromT, toT) + + private val delegateI by lazy { + wrapComponent(RowComponent( + AlignComponent( + TextComponent( + IMinecraft.instance.defaultFontRenderer, + { formatter(setter.get()) }, + 25, + TextComponent.TextAlignment.CENTER, false, false + ), + GetSetter.constant(HorizontalAlign.CENTER), + GetSetter.constant(VerticalAlign.CENTER) + ), + SliderComponent( + mappedSetter, + fromT(minValue), + fromT(maxValue), + minStep, + 40 + ) + )) + } + } + + init { + register(BooleanHandler::class.java) { handler, option, categoryAccordionId, configObject -> + object : ProcessedEditableOptionFirm(option, categoryAccordionId, configObject) { + override fun createEditor(): GuiOptionEditor { + return GuiOptionEditorBoolean(this, -1, configObject) + } + + override fun get(): Any { + return managedOption.value + } + + override fun getType(): Type { + return Boolean::class.java + } + + override fun set(value: Any?): Boolean { + managedOption.value = value as Boolean + return true + } + } + } + register(StringHandler::class.java) { handler, option, categoryAccordionId, configObject -> + object : ProcessedEditableOptionFirm(option, categoryAccordionId, configObject) { + override fun createEditor(): GuiOptionEditor { + return GuiOptionEditorText(this) + } + + override fun get(): Any { + return managedOption.value + } + + override fun getType(): Type { + return String::class.java + } + + override fun set(value: Any?): Boolean { + managedOption.value = value as String + return true + } + } + } + register(ClickHandler::class.java) { handler, option, categoryAccordionId, configObject -> + object : ProcessedEditableOptionFirm(option, categoryAccordionId, configObject) { + override fun createEditor(): GuiOptionEditor { + return GuiOptionEditorButton(this, -1, "Click", configObject) + } + + override fun get(): Any { + return Runnable { handler.runnable() } + } + + override fun getType(): Type { + return Runnable::class.java + } + + override fun set(value: Any?): Boolean { + ErrorUtil.softError("Trying to set a buttons data") + return false + } + } + } + register(HudMetaHandler::class.java) { handler, option, categoryAccordionId, configObject -> + object : ProcessedEditableOptionFirm(option, categoryAccordionId, configObject) { + override fun createEditor(): GuiOptionEditor { + return GuiOptionEditorButton(this, -1, "Edit HUD", configObject) + } + + override fun get(): Any { + return Runnable { + handler.openEditor(option, MC.screen!!) + } + } + + override fun getType(): Type { + return Runnable::class.java + } + + override fun set(value: Any?): Boolean { + ErrorUtil.softError("Trying to assign to a hud meta") + return false + } + } + } + register(DurationHandler::class.java) { handler, option, categoryAccordionId, configObject -> + object : ProcessedEditableOptionFirm(option, categoryAccordionId, configObject) { + override fun createEditor(): GuiOptionEditor { + return CustomSliderEditor( + this, + option, + { it.toDouble(DurationUnit.SECONDS).toFloat() }, + { it.toDouble().seconds }, + handler.min, + handler.max, + 0.1F, + FirmFormatters::formatTimespan + ) + } + + override fun get(): Any { + ErrorUtil.softError("Getting on a slider component") + return Unit + } + + override fun getType(): Type { + return Nothing::class.java + } + + override fun set(value: Any?): Boolean { + ErrorUtil.softError("Setting on a slider component") + return false + } + } + } + register(IntegerHandler::class.java) { handler, option, categoryAccordionId, configObject -> + object : ProcessedEditableOptionFirm(option, categoryAccordionId, configObject) { + override fun createEditor(): GuiOptionEditor { + return CustomSliderEditor( + this, + option, + { it.toFloat() }, + { it.toInt() }, + handler.min, + handler.max, + 1F, + Integer::toString + ) + } + + override fun get(): Any { + ErrorUtil.softError("Getting on a slider component") + return Unit + } + + override fun getType(): Type { + return Nothing::class.java + } + + override fun set(value: Any?): Boolean { + ErrorUtil.softError("Setting on a slider component") + return false + } + } + } + register(KeyBindingHandler::class.java) { handler, option, categoryAccordionId, configObject -> + object : ProcessedEditableOptionFirm(option, categoryAccordionId, configObject) { + override fun createEditor(): GuiOptionEditor { + return object : ComponentEditor(this) { + val button = wrapComponent(handler.createButtonComponent(option)) + override fun getDelegate(): GuiComponent { + return button + } + } + } + + override fun get(): Any { + ErrorUtil.softError("Getting on a keybinding") + return Unit + } + + override fun getType(): Type { + return Nothing::class.java + } + + override fun set(value: Any?): Boolean { + ErrorUtil.softError("Setting on a keybinding") + return false + } + } + } + } + + override fun open(parent: Screen?): Screen { + val configObject = object : Config() { + override fun saveNow() { + ManagedConfig.allManagedConfigs.getAll().forEach { it.save() } + } + + override fun shouldAutoFocusSearchbar(): Boolean { + return true + } + } + val categories = ManagedConfig.Category.entries.map { + val options = mutableListOf() + var nextAccordionId = 720 + it.configs.forEach { config -> + val categoryAccordionId = nextAccordionId++ + options.add(object : ProcessedOptionFirm(-1, configObject) { + override fun getDebugDeclarationLocation(): String { + return "FirmamentConfig:$config.name" + } + + override fun getName(): String { + return config.labelText.string + } + + override fun getDescription(): String { + return "Missing description" + } + + override fun get(): Any { + return Unit + } + + override fun getType(): Type { + return Unit.javaClass + } + + override fun set(value: Any?): Boolean { + return false + } + + override fun createEditor(): GuiOptionEditor { + return GuiOptionEditorAccordion(this, categoryAccordionId) + } + }) + config.allOptions.forEach { (key, option) -> + val processedOption = getHandler(option, categoryAccordionId, configObject) + options.add(processedOption) + } + } + + return@map ProcessedCategoryFirm(it, options) + } + val editor = MoulConfigEditor(ProcessedCategory.collect(categories), configObject) + return GuiElementWrapper(editor) // TODO : add parent support + } + +} diff --git a/src/compat/moulconfig/java/ProcessedCategoryFirm.kt b/src/compat/moulconfig/java/ProcessedCategoryFirm.kt new file mode 100644 index 0000000..19e1112 --- /dev/null +++ b/src/compat/moulconfig/java/ProcessedCategoryFirm.kt @@ -0,0 +1,47 @@ +package moe.nea.firmament.compat.moulconfig + +import io.github.notenoughupdates.moulconfig.gui.editors.GuiOptionEditorAccordion +import io.github.notenoughupdates.moulconfig.processor.ProcessedCategory +import io.github.notenoughupdates.moulconfig.processor.ProcessedOption +import moe.nea.firmament.gui.config.ManagedConfig + +class ProcessedCategoryFirm( + val category: ManagedConfig.Category, + private val options: List +) : ProcessedCategory { + val accordions = options.filter { it.editor is GuiOptionEditorAccordion } + .associateBy { (it.editor as GuiOptionEditorAccordion).accordionId } + init { + for (option in options) { + option.category = this + } + } + + override fun getDebugDeclarationLocation(): String? { + return "FirmamentCategory.${category.name}" + } + + override fun getDisplayName(): String { + return category.labelText.string + } + + override fun getDescription(): String { + return "Missing description" // TODO: add description + } + + override fun getIdentifier(): String { + return category.name + } + + override fun getParentCategoryId(): String? { + return null + } + + override fun getOptions(): List { + return options + } + + override fun getAccordionAnchors(): Map { + return accordions + } +} diff --git a/src/compat/moulconfig/java/ProcessedEditableOptionFirm.kt b/src/compat/moulconfig/java/ProcessedEditableOptionFirm.kt new file mode 100644 index 0000000..c42ad3f --- /dev/null +++ b/src/compat/moulconfig/java/ProcessedEditableOptionFirm.kt @@ -0,0 +1,27 @@ +package moe.nea.firmament.compat.moulconfig + +import io.github.notenoughupdates.moulconfig.Config +import moe.nea.firmament.gui.config.ManagedOption + +abstract class ProcessedEditableOptionFirm( + val managedOption: ManagedOption, + categoryAccordionId: Int, + configObject: Config, +) : ProcessedOptionFirm(categoryAccordionId, configObject) { + val managedConfig = managedOption.element + override fun getDebugDeclarationLocation(): String { + return "FirmamentOption:${managedConfig.name}:${managedOption.propertyName}" + } + + override fun getName(): String { + return managedOption.labelText.string + } + + override fun getDescription(): String { + return "Missing description" // TODO: add description + } + + override fun explicitNotifyChange() { + managedConfig.save() + } +} diff --git a/src/compat/moulconfig/java/ProcessedOptionFirm.kt b/src/compat/moulconfig/java/ProcessedOptionFirm.kt new file mode 100644 index 0000000..4d0096c --- /dev/null +++ b/src/compat/moulconfig/java/ProcessedOptionFirm.kt @@ -0,0 +1,39 @@ +package moe.nea.firmament.compat.moulconfig + +import io.github.notenoughupdates.moulconfig.Config +import io.github.notenoughupdates.moulconfig.annotations.SearchTag +import io.github.notenoughupdates.moulconfig.gui.GuiOptionEditor +import io.github.notenoughupdates.moulconfig.processor.ProcessedCategory +import io.github.notenoughupdates.moulconfig.processor.ProcessedOption + +abstract class ProcessedOptionFirm( + private val accordionId: Int, + private val config: Config +) : ProcessedOption { + lateinit var category: ProcessedCategoryFirm + override fun getAccordionId(): Int { + return accordionId + } + + protected abstract fun createEditor(): GuiOptionEditor + val editorInstance by lazy { createEditor() } + + override fun getSearchTags(): Array { + return emptyArray() + } + + override fun getEditor(): GuiOptionEditor { + return editorInstance + } + + override fun getCategory(): ProcessedCategory { + return category + } + + override fun getConfig(): Config { + return config + } + + override fun explicitNotifyChange() { + } +} diff --git a/src/main/kotlin/gui/config/AllConfigsGui.kt b/src/main/kotlin/gui/config/AllConfigsGui.kt index 0e68bc8..16eb0a4 100644 --- a/src/main/kotlin/gui/config/AllConfigsGui.kt +++ b/src/main/kotlin/gui/config/AllConfigsGui.kt @@ -4,7 +4,6 @@ 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 moe.nea.firmament.repo.RepoManager import moe.nea.firmament.util.MC import moe.nea.firmament.util.MoulConfigUtils import moe.nea.firmament.util.ScreenUtil.setScreenLater @@ -18,6 +17,7 @@ object AllConfigsGui { object ConfigConfig : ManagedConfig("configconfig", Category.META) { val enableYacl by toggle("enable-yacl") { false } + val enableMoulConfig by toggle("enable-moulconfig") { false } } fun List.toObservableList(): ObservableList = ObservableList(this) @@ -67,7 +67,11 @@ object AllConfigsGui { } fun makeScreen(parent: Screen? = null): Screen { - val wantedKey = if (ConfigConfig.enableYacl) "yacl" else "builtin" + val wantedKey = when { + ConfigConfig.enableMoulConfig -> "moulconfig" + ConfigConfig.enableYacl -> "yacl" + else -> "builtin" + } val provider = FirmamentConfigScreenProvider.providers.find { it.key == wantedKey } ?: FirmamentConfigScreenProvider.providers.first() return provider.open(parent) diff --git a/src/main/kotlin/gui/config/FirmamentConfigScreenProvider.kt b/src/main/kotlin/gui/config/FirmamentConfigScreenProvider.kt index 82e151e..5aaa5fa 100644 --- a/src/main/kotlin/gui/config/FirmamentConfigScreenProvider.kt +++ b/src/main/kotlin/gui/config/FirmamentConfigScreenProvider.kt @@ -1,9 +1,7 @@ package moe.nea.firmament.gui.config -import java.util.ServiceLoader -import kotlin.streams.asSequence import net.minecraft.client.gui.screen.Screen -import moe.nea.firmament.Firmament +import moe.nea.firmament.util.compatloader.CompatLoader interface FirmamentConfigScreenProvider { val key: String @@ -11,17 +9,10 @@ interface FirmamentConfigScreenProvider { fun open(parent: Screen?): Screen - companion object { - private val loader = ServiceLoader.load(FirmamentConfigScreenProvider::class.java) - + companion object : CompatLoader(FirmamentConfigScreenProvider::class) { val providers by lazy { - loader.stream().asSequence().mapNotNull { service -> - kotlin.runCatching { service.get() } - .getOrElse { - Firmament.logger.warn("Could not load config provider ${service.type()}", it) - null - } - }.filter { it.isEnabled } + allValidInstances + .filter { it.isEnabled } .sortedWith(Comparator.comparing( { it.key }, Comparator { left, right -> diff --git a/src/main/kotlin/gui/config/KeyBindingHandler.kt b/src/main/kotlin/gui/config/KeyBindingHandler.kt index 7ec7e81..0cf0c46 100644 --- a/src/main/kotlin/gui/config/KeyBindingHandler.kt +++ b/src/main/kotlin/gui/config/KeyBindingHandler.kt @@ -30,7 +30,7 @@ class KeyBindingHandler(val name: String, val managedConfig: ManagedConfig) : return Json.decodeFromJsonElement(element) } - override fun emitGuiElements(opt: ManagedOption, guiAppender: GuiAppender) { + fun createButtonComponent(opt: ManagedOption): FirmButtonComponent { lateinit var button: FirmButtonComponent val sm = KeyBindingStateManager( { opt.value }, @@ -67,7 +67,11 @@ class KeyBindingHandler(val name: String, val managedConfig: ManagedConfig) : } } sm.updateLabel() - guiAppender.appendLabeledRow(opt.labelText, button) + return button + } + + override fun emitGuiElements(opt: ManagedOption, guiAppender: GuiAppender) { + guiAppender.appendLabeledRow(opt.labelText, createButtonComponent(opt)) } } diff --git a/src/main/kotlin/gui/config/ManagedConfig.kt b/src/main/kotlin/gui/config/ManagedConfig.kt index 44c6e59..c8b6ce8 100644 --- a/src/main/kotlin/gui/config/ManagedConfig.kt +++ b/src/main/kotlin/gui/config/ManagedConfig.kt @@ -176,7 +176,7 @@ abstract class ManagedConfig( } val translationKey get() = "firmament.config.${name}" - val labelText = Text.translatable(translationKey) + val labelText: Text = Text.translatable(translationKey) fun getConfigEditor(parent: Screen? = null): Screen { var screen: Screen? = null diff --git a/src/main/kotlin/util/MoulConfigUtils.kt b/src/main/kotlin/util/MoulConfigUtils.kt index 54528dd..2e52092 100644 --- a/src/main/kotlin/util/MoulConfigUtils.kt +++ b/src/main/kotlin/util/MoulConfigUtils.kt @@ -234,6 +234,19 @@ object MoulConfigUtils { // TODO: move this utility into moulconfig (also rework guicontext into an interface so i can make this mesh better into vanilla) fun GuiContext.adopt(element: GuiComponent) = element.foldRecursive(Unit, { comp, unit -> comp.context = this }) + inline fun GetSetter.xmap(crossinline fromT: (T) -> R, crossinline toT: (R) -> T): GetSetter { + val outer = this + return object : GetSetter { + override fun get(): R { + return fromT(outer.get()) + } + + override fun set(newValue: R) { + outer.set(toT(newValue)) + } + } + } + fun clickMCComponentInPlace( component: GuiComponent, x: Int, diff --git a/src/test/kotlin/root.kt b/src/test/kotlin/root.kt index 679ecb4..045fdd5 100644 --- a/src/test/kotlin/root.kt +++ b/src/test/kotlin/root.kt @@ -4,7 +4,7 @@ import net.minecraft.Bootstrap import net.minecraft.SharedConstants import moe.nea.firmament.util.TimeMark - object FirmTestBootstrap { +object FirmTestBootstrap { val loadStart = TimeMark.now() init { diff --git a/src/test/resources/testdata/chat/sacks/gain-and-lose-regular.snbt b/src/test/resources/testdata/chat/sacks/gain-and-lose-regular.snbt new file mode 100644 index 0000000..924a558 --- /dev/null +++ b/src/test/resources/testdata/chat/sacks/gain-and-lose-regular.snbt @@ -0,0 +1,101 @@ +{ + color: "#FFAA00", + extra: [ + { + color: "#55FF55", + hoverEvent: { + action: "show_text", + contents: { + color: "#55FF55", + extra: [ + { + color: "#55FF55", + strikethrough: 0b, + text: " +1 " + }, + { + color: "#FFFF55", + strikethrough: 0b, + text: "Rotten Flesh" + }, + { + color: "#555555", + strikethrough: 0b, + text: " (Combat Sack)" + }, + { + strikethrough: 0b, + text: " + +" + }, + { + color: "#555555", + strikethrough: 0b, + text: "This message can be disabled in the settings." + } + ], + strikethrough: 0b, + text: "Added items: +" + } + }, + strikethrough: 0b, + text: "+1" + }, + { + color: "#FFFF55", + hoverEvent: { + action: "show_text", + contents: { + color: "#55FF55", + extra: [ + { + color: "#55FF55", + strikethrough: 0b, + text: " +1 " + }, + { + color: "#FFFF55", + strikethrough: 0b, + text: "Rotten Flesh" + }, + { + color: "#555555", + strikethrough: 0b, + text: " (Combat Sack)" + }, + { + strikethrough: 0b, + text: " + +" + }, + { + color: "#555555", + strikethrough: 0b, + text: "This message can be disabled in the settings." + } + ], + strikethrough: 0b, + text: "Added items: +" + } + }, + strikethrough: 0b, + text: " item" + }, + { + color: "#FFFF55", + strikethrough: 0b, + text: "." + }, + { + color: "#555555", + strikethrough: 0b, + text: " (Last 5s.)" + } + ], + strikethrough: 0b, + text: "[Sacks] " +} diff --git a/src/test/resources/testdata/chat/sacks/gain-rotten-flesh.snbt b/src/test/resources/testdata/chat/sacks/gain-rotten-flesh.snbt new file mode 100644 index 0000000..924a558 --- /dev/null +++ b/src/test/resources/testdata/chat/sacks/gain-rotten-flesh.snbt @@ -0,0 +1,101 @@ +{ + color: "#FFAA00", + extra: [ + { + color: "#55FF55", + hoverEvent: { + action: "show_text", + contents: { + color: "#55FF55", + extra: [ + { + color: "#55FF55", + strikethrough: 0b, + text: " +1 " + }, + { + color: "#FFFF55", + strikethrough: 0b, + text: "Rotten Flesh" + }, + { + color: "#555555", + strikethrough: 0b, + text: " (Combat Sack)" + }, + { + strikethrough: 0b, + text: " + +" + }, + { + color: "#555555", + strikethrough: 0b, + text: "This message can be disabled in the settings." + } + ], + strikethrough: 0b, + text: "Added items: +" + } + }, + strikethrough: 0b, + text: "+1" + }, + { + color: "#FFFF55", + hoverEvent: { + action: "show_text", + contents: { + color: "#55FF55", + extra: [ + { + color: "#55FF55", + strikethrough: 0b, + text: " +1 " + }, + { + color: "#FFFF55", + strikethrough: 0b, + text: "Rotten Flesh" + }, + { + color: "#555555", + strikethrough: 0b, + text: " (Combat Sack)" + }, + { + strikethrough: 0b, + text: " + +" + }, + { + color: "#555555", + strikethrough: 0b, + text: "This message can be disabled in the settings." + } + ], + strikethrough: 0b, + text: "Added items: +" + } + }, + strikethrough: 0b, + text: " item" + }, + { + color: "#FFFF55", + strikethrough: 0b, + text: "." + }, + { + color: "#555555", + strikethrough: 0b, + text: " (Last 5s.)" + } + ], + strikethrough: 0b, + text: "[Sacks] " +} -- cgit