diff options
author | Linnea Gräf <nea@nea.moe> | 2024-08-28 19:04:24 +0200 |
---|---|---|
committer | Linnea Gräf <nea@nea.moe> | 2024-08-28 19:04:24 +0200 |
commit | d2f240ff0ca0d27f417f837e706c781a98c31311 (patch) | |
tree | 0db7aff6cc14deaf36eed83889d59fd6b3a6f599 /src/main/kotlin/features/mining | |
parent | a6906308163aa3b2d18fa1dc1aa71ac9bbcc83ab (diff) | |
download | firmament-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.kt | 81 | ||||
-rw-r--r-- | src/main/kotlin/features/mining/PickaxeAbility.kt | 176 | ||||
-rw-r--r-- | src/main/kotlin/features/mining/PristineProfitTracker.kt | 133 |
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() + } + } +} |