aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/main/java/moe/nea/ledger/mixin/OnInitializationCompletePatch.java18
-rw-r--r--src/main/kotlin/moe/nea/ledger/DebugDataCommand.kt34
-rw-r--r--src/main/kotlin/moe/nea/ledger/ItemChange.kt7
-rw-r--r--src/main/kotlin/moe/nea/ledger/ItemId.kt3
-rw-r--r--src/main/kotlin/moe/nea/ledger/ItemIdProvider.kt76
-rw-r--r--src/main/kotlin/moe/nea/ledger/ItemUtil.kt22
-rw-r--r--src/main/kotlin/moe/nea/ledger/Ledger.kt29
-rw-r--r--src/main/kotlin/moe/nea/ledger/NumberUtil.kt8
-rw-r--r--src/main/kotlin/moe/nea/ledger/QueryCommand.kt32
-rw-r--r--src/main/kotlin/moe/nea/ledger/TransactionType.kt3
-rw-r--r--src/main/kotlin/moe/nea/ledger/TriggerCommand.kt34
-rw-r--r--src/main/kotlin/moe/nea/ledger/config/LedgerConfig.kt4
-rw-r--r--src/main/kotlin/moe/nea/ledger/config/MainOptions.kt27
-rw-r--r--src/main/kotlin/moe/nea/ledger/config/UpdateUi.kt17
-rw-r--r--src/main/kotlin/moe/nea/ledger/config/UpdateUiMarker.kt6
-rw-r--r--src/main/kotlin/moe/nea/ledger/events/ChatReceived.kt2
-rw-r--r--src/main/kotlin/moe/nea/ledger/events/InitializationComplete.kt6
-rw-r--r--src/main/kotlin/moe/nea/ledger/events/SupplyDebugInfo.kt10
-rw-r--r--src/main/kotlin/moe/nea/ledger/events/TriggerEvent.kt7
-rw-r--r--src/main/kotlin/moe/nea/ledger/modules/ExternalDataProvider.kt43
-rw-r--r--src/main/kotlin/moe/nea/ledger/modules/ForgeDetection.kt48
-rw-r--r--src/main/kotlin/moe/nea/ledger/modules/GambleDetection.kt61
-rw-r--r--src/main/kotlin/moe/nea/ledger/modules/MineshaftCorpseDetection.kt81
-rw-r--r--src/main/kotlin/moe/nea/ledger/modules/NpcDetection.kt52
-rw-r--r--src/main/kotlin/moe/nea/ledger/modules/UpdateChecker.kt167
-rw-r--r--src/main/kotlin/moe/nea/ledger/modules/VisitorDetection.kt24
-rw-r--r--src/main/kotlin/moe/nea/ledger/utils/BorderedTextTracker.kt41
-rw-r--r--src/main/kotlin/moe/nea/ledger/utils/ErrorUtil.kt14
-rw-r--r--src/main/kotlin/moe/nea/ledger/utils/GsonUtil.kt10
-rw-r--r--src/main/kotlin/moe/nea/ledger/utils/MinecraftExecutor.kt10
-rw-r--r--src/main/kotlin/moe/nea/ledger/utils/di/DI.kt42
-rw-r--r--src/main/kotlin/moe/nea/ledger/utils/di/DIProvider.kt1
-rw-r--r--src/main/kotlin/moe/nea/ledger/utils/network/Request.kt40
-rw-r--r--src/main/kotlin/moe/nea/ledger/utils/network/RequestUtil.kt63
-rw-r--r--src/main/kotlin/moe/nea/ledger/utils/network/Response.kt19
-rw-r--r--src/main/kotlin/moe/nea/ledger/utils/telemetry/ExceptionContextValue.kt5
-rw-r--r--src/main/resources/ledgerkeystore.jksbin0 -> 104393 bytes
37 files changed, 1014 insertions, 52 deletions
diff --git a/src/main/java/moe/nea/ledger/mixin/OnInitializationCompletePatch.java b/src/main/java/moe/nea/ledger/mixin/OnInitializationCompletePatch.java
new file mode 100644
index 0000000..fc9afb7
--- /dev/null
+++ b/src/main/java/moe/nea/ledger/mixin/OnInitializationCompletePatch.java
@@ -0,0 +1,18 @@
+package moe.nea.ledger.mixin;
+
+import moe.nea.ledger.events.InitializationComplete;
+import net.minecraft.client.Minecraft;
+import net.minecraftforge.common.MinecraftForge;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+@Mixin(Minecraft.class)
+public class OnInitializationCompletePatch {
+
+ @Inject(method = "startGame", at = @At(value = "INVOKE", target = "Lnet/minecraftforge/fml/client/FMLClientHandler;onInitializationComplete()V"))
+ private void onInitComplete(CallbackInfo ci) {
+ MinecraftForge.EVENT_BUS.post(new InitializationComplete());
+ }
+}
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) {
diff --git a/src/main/resources/ledgerkeystore.jks b/src/main/resources/ledgerkeystore.jks
new file mode 100644
index 0000000..b71185a
--- /dev/null
+++ b/src/main/resources/ledgerkeystore.jks
Binary files differ