Refactor source layout
Introduce compat source sets and move all kotlin sources to the main directory [no changelog]
This commit is contained in:
81
src/main/kotlin/features/mining/Histogram.kt
Normal file
81
src/main/kotlin/features/mining/Histogram.kt
Normal file
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
176
src/main/kotlin/features/mining/PickaxeAbility.kt
Normal file
176
src/main/kotlin/features/mining/PickaxeAbility.kt
Normal file
@@ -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()
|
||||
}
|
||||
}
|
||||
133
src/main/kotlin/features/mining/PristineProfitTracker.kt
Normal file
133
src/main/kotlin/features/mining/PristineProfitTracker.kt
Normal file
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user