From e6142bb93619dee768fc18b87ffdd28558d4bcab Mon Sep 17 00:00:00 2001 From: Linnea Gräf Date: Sun, 13 Oct 2024 17:32:10 +0200 Subject: Make pickaxe ability display use AbilityUtils [no changelog] --- src/main/kotlin/Firmament.kt | 173 +++++++------- src/main/kotlin/events/HudRenderEvent.kt | 4 + src/main/kotlin/features/debug/PowerUserTools.kt | 7 +- src/main/kotlin/features/mining/PickaxeAbility.kt | 262 ++++++++++----------- .../kotlin/features/texturepack/NumberMatcher.kt | 231 +++++++++--------- src/main/kotlin/util/ErrorUtil.kt | 16 ++ src/main/kotlin/util/MC.kt | 131 ++++++----- src/main/kotlin/util/TimeMark.kt | 82 ++++--- src/main/kotlin/util/mc/SNbtFormatter.kt | 138 +++++++++++ src/main/kotlin/util/regex.kt | 14 +- src/main/kotlin/util/skyblock/AbilityUtils.kt | 138 +++++++++++ src/main/kotlin/util/textutil.kt | 2 + .../resources/assets/firmament/lang/en_us.json | 4 +- src/main/resources/firmament.accesswidener | 5 + .../kotlin/moe/nea/firmament/test/ColorCode.kt | 58 ----- src/test/kotlin/root.kt | 29 +++ src/test/kotlin/testutil/ItemResources.kt | 30 +++ src/test/kotlin/util/ColorCodeTest.kt | 59 +++++ src/test/kotlin/util/skyblock/AbilityUtilsTest.kt | 79 +++++++ .../testdata/items/aspect-of-the-void.snbt | 59 +++++ .../resources/testdata/items/diamond-pickaxe.snbt | 48 ++++ .../resources/testdata/items/titanium-drill.snbt | 97 ++++++++ 22 files changed, 1164 insertions(+), 502 deletions(-) create mode 100644 src/main/kotlin/util/ErrorUtil.kt create mode 100644 src/main/kotlin/util/mc/SNbtFormatter.kt create mode 100644 src/main/kotlin/util/skyblock/AbilityUtils.kt delete mode 100644 src/test/kotlin/moe/nea/firmament/test/ColorCode.kt create mode 100644 src/test/kotlin/root.kt create mode 100644 src/test/kotlin/testutil/ItemResources.kt create mode 100644 src/test/kotlin/util/ColorCodeTest.kt create mode 100644 src/test/kotlin/util/skyblock/AbilityUtilsTest.kt create mode 100644 src/test/resources/testdata/items/aspect-of-the-void.snbt create mode 100644 src/test/resources/testdata/items/diamond-pickaxe.snbt create mode 100644 src/test/resources/testdata/items/titanium-drill.snbt (limited to 'src') diff --git a/src/main/kotlin/Firmament.kt b/src/main/kotlin/Firmament.kt index c1801f4..343ec40 100644 --- a/src/main/kotlin/Firmament.kt +++ b/src/main/kotlin/Firmament.kt @@ -1,5 +1,3 @@ - - package moe.nea.firmament import com.mojang.brigadier.CommandDispatcher @@ -33,7 +31,6 @@ import kotlinx.coroutines.plus import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream import kotlin.coroutines.EmptyCoroutineContext -import net.minecraft.client.render.chunk.SectionBuilder import net.minecraft.command.CommandRegistryAccess import net.minecraft.util.Identifier import moe.nea.firmament.commands.registerFirmamentCommand @@ -51,98 +48,98 @@ import moe.nea.firmament.util.SBData import moe.nea.firmament.util.data.IDataHolder object Firmament { - const val MOD_ID = "firmament" + const val MOD_ID = "firmament" - val DEBUG = System.getProperty("firmament.debug") == "true" - val DATA_DIR: Path = Path.of(".firmament").also { Files.createDirectories(it) } - val CONFIG_DIR: Path = Path.of("config/firmament").also { Files.createDirectories(it) } - val logger: Logger = LogManager.getLogger("Firmament") - private val metadata: ModMetadata by lazy { - FabricLoader.getInstance().getModContainer(MOD_ID).orElseThrow().metadata - } - val version: Version by lazy { metadata.version } + val DEBUG = System.getProperty("firmament.debug") == "true" + val DATA_DIR: Path = Path.of(".firmament").also { Files.createDirectories(it) } + val CONFIG_DIR: Path = Path.of("config/firmament").also { Files.createDirectories(it) } + val logger: Logger = LogManager.getLogger("Firmament") + private val metadata: ModMetadata by lazy { + FabricLoader.getInstance().getModContainer(MOD_ID).orElseThrow().metadata + } + val version: Version by lazy { metadata.version } - val json = Json { - prettyPrint = DEBUG - isLenient = true - ignoreUnknownKeys = true - encodeDefaults = true - } + val json = Json { + prettyPrint = DEBUG + isLenient = true + ignoreUnknownKeys = true + encodeDefaults = true + } - val httpClient by lazy { - HttpClient { - install(ContentNegotiation) { - json(json) - } - install(ContentEncoding) { - gzip() - deflate() - } - install(UserAgent) { - agent = "Firmament/$version" - } - if (DEBUG) - install(Logging) { - level = LogLevel.INFO - } - install(HttpCache) - } - } + val httpClient by lazy { + HttpClient { + install(ContentNegotiation) { + json(json) + } + install(ContentEncoding) { + gzip() + deflate() + } + install(UserAgent) { + agent = "Firmament/$version" + } + if (DEBUG) + install(Logging) { + level = LogLevel.INFO + } + install(HttpCache) + } + } - val globalJob = Job() - val coroutineScope = - CoroutineScope(EmptyCoroutineContext + CoroutineName("Firmament")) + SupervisorJob(globalJob) + val globalJob = Job() + val coroutineScope = + CoroutineScope(EmptyCoroutineContext + CoroutineName("Firmament")) + SupervisorJob(globalJob) - private fun registerCommands( - dispatcher: CommandDispatcher, - @Suppress("UNUSED_PARAMETER") - ctx: CommandRegistryAccess - ) { - registerFirmamentCommand(dispatcher) - CommandEvent.publish(CommandEvent(dispatcher, ctx, MC.networkHandler?.commandDispatcher)) - } + private fun registerCommands( + dispatcher: CommandDispatcher, + @Suppress("UNUSED_PARAMETER") + ctx: CommandRegistryAccess + ) { + registerFirmamentCommand(dispatcher) + CommandEvent.publish(CommandEvent(dispatcher, ctx, MC.networkHandler?.commandDispatcher)) + } - @JvmStatic - fun onInitialize() { - } + @JvmStatic + fun onInitialize() { + } - @JvmStatic - fun onClientInitialize() { - FeatureManager.subscribeEvents() - var tick = 0 - ClientTickEvents.END_CLIENT_TICK.register(ClientTickEvents.EndTick { instance -> - TickEvent.publish(TickEvent(tick++)) - }) - IDataHolder.registerEvents() - RepoManager.initialize() - SBData.init() - FeatureManager.autoload() - HypixelStaticData.spawnDataCollectionLoop() - ClientCommandRegistrationCallback.EVENT.register(this::registerCommands) - ClientLifecycleEvents.CLIENT_STARTED.register(ClientLifecycleEvents.ClientStarted { - ClientStartedEvent.publish(ClientStartedEvent()) - }) - ClientLifecycleEvents.CLIENT_STOPPING.register(ClientLifecycleEvents.ClientStopping { - logger.info("Shutting down Firmament coroutines") - globalJob.cancel() - }) - registerFirmamentEvents() - ItemTooltipCallback.EVENT.register { stack, context, type, lines -> - ItemTooltipEvent.publish(ItemTooltipEvent(stack, context, type, lines)) - } - ScreenEvents.AFTER_INIT.register(ScreenEvents.AfterInit { client, screen, scaledWidth, scaledHeight -> - ScreenEvents.afterRender(screen) - .register(ScreenEvents.AfterRender { screen, drawContext, mouseX, mouseY, tickDelta -> - ScreenRenderPostEvent.publish(ScreenRenderPostEvent(screen, mouseX, mouseY, tickDelta, drawContext)) - }) - }) - } + @JvmStatic + fun onClientInitialize() { + FeatureManager.subscribeEvents() + var tick = 0 + ClientTickEvents.END_CLIENT_TICK.register(ClientTickEvents.EndTick { instance -> + TickEvent.publish(TickEvent(tick++)) + }) + IDataHolder.registerEvents() + RepoManager.initialize() + SBData.init() + FeatureManager.autoload() + HypixelStaticData.spawnDataCollectionLoop() + ClientCommandRegistrationCallback.EVENT.register(this::registerCommands) + ClientLifecycleEvents.CLIENT_STARTED.register(ClientLifecycleEvents.ClientStarted { + ClientStartedEvent.publish(ClientStartedEvent()) + }) + ClientLifecycleEvents.CLIENT_STOPPING.register(ClientLifecycleEvents.ClientStopping { + logger.info("Shutting down Firmament coroutines") + globalJob.cancel() + }) + registerFirmamentEvents() + ItemTooltipCallback.EVENT.register { stack, context, type, lines -> + ItemTooltipEvent.publish(ItemTooltipEvent(stack, context, type, lines)) + } + ScreenEvents.AFTER_INIT.register(ScreenEvents.AfterInit { client, screen, scaledWidth, scaledHeight -> + ScreenEvents.afterRender(screen) + .register(ScreenEvents.AfterRender { screen, drawContext, mouseX, mouseY, tickDelta -> + ScreenRenderPostEvent.publish(ScreenRenderPostEvent(screen, mouseX, mouseY, tickDelta, drawContext)) + }) + }) + } - fun identifier(path: String) = Identifier.of(MOD_ID, path) - inline fun tryDecodeJsonFromStream(inputStream: InputStream): Result { - return runCatching { - json.decodeFromStream(inputStream) - } - } + fun identifier(path: String) = Identifier.of(MOD_ID, path) + inline fun tryDecodeJsonFromStream(inputStream: InputStream): Result { + return runCatching { + json.decodeFromStream(inputStream) + } + } } diff --git a/src/main/kotlin/events/HudRenderEvent.kt b/src/main/kotlin/events/HudRenderEvent.kt index 555b3c8..a773a93 100644 --- a/src/main/kotlin/events/HudRenderEvent.kt +++ b/src/main/kotlin/events/HudRenderEvent.kt @@ -4,10 +4,14 @@ package moe.nea.firmament.events import net.minecraft.client.gui.DrawContext import net.minecraft.client.render.RenderTickCounter +import net.minecraft.world.GameMode +import moe.nea.firmament.util.MC /** * Called when hud elements should be rendered, before the screen, but after the world. */ data class HudRenderEvent(val context: DrawContext, val tickDelta: RenderTickCounter) : FirmamentEvent() { + val isRenderingHud = !MC.options.hudHidden + val isRenderingCursor = MC.interactionManager?.currentGameMode != GameMode.SPECTATOR && isRenderingHud companion object : FirmamentEventBus() } diff --git a/src/main/kotlin/features/debug/PowerUserTools.kt b/src/main/kotlin/features/debug/PowerUserTools.kt index 7ce14c0..529f011 100644 --- a/src/main/kotlin/features/debug/PowerUserTools.kt +++ b/src/main/kotlin/features/debug/PowerUserTools.kt @@ -28,6 +28,7 @@ import moe.nea.firmament.mixins.accessor.AccessorHandledScreen import moe.nea.firmament.util.ClipboardUtils import moe.nea.firmament.util.MC import moe.nea.firmament.util.focusedItemStack +import moe.nea.firmament.util.mc.SNbtFormatter.Companion.toPrettyString import moe.nea.firmament.util.mc.displayNameAccordingToNbt import moe.nea.firmament.util.mc.loreAccordingToNbt import moe.nea.firmament.util.skyBlockId @@ -44,6 +45,7 @@ object PowerUserTools : FirmamentFeature { val copyLoreData by keyBindingWithDefaultUnbound("copy-lore") val copySkullTexture by keyBindingWithDefaultUnbound("copy-skull-texture") val copyEntityData by keyBindingWithDefaultUnbound("entity-data") + val copyItemStack by keyBindingWithDefaultUnbound("copy-item-stack") } override val config @@ -125,7 +127,7 @@ object PowerUserTools : FirmamentFeature { Pair(item, Text.stringifiedTranslatable("firmament.tooltip.copied.modelid", model.toString())) } else if (it.matches(TConfig.copyNbtData)) { // TODO: copy full nbt - val nbt = item.get(DataComponentTypes.CUSTOM_DATA)?.nbt?.toString() ?: "" + val nbt = item.get(DataComponentTypes.CUSTOM_DATA)?.nbt?.toPrettyString() ?: "" ClipboardUtils.setTextContent(nbt) lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.nbt")) } else if (it.matches(TConfig.copyLoreData)) { @@ -157,6 +159,9 @@ object PowerUserTools : FirmamentFeature { Text.stringifiedTranslatable("firmament.tooltip.copied.skull-id", skullTexture.toString()) ) println("Copied skull id: $skullTexture") + } else if (it.matches(TConfig.copyItemStack)) { + ClipboardUtils.setTextContent(item.encode(MC.currentOrDefaultRegistries).toPrettyString()) + lastCopiedStack = Pair(item, Text.stringifiedTranslatable("firmament.tooltip.copied.stack")) } } diff --git a/src/main/kotlin/features/mining/PickaxeAbility.kt b/src/main/kotlin/features/mining/PickaxeAbility.kt index 192419f..1853d65 100644 --- a/src/main/kotlin/features/mining/PickaxeAbility.kt +++ b/src/main/kotlin/features/mining/PickaxeAbility.kt @@ -1,4 +1,3 @@ - package moe.nea.firmament.features.mining import java.util.regex.Pattern @@ -30,155 +29,146 @@ import moe.nea.firmament.util.parseShortNumber import moe.nea.firmament.util.parseTimePattern import moe.nea.firmament.util.render.RenderCircleProgress import moe.nea.firmament.util.render.lerp +import moe.nea.firmament.util.skyblock.AbilityUtils import moe.nea.firmament.util.toShedaniel import moe.nea.firmament.util.unformattedString import moe.nea.firmament.util.useMatch object PickaxeAbility : FirmamentFeature { - override val identifier: String - get() = "pickaxe-info" - - - object TConfig : ManagedConfig(identifier) { - val cooldownEnabled by toggle("ability-cooldown") { true } - val cooldownScale by integer("ability-scale", 16, 64) { 16 } - val drillFuelBar by toggle("fuel-bar") { true } - } - - var lobbyJoinTime = TimeMark.farPast() - var lastUsage = mutableMapOf() - var abilityOverride: String? = null - var defaultAbilityDurations = mutableMapOf( - "Mining Speed Boost" to 120.seconds, - "Pickobulus" to 110.seconds, - "Gemstone Infusion" to 140.seconds, - "Hazardous Miner" to 140.seconds, - "Maniac Miner" to 59.seconds, - "Vein Seeker" to 60.seconds - ) - - override val config: ManagedConfig - get() = TConfig - - fun getCooldownPercentage(name: String, cooldown: Duration): Double { - val sinceLastUsage = lastUsage[name]?.passedTime() ?: Duration.INFINITE - val sinceLobbyJoin = lobbyJoinTime.passedTime() + override val identifier: String + get() = "pickaxe-info" + + + object TConfig : ManagedConfig(identifier) { + val cooldownEnabled by toggle("ability-cooldown") { true } + val cooldownScale by integer("ability-scale", 16, 64) { 16 } + val drillFuelBar by toggle("fuel-bar") { true } + } + + var lobbyJoinTime = TimeMark.farPast() + var lastUsage = mutableMapOf() + var abilityOverride: String? = null + var defaultAbilityDurations = mutableMapOf( + "Mining Speed Boost" to 120.seconds, + "Pickobulus" to 110.seconds, + "Gemstone Infusion" to 140.seconds, + "Hazardous Miner" to 140.seconds, + "Maniac Miner" to 59.seconds, + "Vein Seeker" to 60.seconds + ) + + override val config: ManagedConfig + get() = TConfig + + fun getCooldownPercentage(name: String, cooldown: Duration): Double { + val sinceLastUsage = lastUsage[name]?.passedTime() ?: Duration.INFINITE + val sinceLobbyJoin = lobbyJoinTime.passedTime() if (SBData.skyblockLocation == SkyBlockIsland.MINESHAFT) { if (sinceLobbyJoin < sinceLastUsage) { return 1.0 } } - if (sinceLastUsage < cooldown) - return sinceLastUsage / cooldown - return 1.0 - } - - @Subscribe - fun onSlotClick(it: SlotClickEvent) { - if (MC.screen?.title?.unformattedString == "Heart of the Mountain") { - val name = it.stack.displayNameAccordingToNbt?.unformattedString ?: return - val cooldown = it.stack.loreAccordingToNbt.firstNotNullOfOrNull { - cooldownPattern.useMatch(it.unformattedString) { - parseTimePattern(group("cooldown")) - } - } ?: return - defaultAbilityDurations[name] = cooldown - } - } - - @Subscribe - fun onDurabilityBar(it: DurabilityBarEvent) { - if (!TConfig.drillFuelBar) return - val lore = it.item.loreAccordingToNbt - if (lore.lastOrNull()?.unformattedString?.contains("DRILL") != true) return - val maxFuel = lore.firstNotNullOfOrNull { - fuelPattern.useMatch(it.unformattedString) { - parseShortNumber(group("maxFuel")) - } - } ?: return - val extra = it.item.extraAttributes - if (!extra.contains("drill_fuel")) return - val fuel = extra.getInt("drill_fuel") - val percentage = fuel / maxFuel.toFloat() - it.barOverride = DurabilityBarEvent.DurabilityBar( - lerp( - DyeColor.RED.toShedaniel(), - DyeColor.GREEN.toShedaniel(), - percentage - ), percentage - ) - } - - @Subscribe - fun onChatMessage(it: ProcessChatEvent) { - abilityUsePattern.useMatch(it.unformattedString) { - lastUsage[group("name")] = TimeMark.now() - } - abilitySwitchPattern.useMatch(it.unformattedString) { - abilityOverride = group("ability") - } - } - - @Subscribe - fun onWorldReady(event: WorldReadyEvent) { - lobbyJoinTime = TimeMark.now() - abilityOverride = null - } + if (sinceLastUsage < cooldown) + return sinceLastUsage / cooldown + return 1.0 + } + + @Subscribe + fun onSlotClick(it: SlotClickEvent) { + if (MC.screen?.title?.unformattedString == "Heart of the Mountain") { + val name = it.stack.displayNameAccordingToNbt.unformattedString + val cooldown = it.stack.loreAccordingToNbt.firstNotNullOfOrNull { + cooldownPattern.useMatch(it.unformattedString) { + parseTimePattern(group("cooldown")) + } + } ?: return + defaultAbilityDurations[name] = cooldown + } + } + + @Subscribe + fun onDurabilityBar(it: DurabilityBarEvent) { + if (!TConfig.drillFuelBar) return + val lore = it.item.loreAccordingToNbt + if (lore.lastOrNull()?.unformattedString?.contains("DRILL") != true) return + val maxFuel = lore.firstNotNullOfOrNull { + fuelPattern.useMatch(it.unformattedString) { + parseShortNumber(group("maxFuel")) + } + } ?: return + val extra = it.item.extraAttributes + if (!extra.contains("drill_fuel")) return + val fuel = extra.getInt("drill_fuel") + val percentage = fuel / maxFuel.toFloat() + it.barOverride = DurabilityBarEvent.DurabilityBar( + lerp( + DyeColor.RED.toShedaniel(), + DyeColor.GREEN.toShedaniel(), + percentage + ), percentage + ) + } + + @Subscribe + fun onChatMessage(it: ProcessChatEvent) { + abilityUsePattern.useMatch(it.unformattedString) { + lastUsage[group("name")] = TimeMark.now() + } + abilitySwitchPattern.useMatch(it.unformattedString) { + abilityOverride = group("ability") + } + } + + @Subscribe + fun onWorldReady(event: WorldReadyEvent) { + lobbyJoinTime = TimeMark.now() + abilityOverride = null + } @Subscribe fun onProfileSwitch(event: ProfileSwitchEvent) { lastUsage.clear() } - val abilityUsePattern = Pattern.compile("You used your (?.*) Pickaxe Ability!") - val fuelPattern = Pattern.compile("Fuel: .*/(?$SHORT_NUMBER_FORMAT)") - - data class PickaxeAbilityData( - val name: String, - val cooldown: Duration, - ) - - fun getCooldownFromLore(itemStack: ItemStack): PickaxeAbilityData? { - val lore = itemStack.loreAccordingToNbt - if (!lore.any { it.unformattedString.contains("Breaking Power") }) - return null - val cooldown = lore.firstNotNullOfOrNull { - cooldownPattern.useMatch(it.unformattedString) { - parseTimePattern(group("cooldown")) - } - } ?: return null - val name = lore.firstNotNullOfOrNull { - abilityPattern.useMatch(it.unformattedString) { - group("name") - } - } ?: return null - return PickaxeAbilityData(name, cooldown) - } - - - val cooldownPattern = Pattern.compile("Cooldown: (?$TIME_PATTERN)") - val abilityPattern = Pattern.compile("(⦾ )?Ability: (?.*) {2}RIGHT CLICK") - val abilitySwitchPattern = - Pattern.compile("You selected (?.*) as your Pickaxe Ability\\. This ability will apply to all of your pickaxes!") - - - @Subscribe - fun renderHud(event: HudRenderEvent) { - if (!TConfig.cooldownEnabled) return - var ability = getCooldownFromLore(MC.player?.getStackInHand(Hand.MAIN_HAND) ?: return) ?: return - defaultAbilityDurations[ability.name] = ability.cooldown - val ao = abilityOverride - if (ao != ability.name && ao != null) { - ability = PickaxeAbilityData(ao, defaultAbilityDurations[ao] ?: 120.seconds) - } - event.context.matrices.push() - event.context.matrices.translate(MC.window.scaledWidth / 2F, MC.window.scaledHeight / 2F, 0F) - event.context.matrices.scale(TConfig.cooldownScale.toFloat(), TConfig.cooldownScale.toFloat(), 1F) - RenderCircleProgress.renderCircle( - event.context, Identifier.of("firmament", "textures/gui/circle.png"), - getCooldownPercentage(ability.name, ability.cooldown).toFloat(), - 0f, 1f, 0f, 1f - ) - event.context.matrices.pop() - } + val abilityUsePattern = Pattern.compile("You used your (?.*) Pickaxe Ability!") + val fuelPattern = Pattern.compile("Fuel: .*/(?$SHORT_NUMBER_FORMAT)") + val pickaxeAbilityCooldownPattern = Pattern.compile("Your pickaxe ability is on cooldown for (?$TIME_PATTERN)\\.") + + data class PickaxeAbilityData( + val name: String, + val cooldown: Duration, + ) + + fun getCooldownFromLore(itemStack: ItemStack): PickaxeAbilityData? { + val lore = itemStack.loreAccordingToNbt + if (!lore.any { it.unformattedString.contains("Breaking Power") }) + return null + val ability = AbilityUtils.getAbilities(itemStack).firstOrNull() ?: return null + return PickaxeAbilityData(ability.name, ability.cooldown ?: return null) + } + + val cooldownPattern = Pattern.compile("Cooldown: (?$TIME_PATTERN)") + val abilitySwitchPattern = + Pattern.compile("You selected (?.*) as your Pickaxe Ability\\. This ability will apply to all of your pickaxes!") + + @Subscribe + fun renderHud(event: HudRenderEvent) { + if (!TConfig.cooldownEnabled) return + if (!event.isRenderingCursor) return + var ability = getCooldownFromLore(MC.player?.getStackInHand(Hand.MAIN_HAND) ?: return) ?: return + defaultAbilityDurations[ability.name] = ability.cooldown + val ao = abilityOverride + if (ao != ability.name && ao != null) { + ability = PickaxeAbilityData(ao, defaultAbilityDurations[ao] ?: 120.seconds) + } + event.context.matrices.push() + event.context.matrices.translate(MC.window.scaledWidth / 2F, MC.window.scaledHeight / 2F, 0F) + event.context.matrices.scale(TConfig.cooldownScale.toFloat(), TConfig.cooldownScale.toFloat(), 1F) + RenderCircleProgress.renderCircle( + event.context, Identifier.of("firmament", "textures/gui/circle.png"), + getCooldownPercentage(ability.name, ability.cooldown).toFloat(), + 0f, 1f, 0f, 1f + ) + event.context.matrices.pop() + } } diff --git a/src/main/kotlin/features/texturepack/NumberMatcher.kt b/src/main/kotlin/features/texturepack/NumberMatcher.kt index 7e6665f..e6f2d01 100644 --- a/src/main/kotlin/features/texturepack/NumberMatcher.kt +++ b/src/main/kotlin/features/texturepack/NumberMatcher.kt @@ -1,4 +1,3 @@ - package moe.nea.firmament.features.texturepack import com.google.gson.JsonElement @@ -6,120 +5,120 @@ import com.google.gson.JsonPrimitive import moe.nea.firmament.util.useMatch abstract class NumberMatcher { - abstract fun test(number: Number): Boolean - - - companion object { - fun parse(jsonElement: JsonElement): NumberMatcher? { - if (jsonElement is JsonPrimitive) { - if (jsonElement.isString) { - val string = jsonElement.asString - return parseRange(string) ?: parseOperator(string) - } - if (jsonElement.isNumber) { - val number = jsonElement.asNumber - val hasDecimals = (number.toString().contains(".")) - return MatchNumberExact(if (hasDecimals) number.toLong() else number.toDouble()) - } - } - return null - } - - private val intervalSpec = - "(?[\\[\\(])(?[0-9.]+)?,(?[0-9.]+)?(?[\\]\\)])" - .toPattern() - - fun parseRange(string: String): RangeMatcher? { - intervalSpec.useMatch(string) { - // Open in the set-theory sense, meaning does not include its end. - val beginningOpen = group("beginningOpen") == "(" - val endingOpen = group("endingOpen") == ")" - val beginning = group("beginning")?.toDouble() - val ending = group("ending")?.toDouble() - return RangeMatcher(beginning, !beginningOpen, ending, !endingOpen) - } - return null - } - - enum class Operator(val operator: String) { - LESS("<") { - override fun matches(comparisonResult: Int): Boolean { - return comparisonResult < 0 - } - }, - LESS_EQUALS("<=") { - override fun matches(comparisonResult: Int): Boolean { - return comparisonResult <= 0 - } - }, - GREATER(">") { - override fun matches(comparisonResult: Int): Boolean { - return comparisonResult > 0 - } - }, - GREATER_EQUALS(">=") { - override fun matches(comparisonResult: Int): Boolean { - return comparisonResult >= 0 - } - }, - ; - - abstract fun matches(comparisonResult: Int): Boolean - } - - private val operatorPattern = "(?${Operator.entries.joinToString("|") {it.operator}})(?[0-9.]+)".toPattern() - - fun parseOperator(string: String): OperatorMatcher? { - operatorPattern.useMatch(string) { - val operatorName = group("operator") - val operator = Operator.entries.find { it.operator == operatorName }!! - val value = group("value").toDouble() - return OperatorMatcher(operator, value) - } - return null - } - - data class OperatorMatcher(val operator: Operator, val value: Double) : NumberMatcher() { - override fun test(number: Number): Boolean { - return operator.matches(number.toDouble().compareTo(value)) - } - } - - - data class MatchNumberExact(val number: Number) : NumberMatcher() { - override fun test(number: Number): Boolean { - return when (this.number) { - is Double -> number.toDouble() == this.number.toDouble() - else -> number.toLong() == this.number.toLong() - } - } - } - - data class RangeMatcher( - val beginning: Double?, - val beginningInclusive: Boolean, - val ending: Double?, - val endingInclusive: Boolean, - ) : NumberMatcher() { - override fun test(number: Number): Boolean { - val value = number.toDouble() - if (beginning != null) { - if (beginningInclusive) { - if (value < beginning) return false - } else { - if (value <= beginning) return false - } - } - if (ending != null) { - if (endingInclusive) { - if (value > ending) return false - } else { - if (value >= ending) return false - } - } - return true - } - } - } + abstract fun test(number: Number): Boolean + + + companion object { + fun parse(jsonElement: JsonElement): NumberMatcher? { + if (jsonElement is JsonPrimitive) { + if (jsonElement.isString) { + val string = jsonElement.asString + return parseRange(string) ?: parseOperator(string) + } + if (jsonElement.isNumber) { + val number = jsonElement.asNumber + val hasDecimals = (number.toString().contains(".")) + return MatchNumberExact(if (hasDecimals) number.toLong() else number.toDouble()) + } + } + return null + } + + private val intervalSpec = + "(?[\\[\\(])(?[0-9.]+)?,(?[0-9.]+)?(?[\\]\\)])" + .toPattern() + + fun parseRange(string: String): RangeMatcher? { + intervalSpec.useMatch(string) { + // Open in the set-theory sense, meaning does not include its end. + val beginningOpen = group("beginningOpen") == "(" + val endingOpen = group("endingOpen") == ")" + val beginning = group("beginning")?.toDouble() + val ending = group("ending")?.toDouble() + return RangeMatcher(beginning, !beginningOpen, ending, !endingOpen) + } + return null + } + + enum class Operator(val operator: String) { + LESS("<") { + override fun matches(comparisonResult: Int): Boolean { + return comparisonResult < 0 + } + }, + LESS_EQUALS("<=") { + override fun matches(comparisonResult: Int): Boolean { + return comparisonResult <= 0 + } + }, + GREATER(">") { + override fun matches(comparisonResult: Int): Boolean { + return comparisonResult > 0 + } + }, + GREATER_EQUALS(">=") { + override fun matches(comparisonResult: Int): Boolean { + return comparisonResult >= 0 + } + }, + ; + + abstract fun matches(comparisonResult: Int): Boolean + } + + private val operatorPattern = + "(?${Operator.entries.joinToString("|") { it.operator }})(?[0-9.]+)".toPattern() + + fun parseOperator(string: String): OperatorMatcher? { + return operatorPattern.useMatch(string) { + val operatorName = group("operator") + val operator = Operator.entries.find { it.operator == operatorName }!! + val value = group("value").toDouble() + OperatorMatcher(operator, value) + } + } + + data class OperatorMatcher(val operator: Operator, val value: Double) : NumberMatcher() { + override fun test(number: Number): Boolean { + return operator.matches(number.toDouble().compareTo(value)) + } + } + + + data class MatchNumberExact(val number: Number) : NumberMatcher() { + override fun test(number: Number): Boolean { + return when (this.number) { + is Double -> number.toDouble() == this.number.toDouble() + else -> number.toLong() == this.number.toLong() + } + } + } + + data class RangeMatcher( + val beginning: Double?, + val beginningInclusive: Boolean, + val ending: Double?, + val endingInclusive: Boolean, + ) : NumberMatcher() { + override fun test(number: Number): Boolean { + val value = number.toDouble() + if (beginning != null) { + if (beginningInclusive) { + if (value < beginning) return false + } else { + if (value <= beginning) return false + } + } + if (ending != null) { + if (endingInclusive) { + if (value > ending) return false + } else { + if (value >= ending) return false + } + } + return true + } + } + } } diff --git a/src/main/kotlin/util/ErrorUtil.kt b/src/main/kotlin/util/ErrorUtil.kt new file mode 100644 index 0000000..4f229af --- /dev/null +++ b/src/main/kotlin/util/ErrorUtil.kt @@ -0,0 +1,16 @@ +package moe.nea.firmament.util + +import moe.nea.firmament.Firmament + +object ErrorUtil { + var aggressiveErrors = run { + Thread.currentThread().stackTrace.any { it.className.startsWith("org.junit.") } || Firmament.DEBUG + } + + @Suppress("NOTHING_TO_INLINE") // Suppressed since i want the logger to not pick up the ErrorUtil stack-frame + inline fun softError(message: String) { + if (aggressiveErrors) error(message) + else Firmament.logger.error(message) + } + +} diff --git a/src/main/kotlin/util/MC.kt b/src/main/kotlin/util/MC.kt index db8eccb..09aa7aa 100644 --- a/src/main/kotlin/util/MC.kt +++ b/src/main/kotlin/util/MC.kt @@ -4,7 +4,9 @@ import io.github.moulberry.repo.data.Coordinate import java.util.concurrent.ConcurrentLinkedQueue import net.minecraft.client.MinecraftClient import net.minecraft.client.gui.screen.ingame.HandledScreen +import net.minecraft.client.option.GameOptions import net.minecraft.client.render.WorldRenderer +import net.minecraft.item.Item import net.minecraft.network.packet.c2s.play.CommandExecutionC2SPacket import net.minecraft.registry.BuiltinRegistries import net.minecraft.registry.RegistryKeys @@ -16,79 +18,82 @@ import moe.nea.firmament.events.TickEvent object MC { - private val messageQueue = ConcurrentLinkedQueue() + private val messageQueue = ConcurrentLinkedQueue() - init { - TickEvent.subscribe("MC:push") { - while (true) { - inGameHud.chatHud.addMessage(messageQueue.poll() ?: break) - } - while (true) { - (nextTickTodos.poll() ?: break).invoke() - } - } - } + init { + TickEvent.subscribe("MC:push") { + while (true) { + inGameHud.chatHud.addMessage(messageQueue.poll() ?: break) + } + while (true) { + (nextTickTodos.poll() ?: break).invoke() + } + } + } - fun sendChat(text: Text) { - if (instance.isOnThread) - inGameHud.chatHud.addMessage(text) - else - messageQueue.add(text) - } + fun sendChat(text: Text) { + if (instance.isOnThread) + inGameHud.chatHud.addMessage(text) + else + messageQueue.add(text) + } - fun sendServerCommand(command: String) { - val nh = player?.networkHandler ?: return - nh.sendPacket( - CommandExecutionC2SPacket( - command, - ) - ) - } + fun sendServerCommand(command: String) { + val nh = player?.networkHandler ?: return + nh.sendPacket( + CommandExecutionC2SPacket( + command, + ) + ) + } - fun sendServerChat(text: String) { - player?.networkHandler?.sendChatMessage(text) - } + fun sendServerChat(text: String) { + player?.networkHandler?.sendChatMessage(text) + } - fun sendCommand(command: String) { - player?.networkHandler?.sendCommand(command) - } + fun sendCommand(command: String) { + player?.networkHandler?.sendCommand(command) + } - fun onMainThread(block: () -> Unit) { - if (instance.isOnThread) - block() - else - instance.send(block) - } + fun onMainThread(block: () -> Unit) { + if (instance.isOnThread) + block() + else + instance.send(block) + } - private val nextTickTodos = ConcurrentLinkedQueue<() -> Unit>() - fun nextTick(function: () -> Unit) { - nextTickTodos.add(function) - } + private val nextTickTodos = ConcurrentLinkedQueue<() -> Unit>() + fun nextTick(function: () -> Unit) { + nextTickTodos.add(function) + } - inline val resourceManager get() = (instance.resourceManager as ReloadableResourceManagerImpl) - inline val worldRenderer: WorldRenderer get() = instance.worldRenderer - inline val networkHandler get() = player?.networkHandler - inline val instance get() = MinecraftClient.getInstance() - inline val keyboard get() = instance.keyboard - inline val textureManager get() = instance.textureManager - inline val inGameHud get() = instance.inGameHud - inline val font get() = instance.textRenderer - inline val soundManager get() = instance.soundManager - inline val player get() = instance.player - inline val camera get() = instance.cameraEntity - inline val guiAtlasManager get() = instance.guiAtlasManager - inline val world get() = instance.world - inline var screen - get() = instance.currentScreen - set(value) = instance.setScreen(value) - inline val handledScreen: HandledScreen<*>? get() = instance.currentScreen as? HandledScreen<*> - inline val window get() = instance.window - inline val currentRegistries: RegistryWrapper.WrapperLookup? get() = world?.registryManager - val defaultRegistries: RegistryWrapper.WrapperLookup = BuiltinRegistries.createWrapperLookup() - val defaultItems = defaultRegistries.getWrapperOrThrow(RegistryKeys.ITEM) + inline val resourceManager get() = (instance.resourceManager as ReloadableResourceManagerImpl) + inline val worldRenderer: WorldRenderer get() = instance.worldRenderer + inline val networkHandler get() = player?.networkHandler + inline val instance get() = MinecraftClient.getInstance() + inline val keyboard get() = instance.keyboard + inline val interactionManager get() = instance.interactionManager + inline val textureManager get() = instance.textureManager + inline val options get() = instance.options + inline val inGameHud get() = instance.inGameHud + inline val font get() = instance.textRenderer + inline val soundManager get() = instance.soundManager + inline val player get() = instance.player + inline val camera get() = instance.cameraEntity + inline val guiAtlasManager get() = instance.guiAtlasManager + inline val world get() = instance.world + inline var screen + get() = instance.currentScreen + set(value) = instance.setScreen(value) + inline val handledScreen: HandledScreen<*>? get() = instance.currentScreen as? HandledScreen<*> + inline val window get() = instance.window + inline val currentRegistries: RegistryWrapper.WrapperLookup? get() = world?.registryManager + val defaultRegistries: RegistryWrapper.WrapperLookup = BuiltinRegistries.createWrapperLookup() + inline val currentOrDefaultRegistries get() = currentRegistries ?: defaultRegistries + val defaultItems: RegistryWrapper.Impl = defaultRegistries.getWrapperOrThrow(RegistryKeys.ITEM) } val Coordinate.blockPos: BlockPos - get() = BlockPos(x, y, z) + get() = BlockPos(x, y, z) diff --git a/src/main/kotlin/util/TimeMark.kt b/src/main/kotlin/util/TimeMark.kt index 1264212..4a076ac 100644 --- a/src/main/kotlin/util/TimeMark.kt +++ b/src/main/kotlin/util/TimeMark.kt @@ -1,44 +1,52 @@ - - package moe.nea.firmament.util import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds class TimeMark private constructor(private val timeMark: Long) : Comparable { - fun passedTime() = if (timeMark == 0L) Duration.INFINITE else (System.currentTimeMillis() - timeMark).milliseconds - - operator fun minus(other: TimeMark): Duration { - if (other.timeMark == timeMark) - return 0.milliseconds - if (other.timeMark == 0L) - return Duration.INFINITE - if (timeMark == 0L) - return -Duration.INFINITE - return (timeMark - other.timeMark).milliseconds - } - - companion object { - fun now() = TimeMark(System.currentTimeMillis()) - fun farPast() = TimeMark(0L) - fun ago(timeDelta: Duration): TimeMark { - if (timeDelta.isFinite()) { - return TimeMark(System.currentTimeMillis() - timeDelta.inWholeMilliseconds) - } - require(timeDelta.isPositive()) - return farPast() - } - } - - override fun hashCode(): Int { - return timeMark.hashCode() - } - - override fun equals(other: Any?): Boolean { - return other is TimeMark && other.timeMark == timeMark - } - - override fun compareTo(other: TimeMark): Int { - return this.timeMark.compareTo(other.timeMark) - } + fun passedTime() = + if (timeMark == 0L) Duration.INFINITE + else (System.currentTimeMillis() - timeMark).milliseconds + + fun passedAt(fakeNow: TimeMark) = + if (timeMark == 0L) Duration.INFINITE + else (fakeNow.timeMark - timeMark).milliseconds + + operator fun minus(other: TimeMark): Duration { + if (other.timeMark == timeMark) + return 0.milliseconds + if (other.timeMark == 0L) + return Duration.INFINITE + if (timeMark == 0L) + return -Duration.INFINITE + return (timeMark - other.timeMark).milliseconds + } + + companion object { + fun now() = TimeMark(System.currentTimeMillis()) + fun farPast() = TimeMark(0L) + fun ago(timeDelta: Duration): TimeMark { + if (timeDelta.isFinite()) { + return TimeMark(System.currentTimeMillis() - timeDelta.inWholeMilliseconds) + } + require(timeDelta.isPositive()) + return farPast() + } + } + + override fun hashCode(): Int { + return timeMark.hashCode() + } + + override fun equals(other: Any?): Boolean { + return other is TimeMark && other.timeMark == timeMark + } + + override fun toString(): String { + return "https://time.is/$timeMark" + } + + override fun compareTo(other: TimeMark): Int { + return this.timeMark.compareTo(other.timeMark) + } } diff --git a/src/main/kotlin/util/mc/SNbtFormatter.kt b/src/main/kotlin/util/mc/SNbtFormatter.kt new file mode 100644 index 0000000..e773927 --- /dev/null +++ b/src/main/kotlin/util/mc/SNbtFormatter.kt @@ -0,0 +1,138 @@ +package moe.nea.firmament.util.mc + +import net.minecraft.nbt.NbtByte +import net.minecraft.nbt.NbtByteArray +import net.minecraft.nbt.NbtCompound +import net.minecraft.nbt.NbtDouble +import net.minecraft.nbt.NbtElement +import net.minecraft.nbt.NbtEnd +import net.minecraft.nbt.NbtFloat +import net.minecraft.nbt.NbtInt +import net.minecraft.nbt.NbtIntArray +import net.minecraft.nbt.NbtList +import net.minecraft.nbt.NbtLong +import net.minecraft.nbt.NbtLongArray +import net.minecraft.nbt.NbtShort +import net.minecraft.nbt.NbtString +import net.minecraft.nbt.visitor.NbtElementVisitor + +class SNbtFormatter private constructor() : NbtElementVisitor { + private val result = StringBuilder() + private var indent = 0 + private fun writeIndent() { + result.append("\t".repeat(indent)) + } + + private fun pushIndent() { + indent++ + } + + private fun popIndent() { + indent-- + } + + fun apply(element: NbtElement): StringBuilder { + element.accept(this) + return result + } + + + override fun visitString(element: NbtString) { + result.append(NbtString.escape(element.asString())) + } + + override fun visitByte(element: NbtByte) { + result.append(element.numberValue()).append("b") + } + + override fun visitShort(element: NbtShort) { + result.append(element.shortValue()).append("s") + } + + override fun visitInt(element: NbtInt) { + result.append(element.intValue()) + } + + override fun visitLong(element: NbtLong) { + result.append(element.longValue()).append("L") + } + + override fun visitFloat(element: NbtFloat) { + result.append(element.floatValue()).append("f") + } + + override fun visitDouble(element: NbtDouble) { + result.append(element.doubleValue()).append("d") + } + + private fun visitArrayContents(array: List) { + array.forEachIndexed { index, element -> + writeIndent() + element.accept(this) + if (array.size != index + 1) { + result.append(",") + } + result.append("\n") + } + } + + private fun writeArray(arrayTypeTag: String, array: List) { + result.append("[").append(arrayTypeTag).append("\n") + pushIndent() + visitArrayContents(array) + popIndent() + writeIndent() + result.append("]") + + } + + override fun visitByteArray(element: NbtByteArray) { + writeArray("B;", element) + } + + override fun visitIntArray(element: NbtIntArray) { + writeArray("I;", element) + } + + override fun visitLongArray(element: NbtLongArray) { + writeArray("L;", element) + } + + override fun visitList(element: NbtList) { + writeArray("", element) + } + + override fun visitCompound(compound: NbtCompound) { + result.append("{\n") + pushIndent() + val keys = compound.keys.sorted() + keys.forEachIndexed { index, key -> + writeIndent() + val element = compound[key] ?: error("Key '$key' found but not present in compound: $compound") + val escapedName = if (key.matches(SIMPLE_NAME)) key else NbtString.escape(key) + result.append(escapedName).append(": ") + element.accept(this) + if (keys.size != index + 1) { + result.append(",") + } + result.append("\n") + } + popIndent() + writeIndent() + result.append("}") + } + + override fun visitEnd(element: NbtEnd) { + result.append("END") + } + + companion object { + fun prettify(nbt: NbtElement): String { + return SNbtFormatter().apply(nbt).toString() + } + + fun NbtElement.toPrettyString() = prettify(this) + + private val SIMPLE_NAME = "[A-Za-z0-9._+-]+".toRegex() + } +} diff --git a/src/main/kotlin/util/regex.kt b/src/main/kotlin/util/regex.kt index 78c90e8..a44435c 100644 --- a/src/main/kotlin/util/regex.kt +++ b/src/main/kotlin/util/regex.kt @@ -1,8 +1,14 @@ +@file:OptIn(ExperimentalTypeInference::class, ExperimentalContracts::class) + package moe.nea.firmament.util import java.util.regex.Matcher import java.util.regex.Pattern import org.intellij.lang.annotations.Language +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract +import kotlin.experimental.ExperimentalTypeInference import kotlin.time.Duration import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds @@ -10,10 +16,14 @@ import kotlin.time.Duration.Companion.seconds inline fun String.ifMatches(regex: Regex, block: (MatchResult) -> T): T? = regex.matchEntire(this)?.let(block) -inline fun Pattern.useMatch(string: String, block: Matcher.() -> T): T? = - matcher(string) +inline fun Pattern.useMatch(string: String, block: Matcher.() -> T): T? { + contract { + callsInPlace(block, InvocationKind.AT_MOST_ONCE) + } + return matcher(string) .takeIf(Matcher::matches) ?.let(block) +} @Language("RegExp") val TIME_PATTERN = "[0-9]+[ms]" diff --git a/src/main/kotlin/util/skyblock/AbilityUtils.kt b/src/main/kotlin/util/skyblock/AbilityUtils.kt new file mode 100644 index 0000000..0f0adbe --- /dev/null +++ b/src/main/kotlin/util/skyblock/AbilityUtils.kt @@ -0,0 +1,138 @@ +package moe.nea.firmament.util.skyblock + +import kotlin.time.Duration +import net.minecraft.item.ItemStack +import net.minecraft.text.Text +import moe.nea.firmament.util.ErrorUtil +import moe.nea.firmament.util.directLiteralStringContent +import moe.nea.firmament.util.mc.loreAccordingToNbt +import moe.nea.firmament.util.parseShortNumber +import moe.nea.firmament.util.parseTimePattern +import moe.nea.firmament.util.unformattedString +import moe.nea.firmament.util.useMatch + +object AbilityUtils { + data class ItemAbility( + val name: String, + val hasPowerScroll: Boolean, + val activation: AbilityActivation, + val manaCost: Int?, + val descriptionLines: List, + val cooldown: Duration?, + ) + + @JvmInline + value class AbilityActivation( + val label: String + ) { + companion object { + val RIGHT_CLICK = AbilityActivation("RIGHT CLICK") + val SNEAK_RIGHT_CLICK = AbilityActivation("SNEAK RIGHT CLICK") + val SNEAK = AbilityActivation("SNEAK") + val EMPTY = AbilityActivation("") + fun of(text: String?): AbilityActivation { + val trimmed = text?.trim() + if (trimmed.isNullOrBlank()) + return EMPTY + return AbilityActivation(trimmed) + } + } + } + + private val abilityNameRegex = "Ability: (?.*?) *".toPattern() + private fun findAbility(iterator: ListIterator): ItemAbility? { + if (!iterator.hasNext()) { + return null + } + val line = iterator.next() + // The actual information about abilities is stored in the siblings + if (line.directLiteralStringContent != "") return null + var powerScroll: Boolean = false // This should instead determine the power scroll based on text colour + var abilityName: String? = null + var activation: String? = null + var hasProcessedActivation = false + for (sibling in line.siblings) { + val directContent = sibling.directLiteralStringContent ?: continue + if (directContent == "⦾ ") { + powerScroll = true + continue + } + if (!hasProcessedActivation && abilityName != null) { + hasProcessedActivation = true + activation = directContent + continue + } + abilityNameRegex.useMatch(directContent) { + abilityName = group("name") + continue + } + if (abilityName != null) { + ErrorUtil.softError("Found abilityName $abilityName without finding but encountered unprocessable element in: $line") + } + return null + } + if (abilityName == null) return null + val descriptionLines = mutableListOf() + var manaCost: Int? = null + var cooldown: Duration? = null + while (iterator.hasNext()) { + val descriptionLine = iterator.next() + if (descriptionLine.unformattedString == "") break + var nextIsManaCost = false + var isSpecialLine = false + var nextIsDuration = false + for (sibling in descriptionLine.siblings) { + val directContent = sibling.directLiteralStringContent ?: continue + if ("Mana Cost: " == directContent) { // TODO: 'Soulflow Cost: ' support (or maybe a generic 'XXX Cost: ') + nextIsManaCost = true + isSpecialLine = true + continue + } + if ("Cooldown: " == directContent) { + nextIsDuration = true + isSpecialLine = true + continue + } + if (nextIsDuration) { + nextIsDuration = false + cooldown = parseTimePattern(directContent) + continue + } + if (nextIsManaCost) { + nextIsManaCost = false + manaCost = parseShortNumber(directContent).toInt() + continue + } + if (isSpecialLine) { + ErrorUtil.softError("Unknown special line segment: '$sibling' in '$descriptionLine'") + } + } + if (!isSpecialLine) { + descriptionLines.add(descriptionLine) + } + } + return ItemAbility( + abilityName, + powerScroll, + AbilityActivation.of(activation), + manaCost, + descriptionLines, + cooldown + ) + } + + fun getAbilities(lore: List): List { + val iterator = lore.listIterator() + val abilities = mutableListOf() + while (iterator.hasNext()) { + findAbility(iterator)?.let(abilities::add) + } + + return abilities + } + + fun getAbilities(itemStack: ItemStack): List { + return getAbilities(itemStack.loreAccordingToNbt) + } + +} diff --git a/src/main/kotlin/util/textutil.kt b/src/main/kotlin/util/textutil.kt index 36924a6..1cef5d4 100644 --- a/src/main/kotlin/util/textutil.kt +++ b/src/main/kotlin/util/textutil.kt @@ -90,6 +90,8 @@ fun CharSequence.removeColorCodes(keepNonColorCodes: Boolean = false): String { val Text.unformattedString: String get() = string.removeColorCodes() +val Text.directLiteralStringContent: String? get() = (this.content as? PlainTextContent)?.string() + fun Text.allSiblings(): List = listOf(this) + siblings.flatMap { it.allSiblings() } fun MutableText.withColor(formatting: Formatting) = this.styled { it.withColor(formatting).withItalic(false) } diff --git a/src/main/resources/assets/firmament/lang/en_us.json b/src/main/resources/assets/firmament/lang/en_us.json index d3ae936..131eae2 100644 --- a/src/main/resources/assets/firmament/lang/en_us.json +++ b/src/main/resources/assets/firmament/lang/en_us.json @@ -182,8 +182,9 @@ "firmament.config.power-user.copy-texture-pack-id": "Copy Texture Pack Id", "firmament.config.power-user.copy-skull-texture": "Copy Placed Skull Id", "firmament.config.power-user.entity-data": "Show Entity Data", - "firmament.config.power-user.copy-nbt-data": "Copy NBT data", + "firmament.config.power-user.copy-nbt-data": "Copy ExtraAttributes data", "firmament.config.power-user.copy-lore": "Copy Name + Lore", + "firmament.config.power-user.copy-item-stack": "Copy ItemStack", "firmament.config.power-user": "Power Users", "firmament.tooltip.skyblockid": "SkyBlock Id: %s", "firmament.tooltip.copied.skyblockid.fail": "Failed to copy SkyBlock Id", @@ -194,6 +195,7 @@ "firmament.tooltip.copied.skull.fail": "Failed to copy skull id.", "firmament.tooltip.copied.nbt": "Copied NBT data", "firmament.tooltip.copied.lore": "Copied Name and Lore", + "firmament.tooltip.copied.stack": "Copied ItemStack", "firmament.config.compatibility": "Intermod Features", "firmament.config.compatibility.explosion-enabled": "Redirect Enhanced Explosions", "firmament.config.compatibility.explosion-power": "Enhanced Explosion Power", diff --git a/src/main/resources/firmament.accesswidener b/src/main/resources/firmament.accesswidener index 49d4383..60b31e3 100644 --- a/src/main/resources/firmament.accesswidener +++ b/src/main/resources/firmament.accesswidener @@ -22,3 +22,8 @@ mutable field net/minecraft/screen/slot/Slot y I accessible field net/minecraft/entity/player/PlayerEntity PLAYER_MODEL_PARTS Lnet/minecraft/entity/data/TrackedData; accessible field net/minecraft/client/render/WorldRenderer chunks Lnet/minecraft/client/render/BuiltChunkStorage; +# Fix package-private access methods +accessible method net/minecraft/registry/entry/RegistryEntry$Reference setRegistryKey (Lnet/minecraft/registry/RegistryKey;)V +accessible method net/minecraft/entity/LivingEntity getHitbox ()Lnet/minecraft/util/math/Box; +accessible method net/minecraft/registry/entry/RegistryEntryList$Named (Lnet/minecraft/registry/entry/RegistryEntryOwner;Lnet/minecraft/registry/tag/TagKey;)V +accessible method net/minecraft/registry/entry/RegistryEntry$Reference setValue (Ljava/lang/Object;)V diff --git a/src/test/kotlin/moe/nea/firmament/test/ColorCode.kt b/src/test/kotlin/moe/nea/firmament/test/ColorCode.kt deleted file mode 100644 index 5889bc7..0000000 --- a/src/test/kotlin/moe/nea/firmament/test/ColorCode.kt +++ /dev/null @@ -1,58 +0,0 @@ - -package moe.nea.firmament.test - -import org.junit.jupiter.api.Assertions -import org.junit.jupiter.api.Test -import moe.nea.firmament.util.removeColorCodes - - -class ColorCode { - @Test - fun testWhatever() { - Assertions.assertEquals("", "".removeColorCodes().toString()) - Assertions.assertEquals("", "§".removeColorCodes().toString()) - Assertions.assertEquals("", "§a".removeColorCodes().toString()) - Assertions.assertEquals("ab", "a§ab".removeColorCodes().toString()) - Assertions.assertEquals("ab", "a§ab§§".removeColorCodes().toString()) - Assertions.assertEquals("abc", "a§ab§§c".removeColorCodes().toString()) - Assertions.assertEquals("bc", "§ab§§c".removeColorCodes().toString()) - Assertions.assertEquals("b§lc", "§ab§l§§c".removeColorCodes(true).toString()) - Assertions.assertEquals("b§lc§l", "§ab§l§§c§l".removeColorCodes(true).toString()) - Assertions.assertEquals("§lb§lc", "§l§ab§l§§c".removeColorCodes(true).toString()) - } - - @Test - fun testEdging() { - Assertions.assertEquals("", "§".removeColorCodes()) - Assertions.assertEquals("a", "a§".removeColorCodes()) - Assertions.assertEquals("b", "§ab§".removeColorCodes()) - } - - @Test - fun `testDouble§`() { - Assertions.assertEquals("1", "§§1".removeColorCodes()) - } - - @Test - fun testKeepNonColor() { - Assertions.assertEquals("§k§l§m§n§o§r", "§k§l§m§f§n§o§r".removeColorCodes(true)) - } - - @Test - fun testPlainString() { - Assertions.assertEquals("bcdefgp", "bcdefgp".removeColorCodes()) - Assertions.assertEquals("", "".removeColorCodes()) - } - - @Test - fun testSomeNormalTestCases() { - Assertions.assertEquals( - "You are not currently in a party.", - "§r§cYou are not currently in a party.§r".removeColorCodes() - ) - Assertions.assertEquals( - "Ancient Necron's Chestplate ✪✪✪✪", - "§dAncient Necron's Chestplate §6✪§6✪§6✪§6✪".removeColorCodes() - ) - } -} diff --git a/src/test/kotlin/root.kt b/src/test/kotlin/root.kt new file mode 100644 index 0000000..679ecb4 --- /dev/null +++ b/src/test/kotlin/root.kt @@ -0,0 +1,29 @@ +package moe.nea.firmament.test + +import net.minecraft.Bootstrap +import net.minecraft.SharedConstants +import moe.nea.firmament.util.TimeMark + + object FirmTestBootstrap { + val loadStart = TimeMark.now() + + init { + println("Bootstrap started at $loadStart") + } + + init { + SharedConstants.createGameVersion() + Bootstrap.initialize() + } + + val loadEnd = TimeMark.now() + + val loadDuration = loadStart.passedAt(loadEnd) + + init { + println("Bootstrap completed at $loadEnd after $loadDuration") + } + + fun bootstrapMinecraft() { + } +} diff --git a/src/test/kotlin/testutil/ItemResources.kt b/src/test/kotlin/testutil/ItemResources.kt new file mode 100644 index 0000000..bd3c438 --- /dev/null +++ b/src/test/kotlin/testutil/ItemResources.kt @@ -0,0 +1,30 @@ +package moe.nea.firmament.test.testutil + +import net.minecraft.item.ItemStack +import net.minecraft.nbt.NbtCompound +import net.minecraft.nbt.NbtOps +import net.minecraft.nbt.StringNbtReader +import moe.nea.firmament.test.FirmTestBootstrap + +object ItemResources { + init { + FirmTestBootstrap.bootstrapMinecraft() + } + + fun loadString(path: String): String { + require(!path.startsWith("/")) + return ItemResources::class.java.classLoader + .getResourceAsStream(path)!! + .readAllBytes().decodeToString() + } + + fun loadSNbt(path: String): NbtCompound { + return StringNbtReader.parse(loadString(path)) + } + + fun loadItem(name: String): ItemStack { + // TODO: make the load work with enchantments + return ItemStack.CODEC.parse(NbtOps.INSTANCE, loadSNbt("testdata/items/$name.snbt")) + .getOrThrow { IllegalStateException("Could not load test item '$name': $it") } + } +} diff --git a/src/test/kotlin/util/ColorCodeTest.kt b/src/test/kotlin/util/ColorCodeTest.kt new file mode 100644 index 0000000..d9de36a --- /dev/null +++ b/src/test/kotlin/util/ColorCodeTest.kt @@ -0,0 +1,59 @@ +package moe.nea.firmament.test.util + +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import net.minecraft.Bootstrap +import net.minecraft.SharedConstants +import moe.nea.firmament.util.removeColorCodes + + +class ColorCodeTest { + @Test + fun testWhatever() { + Assertions.assertEquals("", "".removeColorCodes().toString()) + Assertions.assertEquals("", "§".removeColorCodes().toString()) + Assertions.assertEquals("", "§a".removeColorCodes().toString()) + Assertions.assertEquals("ab", "a§ab".removeColorCodes().toString()) + Assertions.assertEquals("ab", "a§ab§§".removeColorCodes().toString()) + Assertions.assertEquals("abc", "a§ab§§c".removeColorCodes().toString()) + Assertions.assertEquals("bc", "§ab§§c".removeColorCodes().toString()) + Assertions.assertEquals("b§lc", "§ab§l§§c".removeColorCodes(true).toString()) + Assertions.assertEquals("b§lc§l", "§ab§l§§c§l".removeColorCodes(true).toString()) + Assertions.assertEquals("§lb§lc", "§l§ab§l§§c".removeColorCodes(true).toString()) + } + + @Test + fun testEdging() { + Assertions.assertEquals("", "§".removeColorCodes()) + Assertions.assertEquals("a", "a§".removeColorCodes()) + Assertions.assertEquals("b", "§ab§".removeColorCodes()) + } + + @Test + fun `testDouble§`() { + Assertions.assertEquals("1", "§§1".removeColorCodes()) + } + + @Test + fun testKeepNonColor() { + Assertions.assertEquals("§k§l§m§n§o§r", "§k§l§m§f§n§o§r".removeColorCodes(true)) + } + + @Test + fun testPlainString() { + Assertions.assertEquals("bcdefgp", "bcdefgp".removeColorCodes()) + Assertions.assertEquals("", "".removeColorCodes()) + } + + @Test + fun testSomeNormalTestCases() { + Assertions.assertEquals( + "You are not currently in a party.", + "§r§cYou are not currently in a party.§r".removeColorCodes() + ) + Assertions.assertEquals( + "Ancient Necron's Chestplate ✪✪✪✪", + "§dAncient Necron's Chestplate §6✪§6✪§6✪§6✪".removeColorCodes() + ) + } +} diff --git a/src/test/kotlin/util/skyblock/AbilityUtilsTest.kt b/src/test/kotlin/util/skyblock/AbilityUtilsTest.kt new file mode 100644 index 0000000..abe739d --- /dev/null +++ b/src/test/kotlin/util/skyblock/AbilityUtilsTest.kt @@ -0,0 +1,79 @@ +package moe.nea.firmament.test.util.skyblock + +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds +import net.minecraft.text.Text +import moe.nea.firmament.test.testutil.ItemResources +import moe.nea.firmament.util.skyblock.AbilityUtils +import moe.nea.firmament.util.unformattedString + +class AbilityUtilsTest { + + fun List.stripDescriptions() = map { + it.copy(descriptionLines = it.descriptionLines.map { Text.literal(it.unformattedString) }) + } + + @Test + fun testUnpoweredDrill() { + Assertions.assertEquals( + listOf( + AbilityUtils.ItemAbility( + "Pickobulus", + false, + AbilityUtils.AbilityActivation.RIGHT_CLICK, + null, + listOf("Throw your pickaxe to create an", + "explosion mining all ores in a 3 block", + "radius.").map(Text::literal), + 48.seconds + ) + ), + AbilityUtils.getAbilities(ItemResources.loadItem("titanium-drill")).stripDescriptions() + ) + } + + @Test + fun testPoweredPickaxe() { + Assertions.assertEquals( + listOf( + AbilityUtils.ItemAbility( + "Mining Speed Boost", + true, + AbilityUtils.AbilityActivation.RIGHT_CLICK, + null, + listOf("Grants +200% ⸕ Mining Speed for", + "10s.").map(Text::literal), + 2.minutes + ) + ), + AbilityUtils.getAbilities(ItemResources.loadItem("diamond-pickaxe")).stripDescriptions() + ) + } + + @Test + fun testAOTV() { + Assertions.assertEquals( + listOf( + AbilityUtils.ItemAbility( + "Instant Transmission", true, AbilityUtils.AbilityActivation.RIGHT_CLICK, 23, + listOf("Teleport 12 blocks ahead of you and", + "gain +50 ✦ Speed for 3 seconds.").map(Text::literal), + null + ), + AbilityUtils.ItemAbility( + "Ether Transmission", + false, + AbilityUtils.AbilityActivation.SNEAK_RIGHT_CLICK, + 90, + listOf("Teleport to your targeted block up", + "to 61 blocks away.", + "Soulflow Cost: 1").map(Text::literal), + null + ) + ), + AbilityUtils.getAbilities(ItemResources.loadItem("aspect-of-the-void")).stripDescriptions() + ) + } +} diff --git a/src/test/resources/testdata/items/aspect-of-the-void.snbt b/src/test/resources/testdata/items/aspect-of-the-void.snbt new file mode 100644 index 0000000..180c069 --- /dev/null +++ b/src/test/resources/testdata/items/aspect-of-the-void.snbt @@ -0,0 +1,59 @@ +{ + components: { + "minecraft:attribute_modifiers": { + modifiers: [ + ], + show_in_tooltip: 0b + }, + "minecraft:custom_data": { + donated_museum: 1b, + enchantments: { + ultimate_wise: 5 + }, + ethermerge: 1b, + gems: { + }, + id: "ASPECT_OF_THE_VOID", + modifier: "heroic", + originTag: "ASPECT_OF_THE_VOID", + power_ability_scroll: "SAPPHIRE_POWER_SCROLL", + timestamp: 1641640380000L, + tuned_transmission: 4, + uuid: "b0572534-eb14-46cd-90c6-0df878fd56a2" + }, + "minecraft:custom_name": '{"extra":[{"color":"dark_purple","text":"Heroic Aspect of the Void"}],"italic":false,"text":""}', + "minecraft:enchantment_glint_override": 1b, + "minecraft:hide_additional_tooltip": { + }, + "minecraft:lore": [ + '{"extra":[{"color":"gray","text":"Damage: "},{"color":"red","text":"+120"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Strength: "},{"color":"red","text":"+132 "},{"color":"blue","text":"(+32)"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Bonus Attack Speed: "},{"color":"red","text":"+3% "},{"color":"blue","text":"(+3%)"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Intelligence: "},{"color":"green","text":"+80 "},{"color":"blue","text":"(+80)"}],"italic":false,"text":""}', + '{"extra":[" ",{"color":"dark_gray","text":"["},{"color":"gray","text":"✎"},{"color":"dark_gray","text":"]"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"blue","text":""},{"bold":true,"color":"light_purple","text":"Ultimate Wise V"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Reduces the ability mana cost of this"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"item by "},{"color":"green","text":"50%"},{"color":"gray","text":"."}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"bold":true,"color":"aqua","text":"⦾ "},{"color":"gold","text":"Ability: Instant Transmission "},{"bold":true,"color":"yellow","text":"RIGHT CLICK"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Teleport "},{"color":"green","text":"12 blocks"},{"color":"gray","text":" ahead of you and"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"gain "},{"color":"green","text":"+50 "},{"color":"white","text":"✦ Speed"},{"color":"gray","text":" for "},{"color":"green","text":"3 seconds"},{"color":"gray","text":"."}],"italic":false,"text":""}', + '{"extra":[{"color":"dark_gray","text":"Mana Cost: "},{"color":"dark_aqua","text":"23"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gold","text":"Ability: Ether Transmission "},{"bold":true,"color":"yellow","text":"SNEAK RIGHT CLICK"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Teleport to your targeted block up"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"to "},{"color":"green","text":"61 blocks "},{"color":"gray","text":"away."}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"dark_gray","text":"Soulflow Cost: "},{"color":"dark_aqua","text":"1"}],"italic":false,"text":""}', + '{"extra":[{"color":"dark_gray","text":"Mana Cost: "},{"color":"dark_aqua","text":"90"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"bold":true,"color":"dark_gray","text":"* "},{"color":"dark_gray","text":"Co-op Soulbound "},{"bold":true,"color":"dark_gray","text":"*"}],"italic":false,"text":""}', + '{"extra":[{"bold":true,"color":"dark_purple","text":"EPIC SWORD"}],"italic":false,"text":""}' + ], + "minecraft:unbreakable": { + show_in_tooltip: 0b + } + }, + count: 1, + id: "minecraft:diamond_shovel" +} diff --git a/src/test/resources/testdata/items/diamond-pickaxe.snbt b/src/test/resources/testdata/items/diamond-pickaxe.snbt new file mode 100644 index 0000000..cce12f9 --- /dev/null +++ b/src/test/resources/testdata/items/diamond-pickaxe.snbt @@ -0,0 +1,48 @@ +{ + components: { + "minecraft:attribute_modifiers": { + modifiers: [ + ], + show_in_tooltip: 0b + }, + "minecraft:custom_data": { + enchantments: { + efficiency: 10 + }, + id: "DIAMOND_PICKAXE", + power_ability_scroll: "SAPPHIRE_POWER_SCROLL", + timestamp: 1659795180000L, + uuid: "d213f48e-d927-4748-a58c-eb80735025b7" + }, + "minecraft:custom_name": '{"extra":[{"color":"green","text":"Diamond Pickaxe"}],"italic":false,"text":""}', + "minecraft:enchantments": { + levels: { + } + }, + "minecraft:hide_additional_tooltip": { + }, + "minecraft:lore": [ + '{"extra":[{"color":"dark_gray","text":"Breaking Power 4"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Damage: "},{"color":"red","text":"+30"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Mining Speed: "},{"color":"green","text":"+220"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"blue","text":"Efficiency X"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Increases how quickly your tool"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"breaks blocks."}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"bold":true,"color":"aqua","text":"⦾ "},{"color":"gold","text":"Ability: Mining Speed Boost "},{"bold":true,"color":"yellow","text":"RIGHT CLICK"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Grants "},{"color":"gold","text":"+200% "},{"color":"gold","text":"⸕ Mining Speed "},{"color":"gray","text":"for"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"green","text":"10s"},{"color":"gray","text":"."}],"italic":false,"text":""}', + '{"extra":[{"color":"dark_gray","text":"Cooldown: "},{"color":"green","text":"120s"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"dark_gray","text":"This item can be reforged!"}],"italic":false,"text":""}', + '{"extra":[{"bold":true,"color":"green","text":"UNCOMMON PICKAXE"}],"italic":false,"text":""}' + ], + "minecraft:unbreakable": { + show_in_tooltip: 0b + } + }, + count: 1, + id: "minecraft:diamond_pickaxe" +} diff --git a/src/test/resources/testdata/items/titanium-drill.snbt b/src/test/resources/testdata/items/titanium-drill.snbt new file mode 100644 index 0000000..e3b6819 --- /dev/null +++ b/src/test/resources/testdata/items/titanium-drill.snbt @@ -0,0 +1,97 @@ +{ + components: { + "minecraft:attribute_modifiers": { + modifiers: [ + ], + show_in_tooltip: 0b + }, + "minecraft:custom_data": { + compact_blocks: 1023815, + donated_museum: 1b, + drill_fuel: 16621, + drill_part_fuel_tank: "titanium_fuel_tank", + drill_part_upgrade_module: "goblin_omelette_blue_cheese", + enchantments: { + compact: 10, + efficiency: 5, + experience: 3, + fortune: 3, + paleontologist: 2, + pristine: 5 + }, + gems: { + AMBER_0: { + quality: "PERFECT", + uuid: "d28be6ae-75eb-49e4-90d8-31759db18d79" + }, + JADE_0: { + quality: "PERFECT", + uuid: "657fea0b-88e2-483d-9d2c-0b821797a55a" + }, + MINING_0: { + quality: "PERFECT", + uuid: "257bdcd2-585b-48b9-9517-a2e841dc0574" + }, + MINING_0_gem: "TOPAZ", + unlocked_slots: [ + "JADE_0", + "MINING_0" + ] + }, + id: "TITANIUM_DRILL_4", + modifier: "auspicious", + rarity_upgrades: 1, + timestamp: 1700577120000L, + uuid: "367b85ab-5bb4-43b6-a055-084cbaaafc1c" + }, + "minecraft:custom_name": '{"extra":[{"color":"light_purple","text":"Auspicious Titanium Drill DR-X655"}],"italic":false,"text":""}', + "minecraft:enchantment_glint_override": 1b, + "minecraft:hide_additional_tooltip": { + }, + "minecraft:lore": [ + '{"extra":[{"color":"dark_gray","text":"Breaking Power 9"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Damage: "},{"color":"red","text":"+75"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Mining Speed: "},{"color":"green","text":"+1,885 "},{"color":"blue","text":"(+75) "},{"color":"light_purple","text":"(+100)"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Pristine: "},{"color":"green","text":"+4.5 "},{"color":"light_purple","text":"(+2)"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Mining Fortune: "},{"color":"green","text":"+220 "},{"color":"blue","text":"(+20) "},{"color":"light_purple","text":"(+50)"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Mining Wisdom: "},{"color":"green","text":"+10"}],"italic":false,"text":""}', + '{"extra":[" ",{"color":"gold","text":"["},{"color":"gold","text":"⸕"},{"color":"gold","text":"] "},{"color":"gold","text":"["},{"color":"green","text":"☘"},{"color":"gold","text":"] "},{"color":"gold","text":"["},{"color":"yellow","text":"✦"},{"color":"gold","text":"]"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"blue","text":"Compact X"}],"italic":false,"text":""}', + '{"extra":[{"color":"blue","text":"Efficiency V"}],"italic":false,"text":""}', + '{"extra":[{"color":"blue","text":"Experience III"}],"italic":false,"text":""}', + '{"extra":[{"color":"blue","text":"Fortune III"}],"italic":false,"text":""}', + '{"extra":[{"color":"blue","text":"Paleontologist II"}],"italic":false,"text":""}', + '{"extra":[{"color":"blue","text":"Prismatic V"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"green","text":"Titanium-Infused Fuel Tank."}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"gray","text":""},{"color":"dark_green","text":"25,000 Max Fuel Capacity."}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"gray","text":""},{"color":"green","text":"-4% Pickaxe Ability Cooldown."}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"gray","text":"Drill Engine: "},{"color":"red","text":"Not Installed"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"gray","text":"Increases "},{"color":"gold","text":"⸕ Mining Speed "},{"color":"gray","text":"with part"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"installed."}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"green","text":"Blue Cheese Goblin Omelette Part."}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Adds "},{"color":"green","text":"+1 Level "},{"color":"gray","text":"to all of your unlocked "},{"color":"dark_purple","text":"Heart of"}],"italic":false,"text":""}', + '{"extra":[{"color":"dark_purple","text":"the Mountain "},{"color":"gray","text":"perks."}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"gray","text":"Fuel: "},{"color":"dark_green","text":"16,621"},{"color":"dark_gray","text":"/25k"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gold","text":"Ability: Pickobulus "},{"bold":true,"color":"yellow","text":"RIGHT CLICK"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Throw your pickaxe to create an"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"explosion mining all ores in a "},{"color":"green","text":"3 "},{"color":"gray","text":"block"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"radius."}],"italic":false,"text":""}', + '{"extra":[{"color":"dark_gray","text":"Cooldown: "},{"color":"green","text":"48s"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"blue","text":"Auspicious Bonus"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Grants "},{"color":"gold","text":"+0.9% "},{"color":"gold","text":"☘ Mining Fortune"},{"color":"gray","text":"."}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"bold":true,"color":"dark_gray","text":"* "},{"color":"dark_gray","text":"Co-op Soulbound "},{"bold":true,"color":"dark_gray","text":"*"}],"italic":false,"text":""}', + '{"extra":[{"bold":true,"color":"light_purple","obfuscated":true,"text":"a"},"",{"bold":false,"extra":[" "],"italic":false,"obfuscated":false,"strikethrough":false,"text":"","underlined":false},{"bold":true,"color":"light_purple","text":"MYTHIC DRILL "},{"bold":true,"color":"light_purple","obfuscated":true,"text":"a"}],"italic":false,"text":""}' + ] + }, + count: 1, + id: "minecraft:prismarine_shard" +} -- cgit