aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorLinnea Gräf <nea@nea.moe>2025-06-26 18:21:02 +0200
committerLinnea Gräf <nea@nea.moe>2025-06-26 18:21:11 +0200
commit1c5d0df368471031f892330de7628ff78a6204ed (patch)
tree6d222fb45f3fbacff255de5347314204189a8b3c /src
parente926550bd19bddb0a0e026723bc6113ac09ea76f (diff)
downloadFirmament-1c5d0df368471031f892330de7628ff78a6204ed.tar.gz
Firmament-1c5d0df368471031f892330de7628ff78a6204ed.tar.bz2
Firmament-1c5d0df368471031f892330de7628ff78a6204ed.zip
feat(internal): Add a tab list api
Diffstat (limited to 'src')
-rw-r--r--src/main/java/moe/nea/firmament/mixins/accessor/AccessorPlayerListHud.java31
-rw-r--r--src/main/kotlin/commands/rome.kt3
-rw-r--r--src/main/kotlin/features/debug/AnimatedClothingScanner.kt2
-rw-r--r--src/main/kotlin/features/debug/DeveloperFeatures.kt10
-rw-r--r--src/main/kotlin/features/debug/SoundVisualizer.kt2
-rw-r--r--src/main/kotlin/features/debug/itemeditor/ItemExporter.kt3
-rw-r--r--src/main/kotlin/util/StringUtil.kt2
-rw-r--r--src/main/kotlin/util/mc/MCTabListAPI.kt96
-rw-r--r--src/main/kotlin/util/skyblock/TabListAPI.kt41
-rw-r--r--src/main/resources/firmament.accesswidener2
-rw-r--r--src/test/kotlin/testutil/ItemResources.kt34
-rw-r--r--src/test/kotlin/util/skyblock/TabListAPITest.kt48
-rw-r--r--src/test/resources/testdata/tablist/dungeon_hub.snbt1170
13 files changed, 1426 insertions, 18 deletions
diff --git a/src/main/java/moe/nea/firmament/mixins/accessor/AccessorPlayerListHud.java b/src/main/java/moe/nea/firmament/mixins/accessor/AccessorPlayerListHud.java
new file mode 100644
index 0000000..81ea0fd
--- /dev/null
+++ b/src/main/java/moe/nea/firmament/mixins/accessor/AccessorPlayerListHud.java
@@ -0,0 +1,31 @@
+package moe.nea.firmament.mixins.accessor;
+
+import net.minecraft.client.gui.hud.PlayerListHud;
+import net.minecraft.client.network.PlayerListEntry;
+import net.minecraft.text.Text;
+import org.jetbrains.annotations.Nullable;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.gen.Accessor;
+import org.spongepowered.asm.mixin.gen.Invoker;
+
+import java.util.Comparator;
+import java.util.List;
+
+@Mixin(PlayerListHud.class)
+public interface AccessorPlayerListHud {
+
+ @Accessor("ENTRY_ORDERING")
+ static Comparator<PlayerListEntry> getEntryOrdering() {
+ throw new AssertionError();
+ }
+
+ @Invoker("collectPlayerEntries")
+ List<PlayerListEntry> collectPlayerEntries_firmament();
+
+ @Accessor("footer")
+ @Nullable Text getFooter_firmament();
+
+ @Accessor("header")
+ @Nullable Text getHeader_firmament();
+
+}
diff --git a/src/main/kotlin/commands/rome.kt b/src/main/kotlin/commands/rome.kt
index 9fc5386..46ddef1 100644
--- a/src/main/kotlin/commands/rome.kt
+++ b/src/main/kotlin/commands/rome.kt
@@ -12,6 +12,7 @@ import moe.nea.firmament.apis.UrsaManager
import moe.nea.firmament.events.CommandEvent
import moe.nea.firmament.events.FirmamentEventBus
import moe.nea.firmament.features.debug.DebugLogger
+import moe.nea.firmament.features.debug.DeveloperFeatures
import moe.nea.firmament.features.debug.PowerUserTools
import moe.nea.firmament.features.inventory.buttons.InventoryButtons
import moe.nea.firmament.features.inventory.storageoverlay.StorageOverlayScreen
@@ -202,7 +203,7 @@ fun firmamentCommand() = literal("firmament") {
}
}
}
- thenLiteral("dev") {
+ thenLiteral(DeveloperFeatures.DEVELOPER_SUBCOMMAND) {
thenLiteral("simulate") {
thenArgument("message", RestArgumentType) { message ->
thenExecute {
diff --git a/src/main/kotlin/features/debug/AnimatedClothingScanner.kt b/src/main/kotlin/features/debug/AnimatedClothingScanner.kt
index 9f9f135..4edccfb 100644
--- a/src/main/kotlin/features/debug/AnimatedClothingScanner.kt
+++ b/src/main/kotlin/features/debug/AnimatedClothingScanner.kt
@@ -62,7 +62,7 @@ object AnimatedClothingScanner {
@Subscribe
fun onSubCommand(event: CommandEvent.SubCommand) {
- event.subcommand("dev") {
+ event.subcommand(DeveloperFeatures.DEVELOPER_SUBCOMMAND) {
thenLiteral("stealthisfit") {
thenLiteral("clear") {
thenExecute {
diff --git a/src/main/kotlin/features/debug/DeveloperFeatures.kt b/src/main/kotlin/features/debug/DeveloperFeatures.kt
index af1e92e..fd236f9 100644
--- a/src/main/kotlin/features/debug/DeveloperFeatures.kt
+++ b/src/main/kotlin/features/debug/DeveloperFeatures.kt
@@ -25,6 +25,7 @@ import moe.nea.firmament.util.asm.AsmAnnotationUtil
import moe.nea.firmament.util.iterate
object DeveloperFeatures : FirmamentFeature {
+ val DEVELOPER_SUBCOMMAND: String = "dev"
override val identifier: String
get() = "developer"
override val config: TConfig
@@ -103,9 +104,12 @@ object DeveloperFeatures : FirmamentFeature {
MC.sendChat(Text.translatable("firmament.dev.resourcerebuild.start"))
val startTime = TimeMark.now()
process.toHandle().onExit().thenApply {
- MC.sendChat(Text.stringifiedTranslatable(
- "firmament.dev.resourcerebuild.done",
- startTime.passedTime()))
+ MC.sendChat(
+ Text.stringifiedTranslatable(
+ "firmament.dev.resourcerebuild.done",
+ startTime.passedTime()
+ )
+ )
Unit
}
} else {
diff --git a/src/main/kotlin/features/debug/SoundVisualizer.kt b/src/main/kotlin/features/debug/SoundVisualizer.kt
index cc14122..f805e6b 100644
--- a/src/main/kotlin/features/debug/SoundVisualizer.kt
+++ b/src/main/kotlin/features/debug/SoundVisualizer.kt
@@ -20,7 +20,7 @@ object SoundVisualizer {
@Subscribe
fun onSubCommand(event: CommandEvent.SubCommand) {
- event.subcommand("dev") {
+ event.subcommand(DeveloperFeatures.DEVELOPER_SUBCOMMAND) {
thenLiteral("sounds") {
thenExecute {
showSounds = !showSounds
diff --git a/src/main/kotlin/features/debug/itemeditor/ItemExporter.kt b/src/main/kotlin/features/debug/itemeditor/ItemExporter.kt
index 6602c6d..d7d17aa 100644
--- a/src/main/kotlin/features/debug/itemeditor/ItemExporter.kt
+++ b/src/main/kotlin/features/debug/itemeditor/ItemExporter.kt
@@ -26,6 +26,7 @@ import moe.nea.firmament.commands.thenExecute
import moe.nea.firmament.commands.thenLiteral
import moe.nea.firmament.events.CommandEvent
import moe.nea.firmament.events.HandledScreenKeyPressedEvent
+import moe.nea.firmament.features.debug.DeveloperFeatures
import moe.nea.firmament.features.debug.ExportedTestConstantMeta
import moe.nea.firmament.features.debug.PowerUserTools
import moe.nea.firmament.repo.RepoDownloadManager
@@ -97,7 +98,7 @@ object ItemExporter {
@Subscribe
fun onCommand(event: CommandEvent.SubCommand) {
- event.subcommand("dev") {
+ event.subcommand(DeveloperFeatures.DEVELOPER_SUBCOMMAND) {
thenLiteral("reexportlore") {
thenArgument("itemid", StringArgumentType.string()) { itemid ->
suggestsList { RepoManager.neuRepo.items.items.keys }
diff --git a/src/main/kotlin/util/StringUtil.kt b/src/main/kotlin/util/StringUtil.kt
index 68e161a..dc98dc0 100644
--- a/src/main/kotlin/util/StringUtil.kt
+++ b/src/main/kotlin/util/StringUtil.kt
@@ -9,6 +9,8 @@ object StringUtil {
return string.replace(",", "").toInt()
}
+ fun String.title() = replaceFirstChar { it.titlecase() }
+
fun Iterable<String>.unwords() = joinToString(" ")
fun nextLexicographicStringOfSameLength(string: String): String {
val next = StringBuilder(string)
diff --git a/src/main/kotlin/util/mc/MCTabListAPI.kt b/src/main/kotlin/util/mc/MCTabListAPI.kt
new file mode 100644
index 0000000..66bdd55
--- /dev/null
+++ b/src/main/kotlin/util/mc/MCTabListAPI.kt
@@ -0,0 +1,96 @@
+package moe.nea.firmament.util.mc
+
+import com.mojang.serialization.Codec
+import com.mojang.serialization.codecs.RecordCodecBuilder
+import java.util.Optional
+import org.jetbrains.annotations.TestOnly
+import net.minecraft.client.gui.hud.PlayerListHud
+import net.minecraft.nbt.NbtOps
+import net.minecraft.scoreboard.Team
+import net.minecraft.text.Text
+import net.minecraft.text.TextCodecs
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.commands.thenExecute
+import moe.nea.firmament.commands.thenLiteral
+import moe.nea.firmament.events.CommandEvent
+import moe.nea.firmament.events.TickEvent
+import moe.nea.firmament.features.debug.DeveloperFeatures
+import moe.nea.firmament.features.debug.ExportedTestConstantMeta
+import moe.nea.firmament.mixins.accessor.AccessorPlayerListHud
+import moe.nea.firmament.util.ClipboardUtils
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.intoOptional
+import moe.nea.firmament.util.mc.SNbtFormatter.Companion.toPrettyString
+
+object MCTabListAPI {
+
+ fun PlayerListHud.cast() = this as AccessorPlayerListHud
+
+ @Subscribe
+ fun onTick(event: TickEvent) {
+ _currentTabList = null
+ }
+
+ @Subscribe
+ fun devCommand(event: CommandEvent.SubCommand) {
+ event.subcommand(DeveloperFeatures.DEVELOPER_SUBCOMMAND) {
+ thenLiteral("copytablist") {
+ thenExecute {
+ currentTabList.body.forEach {
+ MC.sendChat(Text.literal(TextCodecs.CODEC.encodeStart(NbtOps.INSTANCE, it).orThrow.toString()))
+ }
+ var compound = CurrentTabList.CODEC.encodeStart(NbtOps.INSTANCE, currentTabList).orThrow
+ compound = ExportedTestConstantMeta.SOURCE_CODEC.encode(
+ ExportedTestConstantMeta.current,
+ NbtOps.INSTANCE,
+ compound
+ ).orThrow
+ ClipboardUtils.setTextContent(
+ compound.toPrettyString()
+ )
+ }
+ }
+ }
+ }
+
+ @get:TestOnly
+ @set:TestOnly
+ var _currentTabList: CurrentTabList? = null
+
+ val currentTabList get() = _currentTabList ?: getTabListNow().also { _currentTabList = it }
+
+ data class CurrentTabList(
+ val header: Optional<Text>,
+ val footer: Optional<Text>,
+ val body: List<Text>,
+ ) {
+ companion object {
+ val CODEC: Codec<CurrentTabList> = RecordCodecBuilder.create {
+ it.group(
+ TextCodecs.CODEC.optionalFieldOf("header").forGetter(CurrentTabList::header),
+ TextCodecs.CODEC.optionalFieldOf("footer").forGetter(CurrentTabList::footer),
+ TextCodecs.CODEC.listOf().fieldOf("body").forGetter(CurrentTabList::body),
+ ).apply(it, ::CurrentTabList)
+ }
+ }
+ }
+
+ private fun getTabListNow(): CurrentTabList {
+ // This is a precondition for PlayerListHud.collectEntries to be valid
+ MC.networkHandler ?: return CurrentTabList(Optional.empty(), Optional.empty(), emptyList())
+ val hud = MC.inGameHud.playerListHud.cast()
+ val entries = hud.collectPlayerEntries_firmament()
+ .map {
+ it.displayName ?: run {
+ val team = it.scoreboardTeam
+ val name = it.profile.name
+ Team.decorateName(team, Text.literal(name))
+ }
+ }
+ return CurrentTabList(
+ header = hud.header_firmament.intoOptional(),
+ footer = hud.footer_firmament.intoOptional(),
+ body = entries,
+ )
+ }
+}
diff --git a/src/main/kotlin/util/skyblock/TabListAPI.kt b/src/main/kotlin/util/skyblock/TabListAPI.kt
new file mode 100644
index 0000000..6b937da
--- /dev/null
+++ b/src/main/kotlin/util/skyblock/TabListAPI.kt
@@ -0,0 +1,41 @@
+package moe.nea.firmament.util.skyblock
+
+import org.intellij.lang.annotations.Language
+import net.minecraft.text.Text
+import moe.nea.firmament.util.StringUtil.title
+import moe.nea.firmament.util.StringUtil.unwords
+import moe.nea.firmament.util.mc.MCTabListAPI
+import moe.nea.firmament.util.unformattedString
+
+object TabListAPI {
+
+ fun getWidgetLines(widgetName: WidgetName, includeTitle: Boolean = false, from: MCTabListAPI.CurrentTabList = MCTabListAPI.currentTabList): List<Text> {
+ return from.body
+ .dropWhile { !widgetName.matchesTitle(it) }
+ .takeWhile { it.string.isNotBlank() && !it.string.startsWith(" ") }
+ .let { if (includeTitle) it else it.drop(1) }
+ }
+
+ enum class WidgetName(regex: Regex?) {
+ COMMISSIONS,
+ SKILLS("Skills:( .*)?"),
+ PROFILE("Profile: (.*)"),
+ COLLECTION,
+ ESSENCE,
+ PET
+ ;
+
+ fun matchesTitle(it: Text): Boolean {
+ return regex.matches(it.unformattedString)
+ }
+
+ constructor() : this(null)
+ constructor(@Language("RegExp") regex: String) : this(Regex(regex))
+
+ val label =
+ name.split("_").map { it.lowercase().title() }.unwords()
+ val regex = regex ?: Regex.fromLiteral("$label:")
+
+ }
+
+}
diff --git a/src/main/resources/firmament.accesswidener b/src/main/resources/firmament.accesswidener
index eb78b8b..71f63ac 100644
--- a/src/main/resources/firmament.accesswidener
+++ b/src/main/resources/firmament.accesswidener
@@ -2,7 +2,9 @@ accessWidener v2 named
accessible class net/minecraft/client/render/RenderLayer$MultiPhase
accessible class net/minecraft/client/render/RenderLayer$MultiPhaseParameters
accessible class net/minecraft/client/font/TextRenderer$Drawer
+
accessible field net/minecraft/client/gui/hud/InGameHud SCOREBOARD_ENTRY_COMPARATOR Ljava/util/Comparator;
+
accessible field net/minecraft/client/network/ClientPlayNetworkHandler combinedDynamicRegistries Lnet/minecraft/registry/DynamicRegistryManager$Immutable;
accessible method net/minecraft/registry/RegistryOps <init> (Lcom/mojang/serialization/DynamicOps;Lnet/minecraft/registry/RegistryOps$RegistryInfoGetter;)V
accessible class net/minecraft/registry/RegistryOps$CachedRegistryInfoGetter
diff --git a/src/test/kotlin/testutil/ItemResources.kt b/src/test/kotlin/testutil/ItemResources.kt
index 17198f1..e996fc2 100644
--- a/src/test/kotlin/testutil/ItemResources.kt
+++ b/src/test/kotlin/testutil/ItemResources.kt
@@ -1,8 +1,6 @@
package moe.nea.firmament.test.testutil
import com.mojang.datafixers.DSL
-import com.mojang.datafixers.DataFixUtils
-import com.mojang.datafixers.types.templates.Named
import com.mojang.serialization.Dynamic
import com.mojang.serialization.JsonOps
import net.minecraft.SharedConstants
@@ -20,6 +18,7 @@ import net.minecraft.text.TextCodecs
import moe.nea.firmament.features.debug.ExportedTestConstantMeta
import moe.nea.firmament.test.FirmTestBootstrap
import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.mc.MCTabListAPI
object ItemResources {
init {
@@ -36,11 +35,12 @@ object ItemResources {
fun loadSNbt(path: String): NbtCompound {
return StringNbtReader.readCompound(loadString(path))
}
+
fun getNbtOps(): RegistryOps<NbtElement> = MC.currentOrDefaultRegistries.getOps(NbtOps.INSTANCE)
fun tryMigrateNbt(
nbtCompound: NbtCompound,
- typ: DSL.TypeReference,
+ typ: DSL.TypeReference?,
): NbtElement {
val source = nbtCompound.get("source", ExportedTestConstantMeta.CODEC)
nbtCompound.remove("source")
@@ -49,21 +49,33 @@ object ItemResources {
// Per 1.21.5 text components are wrapped in a string, which firmament unwrapped in the snbt files
NbtString.of(
NbtOps.INSTANCE.convertTo(JsonOps.INSTANCE, nbtCompound)
- .toString())
+ .toString()
+ )
} else {
nbtCompound
}
- return Schemas.getFixer()
- .update(
- typ,
- Dynamic(NbtOps.INSTANCE, wrappedNbtSource),
- source.get().dataVersion,
- SharedConstants.getGameVersion().saveVersion.id
- ).value
+ if (typ != null) {
+ return Schemas.getFixer()
+ .update(
+ typ,
+ Dynamic(NbtOps.INSTANCE, wrappedNbtSource),
+ source.get().dataVersion,
+ SharedConstants.getGameVersion().saveVersion.id
+ ).value
+ } else {
+ wrappedNbtSource
+ }
}
return nbtCompound
}
+ fun loadTablist(name: String): MCTabListAPI.CurrentTabList {
+ return MCTabListAPI.CurrentTabList.CODEC.parse(
+ getNbtOps(),
+ tryMigrateNbt(loadSNbt("testdata/tablist/$name.snbt"), null),
+ ).getOrThrow { IllegalStateException("Could not load tablist '$name': $it") }
+ }
+
fun loadText(name: String): Text {
return TextCodecs.CODEC.parse(
getNbtOps(),
diff --git a/src/test/kotlin/util/skyblock/TabListAPITest.kt b/src/test/kotlin/util/skyblock/TabListAPITest.kt
new file mode 100644
index 0000000..26eafe0
--- /dev/null
+++ b/src/test/kotlin/util/skyblock/TabListAPITest.kt
@@ -0,0 +1,48 @@
+package moe.nea.firmament.test.util.skyblock
+
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.Test
+import moe.nea.firmament.test.testutil.ItemResources
+import moe.nea.firmament.util.skyblock.TabListAPI
+
+class TabListAPITest {
+ val tablist = ItemResources.loadTablist("dungeon_hub")
+
+ @Test
+ fun checkWithTitle() {
+ Assertions.assertEquals(
+ listOf(
+ "Profile: Strawberry",
+ " SB Level: [210] 26/100 XP",
+ " Bank: 1.4B",
+ " Interest: 12 Hours (689.1k)",
+ ),
+ TabListAPI.getWidgetLines(TabListAPI.WidgetName.PROFILE, includeTitle = true, from = tablist).map { it.string })
+ }
+
+ @Test
+ fun checkEndOfColumn() {
+ Assertions.assertEquals(
+ listOf(
+ " Bonzo IV: 110/150",
+ " Scarf II: 25/50",
+ " The Professor IV: 141/150",
+ " Thorn I: 29/50",
+ " Livid II: 91/100",
+ " Sadan V: 388/500",
+ " Necron VI: 531/750",
+ ),
+ TabListAPI.getWidgetLines(TabListAPI.WidgetName.COLLECTION, from = tablist).map { it.string }
+ )
+ }
+
+ @Test
+ fun checkWithoutTitle() {
+ Assertions.assertEquals(
+ listOf(
+ " Undead: 1,907",
+ " Wither: 318",
+ ),
+ TabListAPI.getWidgetLines(TabListAPI.WidgetName.ESSENCE, from = tablist).map { it.string })
+ }
+}
diff --git a/src/test/resources/testdata/tablist/dungeon_hub.snbt b/src/test/resources/testdata/tablist/dungeon_hub.snbt
new file mode 100644
index 0000000..fed57ad
--- /dev/null
+++ b/src/test/resources/testdata/tablist/dungeon_hub.snbt
@@ -0,0 +1,1170 @@
+{
+ body: [
+ {
+ extra: [
+ " ",
+ {
+ bold: 1b,
+ color: "green",
+ text: "Players "
+ },
+ {
+ color: "white",
+ text: "(15)"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ {
+ color: "dark_gray",
+ text: "["
+ },
+ {
+ color: "aqua",
+ text: "210"
+ },
+ {
+ color: "dark_gray",
+ text: "] "
+ },
+ {
+ color: "aqua",
+ text: "lrg89"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ {
+ color: "dark_gray",
+ text: "["
+ },
+ {
+ color: "light_purple",
+ text: "322"
+ },
+ {
+ color: "dark_gray",
+ text: "] "
+ },
+ {
+ color: "aqua",
+ text: "Basilickk"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ {
+ color: "dark_gray",
+ text: "["
+ },
+ {
+ color: "light_purple",
+ text: "330"
+ },
+ {
+ color: "dark_gray",
+ text: "] "
+ },
+ {
+ color: "aqua",
+ text: "Schauli23 "
+ },
+ {
+ color: "gray",
+ text: "Σ"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ {
+ color: "dark_gray",
+ text: "["
+ },
+ {
+ color: "dark_green",
+ text: "187"
+ },
+ {
+ color: "dark_gray",
+ text: "] "
+ },
+ {
+ color: "aqua",
+ text: "bombardiro13"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ {
+ color: "dark_gray",
+ text: "["
+ },
+ {
+ color: "yellow",
+ text: "119"
+ },
+ {
+ color: "dark_gray",
+ text: "] "
+ },
+ {
+ color: "aqua",
+ text: "Horuu"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ {
+ color: "dark_gray",
+ text: "["
+ },
+ {
+ color: "dark_green",
+ text: "188"
+ },
+ {
+ color: "dark_gray",
+ text: "] "
+ },
+ {
+ color: "green",
+ text: "Kirito_Hacker "
+ },
+ {
+ bold: 1b,
+ color: "gray",
+ text: "ꕁ"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ {
+ color: "dark_gray",
+ text: "["
+ },
+ {
+ color: "blue",
+ text: "281"
+ },
+ {
+ color: "dark_gray",
+ text: "] "
+ },
+ {
+ color: "green",
+ text: "LasseFTW1N "
+ },
+ {
+ bold: 1b,
+ color: "dark_purple",
+ text: "࿇"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ {
+ color: "dark_gray",
+ text: "["
+ },
+ {
+ color: "dark_aqua",
+ text: "274"
+ },
+ {
+ color: "dark_gray",
+ text: "] "
+ },
+ {
+ color: "green",
+ text: "VN_Tuan "
+ },
+ {
+ bold: 1b,
+ color: "aqua",
+ text: "ᛝ"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ {
+ color: "dark_gray",
+ text: "["
+ },
+ {
+ color: "aqua",
+ text: "205"
+ },
+ {
+ color: "dark_gray",
+ text: "] "
+ },
+ {
+ color: "green",
+ text: "buttonpurse_1212"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ {
+ color: "dark_gray",
+ text: "["
+ },
+ {
+ color: "dark_green",
+ text: "193"
+ },
+ {
+ color: "dark_gray",
+ text: "] "
+ },
+ {
+ color: "green",
+ text: "Moly____ "
+ },
+ {
+ bold: 1b,
+ color: "gray",
+ text: "⚛"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ {
+ color: "dark_gray",
+ text: "["
+ },
+ {
+ color: "dark_green",
+ text: "187"
+ },
+ {
+ color: "dark_gray",
+ text: "] "
+ },
+ {
+ color: "green",
+ text: "BehavingTurtle4"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ {
+ color: "dark_gray",
+ text: "["
+ },
+ {
+ color: "dark_green",
+ text: "169"
+ },
+ {
+ color: "dark_gray",
+ text: "] "
+ },
+ {
+ color: "green",
+ text: "Kalmaria "
+ },
+ {
+ color: "gold",
+ text: "ௐ"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ {
+ color: "dark_gray",
+ text: "["
+ },
+ {
+ color: "yellow",
+ text: "84"
+ },
+ {
+ color: "dark_gray",
+ text: "] "
+ },
+ {
+ color: "green",
+ text: "Cxter"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ {
+ color: "dark_gray",
+ text: "["
+ },
+ {
+ color: "white",
+ text: "48"
+ },
+ {
+ color: "dark_gray",
+ text: "] "
+ },
+ {
+ color: "gray",
+ text: "FredyFazballs"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ {
+ color: "dark_gray",
+ text: "["
+ },
+ {
+ color: "gray",
+ text: "21"
+ },
+ {
+ color: "dark_gray",
+ text: "] "
+ },
+ {
+ color: "gray",
+ text: "Finn1446"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ " ",
+ {
+ bold: 1b,
+ color: "green",
+ text: "Players "
+ },
+ {
+ color: "white",
+ text: "(15)"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ " ",
+ {
+ bold: 1b,
+ color: "dark_aqua",
+ text: "Info"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ {
+ bold: 1b,
+ color: "aqua",
+ text: "Area: "
+ },
+ {
+ color: "gray",
+ text: "Dungeon Hub"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ " Server: ",
+ {
+ color: "dark_gray",
+ text: "mini90J"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ " Gems: ",
+ {
+ color: "green",
+ text: "65"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ " Fairy Souls: ",
+ {
+ color: "light_purple",
+ text: "7"
+ },
+ {
+ color: "dark_purple",
+ text: "/"
+ },
+ {
+ color: "light_purple",
+ text: "7"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ " Unclaimed chests: ",
+ {
+ color: "gold",
+ text: "0"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ {
+ bold: 1b,
+ text: ""
+ },
+ {
+ bold: 1b,
+ color: "yellow",
+ text: "Profile: "
+ },
+ {
+ color: "green",
+ text: "Strawberry"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ " SB Level",
+ {
+ color: "white",
+ text: ": "
+ },
+ {
+ color: "dark_gray",
+ text: "["
+ },
+ {
+ color: "aqua",
+ text: "210"
+ },
+ {
+ color: "dark_gray",
+ text: "] "
+ },
+ {
+ color: "aqua",
+ text: "26"
+ },
+ {
+ color: "dark_aqua",
+ text: "/"
+ },
+ {
+ color: "aqua",
+ text: "100 XP"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ " Bank: ",
+ {
+ color: "gold",
+ text: "1.4B"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ " Interest: ",
+ {
+ color: "yellow",
+ text: "12 Hours"
+ },
+ {
+ color: "gold",
+ text: " (689.1k)"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ {
+ bold: 1b,
+ color: "yellow",
+ text: "Collection:"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ " Bonzo IV: ",
+ {
+ color: "yellow",
+ text: "110"
+ },
+ {
+ color: "gold",
+ text: "/"
+ },
+ {
+ color: "yellow",
+ text: "150"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ " Scarf II: ",
+ {
+ color: "yellow",
+ text: "25"
+ },
+ {
+ color: "gold",
+ text: "/"
+ },
+ {
+ color: "yellow",
+ text: "50"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ " The Professor IV: ",
+ {
+ color: "yellow",
+ text: "141"
+ },
+ {
+ color: "gold",
+ text: "/"
+ },
+ {
+ color: "yellow",
+ text: "150"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ " Thorn I: ",
+ {
+ color: "yellow",
+ text: "29"
+ },
+ {
+ color: "gold",
+ text: "/"
+ },
+ {
+ color: "yellow",
+ text: "50"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ " Livid II: ",
+ {
+ color: "yellow",
+ text: "91"
+ },
+ {
+ color: "gold",
+ text: "/"
+ },
+ {
+ color: "yellow",
+ text: "100"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ " Sadan V: ",
+ {
+ color: "yellow",
+ text: "388"
+ },
+ {
+ color: "gold",
+ text: "/"
+ },
+ {
+ color: "yellow",
+ text: "500"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ " Necron VI: ",
+ {
+ color: "yellow",
+ text: "531"
+ },
+ {
+ color: "gold",
+ text: "/"
+ },
+ {
+ color: "yellow",
+ text: "750"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ " ",
+ {
+ bold: 1b,
+ color: "dark_aqua",
+ text: "Info"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ {
+ bold: 1b,
+ color: "gold",
+ text: "Dungeons:"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ " ",
+ {
+ color: "white",
+ text: "Catacombs 39: "
+ },
+ {
+ color: "green",
+ text: "15%"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ " ",
+ {
+ color: "green",
+ text: "Mage 36: "
+ },
+ {
+ color: "green",
+ text: "12.9%"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ {
+ bold: 1b,
+ color: "light_purple",
+ text: "RNG Meter"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ " ",
+ {
+ color: "green",
+ text: "Catacombs Floor I"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ " ",
+ {
+ color: "gray",
+ text: "None"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ {
+ bold: 1b,
+ color: "aqua",
+ text: "Essence:"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ " Undead: ",
+ {
+ color: "light_purple",
+ text: "1,907"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ " Wither: ",
+ {
+ color: "light_purple",
+ text: "318"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ {
+ bold: 1b,
+ color: "aqua",
+ text: "Party: "
+ },
+ {
+ color: "gray",
+ text: "No party"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ }
+ ],
+ footer: {
+ extra: [
+ "\n",
+ {
+ extra: [
+ {
+ bold: 1b,
+ color: "green",
+ text: "Active Effects"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ "\n",
+ {
+ extra: [
+ {
+ color: "gray",
+ text: ""
+ },
+ {
+ color: "gray",
+ text: "You have "
+ },
+ {
+ color: "yellow",
+ text: "2 "
+ },
+ {
+ color: "gray",
+ text: 'active effects. Use "'
+ },
+ {
+ color: "gold",
+ text: "/effects"
+ },
+ {
+ color: "gray",
+ text: '" to see them!'
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ "\n",
+ {
+ extra: [
+ {
+ color: "yellow",
+ text: "Haste II"
+ },
+ "",
+ {
+ bold: 0b,
+ italic: 0b,
+ obfuscated: 0b,
+ strikethrough: 0b,
+ text: "",
+ underlined: 0b
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ "\n",
+ {
+ extra: [
+ "",
+ {
+ bold: 0b,
+ extra: [
+ "§s"
+ ],
+ italic: 0b,
+ obfuscated: 0b,
+ strikethrough: 0b,
+ text: "",
+ underlined: 0b
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ "\n",
+ {
+ extra: [
+ {
+ bold: 1b,
+ color: "light_purple",
+ text: "Cookie Buff"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ "\n",
+ {
+ extra: [
+ {
+ color: "gray",
+ text: ""
+ },
+ {
+ color: "gray",
+ text: "Not active! Obtain booster cookies from the community"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ "\n",
+ {
+ extra: [
+ {
+ color: "gray",
+ text: "shop in the hub."
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ "\n",
+ {
+ extra: [
+ "",
+ {
+ bold: 0b,
+ extra: [
+ "§s"
+ ],
+ italic: 0b,
+ obfuscated: 0b,
+ strikethrough: 0b,
+ text: "",
+ underlined: 0b
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ "\n",
+ {
+ extra: [
+ {
+ color: "green",
+ extra: [
+ {
+ bold: 1b,
+ color: "red",
+ text: "STORE.HYPIXEL.NET"
+ }
+ ],
+ text: "Ranks, Boosters & MORE! "
+ }
+ ],
+ italic: 0b,
+ text: ""
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ header: {
+ extra: [
+ {
+ color: "aqua",
+ extra: [
+ {
+ bold: 1b,
+ color: "yellow",
+ text: "MC.HYPIXEL.NET"
+ }
+ ],
+ text: "You are playing on "
+ },
+ "\n",
+ {
+ extra: [
+ "",
+ {
+ bold: 0b,
+ extra: [
+ "§s"
+ ],
+ italic: 0b,
+ obfuscated: 0b,
+ strikethrough: 0b,
+ text: "",
+ underlined: 0b
+ }
+ ],
+ italic: 0b,
+ text: ""
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ source: {
+ dataVersion: 4325,
+ modVersion: "Firmament 3.1.0-dev+mc1.21.5+g2de6cfb"
+ }
+}