aboutsummaryrefslogtreecommitdiff
path: root/src/main/kotlin/features/mining
diff options
context:
space:
mode:
authorLinnea Gräf <nea@nea.moe>2024-08-28 19:04:24 +0200
committerLinnea Gräf <nea@nea.moe>2024-08-28 19:04:24 +0200
commitd2f240ff0ca0d27f417f837e706c781a98c31311 (patch)
tree0db7aff6cc14deaf36eed83889d59fd6b3a6f599 /src/main/kotlin/features/mining
parenta6906308163aa3b2d18fa1dc1aa71ac9bbcc83ab (diff)
downloadFirmament-d2f240ff0ca0d27f417f837e706c781a98c31311.tar.gz
Firmament-d2f240ff0ca0d27f417f837e706c781a98c31311.tar.bz2
Firmament-d2f240ff0ca0d27f417f837e706c781a98c31311.zip
Refactor source layout
Introduce compat source sets and move all kotlin sources to the main directory [no changelog]
Diffstat (limited to 'src/main/kotlin/features/mining')
-rw-r--r--src/main/kotlin/features/mining/Histogram.kt81
-rw-r--r--src/main/kotlin/features/mining/PickaxeAbility.kt176
-rw-r--r--src/main/kotlin/features/mining/PristineProfitTracker.kt133
3 files changed, 390 insertions, 0 deletions
diff --git a/src/main/kotlin/features/mining/Histogram.kt b/src/main/kotlin/features/mining/Histogram.kt
new file mode 100644
index 0000000..ed48437
--- /dev/null
+++ b/src/main/kotlin/features/mining/Histogram.kt
@@ -0,0 +1,81 @@
+
+package moe.nea.firmament.features.mining
+
+import java.util.*
+import kotlin.time.Duration
+import moe.nea.firmament.util.TimeMark
+
+class Histogram<T>(
+ val maxSize: Int,
+ val maxDuration: Duration,
+) {
+
+ data class OrderedTimestamp(val timestamp: TimeMark, val order: Int) : Comparable<OrderedTimestamp> {
+ override fun compareTo(other: OrderedTimestamp): Int {
+ val o = timestamp.compareTo(other.timestamp)
+ if (o != 0) return o
+ return order.compareTo(other.order)
+ }
+ }
+
+ val size: Int get() = dataPoints.size
+ private val dataPoints: NavigableMap<OrderedTimestamp, T> = TreeMap()
+
+ private var order = Int.MIN_VALUE
+
+ fun record(entry: T, timestamp: TimeMark = TimeMark.now()) {
+ dataPoints[OrderedTimestamp(timestamp, order++)] = entry
+ trim()
+ }
+
+ fun oldestUpdate(): TimeMark {
+ trim()
+ return if (dataPoints.isEmpty()) TimeMark.now() else dataPoints.firstKey().timestamp
+ }
+
+ fun latestUpdate(): TimeMark {
+ trim()
+ return if (dataPoints.isEmpty()) TimeMark.farPast() else dataPoints.lastKey().timestamp
+ }
+
+ fun averagePer(valueExtractor: (T) -> Double, perDuration: Duration): Double? {
+ return aggregate(
+ seed = 0.0,
+ operator = { accumulator, entry, _ -> accumulator + valueExtractor(entry) },
+ finish = { sum, beginning, end ->
+ val timespan = end - beginning
+ if (timespan > perDuration)
+ sum / (timespan / perDuration)
+ else null
+ })
+ }
+
+ fun <V, R> aggregate(
+ seed: V,
+ operator: (V, T, TimeMark) -> V,
+ finish: (V, TimeMark, TimeMark) -> R
+ ): R? {
+ trim()
+ var accumulator = seed
+ var min: TimeMark? = null
+ var max: TimeMark? = null
+ dataPoints.forEach { (key, value) ->
+ max = key.timestamp
+ if (min == null)
+ min = key.timestamp
+ accumulator = operator(accumulator, value, key.timestamp)
+ }
+ if (min == null)
+ return null
+ return finish(accumulator, min!!, max!!)
+ }
+
+ private fun trim() {
+ while (maxSize < dataPoints.size) {
+ dataPoints.pollFirstEntry()
+ }
+ dataPoints.headMap(OrderedTimestamp(TimeMark.ago(maxDuration), Int.MAX_VALUE)).clear()
+ }
+
+
+}
diff --git a/src/main/kotlin/features/mining/PickaxeAbility.kt b/src/main/kotlin/features/mining/PickaxeAbility.kt
new file mode 100644
index 0000000..7879f2d
--- /dev/null
+++ b/src/main/kotlin/features/mining/PickaxeAbility.kt
@@ -0,0 +1,176 @@
+
+package moe.nea.firmament.features.mining
+
+import java.util.regex.Pattern
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.seconds
+import net.minecraft.item.ItemStack
+import net.minecraft.util.DyeColor
+import net.minecraft.util.Hand
+import net.minecraft.util.Identifier
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.HudRenderEvent
+import moe.nea.firmament.events.ProcessChatEvent
+import moe.nea.firmament.events.SlotClickEvent
+import moe.nea.firmament.events.WorldReadyEvent
+import moe.nea.firmament.features.FirmamentFeature
+import moe.nea.firmament.gui.config.ManagedConfig
+import moe.nea.firmament.util.DurabilityBarEvent
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.SHORT_NUMBER_FORMAT
+import moe.nea.firmament.util.TIME_PATTERN
+import moe.nea.firmament.util.TimeMark
+import moe.nea.firmament.util.extraAttributes
+import moe.nea.firmament.util.item.displayNameAccordingToNbt
+import moe.nea.firmament.util.item.loreAccordingToNbt
+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.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<String, TimeMark>()
+ var abilityOverride: String? = null
+ var defaultAbilityDurations = mutableMapOf<String, Duration>(
+ "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
+ if (sinceLastUsage < cooldown)
+ return sinceLastUsage / cooldown
+ val sinceLobbyJoin = lobbyJoinTime.passedTime()
+ val halfCooldown = cooldown / 2
+ if (sinceLobbyJoin < halfCooldown) {
+ return (sinceLobbyJoin / halfCooldown)
+ }
+ 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) {
+ lastUsage.clear()
+ lobbyJoinTime = TimeMark.now()
+ abilityOverride = null
+ }
+
+ val abilityUsePattern = Pattern.compile("You used your (?<name>.*) Pickaxe Ability!")
+ val fuelPattern = Pattern.compile("Fuel: .*/(?<maxFuel>$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") == true })
+ 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: (?<cooldown>$TIME_PATTERN)")
+ val abilityPattern = Pattern.compile("Ability: (?<name>.*) {2}RIGHT CLICK")
+ val abilitySwitchPattern =
+ Pattern.compile("You selected (?<ability>.*) 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()
+ }
+}
diff --git a/src/main/kotlin/features/mining/PristineProfitTracker.kt b/src/main/kotlin/features/mining/PristineProfitTracker.kt
new file mode 100644
index 0000000..f1bc7e5
--- /dev/null
+++ b/src/main/kotlin/features/mining/PristineProfitTracker.kt
@@ -0,0 +1,133 @@
+
+package moe.nea.firmament.features.mining
+
+import io.github.notenoughupdates.moulconfig.xml.Bind
+import moe.nea.jarvis.api.Point
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.serializer
+import kotlin.time.Duration.Companion.seconds
+import net.minecraft.text.Text
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.ProcessChatEvent
+import moe.nea.firmament.features.FirmamentFeature
+import moe.nea.firmament.gui.config.ManagedConfig
+import moe.nea.firmament.gui.hud.MoulConfigHud
+import moe.nea.firmament.util.BazaarPriceStrategy
+import moe.nea.firmament.util.FirmFormatters.formatCommas
+import moe.nea.firmament.util.SkyblockId
+import moe.nea.firmament.util.data.ProfileSpecificDataHolder
+import moe.nea.firmament.util.formattedString
+import moe.nea.firmament.util.parseIntWithComma
+import moe.nea.firmament.util.useMatch
+
+object PristineProfitTracker : FirmamentFeature {
+ override val identifier: String
+ get() = "pristine-profit"
+
+ enum class GemstoneKind(
+ val label: String,
+ val flawedId: SkyblockId,
+ ) {
+ SAPPHIRE("Sapphire", SkyblockId("FLAWED_SAPPHIRE_GEM")),
+ RUBY("Ruby", SkyblockId("FLAWED_RUBY_GEM")),
+ AMETHYST("Amethyst", SkyblockId("FLAWED_AMETHYST_GEM")),
+ AMBER("Amber", SkyblockId("FLAWED_AMBER_GEM")),
+ TOPAZ("Topaz", SkyblockId("FLAWED_TOPAZ_GEM")),
+ JADE("Jade", SkyblockId("FLAWED_JADE_GEM")),
+ JASPER("Jasper", SkyblockId("FLAWED_JASPER_GEM")),
+ OPAL("Opal", SkyblockId("FLAWED_OPAL_GEM")),
+ }
+
+ @Serializable
+ data class Data(
+ var maxMoneyPerSecond: Double = 1.0,
+ var maxCollectionPerSecond: Double = 1.0,
+ )
+
+ object DConfig : ProfileSpecificDataHolder<Data>(serializer(), identifier, ::Data)
+
+ override val config: ManagedConfig?
+ get() = TConfig
+
+ object TConfig : ManagedConfig(identifier) {
+ val timeout by duration("timeout", 0.seconds, 120.seconds) { 30.seconds }
+ val gui by position("position", 80, 30) { Point(0.05, 0.2) }
+ }
+
+ val sellingStrategy = BazaarPriceStrategy.SELL_ORDER
+
+ val pristineRegex =
+ "PRISTINE! You found . Flawed (?<kind>${
+ GemstoneKind.entries.joinToString("|") { it.label }
+ }) Gemstone x(?<count>[0-9,]+)!".toPattern()
+
+ val collectionHistogram = Histogram<Double>(10000, 180.seconds)
+ val moneyHistogram = Histogram<Double>(10000, 180.seconds)
+
+ object ProfitHud : MoulConfigHud("pristine_profit", TConfig.gui) {
+ @field:Bind
+ var moneyCurrent: Double = 0.0
+
+ @field:Bind
+ var moneyMax: Double = 1.0
+
+ @field:Bind
+ var moneyText = ""
+
+ @field:Bind
+ var collectionCurrent = 0.0
+
+ @field:Bind
+ var collectionMax = 1.0
+
+ @field:Bind
+ var collectionText = ""
+ override fun shouldRender(): Boolean = collectionHistogram.latestUpdate().passedTime() < TConfig.timeout
+ }
+
+ val SECONDS_PER_HOUR = 3600
+ val ROUGHS_PER_FLAWED = 80
+
+ fun updateUi() {
+ val collectionPerSecond = collectionHistogram.averagePer({ it }, 1.seconds)
+ val moneyPerSecond = moneyHistogram.averagePer({ it }, 1.seconds)
+ if (collectionPerSecond == null || moneyPerSecond == null) return
+ ProfitHud.collectionCurrent = collectionPerSecond
+ ProfitHud.collectionText = Text.stringifiedTranslatable("firmament.pristine-profit.collection",
+ formatCommas(collectionPerSecond * SECONDS_PER_HOUR,
+ 1)).formattedString()
+ ProfitHud.moneyCurrent = moneyPerSecond
+ ProfitHud.moneyText = Text.stringifiedTranslatable("firmament.pristine-profit.money",
+ formatCommas(moneyPerSecond * SECONDS_PER_HOUR, 1))
+ .formattedString()
+ val data = DConfig.data
+ if (data != null) {
+ if (data.maxCollectionPerSecond < collectionPerSecond && collectionHistogram.oldestUpdate()
+ .passedTime() > 30.seconds
+ ) {
+ data.maxCollectionPerSecond = collectionPerSecond
+ DConfig.markDirty()
+ }
+ if (data.maxMoneyPerSecond < moneyPerSecond && moneyHistogram.oldestUpdate().passedTime() > 30.seconds) {
+ data.maxMoneyPerSecond = moneyPerSecond
+ DConfig.markDirty()
+ }
+ ProfitHud.collectionMax = maxOf(data.maxCollectionPerSecond, collectionPerSecond)
+ ProfitHud.moneyMax = maxOf(data.maxMoneyPerSecond, moneyPerSecond)
+ }
+ }
+
+
+ @Subscribe
+ fun onMessage(it: ProcessChatEvent) {
+ pristineRegex.useMatch(it.unformattedString) {
+ val gemstoneKind = GemstoneKind.valueOf(group("kind").uppercase())
+ val flawedCount = parseIntWithComma(group("count"))
+ val moneyAmount = sellingStrategy.getSellPrice(gemstoneKind.flawedId) * flawedCount
+ moneyHistogram.record(moneyAmount)
+ val collectionAmount = flawedCount * ROUGHS_PER_FLAWED
+ collectionHistogram.record(collectionAmount.toDouble())
+ updateUi()
+ }
+ }
+}