feat: MoulConfig config gui

This commit is contained in:
Linnea Gräf
2024-11-12 17:02:08 +01:00
parent 9763a4caa4
commit b774daef5b
14 changed files with 691 additions and 22 deletions

View File

@@ -150,7 +150,10 @@ fun createIsolatedSourceSet(name: String, path: String = "compat/$name", isEnabl
} }
compatSourceSets.add(ss) compatSourceSets.add(ss)
loom.createRemapConfigurations(ss) loom.createRemapConfigurations(ss)
if (!isEnabled) return ss if (!isEnabled) {
ss.output.files.forEach { it.deleteRecursively() }
return ss
}
configurations { configurations {
(ss.implementationConfigurationName) { (ss.implementationConfigurationName) {
extendsFrom(getByName(mainSS.compileClasspathConfigurationName)) extendsFrom(getByName(mainSS.compileClasspathConfigurationName))
@@ -219,7 +222,8 @@ val yaclSourceSet = createIsolatedSourceSet("yacl")
val explosiveEnhancementSourceSet = createIsolatedSourceSet("explosiveEnhancement", isEnabled = false) // TODO: wait for their port val explosiveEnhancementSourceSet = createIsolatedSourceSet("explosiveEnhancement", isEnabled = false) // TODO: wait for their port
val wildfireGenderSourceSet = createIsolatedSourceSet("wildfireGender", isEnabled = false) // TODO: wait on their port val wildfireGenderSourceSet = createIsolatedSourceSet("wildfireGender", isEnabled = false) // TODO: wait on their port
val modmenuSourceSet = createIsolatedSourceSet("modmenu") val modmenuSourceSet = createIsolatedSourceSet("modmenu")
val reiSourceSet = createIsolatedSourceSet("rei") // TODO: read through https://hackmd.io/@shedaniel/rei17_primer val reiSourceSet = createIsolatedSourceSet("rei")
val moulconfigSourceSet = createIsolatedSourceSet("moulconfig")
dependencies { dependencies {
// Minecraft dependencies // Minecraft dependencies
@@ -377,6 +381,7 @@ tasks.shadowJar {
} }
tasks.remapJar { tasks.remapJar {
// injectAccessWidener.set(true)
inputFile.set(tasks.shadowJar.flatMap { it.archiveFile }) inputFile.set(tasks.shadowJar.flatMap { it.archiveFile })
dependsOn(tasks.shadowJar) dependsOn(tasks.shadowJar)
archiveClassifier.set("") archiveClassifier.set("")

View File

@@ -64,7 +64,7 @@ jarvis = "1.1.3"
nealisp = "1.1.0" nealisp = "1.1.0"
# Update from https://github.com/NotEnoughUpdates/MoulConfig/tags # Update from https://github.com/NotEnoughUpdates/MoulConfig/tags
moulconfig = "3.0.0" moulconfig = "3.1.0"
# Update from https://www.curseforge.com/minecraft/mc-mods/configured/files/all?page=1&pageSize=20 # Update from https://www.curseforge.com/minecraft/mc-mods/configured/files/all?page=1&pageSize=20
configured = "5441234" configured = "5441234"

View File

@@ -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<Class<out ManagedConfig.OptionHandler<*>>, ((ManagedConfig.OptionHandler<*>, ManagedOption<*>, accordionId: Int, configObject: Config) -> ProcessedEditableOptionFirm<*>)> =
mutableMapOf()
fun <T : Any, H : ManagedConfig.OptionHandler<T>> register(
handlerClass: Class<H>,
transform: (H, ManagedOption<T>, accordionId: Int, configObject: Config) -> ProcessedEditableOptionFirm<T>
) {
handlers[handlerClass] =
transform as ((ManagedConfig.OptionHandler<*>, ManagedOption<*>, accordionId: Int, configObject: Config) -> ProcessedEditableOptionFirm<*>)
}
fun <T : Any> getHandler(
option: ManagedOption<T>,
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<T>
}
class CustomSliderEditor<T>(
option: ProcessedOption,
setter: GetSetter<T>,
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<Boolean>(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<String>(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<Unit>(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<HudMeta>(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<Duration>(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<Int>(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<SavedKeyBinding>(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<ProcessedOptionFirm>()
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
}
}

View File

@@ -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<ProcessedOptionFirm>
) : 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<ProcessedOption> {
return options
}
override fun getAccordionAnchors(): Map<Int, ProcessedOption> {
return accordions
}
}

View File

@@ -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<T : Any>(
val managedOption: ManagedOption<T>,
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()
}
}

View File

@@ -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<SearchTag> {
return emptyArray()
}
override fun getEditor(): GuiOptionEditor {
return editorInstance
}
override fun getCategory(): ProcessedCategory {
return category
}
override fun getConfig(): Config {
return config
}
override fun explicitNotifyChange() {
}
}

View File

@@ -4,7 +4,6 @@ import io.github.notenoughupdates.moulconfig.observer.ObservableList
import io.github.notenoughupdates.moulconfig.xml.Bind import io.github.notenoughupdates.moulconfig.xml.Bind
import net.minecraft.client.gui.screen.Screen import net.minecraft.client.gui.screen.Screen
import net.minecraft.text.Text import net.minecraft.text.Text
import moe.nea.firmament.repo.RepoManager
import moe.nea.firmament.util.MC import moe.nea.firmament.util.MC
import moe.nea.firmament.util.MoulConfigUtils import moe.nea.firmament.util.MoulConfigUtils
import moe.nea.firmament.util.ScreenUtil.setScreenLater import moe.nea.firmament.util.ScreenUtil.setScreenLater
@@ -18,6 +17,7 @@ object AllConfigsGui {
object ConfigConfig : ManagedConfig("configconfig", Category.META) { object ConfigConfig : ManagedConfig("configconfig", Category.META) {
val enableYacl by toggle("enable-yacl") { false } val enableYacl by toggle("enable-yacl") { false }
val enableMoulConfig by toggle("enable-moulconfig") { false }
} }
fun <T> List<T>.toObservableList(): ObservableList<T> = ObservableList(this) fun <T> List<T>.toObservableList(): ObservableList<T> = ObservableList(this)
@@ -67,7 +67,11 @@ object AllConfigsGui {
} }
fun makeScreen(parent: Screen? = null): Screen { 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 } val provider = FirmamentConfigScreenProvider.providers.find { it.key == wantedKey }
?: FirmamentConfigScreenProvider.providers.first() ?: FirmamentConfigScreenProvider.providers.first()
return provider.open(parent) return provider.open(parent)

View File

@@ -1,9 +1,7 @@
package moe.nea.firmament.gui.config package moe.nea.firmament.gui.config
import java.util.ServiceLoader
import kotlin.streams.asSequence
import net.minecraft.client.gui.screen.Screen import net.minecraft.client.gui.screen.Screen
import moe.nea.firmament.Firmament import moe.nea.firmament.util.compatloader.CompatLoader
interface FirmamentConfigScreenProvider { interface FirmamentConfigScreenProvider {
val key: String val key: String
@@ -11,17 +9,10 @@ interface FirmamentConfigScreenProvider {
fun open(parent: Screen?): Screen fun open(parent: Screen?): Screen
companion object { companion object : CompatLoader<FirmamentConfigScreenProvider>(FirmamentConfigScreenProvider::class) {
private val loader = ServiceLoader.load(FirmamentConfigScreenProvider::class.java)
val providers by lazy { val providers by lazy {
loader.stream().asSequence().mapNotNull { service -> allValidInstances
kotlin.runCatching { service.get() } .filter { it.isEnabled }
.getOrElse {
Firmament.logger.warn("Could not load config provider ${service.type()}", it)
null
}
}.filter { it.isEnabled }
.sortedWith(Comparator.comparing( .sortedWith(Comparator.comparing(
{ it.key }, { it.key },
Comparator<String> { left, right -> Comparator<String> { left, right ->

View File

@@ -30,7 +30,7 @@ class KeyBindingHandler(val name: String, val managedConfig: ManagedConfig) :
return Json.decodeFromJsonElement(element) return Json.decodeFromJsonElement(element)
} }
override fun emitGuiElements(opt: ManagedOption<SavedKeyBinding>, guiAppender: GuiAppender) { fun createButtonComponent(opt: ManagedOption<SavedKeyBinding>): FirmButtonComponent {
lateinit var button: FirmButtonComponent lateinit var button: FirmButtonComponent
val sm = KeyBindingStateManager( val sm = KeyBindingStateManager(
{ opt.value }, { opt.value },
@@ -67,7 +67,11 @@ class KeyBindingHandler(val name: String, val managedConfig: ManagedConfig) :
} }
} }
sm.updateLabel() sm.updateLabel()
guiAppender.appendLabeledRow(opt.labelText, button) return button
}
override fun emitGuiElements(opt: ManagedOption<SavedKeyBinding>, guiAppender: GuiAppender) {
guiAppender.appendLabeledRow(opt.labelText, createButtonComponent(opt))
} }
} }

View File

@@ -176,7 +176,7 @@ abstract class ManagedConfig(
} }
val translationKey get() = "firmament.config.${name}" val translationKey get() = "firmament.config.${name}"
val labelText = Text.translatable(translationKey) val labelText: Text = Text.translatable(translationKey)
fun getConfigEditor(parent: Screen? = null): Screen { fun getConfigEditor(parent: Screen? = null): Screen {
var screen: Screen? = null var screen: Screen? = null

View File

@@ -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) // 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 }) fun GuiContext.adopt(element: GuiComponent) = element.foldRecursive(Unit, { comp, unit -> comp.context = this })
inline fun <T, R> GetSetter<T>.xmap(crossinline fromT: (T) -> R, crossinline toT: (R) -> T): GetSetter<R> {
val outer = this
return object : GetSetter<R> {
override fun get(): R {
return fromT(outer.get())
}
override fun set(newValue: R) {
outer.set(toT(newValue))
}
}
}
fun clickMCComponentInPlace( fun clickMCComponentInPlace(
component: GuiComponent, component: GuiComponent,
x: Int, x: Int,

View File

@@ -4,7 +4,7 @@ import net.minecraft.Bootstrap
import net.minecraft.SharedConstants import net.minecraft.SharedConstants
import moe.nea.firmament.util.TimeMark import moe.nea.firmament.util.TimeMark
object FirmTestBootstrap { object FirmTestBootstrap {
val loadStart = TimeMark.now() val loadStart = TimeMark.now()
init { init {

View File

@@ -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] "
}

View File

@@ -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] "
}