From 977620f1b5218cc8a041742f970974a4bfff29cc Mon Sep 17 00:00:00 2001 From: Linnea Gräf Date: Sat, 5 Oct 2024 12:06:23 +0200 Subject: Add minion hopper tracker --- build.gradle.kts | 7 +- src/main/kotlin/moe/nea/ledger/ExpiringValue.kt | 28 ++++++++ src/main/kotlin/moe/nea/ledger/ItemIdProvider.kt | 74 +++++++++++----------- src/main/kotlin/moe/nea/ledger/Ledger.kt | 1 + src/main/kotlin/moe/nea/ledger/MinionDetection.kt | 42 ++++++++++++ src/main/kotlin/moe/nea/ledger/NumberUtil.kt | 68 +++++++++++++++----- src/test/kotlin/moe/nea/ledger/NumberUtilKtTest.kt | 17 +++++ 7 files changed, 184 insertions(+), 53 deletions(-) create mode 100644 src/main/kotlin/moe/nea/ledger/ExpiringValue.kt create mode 100644 src/main/kotlin/moe/nea/ledger/MinionDetection.kt create mode 100644 src/test/kotlin/moe/nea/ledger/NumberUtilKtTest.kt diff --git a/build.gradle.kts b/build.gradle.kts index c29d146..5ac39e7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,7 +6,7 @@ plugins { id("gg.essential.loom") version "0.10.0.+" id("dev.architectury.architectury-pack200") version "0.1.3" id("com.github.johnrengelman.shadow") version "8.1.1" - kotlin("jvm") version "1.8.21" + kotlin("jvm") version "2.0.20" } val baseGroup: String by project @@ -84,10 +84,15 @@ dependencies { shadowImpl("org.xerial:sqlite-jdbc:3.45.3.0") shadowImpl("org.notenoughupdates.moulconfig:legacy:3.0.0-beta.9") runtimeOnly("me.djtheredstoner:DevAuth-forge-legacy:1.1.2") + testImplementation("org.junit.jupiter:junit-jupiter:5.9.2") } // Tasks: +tasks.test { + useJUnitPlatform() +} + tasks.withType(JavaCompile::class) { options.encoding = "UTF-8" } diff --git a/src/main/kotlin/moe/nea/ledger/ExpiringValue.kt b/src/main/kotlin/moe/nea/ledger/ExpiringValue.kt new file mode 100644 index 0000000..dac4751 --- /dev/null +++ b/src/main/kotlin/moe/nea/ledger/ExpiringValue.kt @@ -0,0 +1,28 @@ +package moe.nea.ledger + +import kotlin.time.Duration +import kotlin.time.Duration.Companion.nanoseconds + +class ExpiringValue(private val value: T) { + val lastSeenAt: Long = System.nanoTime() + val age get() = (System.nanoTime() - lastSeenAt).nanoseconds + var taken = false + fun get(expiry: Duration): T? { + return if (!taken && expiry > age) value + else null + } + + companion object { + fun empty(): ExpiringValue { + val value = ExpiringValue(Unit) + value.take() + @Suppress("UNCHECKED_CAST") + return value as ExpiringValue + } + } + + fun consume(expiry: Duration): T? = get(expiry).also { take() } + fun take() { + taken = true + } +} \ No newline at end of file diff --git a/src/main/kotlin/moe/nea/ledger/ItemIdProvider.kt b/src/main/kotlin/moe/nea/ledger/ItemIdProvider.kt index 3da0a07..fa0d8fa 100644 --- a/src/main/kotlin/moe/nea/ledger/ItemIdProvider.kt +++ b/src/main/kotlin/moe/nea/ledger/ItemIdProvider.kt @@ -6,44 +6,46 @@ import net.minecraft.nbt.NBTTagCompound import net.minecraftforge.client.event.GuiScreenEvent import net.minecraftforge.common.MinecraftForge import net.minecraftforge.fml.common.eventhandler.SubscribeEvent +import org.lwjgl.input.Mouse class ItemIdProvider { - @SubscribeEvent - fun onMouseInput(event: GuiScreenEvent.MouseInputEvent.Pre) { - MinecraftForge.EVENT_BUS.post(BeforeGuiAction(event.gui)) - } - - @SubscribeEvent - fun onKeyInput(event: GuiScreenEvent.KeyboardInputEvent.Pre) { - MinecraftForge.EVENT_BUS.post(BeforeGuiAction(event.gui)) - } - - private val knownNames = mutableMapOf() - - @SubscribeEvent - fun saveInventoryIds(event: BeforeGuiAction) { - val chest = (event.gui as? GuiChest) ?: return - val slots = chest.inventorySlots as ContainerChest - val chestName = slots.lowerChestInventory.name.unformattedString() - val isOrderMenu = chestName == "Your Bazaar Orders" || chestName == "Co-op Bazaar Orders" - slots.inventorySlots.forEach { - val stack = it.stack ?: return@forEach - val nbt = stack.tagCompound ?: NBTTagCompound() - val display = nbt.getCompoundTag("display") - var name = display.getString("Name").unformattedString() - if (isOrderMenu) - name = name.removePrefix("BUY ").removePrefix("SELL ") - name = name.trim() - val id = stack.getInternalId() - if (id != null && name.isNotBlank()) { - knownNames[name] = id - } - } - } - - fun findForName(name: String): String? { - return knownNames[name] - } + @SubscribeEvent + fun onMouseInput(event: GuiScreenEvent.MouseInputEvent.Pre) { + if (Mouse.getEventButton() == -1) return + MinecraftForge.EVENT_BUS.post(BeforeGuiAction(event.gui)) + } + + @SubscribeEvent + fun onKeyInput(event: GuiScreenEvent.KeyboardInputEvent.Pre) { + MinecraftForge.EVENT_BUS.post(BeforeGuiAction(event.gui)) + } + + private val knownNames = mutableMapOf() + + @SubscribeEvent + fun saveInventoryIds(event: BeforeGuiAction) { + val chest = (event.gui as? GuiChest) ?: return + val slots = chest.inventorySlots as ContainerChest + val chestName = slots.lowerChestInventory.name.unformattedString() + val isOrderMenu = chestName == "Your Bazaar Orders" || chestName == "Co-op Bazaar Orders" + slots.inventorySlots.forEach { + val stack = it.stack ?: return@forEach + val nbt = stack.tagCompound ?: NBTTagCompound() + val display = nbt.getCompoundTag("display") + var name = display.getString("Name").unformattedString() + if (isOrderMenu) + name = name.removePrefix("BUY ").removePrefix("SELL ") + name = name.trim() + val id = stack.getInternalId() + if (id != null && name.isNotBlank()) { + knownNames[name] = id + } + } + } + + fun findForName(name: String): String? { + return knownNames[name] + } } \ No newline at end of file diff --git a/src/main/kotlin/moe/nea/ledger/Ledger.kt b/src/main/kotlin/moe/nea/ledger/Ledger.kt index 96cddab..784c111 100644 --- a/src/main/kotlin/moe/nea/ledger/Ledger.kt +++ b/src/main/kotlin/moe/nea/ledger/Ledger.kt @@ -106,6 +106,7 @@ class Ledger { AuctionHouseDetection(ledger, ids), BitsDetection(ledger), BitsShop(ledger), + MinionDetection(ledger), ).forEach(MinecraftForge.EVENT_BUS::register) } diff --git a/src/main/kotlin/moe/nea/ledger/MinionDetection.kt b/src/main/kotlin/moe/nea/ledger/MinionDetection.kt new file mode 100644 index 0000000..bb2a187 --- /dev/null +++ b/src/main/kotlin/moe/nea/ledger/MinionDetection.kt @@ -0,0 +1,42 @@ +package moe.nea.ledger + +import net.minecraft.client.gui.inventory.GuiChest +import net.minecraft.inventory.ContainerChest +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent +import java.time.Instant +import kotlin.time.Duration.Companion.seconds + +class MinionDetection(val ledger: LedgerLogger) { + // §aYou received §r§6367,516.8 coins§r§a! + val hopperCollectPattern = "You received (?$SHORT_NUMBER_PATTERN) coins?!".toPattern() + val minionNamePattern = "(?.*) Minion (?$ROMAN_NUMBER_PATTERN)".toPattern() + + var lastOpenedMinion = ExpiringValue.empty() + + @SubscribeEvent + fun onBeforeClaim(event: BeforeGuiAction) { + val container = event.gui as? GuiChest ?: return + val inv = (container.inventorySlots as ContainerChest).lowerChestInventory + val invName = inv.displayName.unformattedText.unformattedString() + minionNamePattern.useMatcher(invName) { + val name = group("name") + val level = parseRomanNumber(group("level")) + lastOpenedMinion = ExpiringValue(name.uppercase().replace(" ", "_") + "_" + level) + } + } + + + @SubscribeEvent + fun onChat(event: ChatReceived) { + hopperCollectPattern.useMatcher(event.message) { + val minionName = lastOpenedMinion.consume(3.seconds) + ledger.logEntry(LedgerEntry( + "AUTOMERCHANT_PROFIT_COLLECT", + Instant.now(), + parseShortNumber(group("amount")), + minionName, // TODO: switch to its own column idk + )) + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/moe/nea/ledger/NumberUtil.kt b/src/main/kotlin/moe/nea/ledger/NumberUtil.kt index d26b047..b0e47db 100644 --- a/src/main/kotlin/moe/nea/ledger/NumberUtil.kt +++ b/src/main/kotlin/moe/nea/ledger/NumberUtil.kt @@ -6,28 +6,64 @@ import java.util.regex.Pattern // language=regexp val SHORT_NUMBER_PATTERN = "[0-9]+(?:,[0-9]+)*(?:\\.[0-9]+)?[kKmMbB]?" +// language=regexp +val ROMAN_NUMBER_PATTERN = "[IVXLCDM]+" + +val romanNumbers = mapOf( + 'I' to 1, + 'V' to 5, + 'X' to 10, + 'L' to 50, + 'C' to 100, + 'D' to 500, + 'M' to 1000 +) + +fun parseRomanNumber(string: String): Int { + var smallestSeenSoFar = Int.MAX_VALUE + var lastSeenOfSmallest = 0 + var amount = 0 + for (c in string) { + val cV = romanNumbers[c]!! + if (cV == smallestSeenSoFar) { + lastSeenOfSmallest++ + amount += cV + } else if (cV < smallestSeenSoFar) { + smallestSeenSoFar = cV + amount += cV + lastSeenOfSmallest = 1 + } else { + amount -= lastSeenOfSmallest * smallestSeenSoFar * 2 + smallestSeenSoFar = cV + amount += cV + lastSeenOfSmallest = 1 + } + } + return amount +} + val siScalars = mapOf( - 'k' to 1_000.0, - 'K' to 1_000.0, - 'm' to 1_000_000.0, - 'M' to 1_000_000.0, - 'b' to 1_000_000_000.0, - 'B' to 1_000_000_000.0, + 'k' to 1_000.0, + 'K' to 1_000.0, + 'm' to 1_000_000.0, + 'M' to 1_000_000.0, + 'b' to 1_000_000_000.0, + 'B' to 1_000_000_000.0, ) fun parseShortNumber(string: String): Double { - var k = string.replace(",", "") - val scalar = k.last() - var scalarMultiplier = siScalars[scalar] - if (scalarMultiplier == null) { - scalarMultiplier = 1.0 - } else { - k = k.dropLast(1) - } - return k.toDouble() * scalarMultiplier + var k = string.replace(",", "") + val scalar = k.last() + var scalarMultiplier = siScalars[scalar] + if (scalarMultiplier == null) { + scalarMultiplier = 1.0 + } else { + k = k.dropLast(1) + } + return k.toDouble() * scalarMultiplier } inline fun Pattern.useMatcher(string: String, block: Matcher.() -> T): T? = - matcher(string).takeIf { it.matches() }?.let(block) + matcher(string).takeIf { it.matches() }?.let(block) fun String.unformattedString(): String = replace("§.".toRegex(), "") \ No newline at end of file diff --git a/src/test/kotlin/moe/nea/ledger/NumberUtilKtTest.kt b/src/test/kotlin/moe/nea/ledger/NumberUtilKtTest.kt new file mode 100644 index 0000000..4068a42 --- /dev/null +++ b/src/test/kotlin/moe/nea/ledger/NumberUtilKtTest.kt @@ -0,0 +1,17 @@ +package moe.nea.ledger + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class NumberUtilKtTest { + @Test + fun parseRomanNumberTest() { + assertEquals(4, parseRomanNumber("IV")) + assertEquals(1, parseRomanNumber("I")) + assertEquals(14, parseRomanNumber("XIV")) + assertEquals(3, parseRomanNumber("III")) + assertEquals(8, parseRomanNumber("IIX")) + assertEquals(500, parseRomanNumber("DM")) + assertEquals(2024, parseRomanNumber("MMXXIV")) + } +} \ No newline at end of file -- cgit