feat(internal): Add a tab list api
This commit is contained in:
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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)
|
||||
|
||||
96
src/main/kotlin/util/mc/MCTabListAPI.kt
Normal file
96
src/main/kotlin/util/mc/MCTabListAPI.kt
Normal file
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
41
src/main/kotlin/util/skyblock/TabListAPI.kt
Normal file
41
src/main/kotlin/util/skyblock/TabListAPI.kt
Normal file
@@ -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:")
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
48
src/test/kotlin/util/skyblock/TabListAPITest.kt
Normal file
48
src/test/kotlin/util/skyblock/TabListAPITest.kt
Normal file
@@ -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 })
|
||||
}
|
||||
}
|
||||
1170
src/test/resources/testdata/tablist/dungeon_hub.snbt
vendored
Normal file
1170
src/test/resources/testdata/tablist/dungeon_hub.snbt
vendored
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user