diff options
Diffstat (limited to 'src/main/kotlin/moe/nea/ledger')
35 files changed, 996 insertions, 52 deletions
diff --git a/src/main/kotlin/moe/nea/ledger/DebugDataCommand.kt b/src/main/kotlin/moe/nea/ledger/DebugDataCommand.kt new file mode 100644 index 0000000..bab0a78 --- /dev/null +++ b/src/main/kotlin/moe/nea/ledger/DebugDataCommand.kt @@ -0,0 +1,34 @@ +package moe.nea.ledger + +import moe.nea.ledger.events.SupplyDebugInfo +import moe.nea.ledger.utils.di.Inject +import net.minecraft.command.CommandBase +import net.minecraft.command.ICommandSender +import net.minecraftforge.common.MinecraftForge + +class DebugDataCommand : CommandBase() { + + override fun canCommandSenderUseCommand(sender: ICommandSender?): Boolean { + return true + } + + override fun getCommandName(): String { + return "ledgerdebug" + } + + override fun getCommandUsage(sender: ICommandSender?): String { + return "" + } + + @Inject + lateinit var logger: LedgerLogger + + override fun processCommand(sender: ICommandSender?, args: Array<out String>?) { + val debugInfo = SupplyDebugInfo() + MinecraftForge.EVENT_BUS.post(debugInfo) + logger.printOut("Collected debug info:") + debugInfo.data.forEach { + logger.printOut("${it.first}: ${it.second}") + } + } +}
\ No newline at end of file diff --git a/src/main/kotlin/moe/nea/ledger/ItemChange.kt b/src/main/kotlin/moe/nea/ledger/ItemChange.kt index 834cd2b..fda709c 100644 --- a/src/main/kotlin/moe/nea/ledger/ItemChange.kt +++ b/src/main/kotlin/moe/nea/ledger/ItemChange.kt @@ -54,6 +54,13 @@ data class ItemChange( return gain(ItemId.COINS, number) } + fun unpair(direction: ChangeDirection, pair: Pair<ItemId, Double>): ItemChange { + return ItemChange(pair.first, pair.second, direction) + } + + fun unpairGain(pair: Pair<ItemId, Double>) = unpair(ChangeDirection.GAINED, pair) + fun unpairLose(pair: Pair<ItemId, Double>) = unpair(ChangeDirection.LOST, pair) + fun gain(itemId: ItemId, amount: Number): ItemChange { return ItemChange(itemId, amount.toDouble(), ChangeDirection.GAINED) } diff --git a/src/main/kotlin/moe/nea/ledger/ItemId.kt b/src/main/kotlin/moe/nea/ledger/ItemId.kt index 74f8e82..2b83357 100644 --- a/src/main/kotlin/moe/nea/ledger/ItemId.kt +++ b/src/main/kotlin/moe/nea/ledger/ItemId.kt @@ -27,6 +27,9 @@ value class ItemId( val BITS = ItemId("SKYBLOCK_BIT") val COPPER = ItemId("SKYBLOCK_COPPER") val NIL = ItemId("SKYBLOCK_NIL") + val ARCHFIEND_LOW_CLASS = ItemId("ARCHFIEND_DICE") + val ARCHFIEND_HIGH_CLASS = ItemId("HIGH_CLASS_ARCHFIEND_DICE") + val ARCHFIEND_DYE = ItemId("DYE_ARCHFIEND") val DUNGEON_CHEST_KEY = ItemId("DUNGEON_CHEST_KEY") val BOOSTER_COOKIE = ItemId("BOOSTER_COOKIE") val KISMET_FEATHER = ItemId("KISMET_FEATHER") diff --git a/src/main/kotlin/moe/nea/ledger/ItemIdProvider.kt b/src/main/kotlin/moe/nea/ledger/ItemIdProvider.kt index 121586e..7fe0206 100644 --- a/src/main/kotlin/moe/nea/ledger/ItemIdProvider.kt +++ b/src/main/kotlin/moe/nea/ledger/ItemIdProvider.kt @@ -3,6 +3,8 @@ package moe.nea.ledger import moe.nea.ledger.events.BeforeGuiAction import moe.nea.ledger.events.ExtraSupplyIdEvent import moe.nea.ledger.events.RegistrationFinishedEvent +import moe.nea.ledger.events.SupplyDebugInfo +import moe.nea.ledger.modules.ExternalDataProvider import net.minecraft.client.Minecraft import net.minecraft.item.ItemStack import net.minecraft.nbt.NBTTagCompound @@ -27,9 +29,24 @@ class ItemIdProvider { private val knownNames = mutableMapOf<String, ItemId>() + fun createLookupTagFromDisplayName(itemName: String): String { + return itemName.unformattedString().trim().lowercase() + } + + fun saveKnownItem(itemName: String, itemId: ItemId) { + knownNames[createLookupTagFromDisplayName(itemName)] = itemId + } + + @SubscribeEvent + fun onDataLoaded(event: ExternalDataProvider.DataLoaded) { + event.provider.itemNames.forEach { (itemId, itemName) -> + saveKnownItem(itemName, ItemId(itemId)) + } + } + @SubscribeEvent fun onRegistrationFinished(event: RegistrationFinishedEvent) { - MinecraftForge.EVENT_BUS.post(ExtraSupplyIdEvent(knownNames::put)) + MinecraftForge.EVENT_BUS.post(ExtraSupplyIdEvent(::saveKnownItem)) } @SubscribeEvent(priority = EventPriority.HIGH) @@ -40,6 +57,11 @@ class ItemIdProvider { inventory.armorInventory?.forEach { saveFromSlot(it) } } + @SubscribeEvent + fun onDebugData(event: SupplyDebugInfo) { + event.record("knownItemNames", knownNames.size) + } + fun saveFromSlot(stack: ItemStack?, preprocessName: (String) -> String = { it }) { if (stack == null) return val nbt = stack.tagCompound ?: NBTTagCompound() @@ -49,7 +71,7 @@ class ItemIdProvider { name = name.trim() val id = stack.getInternalId() if (id != null && name.isNotBlank()) { - knownNames[name] = id + saveKnownItem(name, id) } } @@ -70,16 +92,21 @@ class ItemIdProvider { // TODO: make use of colour fun findForName(name: String, fallbackToGenerated: Boolean = true): ItemId? { - var id = knownNames[name] + var id = knownNames[createLookupTagFromDisplayName(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 = "(?<amount>$SHORT_NUMBER_PATTERN) Coins?".toPattern() private val stackedItemRegex = "(?<name>.*) x(?<count>$SHORT_NUMBER_PATTERN)".toPattern() private val essenceRegex = "(?<essence>.*) Essence x(?<count>$SHORT_NUMBER_PATTERN)".toPattern() + private val numberedItemRegex = "(?<count>$SHORT_NUMBER_PATTERN) (?<what>.*)".toPattern() fun findCostItemsFromSpan(lore: List<String>): List<Pair<ItemId, Double>> { return lore.iterator().asSequence() @@ -89,14 +116,41 @@ class ItemIdProvider { .toList() } + private val etherialRewardPattern = "\\+(?<amount>${SHORT_NUMBER_PATTERN})x? (?<what>.*)".toPattern() + fun findStackableItemByName(name: String, fallbackToGenerated: Boolean = false): Pair<ItemId, Double>? { - 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"))) @@ -108,6 +162,18 @@ class ItemIdProvider { return Pair(item, count) } } + numberedItemRegex.useMatcher(properName) { + val item = findForName(group("what"), fallbackToGenerated) + if (item != null) { + val count = parseShortNumber(group("count")) + return Pair(item, count) + } + } + return findForName(properName, fallbackToGenerated)?.let { Pair(it, 1.0) } } + + fun getKnownItemIds(): Collection<ItemId> { + return knownNames.values + } }
\ No newline at end of file diff --git a/src/main/kotlin/moe/nea/ledger/ItemUtil.kt b/src/main/kotlin/moe/nea/ledger/ItemUtil.kt index 949f58a..a3d8162 100644 --- a/src/main/kotlin/moe/nea/ledger/ItemUtil.kt +++ b/src/main/kotlin/moe/nea/ledger/ItemUtil.kt @@ -1,5 +1,6 @@ package moe.nea.ledger +import net.minecraft.inventory.IInventory import net.minecraft.item.ItemStack import net.minecraft.nbt.NBTTagCompound @@ -37,7 +38,10 @@ class PetInfo { fun ItemStack.getPetId(): String? { val petInfoStr = getExtraAttributes().getString("petInfo") - val petInfo = runCatching { Ledger.gson.fromJson(petInfoStr, PetInfo::class.java) }.getOrNull() // TODO: error reporting to sentry + val petInfo = runCatching { + Ledger.gson.fromJson(petInfoStr, + PetInfo::class.java) + }.getOrNull() // TODO: error reporting to sentry if (petInfo?.type == null || petInfo.tier == null) return null return petInfo.type + ";" + rarityToIndex(petInfo.tier ?: "") } @@ -62,6 +66,22 @@ fun ItemStack.getLore(): List<String> { } +fun IInventory.asIterable(): Iterable<ItemStack?> = object : Iterable<ItemStack?> { + override fun iterator(): Iterator<ItemStack?> { + return object : Iterator<ItemStack?> { + var i = 0 + override fun hasNext(): Boolean { + return i < this@asIterable.sizeInventory + } + + override fun next(): ItemStack? { + if (!hasNext()) throw NoSuchElementException("$i is out of range for inventory ${this@asIterable}") + return this@asIterable.getStackInSlot(i++) + } + } + } +} + fun ItemStack.getDisplayNameU(): String { val nbt = this.tagCompound ?: NBTTagCompound() val extraAttributes = nbt.getCompoundTag("display") diff --git a/src/main/kotlin/moe/nea/ledger/Ledger.kt b/src/main/kotlin/moe/nea/ledger/Ledger.kt index b8a4073..72bd32f 100644 --- a/src/main/kotlin/moe/nea/ledger/Ledger.kt +++ b/src/main/kotlin/moe/nea/ledger/Ledger.kt @@ -3,6 +3,8 @@ package moe.nea.ledger import com.google.gson.Gson import io.github.notenoughupdates.moulconfig.managed.ManagedConfig import moe.nea.ledger.config.LedgerConfig +import moe.nea.ledger.config.UpdateUi +import moe.nea.ledger.config.UpdateUiMarker import moe.nea.ledger.database.Database import moe.nea.ledger.events.ChatReceived import moe.nea.ledger.events.LateWorldLoadEvent @@ -15,13 +17,21 @@ import moe.nea.ledger.modules.BazaarOrderDetection import moe.nea.ledger.modules.BitsDetection import moe.nea.ledger.modules.BitsShopDetection import moe.nea.ledger.modules.DungeonChestDetection +import moe.nea.ledger.modules.ExternalDataProvider +import moe.nea.ledger.modules.ForgeDetection +import moe.nea.ledger.modules.GambleDetection 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.UpdateChecker import moe.nea.ledger.modules.VisitorDetection -import moe.nea.ledger.utils.di.DI import moe.nea.ledger.utils.ErrorUtil +import moe.nea.ledger.utils.MinecraftExecutor +import moe.nea.ledger.utils.di.DI +import moe.nea.ledger.utils.di.DIProvider +import moe.nea.ledger.utils.network.RequestUtil import net.minecraft.client.Minecraft import net.minecraft.command.ICommand import net.minecraftforge.client.ClientCommandHandler @@ -77,42 +87,56 @@ class Ledger { val logger = LogManager.getLogger("MoneyLedger") val managedConfig = ManagedConfig.create(File("config/money-ledger/config.json"), LedgerConfig::class.java) { checkExpose = false + customProcessor<UpdateUiMarker> { option, ann -> + UpdateUi(option) + } } val gson = Gson() private val tickQueue = ConcurrentLinkedQueue<Runnable>() fun runLater(runnable: Runnable) { tickQueue.add(runnable) } + + val di = DI() } @Mod.EventHandler fun init(event: FMLInitializationEvent) { logger.info("Initializing ledger") - val di = DI() TelemetryProvider.setupFor(di) di.registerSingleton(this) di.registerSingleton(Minecraft.getMinecraft()) di.registerSingleton(gson) + di.register(LedgerConfig::class.java, DIProvider { managedConfig.instance }) di.registerInjectableClasses( AuctionHouseDetection::class.java, BankDetection::class.java, BazaarDetection::class.java, BazaarOrderDetection::class.java, + DebugDataCommand::class.java, BitsDetection::class.java, BitsShopDetection::class.java, ConfigCommand::class.java, Database::class.java, DungeonChestDetection::class.java, ErrorUtil::class.java, + ExternalDataProvider::class.java, ItemIdProvider::class.java, KatDetection::class.java, KuudraChestDetection::class.java, LedgerLogger::class.java, LogChatCommand::class.java, MinionDetection::class.java, + MineshaftCorpseDetection::class.java, + ForgeDetection::class.java, NpcDetection::class.java, + GambleDetection::class.java, + MinecraftExecutor::class.java, + UpdateChecker::class.java, + TriggerCommand::class.java, QueryCommand::class.java, + RequestUtil::class.java, VisitorDetection::class.java, ) val errorUtil = di.provide<ErrorUtil>() @@ -125,7 +149,6 @@ class Ledger { errorUtil.catch { di.provide<Database>().loadAndUpgrade() - error("Lol") } MinecraftForge.EVENT_BUS.post(RegistrationFinishedEvent()) 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 <T> Pattern.useMatcher(string: String, block: Matcher.() -> T): T? = matcher(string).takeIf { it.matches() }?.let(block) +fun <T> 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/QueryCommand.kt b/src/main/kotlin/moe/nea/ledger/QueryCommand.kt index 305069a..9967a4a 100644 --- a/src/main/kotlin/moe/nea/ledger/QueryCommand.kt +++ b/src/main/kotlin/moe/nea/ledger/QueryCommand.kt @@ -9,6 +9,7 @@ import moe.nea.ledger.database.Database import moe.nea.ledger.utils.di.Inject import net.minecraft.command.CommandBase import net.minecraft.command.ICommandSender +import net.minecraft.util.BlockPos import net.minecraft.util.ChatComponentText import net.minecraft.util.ChatStyle import net.minecraft.util.EnumChatFormatting @@ -68,6 +69,18 @@ class QueryCommand : CommandBase() { } } + override fun addTabCompletionOptions( + sender: ICommandSender, + args: Array<out String>, + pos: BlockPos + ): MutableList<String>? { + when (val p = parseArgs(args)) { + is ParseResult.MissingArg -> return null + is ParseResult.Success -> return p.lastFilterM.tabComplete(args.last()) + is ParseResult.UnknownFilter -> return getListOfStringsMatchingLastWord(args, mFilters.keys) + } + } + @Inject lateinit var database: Database private fun executeQuery(parse: ParseResult.Success) { @@ -79,9 +92,13 @@ class QueryCommand : CommandBase() { query.where(ANDExpression(value)) } query.limit(80u) + val dedup = mutableSetOf<UUIDUtil.ULIDWrapper>() query.forEach { val type = it[DBLogEntry.type] val transactionId = it[DBLogEntry.transactionId] + if (!dedup.add(transactionId)) { + return@forEach + } val timestamp = transactionId.getTimestamp() val items = DBItemEntry.selectAll(database.connection) .where(Clause { column(DBItemEntry.transactionId) eq string(transactionId.wrapped) }) @@ -144,20 +161,35 @@ class QueryCommand : CommandBase() { val preparedText = "%" + text.trim('%') + "%" return Clause { column(DBLogEntry.type) like preparedText } } + + override fun tabComplete(partialArg: String): MutableList<String> { + return TransactionType.entries.asSequence().map { it.name }.filter { partialArg in it }.toMutableList() + } } object ItemFilter : FilterM { override val name: String get() = "withitem" + private val itemIdProvider = Ledger.di.provide<ItemIdProvider>() // TODO: close this escape hatch override fun getFilter(text: String): BooleanExpression { return Clause { column(DBItemEntry.itemId) like text } } + + override fun tabComplete(partialArg: String): MutableList<String>? { + return itemIdProvider.getKnownItemIds() + .asSequence() + .map { it.string } + .filter { partialArg in it } + .take(100) + .toMutableList() + } } interface FilterM { val name: String fun getFilter(text: String): BooleanExpression + fun tabComplete(partialArg: String): MutableList<String>? // fun tabCompleteFilter() TODO } diff --git a/src/main/kotlin/moe/nea/ledger/TransactionType.kt b/src/main/kotlin/moe/nea/ledger/TransactionType.kt index b615fcd..f6bbe6a 100644 --- a/src/main/kotlin/moe/nea/ledger/TransactionType.kt +++ b/src/main/kotlin/moe/nea/ledger/TransactionType.kt @@ -14,6 +14,9 @@ enum class TransactionType { BITS_PURSE_STATUS, BOOSTER_COOKIE_ATE, COMMUNITY_SHOP_BUY, + CORPSE_DESECRATED, + FORGED, + DIE_ROLLED, DUNGEON_CHEST_OPEN, KAT_TIMESKIP, KAT_UPGRADE, diff --git a/src/main/kotlin/moe/nea/ledger/TriggerCommand.kt b/src/main/kotlin/moe/nea/ledger/TriggerCommand.kt new file mode 100644 index 0000000..c97627d --- /dev/null +++ b/src/main/kotlin/moe/nea/ledger/TriggerCommand.kt @@ -0,0 +1,34 @@ +package moe.nea.ledger + +import moe.nea.ledger.events.TriggerEvent +import net.minecraft.command.CommandBase +import net.minecraft.command.ICommandSender +import net.minecraft.event.ClickEvent +import net.minecraft.util.ChatComponentText +import net.minecraftforge.common.MinecraftForge + +class TriggerCommand : CommandBase() { + fun getTriggerCommandLine(trigger: String): ClickEvent { + return ClickEvent(ClickEvent.Action.RUN_COMMAND, "/${commandName} $trigger") + } + + override fun getCommandName(): String { + return "__ledgertrigger" + } + + override fun getCommandUsage(sender: ICommandSender?): String { + return "" + } + + override fun processCommand(sender: ICommandSender, args: Array<out String>) { + val event = TriggerEvent(args.joinToString(" ")) + MinecraftForge.EVENT_BUS.post(event) + if (!event.isCanceled) + sender.addChatMessage(ChatComponentText("§cCould not find the given trigger. This is an internal command for ledger.")) + } + + override fun canCommandSenderUseCommand(sender: ICommandSender?): Boolean { + return true + } + +}
\ No newline at end of file diff --git a/src/main/kotlin/moe/nea/ledger/config/LedgerConfig.kt b/src/main/kotlin/moe/nea/ledger/config/LedgerConfig.kt index 367f1e2..91ee5c1 100644 --- a/src/main/kotlin/moe/nea/ledger/config/LedgerConfig.kt +++ b/src/main/kotlin/moe/nea/ledger/config/LedgerConfig.kt @@ -20,6 +20,10 @@ class LedgerConfig : Config() { return DescriptionRendereringBehaviour.EXPAND_PANEL } + @Category(name = "Ledger", desc = "") + @JvmField + val main = MainOptions() + @Category(name = "Synchronization", desc = "") @JvmField val synchronization = SynchronizationOptions() diff --git a/src/main/kotlin/moe/nea/ledger/config/MainOptions.kt b/src/main/kotlin/moe/nea/ledger/config/MainOptions.kt new file mode 100644 index 0000000..1efa970 --- /dev/null +++ b/src/main/kotlin/moe/nea/ledger/config/MainOptions.kt @@ -0,0 +1,27 @@ +package moe.nea.ledger.config + +import io.github.notenoughupdates.moulconfig.annotations.ConfigEditorDropdown +import io.github.notenoughupdates.moulconfig.annotations.ConfigOption + +class MainOptions { + @ConfigOption(name = "Marker for Update UI", desc = "_") + @JvmField + @UpdateUiMarker + @Transient + var testOption = Unit + + @ConfigOption(name = "Check for Updates", desc = "Automatically check for updates on startup") + @ConfigEditorDropdown + @JvmField + var updateCheck = UpdateCheckBehaviour.SEMI + + enum class UpdateCheckBehaviour(val label: String) { + SEMI("Semi-Automatic"), + FULL("Full-Automatic"), + NONE("Don't check"); + + override fun toString(): String { + return label + } + } +} diff --git a/src/main/kotlin/moe/nea/ledger/config/UpdateUi.kt b/src/main/kotlin/moe/nea/ledger/config/UpdateUi.kt new file mode 100644 index 0000000..86ccbf7 --- /dev/null +++ b/src/main/kotlin/moe/nea/ledger/config/UpdateUi.kt @@ -0,0 +1,17 @@ +package moe.nea.ledger.config + +import io.github.notenoughupdates.moulconfig.gui.GuiComponent +import io.github.notenoughupdates.moulconfig.gui.component.TextComponent +import io.github.notenoughupdates.moulconfig.gui.editors.ComponentEditor +import io.github.notenoughupdates.moulconfig.processor.ProcessedOption +import moe.nea.ledger.Ledger + +class UpdateUi(option: ProcessedOption) : ComponentEditor(option) { + val delegate by lazy {// TODO + TextComponent("Hier könnte ihre werbung stehen") + } + + override fun getDelegate(): GuiComponent { + return delegate + } +}
\ No newline at end of file diff --git a/src/main/kotlin/moe/nea/ledger/config/UpdateUiMarker.kt b/src/main/kotlin/moe/nea/ledger/config/UpdateUiMarker.kt new file mode 100644 index 0000000..7a0466a --- /dev/null +++ b/src/main/kotlin/moe/nea/ledger/config/UpdateUiMarker.kt @@ -0,0 +1,6 @@ +package moe.nea.ledger.config + +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FIELD) +annotation class UpdateUiMarker { +}
\ No newline at end of file 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/events/InitializationComplete.kt b/src/main/kotlin/moe/nea/ledger/events/InitializationComplete.kt new file mode 100644 index 0000000..d917039 --- /dev/null +++ b/src/main/kotlin/moe/nea/ledger/events/InitializationComplete.kt @@ -0,0 +1,6 @@ +package moe.nea.ledger.events + +import net.minecraftforge.fml.common.eventhandler.Event + +class InitializationComplete : Event() { +}
\ No newline at end of file diff --git a/src/main/kotlin/moe/nea/ledger/events/SupplyDebugInfo.kt b/src/main/kotlin/moe/nea/ledger/events/SupplyDebugInfo.kt new file mode 100644 index 0000000..cab0a20 --- /dev/null +++ b/src/main/kotlin/moe/nea/ledger/events/SupplyDebugInfo.kt @@ -0,0 +1,10 @@ +package moe.nea.ledger.events + +import net.minecraftforge.fml.common.eventhandler.Event + +class SupplyDebugInfo : Event() { // TODO: collect this in the event recorder + val data = mutableListOf<Pair<String, Any>>() + fun record(key: String, value: Any) { + data.add(key to value) + } +}
\ No newline at end of file diff --git a/src/main/kotlin/moe/nea/ledger/events/TriggerEvent.kt b/src/main/kotlin/moe/nea/ledger/events/TriggerEvent.kt new file mode 100644 index 0000000..3751f43 --- /dev/null +++ b/src/main/kotlin/moe/nea/ledger/events/TriggerEvent.kt @@ -0,0 +1,7 @@ +package moe.nea.ledger.events + +import net.minecraftforge.fml.common.eventhandler.Cancelable +import net.minecraftforge.fml.common.eventhandler.Event + +@Cancelable +data class TriggerEvent(val action: String) : Event() diff --git a/src/main/kotlin/moe/nea/ledger/modules/ExternalDataProvider.kt b/src/main/kotlin/moe/nea/ledger/modules/ExternalDataProvider.kt new file mode 100644 index 0000000..93bb453 --- /dev/null +++ b/src/main/kotlin/moe/nea/ledger/modules/ExternalDataProvider.kt @@ -0,0 +1,43 @@ +package moe.nea.ledger.modules + +import moe.nea.ledger.events.InitializationComplete +import moe.nea.ledger.events.SupplyDebugInfo +import moe.nea.ledger.utils.GsonUtil +import moe.nea.ledger.utils.di.Inject +import moe.nea.ledger.utils.network.Request +import moe.nea.ledger.utils.network.RequestUtil +import net.minecraftforge.common.MinecraftForge +import net.minecraftforge.fml.common.eventhandler.Event +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent +import java.util.concurrent.CompletableFuture + +class ExternalDataProvider @Inject constructor( + val requestUtil: RequestUtil +) { + + fun createAuxillaryDataRequest(path: String): Request { + return requestUtil.createRequest("https://github.com/nea89o/ledger-auxiliary-data/raw/refs/heads/master/$path") + } + + private val itemNameFuture: CompletableFuture<Map<String, String>> = CompletableFuture.supplyAsync { + val request = createAuxillaryDataRequest("data/item_names.json") + val response = request.execute(requestUtil) + val nameMap = response.json(GsonUtil.typeToken<Map<String, String>>()) + return@supplyAsync nameMap + } + + lateinit var itemNames: Map<String, String> + + class DataLoaded(val provider: ExternalDataProvider) : Event() + + @SubscribeEvent + fun onDebugData(debugInfo: SupplyDebugInfo) { + debugInfo.record("externalItemsLoaded", itemNameFuture.isDone && !itemNameFuture.isCompletedExceptionally) + } + + @SubscribeEvent + fun onInitComplete(event: InitializationComplete) { + itemNames = itemNameFuture.join() + MinecraftForge.EVENT_BUS.post(DataLoaded(this)) + } +}
\ No newline at end of file diff --git a/src/main/kotlin/moe/nea/ledger/modules/ForgeDetection.kt b/src/main/kotlin/moe/nea/ledger/modules/ForgeDetection.kt new file mode 100644 index 0000000..95811ed --- /dev/null +++ b/src/main/kotlin/moe/nea/ledger/modules/ForgeDetection.kt @@ -0,0 +1,48 @@ +package moe.nea.ledger.modules + +import moe.nea.ledger.ItemChange +import moe.nea.ledger.LedgerEntry +import moe.nea.ledger.LedgerLogger +import moe.nea.ledger.TransactionType +import moe.nea.ledger.events.GuiClickEvent +import moe.nea.ledger.getInternalId +import moe.nea.ledger.matches +import moe.nea.ledger.unformattedString +import moe.nea.ledger.utils.di.Inject +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent +import java.time.Instant + +class ForgeDetection { + val furnaceSlot = 9 + 4 + val furnaceName = "Forge Slot.*".toPattern() + + @SubscribeEvent + fun onClick(event: GuiClickEvent) { + val slot = event.slotIn ?: return + val clickedItem = slot.stack ?: return + if (clickedItem.displayName.unformattedString() != "Confirm") return + val furnaceSlotName = slot.inventory.getStackInSlot(furnaceSlot)?.displayName?.unformattedString() ?: return + if (!furnaceName.matches(furnaceSlotName)) + return + val cl = (0 until slot.inventory.sizeInventory - 9) + .mapNotNull { + val stack = slot.inventory.getStackInSlot(it) ?: return@mapNotNull null + val x = it % 9 + if (x == 4) return@mapNotNull null + ItemChange( + stack.getInternalId() ?: return@mapNotNull null, + stack.stackSize.toDouble(), + if (x < 4) ItemChange.ChangeDirection.LOST else ItemChange.ChangeDirection.GAINED + ) + } + logger.logEntry(LedgerEntry( + TransactionType.FORGED, + Instant.now(), + cl, + )) + } + + @Inject + lateinit var logger: LedgerLogger + +}
\ No newline at end of file diff --git a/src/main/kotlin/moe/nea/ledger/modules/GambleDetection.kt b/src/main/kotlin/moe/nea/ledger/modules/GambleDetection.kt new file mode 100644 index 0000000..6a339d7 --- /dev/null +++ b/src/main/kotlin/moe/nea/ledger/modules/GambleDetection.kt @@ -0,0 +1,61 @@ +package moe.nea.ledger.modules + +import moe.nea.ledger.ItemChange +import moe.nea.ledger.ItemId +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.useMatcher +import moe.nea.ledger.utils.di.Inject +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent + +class GambleDetection { + + val dieRolled = + "Your (?<isHighClass>High Class )?Archfiend Dice rolled a (?<face>[1-7])!.*" + .toPattern() + + @Inject + lateinit var logger: LedgerLogger + + @SubscribeEvent + fun onChat(event: ChatReceived) { + dieRolled.useMatcher(event.message) { + val isLowClass = group("isHighClass").isNullOrBlank() + val item = if (isLowClass) ItemId.ARCHFIEND_LOW_CLASS else ItemId.ARCHFIEND_HIGH_CLASS + val face = group("face") + val rollCost = if (isLowClass) 666_000.0 else 6_600_000.0 + if (face == "7") { + logger.logEntry(LedgerEntry( + TransactionType.DIE_ROLLED, + event.timestamp, + listOf( + ItemChange.lose(item, 1), + ItemChange.loseCoins(rollCost), + ItemChange.gain(ItemId.ARCHFIEND_DYE, 1), + ) + )) + } else if (face == "6") { + logger.logEntry(LedgerEntry( + TransactionType.DIE_ROLLED, + event.timestamp, + listOf( + ItemChange.lose(item, 1), + ItemChange.loseCoins(rollCost), + ItemChange.gainCoins(if (isLowClass) 15_000_000.0 else 100_000_000.0), + ) + )) + } else { + logger.logEntry(LedgerEntry( + TransactionType.DIE_ROLLED, + event.timestamp, + listOf( + ItemChange(item, 1.0, ItemChange.ChangeDirection.CATALYST), + ItemChange.loseCoins(rollCost), + ) + )) + } + } + } +}
\ 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..60b06ae --- /dev/null +++ b/src/main/kotlin/moe/nea/ledger/modules/MineshaftCorpseDetection.kt @@ -0,0 +1,81 @@ +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 +import moe.nea.ledger.utils.di.Inject + +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 = " (?<corpseKind>.*) 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<ChatReceived>) { + 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, + "VANGUARD" to ItemId("SKELETON_KEY"), + "UMBER" to ItemId("UMBER_KEY"), + "TUNGSTEN" to ItemId("TUNGSTEN_KEY"), + ) + + @Inject + lateinit var logger: LedgerLogger + + @Inject + lateinit var itemIdProvider: ItemIdProvider +}
\ No newline at end of file diff --git a/src/main/kotlin/moe/nea/ledger/modules/NpcDetection.kt b/src/main/kotlin/moe/nea/ledger/modules/NpcDetection.kt index 5906562..95b8aa5 100644 --- a/src/main/kotlin/moe/nea/ledger/modules/NpcDetection.kt +++ b/src/main/kotlin/moe/nea/ledger/modules/NpcDetection.kt @@ -7,9 +7,17 @@ import moe.nea.ledger.LedgerEntry import moe.nea.ledger.LedgerLogger import moe.nea.ledger.SHORT_NUMBER_PATTERN import moe.nea.ledger.TransactionType +import moe.nea.ledger.asIterable +import moe.nea.ledger.events.BeforeGuiAction import moe.nea.ledger.events.ChatReceived +import moe.nea.ledger.events.ExtraSupplyIdEvent +import moe.nea.ledger.getDisplayNameU +import moe.nea.ledger.getInternalId +import moe.nea.ledger.getLore import moe.nea.ledger.parseShortNumber +import moe.nea.ledger.unformattedString import moe.nea.ledger.useMatcher +import moe.nea.ledger.utils.ErrorUtil import moe.nea.ledger.utils.di.Inject import net.minecraftforge.fml.common.eventhandler.SubscribeEvent import java.util.regex.Pattern @@ -21,7 +29,49 @@ class NpcDetection @Inject constructor(val ledger: LedgerLogger, val ids: ItemId val npcSellPattern = Pattern.compile("You sold (?<what>.*) (x(?<count>$SHORT_NUMBER_PATTERN) )?for (?<coins>$SHORT_NUMBER_PATTERN) Coins?!") - // TODO: IMPROVE BUYING FROM NPC TO INCLUDE ITEMS OTHER THAN COINS (KUUDRA KEYS ARE CHEAP) + // You bought InfiniDirt™ Wand! + // You bought Prismapump x4! + val npcBuyWithItemPattern = + "You bought (?<what>.*?)!".toPattern() + var storedPurchases = mutableMapOf<String, List<ItemChange>>() + + @SubscribeEvent + fun onClick(event: BeforeGuiAction) { + (event.chestSlots?.lowerChestInventory?.asIterable() ?: listOf()) + .filterNotNull().forEach { + val name = it.getDisplayNameU().unformattedString() + val id = it.getInternalId() ?: return@forEach + val count = it.stackSize + val cost = ids.findCostItemsFromSpan(it.getLore()) + storedPurchases[name] = listOf(ItemChange.gain(id, count)) + cost.map { ItemChange.unpairLose(it) } + } + } + + @SubscribeEvent + fun addChocolate(event: ExtraSupplyIdEvent) { + event.store("Chocolate", ItemId("SKYBLOCK_CHOCOLATE")) + } + + @Inject + lateinit var errorUtil: ErrorUtil + + @SubscribeEvent + fun onBarteredItemBought(event: ChatReceived) { + npcBuyWithItemPattern.useMatcher(event.message) { + val changes = storedPurchases[group("what")] + if (changes == null) { + errorUtil.reportAdHoc("Item bought for items without associated cost") + } + storedPurchases.clear() + ledger.logEntry( + LedgerEntry( + TransactionType.NPC_BUY, + event.timestamp, + changes ?: listOf() + ) + ) + } + } @SubscribeEvent fun onNpcBuy(event: ChatReceived) { diff --git a/src/main/kotlin/moe/nea/ledger/modules/UpdateChecker.kt b/src/main/kotlin/moe/nea/ledger/modules/UpdateChecker.kt new file mode 100644 index 0000000..0d89ca1 --- /dev/null +++ b/src/main/kotlin/moe/nea/ledger/modules/UpdateChecker.kt @@ -0,0 +1,167 @@ +package moe.nea.ledger.modules + +import com.google.gson.JsonElement +import com.google.gson.JsonPrimitive +import moe.nea.ledger.DevUtil +import moe.nea.ledger.LedgerLogger +import moe.nea.ledger.TriggerCommand +import moe.nea.ledger.config.LedgerConfig +import moe.nea.ledger.config.MainOptions +import moe.nea.ledger.events.RegistrationFinishedEvent +import moe.nea.ledger.events.TriggerEvent +import moe.nea.ledger.gen.BuildConfig +import moe.nea.ledger.utils.ErrorUtil +import moe.nea.ledger.utils.MinecraftExecutor +import moe.nea.ledger.utils.di.Inject +import moe.nea.ledger.utils.network.RequestUtil +import moe.nea.libautoupdate.CurrentVersion +import moe.nea.libautoupdate.GithubReleaseUpdateData +import moe.nea.libautoupdate.GithubReleaseUpdateSource +import moe.nea.libautoupdate.PotentialUpdate +import moe.nea.libautoupdate.UpdateContext +import moe.nea.libautoupdate.UpdateData +import moe.nea.libautoupdate.UpdateTarget +import moe.nea.libautoupdate.UpdateUtils +import net.minecraft.util.ChatComponentText +import net.minecraft.util.ChatStyle +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent +import java.util.concurrent.CompletableFuture + +class UpdateChecker @Inject constructor( + val errorUtil: ErrorUtil, + val requestUtil: RequestUtil, +) { + + @Inject + lateinit var minecraftExecutor: MinecraftExecutor + + val updater = UpdateContext( + NightlyAwareGithubUpdateSource("nea89o", "LocalTransactionLedger"), + if (DevUtil.isDevEnv) UpdateTarget { listOf() } + else UpdateTarget.deleteAndSaveInTheSameFolder(UpdateChecker::class.java), + object : CurrentVersion { + override fun display(): String { + return BuildConfig.VERSION + } + + override fun isOlderThan(element: JsonElement?): Boolean { + if (element !is JsonPrimitive || !element.isString) return true + val newHash = element.asString // TODO: change once i support non nightly update streams + val length = minOf(newHash.length, BuildConfig.GIT_COMMIT.length) + if (newHash.substring(0, length).equals(BuildConfig.GIT_COMMIT.substring(0, length), ignoreCase = true)) + return false + return true + } + + + override fun toString(): String { + return "{gitversion:${BuildConfig.GIT_COMMIT}, version:${BuildConfig.FULL_VERSION}}" + } + }, + "ledger" + ) + + class NightlyAwareGithubUpdateSource(owner: String, repository: String) : + GithubReleaseUpdateSource(owner, repository) { + override fun selectUpdate(updateStream: String, releases: List<GithubRelease>): UpdateData? { + if (updateStream == "nightly") { + return findAsset(releases.find { it.tagName == "nightly" }) + } + return super.selectUpdate(updateStream, releases.filter { it.tagName != "nightly" }) + } + + val releaseRegex = "commit: `(?<hash>[a-f0-9]+)`".toPattern() + + override fun findAsset(release: GithubRelease?): UpdateData? { + val update = super.findAsset(release) as GithubReleaseUpdateData? ?: return null + return GithubReleaseUpdateData( + update.versionName, + releaseRegex.matcher(update.releaseDescription) + .takeIf { it.find() } + ?.run { group("hash") } + ?.let(::JsonPrimitive) + ?: update.versionNumber, + update.sha256, + update.download, + update.releaseDescription, + update.targetCommittish, + update.createdAt, + update.publishedAt, + update.htmlUrl + ) + } + } + + init { + UpdateUtils.patchConnection { + this.requestUtil.enhanceConnection(it) + } + } + + var latestUpdate: PotentialUpdate? = null + var hasNotified = false + + @SubscribeEvent + fun onStartup(event: RegistrationFinishedEvent) { + if (config.main.updateCheck == MainOptions.UpdateCheckBehaviour.NONE) return + launchUpdateCheck() + } + + fun launchUpdateCheck() { + errorUtil.listenToFuture( + updater.checkUpdate("nightly") + .thenAcceptAsync( + { + latestUpdate = it + informAboutUpdates(it) + }, minecraftExecutor) + ) + } + + @Inject + lateinit var config: LedgerConfig + + @Inject + lateinit var triggerCommand: TriggerCommand + + val installTrigger = "execute-download" + + @Inject + lateinit var logger: LedgerLogger + fun informAboutUpdates(potentialUpdate: PotentialUpdate) { + if (hasNotified) return + hasNotified = true + if (!potentialUpdate.isUpdateAvailable) return + logger.printOut( + ChatComponentText("§aThere is a new update for LocalTransactionLedger. Click here to automatically download and install it.") + .setChatStyle(ChatStyle().setChatClickEvent(triggerCommand.getTriggerCommandLine(installTrigger)))) + if (config.main.updateCheck == MainOptions.UpdateCheckBehaviour.FULL) { + downloadUpdate() + } + } + + var updateFuture: CompletableFuture<Void>? = null + + fun downloadUpdate() { + val l = latestUpdate ?: return + if (updateFuture != null) return + // TODO: inject into findAsset to overwrite the tag id with the commit id + logger.printOut("§aTrying to download ledger update ${l.update.versionName}") + updateFuture = + latestUpdate?.launchUpdate() + ?.thenAcceptAsync( + { + logger.printOut("§aLedger update downloaded. It will automatically apply after your next restart.") + }, minecraftExecutor) + ?.let(errorUtil::listenToFuture) + } + + @SubscribeEvent + fun onTrigger(event: TriggerEvent) { + if (event.action == installTrigger) { + event.isCanceled = true + downloadUpdate() + } + } + +}
\ 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<ItemId, Double>? { val f = rewardLine.unformattedString().trim() - return parseSpecialReward(f) - ?: idProvider.findStackableItemByName(f, true) - } - - private val specialRewardRegex = "\\+(?<amount>${SHORT_NUMBER_PATTERN})x? (?<what>.*)".toPattern() - - private fun parseSpecialReward(specialLine: String): Pair<ItemId, Double>? { - 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<ChatReceived>? = 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<ChatReceived>) + +}
\ No newline at end of file diff --git a/src/main/kotlin/moe/nea/ledger/utils/ErrorUtil.kt b/src/main/kotlin/moe/nea/ledger/utils/ErrorUtil.kt index 4ba313e..e0c83f9 100644 --- a/src/main/kotlin/moe/nea/ledger/utils/ErrorUtil.kt +++ b/src/main/kotlin/moe/nea/ledger/utils/ErrorUtil.kt @@ -4,12 +4,18 @@ import moe.nea.ledger.utils.di.Inject import moe.nea.ledger.utils.telemetry.ContextValue import moe.nea.ledger.utils.telemetry.EventRecorder import moe.nea.ledger.utils.telemetry.Span +import java.util.concurrent.CompletableFuture class ErrorUtil { @Inject lateinit var reporter: EventRecorder + fun reportAdHoc(message: String) { + report(Exception(message), message) + + } + fun report(exception: Throwable, message: String?) { Span.current().recordException(reporter, exception, message) } @@ -22,6 +28,14 @@ class ErrorUtil { return getOrNull() } + fun <T : CompletableFuture<*>> listenToFuture(t: T): T { + t.handle { ignored, exception -> + if (exception != null) + report(exception, "Uncaught exception in completable future") + } + return t + } + inline fun <T> catch( vararg pairs: Pair<String, ContextValue>, crossinline function: () -> T diff --git a/src/main/kotlin/moe/nea/ledger/utils/GsonUtil.kt b/src/main/kotlin/moe/nea/ledger/utils/GsonUtil.kt new file mode 100644 index 0000000..d3c1f6e --- /dev/null +++ b/src/main/kotlin/moe/nea/ledger/utils/GsonUtil.kt @@ -0,0 +1,10 @@ +package moe.nea.ledger.utils + +import com.google.gson.reflect.TypeToken + +object GsonUtil { + inline fun <reified T> typeToken(): TypeToken<T> { + return object : TypeToken<T>() {} + } + +}
\ No newline at end of file diff --git a/src/main/kotlin/moe/nea/ledger/utils/MinecraftExecutor.kt b/src/main/kotlin/moe/nea/ledger/utils/MinecraftExecutor.kt new file mode 100644 index 0000000..affd86c --- /dev/null +++ b/src/main/kotlin/moe/nea/ledger/utils/MinecraftExecutor.kt @@ -0,0 +1,10 @@ +package moe.nea.ledger.utils + +import net.minecraft.client.Minecraft +import java.util.concurrent.Executor + +class MinecraftExecutor : Executor { + override fun execute(command: Runnable) { + Minecraft.getMinecraft().addScheduledTask(command) + } +}
\ No newline at end of file diff --git a/src/main/kotlin/moe/nea/ledger/utils/di/DI.kt b/src/main/kotlin/moe/nea/ledger/utils/di/DI.kt index 6940f72..a9061d7 100644 --- a/src/main/kotlin/moe/nea/ledger/utils/di/DI.kt +++ b/src/main/kotlin/moe/nea/ledger/utils/di/DI.kt @@ -6,25 +6,35 @@ import java.util.Stack @Suppress("UNCHECKED_CAST") class DI { + private fun formatInjectionStack() = + injectionStack.joinToString(" -> ") + + fun <T : Any> getProvider(type: Class<T>): BaseDIProvider<T, *> { + val provider = providers[type] as BaseDIProvider<T, *>? + ?: error("Could not find provider for type $type") + return provider + } + private fun <T : Any, C> internalProvide(type: Class<T>, element: AnnotatedElement? = null): T { - val provider = providers[type] as BaseDIProvider<T, C> - val context = if (element == null) provider.createEmptyContext() else provider.createContext(element) - val key = Pair(type, context) - val existingValue = values[key] - if (existingValue != null) return existingValue as T - if (type in injectionStack) { - error("Found injection cycle: ${injectionStack.joinToString(" -> ")} -> $type") - } - injectionStack.push(type) - val value = try { - provider.provideWithContext(this, context) + try { + val provider = getProvider(type) as BaseDIProvider<T, C> + val context = if (element == null) provider.createEmptyContext() else provider.createContext(element) + val key = Pair(type, context) + val existingValue = values[key] + if (existingValue != null) return existingValue as T + if (type in injectionStack) { + error("Found injection cycle: ${formatInjectionStack()} -> $type") + } + injectionStack.push(type) + val value = + provider.provideWithContext(this, context) + val cycleCheckCookie = injectionStack.pop() + require(cycleCheckCookie == type) { "Unbalanced stack cookie: $cycleCheckCookie != $type" } + values[key] = value + return value } catch (ex: Exception) { - throw RuntimeException("Could not create instance for type $type", ex) + throw RuntimeException("Could not create instance for type $type (in stack ${formatInjectionStack()})", ex) } - val cycleCheckCookie = injectionStack.pop() - require(cycleCheckCookie == type) { "Unbalanced stack cookie: $cycleCheckCookie != $type" } - values[key] = value - return value } fun <T : Any> provide(type: Class<T>, element: AnnotatedElement? = null): T { diff --git a/src/main/kotlin/moe/nea/ledger/utils/di/DIProvider.kt b/src/main/kotlin/moe/nea/ledger/utils/di/DIProvider.kt index b5ce550..bd5b9ef 100644 --- a/src/main/kotlin/moe/nea/ledger/utils/di/DIProvider.kt +++ b/src/main/kotlin/moe/nea/ledger/utils/di/DIProvider.kt @@ -24,6 +24,7 @@ fun interface DIProvider<T : Any> : BaseDIProvider<T, Unit> { ?: clazz.constructors.find { it.parameterCount == 0 } ?: error("Could not find DI injection entrypoint for class $clazz")) as Constructor<out T> + // TODO: consider using unsafe init to inject the parameters *before* calling the constructor return DIProvider { di -> val typArgs = cons.parameters.map { di.provide(it.type, it) diff --git a/src/main/kotlin/moe/nea/ledger/utils/network/Request.kt b/src/main/kotlin/moe/nea/ledger/utils/network/Request.kt new file mode 100644 index 0000000..ddf2fcc --- /dev/null +++ b/src/main/kotlin/moe/nea/ledger/utils/network/Request.kt @@ -0,0 +1,40 @@ +package moe.nea.ledger.utils.network + +import com.google.gson.JsonElement +import java.net.URL + +data class Request( + val url: URL, + val method: Method, + val body: String?, + val headers: Map<String, String>, +) { + enum class Method { + GET, POST + } + + enum class MediaType(val text: String) { + JSON("application/json"), + TEXT("text/plain"), + HTML("text/html"), + ANY("*/*"), + } + + fun withHeaders(map: Map<String, String>): Request { + // TODO: enforce caselessness? + return this.copy(headers = headers + map) + } + + fun post() = copy(method = Method.POST) + fun get() = copy(method = Method.GET) + + fun json(element: JsonElement) = copy( + headers = headers + mapOf("content-type" to "application/json"), + body = element.toString()) + + fun accept(request: MediaType) = withHeaders(mapOf("accept" to request.text)) + + fun acceptJson() = accept(MediaType.JSON) + + fun execute(requestUtil: RequestUtil) = requestUtil.executeRequest(this) +}
\ No newline at end of file diff --git a/src/main/kotlin/moe/nea/ledger/utils/network/RequestUtil.kt b/src/main/kotlin/moe/nea/ledger/utils/network/RequestUtil.kt new file mode 100644 index 0000000..a49c65a --- /dev/null +++ b/src/main/kotlin/moe/nea/ledger/utils/network/RequestUtil.kt @@ -0,0 +1,63 @@ +package moe.nea.ledger.utils.network + +import moe.nea.ledger.utils.ErrorUtil +import moe.nea.ledger.utils.di.Inject +import java.net.URL +import java.net.URLConnection +import java.security.KeyStore +import java.util.zip.GZIPInputStream +import javax.net.ssl.HttpsURLConnection +import javax.net.ssl.KeyManagerFactory +import javax.net.ssl.SSLContext +import javax.net.ssl.TrustManagerFactory + +class RequestUtil @Inject constructor(val errorUtil: ErrorUtil) { + + private fun createSSLContext(): SSLContext? = errorUtil.catch { + val keyStorePath = RequestUtil::class.java.getResourceAsStream("/ledgerkeystore.jks") + ?: error("Could not locate keystore") + val keyStore = KeyStore.getInstance("JKS") + keyStore.load(keyStorePath, "neuneu".toCharArray()) + val kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()) + val tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) + kmf.init(keyStore, null) + tmf.init(keyStore) + val ctx = SSLContext.getInstance("TLS") + ctx.init(kmf.keyManagers, tmf.trustManagers, null) + return@catch ctx + } + + val sslContext = createSSLContext() + + fun enhanceConnection(connection: URLConnection) { + if (connection is HttpsURLConnection && sslContext != null) { + connection.sslSocketFactory = sslContext.socketFactory + } + } + + fun createRequest(url: String) = createRequest(URL(url)) + fun createRequest(url: URL) = Request(url, Request.Method.GET, null, mapOf()) + + fun executeRequest(request: Request): Response { + val connection = request.url.openConnection() + enhanceConnection(connection) + connection.setRequestProperty("accept-encoding", "gzip") + request.headers.forEach { (k, v) -> + connection.setRequestProperty(k, v) + } + if (request.body != null) { + connection.getOutputStream().write(request.body.encodeToByteArray()) + connection.getOutputStream().close() + } + var stream = connection.getInputStream() + if (connection.contentEncoding == "gzip") { + stream = GZIPInputStream(stream) + } + val text = stream.bufferedReader().readText() + stream.close() + // Do NOT call connection.disconnect() to allow for connection reuse + return Response(request, text, connection.headerFields) + } + + +}
\ No newline at end of file diff --git a/src/main/kotlin/moe/nea/ledger/utils/network/Response.kt b/src/main/kotlin/moe/nea/ledger/utils/network/Response.kt new file mode 100644 index 0000000..daae7f7 --- /dev/null +++ b/src/main/kotlin/moe/nea/ledger/utils/network/Response.kt @@ -0,0 +1,19 @@ +package moe.nea.ledger.utils.network + +import com.google.gson.reflect.TypeToken +import moe.nea.ledger.Ledger + +data class Response( + val source: Request, + // TODO: allow other body processors, to avoid loading everything as strings + val response: String, + val headers: Map<String, List<String>>, +) { + fun <T : Any> json(typ: TypeToken<T>): T { + return Ledger.gson.fromJson(response, typ.type) + } + + fun <T : Any> json(clazz: Class<T>): T { + return Ledger.gson.fromJson(response, clazz) + } +}
\ No newline at end of file diff --git a/src/main/kotlin/moe/nea/ledger/utils/telemetry/ExceptionContextValue.kt b/src/main/kotlin/moe/nea/ledger/utils/telemetry/ExceptionContextValue.kt index df588a8..96b70ec 100644 --- a/src/main/kotlin/moe/nea/ledger/utils/telemetry/ExceptionContextValue.kt +++ b/src/main/kotlin/moe/nea/ledger/utils/telemetry/ExceptionContextValue.kt @@ -22,8 +22,9 @@ class ExceptionContextValue(val exception: Throwable) : ContextValue { obj.addProperty("message", exception.message) // TODO: allow exceptions to implement an "extra info" interface if (searchDepth > 0) { - if (exception.cause != null) { - obj.add("cause", walkExceptions(exception, searchDepth - 1)) + val cause = exception.cause + if (cause != null && cause !== exception) { + obj.add("cause", walkExceptions(cause, searchDepth - 1)) } val suppressions = JsonArray() for (suppressedException in exception.suppressedExceptions) { |