From eda44cb8743c709c15a7ed03381d05e43728e647 Mon Sep 17 00:00:00 2001 From: Linnea Gräf Date: Thu, 19 Dec 2024 20:18:39 +0100 Subject: feat: Add corpse loot detection --- src/main/kotlin/moe/nea/ledger/ItemChange.kt | 6 ++ src/main/kotlin/moe/nea/ledger/ItemIdProvider.kt | 35 +++++++++- src/main/kotlin/moe/nea/ledger/Ledger.kt | 2 + src/main/kotlin/moe/nea/ledger/NumberUtil.kt | 8 +++ src/main/kotlin/moe/nea/ledger/TransactionType.kt | 1 + .../kotlin/moe/nea/ledger/events/ChatReceived.kt | 2 +- .../nea/ledger/modules/MineshaftCorpseDetection.kt | 74 ++++++++++++++++++++++ .../moe/nea/ledger/modules/VisitorDetection.kt | 24 +------ .../moe/nea/ledger/utils/BorderedTextTracker.kt | 41 ++++++++++++ 9 files changed, 167 insertions(+), 26 deletions(-) create mode 100644 src/main/kotlin/moe/nea/ledger/modules/MineshaftCorpseDetection.kt create mode 100644 src/main/kotlin/moe/nea/ledger/utils/BorderedTextTracker.kt (limited to 'src') diff --git a/src/main/kotlin/moe/nea/ledger/ItemChange.kt b/src/main/kotlin/moe/nea/ledger/ItemChange.kt index 834cd2b..186a4e3 100644 --- a/src/main/kotlin/moe/nea/ledger/ItemChange.kt +++ b/src/main/kotlin/moe/nea/ledger/ItemChange.kt @@ -54,6 +54,12 @@ data class ItemChange( return gain(ItemId.COINS, number) } + fun unpair(direction: ChangeDirection, pair: Pair): ItemChange { + return ItemChange(pair.first, pair.second, direction) + } + + fun unpairGain(pair: Pair) = unpair(ChangeDirection.GAINED, pair) + fun gain(itemId: ItemId, amount: Number): ItemChange { return ItemChange(itemId, amount.toDouble(), ChangeDirection.GAINED) } diff --git a/src/main/kotlin/moe/nea/ledger/ItemIdProvider.kt b/src/main/kotlin/moe/nea/ledger/ItemIdProvider.kt index a84aac8..c269c3d 100644 --- a/src/main/kotlin/moe/nea/ledger/ItemIdProvider.kt +++ b/src/main/kotlin/moe/nea/ledger/ItemIdProvider.kt @@ -86,11 +86,15 @@ class ItemIdProvider { fun findForName(name: String, fallbackToGenerated: Boolean = true): ItemId? { var id = knownNames[name] if (id == null && fallbackToGenerated) { - id = ItemId(name.uppercase().replace(" ", "_")) + id = generateName(name) } return id } + fun generateName(name: String): ItemId { + return ItemId(name.uppercase().replace(" ", "_")) + } + private val coinRegex = "(?$SHORT_NUMBER_PATTERN) Coins?".toPattern() private val stackedItemRegex = "(?.*) x(?$SHORT_NUMBER_PATTERN)".toPattern() private val essenceRegex = "(?.*) Essence x(?$SHORT_NUMBER_PATTERN)".toPattern() @@ -103,14 +107,41 @@ class ItemIdProvider { .toList() } + private val etherialRewardPattern = "\\+(?${SHORT_NUMBER_PATTERN})x? (?.*)".toPattern() + fun findStackableItemByName(name: String, fallbackToGenerated: Boolean = false): Pair? { - val properName = name.unformattedString() + val properName = name.unformattedString().trim() if (properName == "FREE" || properName == "This Chest is Free!") { return Pair(ItemId.COINS, 0.0) } coinRegex.useMatcher(properName) { return Pair(ItemId.COINS, parseShortNumber(group("amount"))) } + etherialRewardPattern.useMatcher(properName) { + val id = when (val id = group("what")) { + "Copper" -> ItemId.COPPER + "Bits" -> ItemId.BITS + "Garden Experience" -> ItemId.GARDEN + "Farming XP" -> ItemId.FARMING + "Gold Essence" -> ItemId.GOLD_ESSENCE + "Gemstone Powder" -> ItemId.GEMSTONE_POWDER + "Mithril Powder" -> ItemId.MITHRIL_POWDER + "Pelts" -> ItemId.PELT + "Fine Flour" -> ItemId.FINE_FLOUR + else -> { + id.ifDropLast(" Experience") { + ItemId.skill(generateName(it).string) + } ?: id.ifDropLast(" XP") { + ItemId.skill(generateName(it).string) + } ?: id.ifDropLast(" Powder") { + ItemId("SKYBLOCK_POWDER_${generateName(it).string}") + } ?: id.ifDropLast(" Essence") { + ItemId("ESSENCE_${generateName(it).string}") + } ?: generateName(id) + } + } + return Pair(id, parseShortNumber(group("amount"))) + } essenceRegex.useMatcher(properName) { return Pair(ItemId("ESSENCE_${group("essence").uppercase()}"), parseShortNumber(group("count"))) diff --git a/src/main/kotlin/moe/nea/ledger/Ledger.kt b/src/main/kotlin/moe/nea/ledger/Ledger.kt index 58a8b3d..2d41a73 100644 --- a/src/main/kotlin/moe/nea/ledger/Ledger.kt +++ b/src/main/kotlin/moe/nea/ledger/Ledger.kt @@ -18,6 +18,7 @@ import moe.nea.ledger.modules.DungeonChestDetection import moe.nea.ledger.modules.ExternalDataProvider import moe.nea.ledger.modules.KatDetection import moe.nea.ledger.modules.KuudraChestDetection +import moe.nea.ledger.modules.MineshaftCorpseDetection import moe.nea.ledger.modules.MinionDetection import moe.nea.ledger.modules.NpcDetection import moe.nea.ledger.modules.VisitorDetection @@ -115,6 +116,7 @@ class Ledger { LedgerLogger::class.java, LogChatCommand::class.java, MinionDetection::class.java, + MineshaftCorpseDetection::class.java, NpcDetection::class.java, QueryCommand::class.java, RequestUtil::class.java, diff --git a/src/main/kotlin/moe/nea/ledger/NumberUtil.kt b/src/main/kotlin/moe/nea/ledger/NumberUtil.kt index 008cfbf..438f342 100644 --- a/src/main/kotlin/moe/nea/ledger/NumberUtil.kt +++ b/src/main/kotlin/moe/nea/ledger/NumberUtil.kt @@ -75,9 +75,17 @@ fun parseShortNumber(string: String): Double { return k.toDouble() * scalarMultiplier } +fun Pattern.matches(string: String): Boolean = matcher(string).matches() inline fun Pattern.useMatcher(string: String, block: Matcher.() -> T): T? = matcher(string).takeIf { it.matches() }?.let(block) +fun String.ifDropLast(suffix: String, block: (String) -> T): T? { + if (endsWith(suffix)) { + return block(dropLast(suffix.length)) + } + return null +} + fun String.unformattedString(): String = replace("§.".toRegex(), "") val timeFormat: DateTimeFormatter = DateTimeFormatterBuilder() diff --git a/src/main/kotlin/moe/nea/ledger/TransactionType.kt b/src/main/kotlin/moe/nea/ledger/TransactionType.kt index b615fcd..4e903d8 100644 --- a/src/main/kotlin/moe/nea/ledger/TransactionType.kt +++ b/src/main/kotlin/moe/nea/ledger/TransactionType.kt @@ -14,6 +14,7 @@ enum class TransactionType { BITS_PURSE_STATUS, BOOSTER_COOKIE_ATE, COMMUNITY_SHOP_BUY, + CORPSE_DESECRATED, DUNGEON_CHEST_OPEN, KAT_TIMESKIP, KAT_UPGRADE, diff --git a/src/main/kotlin/moe/nea/ledger/events/ChatReceived.kt b/src/main/kotlin/moe/nea/ledger/events/ChatReceived.kt index e88c7a0..a352c27 100644 --- a/src/main/kotlin/moe/nea/ledger/events/ChatReceived.kt +++ b/src/main/kotlin/moe/nea/ledger/events/ChatReceived.kt @@ -10,6 +10,6 @@ data class ChatReceived( val timestamp: Instant = Instant.now() ) : Event() { constructor(event: ClientChatReceivedEvent) : this( - event.message.unformattedText.unformattedString() + event.message.unformattedText.unformattedString().trimEnd() ) } \ No newline at end of file diff --git a/src/main/kotlin/moe/nea/ledger/modules/MineshaftCorpseDetection.kt b/src/main/kotlin/moe/nea/ledger/modules/MineshaftCorpseDetection.kt new file mode 100644 index 0000000..85c9ad6 --- /dev/null +++ b/src/main/kotlin/moe/nea/ledger/modules/MineshaftCorpseDetection.kt @@ -0,0 +1,74 @@ +package moe.nea.ledger.modules + +import moe.nea.ledger.ItemChange +import moe.nea.ledger.ItemId +import moe.nea.ledger.ItemIdProvider +import moe.nea.ledger.LedgerEntry +import moe.nea.ledger.LedgerLogger +import moe.nea.ledger.TransactionType +import moe.nea.ledger.events.ChatReceived +import moe.nea.ledger.matches +import moe.nea.ledger.useMatcher +import moe.nea.ledger.utils.BorderedTextTracker + +class MineshaftCorpseDetection : BorderedTextTracker() { + /* +[23:39:47] [Client thread/INFO]: [CHAT] §r§a§l▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬§r +[23:39:47] [Client thread/INFO]: [CHAT] §r §r§b§l§r§9§lLAPIS §r§b§lCORPSE LOOT! §r +[23:39:47] [Client thread/INFO]: [CHAT] §r §r§a§lREWARDS§r +[23:39:47] [Client thread/INFO]: [CHAT] §r §r§5+100 HOTM Experience§r +[23:39:47] [Client thread/INFO]: [CHAT] §r §r§a§r§aGreen Goblin Egg§r +[23:39:47] [Client thread/INFO]: [CHAT] §r §r§9Enchanted Glacite §r§8x2§r +[23:39:47] [Client thread/INFO]: [CHAT] §r §r§9☠ Fine Onyx Gemstone§r +[23:39:47] [Client thread/INFO]: [CHAT] §r §r§a☠ Flawed Onyx Gemstone §r§8x20§r +[23:39:47] [Client thread/INFO]: [CHAT] §r §r§a☘ Flawed Peridot Gemstone §r§8x40§r +[23:39:47] [Client thread/INFO]: [CHAT] §r §r§bGlacite Powder §r§8x500§r +[23:39:47] [Client thread/INFO]: [CHAT] §e[SkyHanni] Profit for §9Lapis Corpse§e: §678k§r +[23:39:47] [Client thread/INFO]: [CHAT] §r§a§l▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬§r + */ + + val corpseEnterMessage = " (?.*) CORPSE LOOT!".toPattern() + + override fun shouldEnter(event: ChatReceived): Boolean { + return corpseEnterMessage.matches(event.message) + } + + override fun shouldExit(event: ChatReceived): Boolean { + return genericBorderExit.matches(event.message) + } + + override fun onBorderedTextFinished(enclosed: List) { + // TODO: test this once profile swapping has been re-enabled + val rewards = enclosed.asSequence() + .dropWhile { it.message != " REWARDS" } + .drop(1) + .mapNotNull { + itemIdProvider.findStackableItemByName(it.message, true) + } + .map { ItemChange.unpairGain(it) } + .toMutableList() + val introMessage = enclosed.first() + val corpseTyp = corpseEnterMessage.useMatcher(introMessage.message) { + group("corpseKind") + }!! + val keyTyp = corpseNameToKey[corpseTyp] + if (keyTyp == null) { + errorUtil.reportAdHoc("Unknown corpse type $corpseTyp") + } else if (keyTyp != ItemId.NIL) { + rewards.add(ItemChange.lose(keyTyp, 1)) + } + logger.logEntry( + LedgerEntry( + TransactionType.CORPSE_DESECRATED, + introMessage.timestamp, + rewards + ) + ) + } + + val corpseNameToKey = mapOf( + "LAPIS" to ItemId.NIL + ) + lateinit var logger: LedgerLogger + lateinit var itemIdProvider: ItemIdProvider +} \ No newline at end of file diff --git a/src/main/kotlin/moe/nea/ledger/modules/VisitorDetection.kt b/src/main/kotlin/moe/nea/ledger/modules/VisitorDetection.kt index 2ee581c..f457ae4 100644 --- a/src/main/kotlin/moe/nea/ledger/modules/VisitorDetection.kt +++ b/src/main/kotlin/moe/nea/ledger/modules/VisitorDetection.kt @@ -59,29 +59,7 @@ class VisitorDetection { private fun parseGardenLoreLine(rewardLine: String): Pair? { val f = rewardLine.unformattedString().trim() - return parseSpecialReward(f) - ?: idProvider.findStackableItemByName(f, true) - } - - private val specialRewardRegex = "\\+(?${SHORT_NUMBER_PATTERN})x? (?.*)".toPattern() - - private fun parseSpecialReward(specialLine: String): Pair? { - specialRewardRegex.useMatcher(specialLine) { - val id = when (group("what")) { - "Copper" -> ItemId.COPPER - "Bits" -> ItemId.BITS - "Garden Experience" -> ItemId.GARDEN - "Farming XP" -> ItemId.FARMING - "Gold Essence" -> ItemId.GOLD_ESSENCE - "Gemstone Powder" -> ItemId.GEMSTONE_POWDER - "Mithril Powder" -> ItemId.MITHRIL_POWDER - "Pelts" -> ItemId.PELT - "Fine Flour" -> ItemId.FINE_FLOUR - else -> ItemId.NIL - } - return Pair(id, parseShortNumber(group("amount"))) - } - return null + return idProvider.findStackableItemByName(f, true) } diff --git a/src/main/kotlin/moe/nea/ledger/utils/BorderedTextTracker.kt b/src/main/kotlin/moe/nea/ledger/utils/BorderedTextTracker.kt new file mode 100644 index 0000000..9e621e8 --- /dev/null +++ b/src/main/kotlin/moe/nea/ledger/utils/BorderedTextTracker.kt @@ -0,0 +1,41 @@ +package moe.nea.ledger.utils + +import moe.nea.ledger.events.ChatReceived +import moe.nea.ledger.utils.di.Inject +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent + +abstract class BorderedTextTracker { + + val genericBorderExit = "▬{10,}".toPattern() + + @Inject + lateinit var errorUtil: ErrorUtil + var stack: MutableList? = null + + + @SubscribeEvent + fun receiveText(event: ChatReceived) { + if (stack != null && shouldExit(event)) { + exit() + return + } + if (shouldEnter(event)) { + if (stack != null) { + errorUtil.reportAdHoc("Double entered a bordered message") + exit() + } + stack = mutableListOf() + } + stack?.add(event) + } + + private fun exit() { + onBorderedTextFinished(stack!!) + stack = null + } + + abstract fun shouldEnter(event: ChatReceived): Boolean + abstract fun shouldExit(event: ChatReceived): Boolean + abstract fun onBorderedTextFinished(enclosed: List) + +} \ No newline at end of file -- cgit