From d1e16a47819509ed645bb93e1a173e0a97025cef Mon Sep 17 00:00:00 2001 From: Linnea Gräf Date: Wed, 8 Jan 2025 19:25:29 +0100 Subject: build: Move mod to subproject --- .../main/kotlin/moe/nea/ledger/ConfigCommand.kt | 31 ++++ .../main/kotlin/moe/nea/ledger/DebouncedValue.kt | 38 ++++ .../main/kotlin/moe/nea/ledger/DebugDataCommand.kt | 34 ++++ mod/src/main/kotlin/moe/nea/ledger/DevUtil.kt | 7 + .../main/kotlin/moe/nea/ledger/ExpiringValue.kt | 30 +++ mod/src/main/kotlin/moe/nea/ledger/ItemChange.kt | 84 +++++++++ mod/src/main/kotlin/moe/nea/ledger/ItemId.kt | 35 ++++ .../main/kotlin/moe/nea/ledger/ItemIdProvider.kt | 188 +++++++++++++++++++ mod/src/main/kotlin/moe/nea/ledger/ItemUtil.kt | 90 +++++++++ mod/src/main/kotlin/moe/nea/ledger/Ledger.kt | 205 +++++++++++++++++++++ mod/src/main/kotlin/moe/nea/ledger/LedgerEntry.kt | 29 +++ mod/src/main/kotlin/moe/nea/ledger/LedgerLogger.kt | 136 ++++++++++++++ .../main/kotlin/moe/nea/ledger/LogChatCommand.kt | 27 +++ mod/src/main/kotlin/moe/nea/ledger/MCUUIDUtil.kt | 22 +++ mod/src/main/kotlin/moe/nea/ledger/NumberUtil.kt | 117 ++++++++++++ mod/src/main/kotlin/moe/nea/ledger/QueryCommand.kt | 197 ++++++++++++++++++++ .../main/kotlin/moe/nea/ledger/ScoreboardUtil.kt | 29 +++ .../kotlin/moe/nea/ledger/TelemetryProvider.kt | 66 +++++++ .../main/kotlin/moe/nea/ledger/TransactionType.kt | 35 ++++ .../main/kotlin/moe/nea/ledger/TriggerCommand.kt | 34 ++++ .../kotlin/moe/nea/ledger/config/DebugOptions.kt | 13 ++ .../kotlin/moe/nea/ledger/config/LedgerConfig.kt | 35 ++++ .../kotlin/moe/nea/ledger/config/MainOptions.kt | 27 +++ .../nea/ledger/config/SynchronizationOptions.kt | 11 ++ .../main/kotlin/moe/nea/ledger/config/UpdateUi.kt | 17 ++ .../kotlin/moe/nea/ledger/config/UpdateUiMarker.kt | 6 + .../kotlin/moe/nea/ledger/database/DBLogEntry.kt | 24 +++ .../kotlin/moe/nea/ledger/database/DBUpgrade.kt | 68 +++++++ .../kotlin/moe/nea/ledger/database/Database.kt | 57 ++++++ .../kotlin/moe/nea/ledger/database/Upgrades.kt | 20 ++ .../main/kotlin/moe/nea/ledger/database/schema.dot | 23 +++ .../moe/nea/ledger/events/BeforeGuiAction.kt | 11 ++ .../kotlin/moe/nea/ledger/events/ChatReceived.kt | 15 ++ .../moe/nea/ledger/events/ExtraSupplyIdEvent.kt | 12 ++ .../kotlin/moe/nea/ledger/events/GuiClickEvent.kt | 9 + .../nea/ledger/events/InitializationComplete.kt | 6 + .../nea/ledger/events/RegistrationFinishedEvent.kt | 7 + .../moe/nea/ledger/events/SupplyDebugInfo.kt | 10 + .../kotlin/moe/nea/ledger/events/TriggerEvent.kt | 7 + .../kotlin/moe/nea/ledger/events/WorldLoadEvent.kt | 5 + .../moe/nea/ledger/events/WorldSwitchEvent.kt | 6 + .../ledger/modules/AccessorySwapperDetection.kt | 34 ++++ .../moe/nea/ledger/modules/AllowanceDetection.kt | 37 ++++ .../nea/ledger/modules/AuctionHouseDetection.kt | 143 ++++++++++++++ .../kotlin/moe/nea/ledger/modules/BankDetection.kt | 49 +++++ .../moe/nea/ledger/modules/BazaarDetection.kt | 58 ++++++ .../moe/nea/ledger/modules/BazaarOrderDetection.kt | 95 ++++++++++ .../kotlin/moe/nea/ledger/modules/BitsDetection.kt | 62 +++++++ .../moe/nea/ledger/modules/BitsShopDetection.kt | 66 +++++++ .../moe/nea/ledger/modules/ChestDetection.kt | 48 +++++ .../ledger/modules/DragonEyePlacementDetection.kt | 47 +++++ .../nea/ledger/modules/DragonSacrificeDetection.kt | 72 ++++++++ .../nea/ledger/modules/DungeonChestDetection.kt | 95 ++++++++++ .../moe/nea/ledger/modules/ExternalDataProvider.kt | 43 +++++ .../moe/nea/ledger/modules/EyedropsDetection.kt | 35 ++++ .../moe/nea/ledger/modules/ForgeDetection.kt | 48 +++++ .../moe/nea/ledger/modules/GambleDetection.kt | 62 +++++++ .../moe/nea/ledger/modules/GodPotionDetection.kt | 35 ++++ .../nea/ledger/modules/GodPotionMixinDetection.kt | 38 ++++ .../kotlin/moe/nea/ledger/modules/KatDetection.kt | 95 ++++++++++ .../moe/nea/ledger/modules/KuudraChestDetection.kt | 45 +++++ .../nea/ledger/modules/MineshaftCorpseDetection.kt | 81 ++++++++ .../moe/nea/ledger/modules/MinionDetection.kt | 61 ++++++ .../kotlin/moe/nea/ledger/modules/NpcDetection.kt | 111 +++++++++++ .../kotlin/moe/nea/ledger/modules/UpdateChecker.kt | 167 +++++++++++++++++ .../moe/nea/ledger/modules/VisitorDetection.kt | 87 +++++++++ .../moe/nea/ledger/utils/BorderedTextTracker.kt | 41 +++++ .../main/kotlin/moe/nea/ledger/utils/ErrorUtil.kt | 52 ++++++ .../main/kotlin/moe/nea/ledger/utils/GsonUtil.kt | 10 + .../moe/nea/ledger/utils/MinecraftExecutor.kt | 10 + .../kotlin/moe/nea/ledger/utils/NoSideEffects.kt | 4 + .../kotlin/moe/nea/ledger/utils/network/Request.kt | 40 ++++ .../moe/nea/ledger/utils/network/RequestUtil.kt | 63 +++++++ .../moe/nea/ledger/utils/network/Response.kt | 19 ++ .../nea/ledger/utils/telemetry/BooleanContext.kt | 10 + .../moe/nea/ledger/utils/telemetry/CommonKeys.kt | 9 + .../moe/nea/ledger/utils/telemetry/Context.kt | 57 ++++++ .../moe/nea/ledger/utils/telemetry/ContextValue.kt | 70 +++++++ .../nea/ledger/utils/telemetry/EventRecorder.kt | 9 + .../utils/telemetry/ExceptionContextValue.kt | 39 ++++ .../ledger/utils/telemetry/JsonElementContext.kt | 9 + .../ledger/utils/telemetry/LoggingEventRecorder.kt | 25 +++ .../nea/ledger/utils/telemetry/RecordedEvent.kt | 5 + .../moe/nea/ledger/utils/telemetry/Severity.kt | 8 + .../kotlin/moe/nea/ledger/utils/telemetry/Span.kt | 146 +++++++++++++++ .../nea/ledger/utils/telemetry/StringContext.kt | 11 ++ 86 files changed, 4174 insertions(+) create mode 100644 mod/src/main/kotlin/moe/nea/ledger/ConfigCommand.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/DebouncedValue.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/DebugDataCommand.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/DevUtil.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/ExpiringValue.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/ItemChange.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/ItemId.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/ItemIdProvider.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/ItemUtil.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/Ledger.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/LedgerEntry.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/LedgerLogger.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/LogChatCommand.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/MCUUIDUtil.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/NumberUtil.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/QueryCommand.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/ScoreboardUtil.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/TelemetryProvider.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/TransactionType.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/TriggerCommand.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/config/DebugOptions.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/config/LedgerConfig.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/config/MainOptions.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/config/SynchronizationOptions.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/config/UpdateUi.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/config/UpdateUiMarker.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/database/DBLogEntry.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/database/DBUpgrade.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/database/Database.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/database/Upgrades.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/database/schema.dot create mode 100644 mod/src/main/kotlin/moe/nea/ledger/events/BeforeGuiAction.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/events/ChatReceived.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/events/ExtraSupplyIdEvent.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/events/GuiClickEvent.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/events/InitializationComplete.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/events/RegistrationFinishedEvent.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/events/SupplyDebugInfo.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/events/TriggerEvent.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/events/WorldLoadEvent.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/events/WorldSwitchEvent.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/modules/AccessorySwapperDetection.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/modules/AllowanceDetection.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/modules/AuctionHouseDetection.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/modules/BankDetection.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/modules/BazaarDetection.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/modules/BazaarOrderDetection.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/modules/BitsDetection.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/modules/BitsShopDetection.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/modules/ChestDetection.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/modules/DragonEyePlacementDetection.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/modules/DragonSacrificeDetection.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/modules/DungeonChestDetection.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/modules/ExternalDataProvider.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/modules/EyedropsDetection.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/modules/ForgeDetection.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/modules/GambleDetection.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/modules/GodPotionDetection.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/modules/GodPotionMixinDetection.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/modules/KatDetection.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/modules/KuudraChestDetection.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/modules/MineshaftCorpseDetection.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/modules/MinionDetection.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/modules/NpcDetection.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/modules/UpdateChecker.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/modules/VisitorDetection.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/utils/BorderedTextTracker.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/utils/ErrorUtil.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/utils/GsonUtil.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/utils/MinecraftExecutor.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/utils/NoSideEffects.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/utils/network/Request.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/utils/network/RequestUtil.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/utils/network/Response.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/BooleanContext.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/CommonKeys.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/Context.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/ContextValue.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/EventRecorder.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/ExceptionContextValue.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/JsonElementContext.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/LoggingEventRecorder.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/RecordedEvent.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/Severity.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/Span.kt create mode 100644 mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/StringContext.kt (limited to 'mod/src/main/kotlin') diff --git a/mod/src/main/kotlin/moe/nea/ledger/ConfigCommand.kt b/mod/src/main/kotlin/moe/nea/ledger/ConfigCommand.kt new file mode 100644 index 0000000..5b964c8 --- /dev/null +++ b/mod/src/main/kotlin/moe/nea/ledger/ConfigCommand.kt @@ -0,0 +1,31 @@ +package moe.nea.ledger + +import io.github.notenoughupdates.moulconfig.common.IMinecraft +import net.minecraft.command.CommandBase +import net.minecraft.command.ICommandSender + +class ConfigCommand : CommandBase() { + override fun canCommandSenderUseCommand(sender: ICommandSender?): Boolean { + return true + } + + override fun getCommandName(): String { + return "ledgerconfig" + } + + override fun getCommandUsage(sender: ICommandSender?): String { + return "" + } + + override fun processCommand(sender: ICommandSender?, args: Array) { + val editor = Ledger.managedConfig.getEditor() + editor.search(args.joinToString(" ")) + Ledger.runLater { + IMinecraft.instance.openWrappedScreen(editor) + } + } + + override fun getCommandAliases(): List { + return listOf("moneyledger") + } +} \ No newline at end of file diff --git a/mod/src/main/kotlin/moe/nea/ledger/DebouncedValue.kt b/mod/src/main/kotlin/moe/nea/ledger/DebouncedValue.kt new file mode 100644 index 0000000..66fba8d --- /dev/null +++ b/mod/src/main/kotlin/moe/nea/ledger/DebouncedValue.kt @@ -0,0 +1,38 @@ +package moe.nea.ledger + +import kotlin.time.Duration +import kotlin.time.Duration.Companion.nanoseconds +import kotlin.time.Duration.Companion.seconds + +class DebouncedValue(private val value: T) { + companion object { + fun farFuture(): DebouncedValue { + val value = DebouncedValue(Unit) + value.take() + @Suppress("UNCHECKED_CAST") + return value as DebouncedValue + } + } + + val lastSeenAt = System.nanoTime() + val age get() = (System.nanoTime() - lastSeenAt).nanoseconds + var taken = false + private set + + fun get(debounce: Duration): T? { + return if (!taken && age >= debounce) value + else null + } + + fun replace(): T? { + return consume(0.seconds) + } + + fun consume(debounce: Duration): T? { + return get(debounce)?.also { take() } + } + + fun take() { + taken = true + } +} \ No newline at end of file diff --git a/mod/src/main/kotlin/moe/nea/ledger/DebugDataCommand.kt b/mod/src/main/kotlin/moe/nea/ledger/DebugDataCommand.kt new file mode 100644 index 0000000..bab0a78 --- /dev/null +++ b/mod/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?) { + 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/mod/src/main/kotlin/moe/nea/ledger/DevUtil.kt b/mod/src/main/kotlin/moe/nea/ledger/DevUtil.kt new file mode 100644 index 0000000..d0dd653 --- /dev/null +++ b/mod/src/main/kotlin/moe/nea/ledger/DevUtil.kt @@ -0,0 +1,7 @@ +package moe.nea.ledger + +import net.minecraft.launchwrapper.Launch + +object DevUtil { + val isDevEnv = Launch.blackboard["fml.deobfuscatedEnvironment"] as Boolean +} \ No newline at end of file diff --git a/mod/src/main/kotlin/moe/nea/ledger/ExpiringValue.kt b/mod/src/main/kotlin/moe/nea/ledger/ExpiringValue.kt new file mode 100644 index 0000000..b50b14e --- /dev/null +++ b/mod/src/main/kotlin/moe/nea/ledger/ExpiringValue.kt @@ -0,0 +1,30 @@ +package moe.nea.ledger + +import kotlin.time.Duration +import kotlin.time.Duration.Companion.nanoseconds + +class ExpiringValue(private val value: T) { + val lastSeenAt: Long = System.nanoTime() + val age get() = (System.nanoTime() - lastSeenAt).nanoseconds + var taken = false + private set + + fun get(expiry: Duration): T? { + return if (!taken && age < expiry) value + else null + } + + companion object { + fun empty(): ExpiringValue { + val value = ExpiringValue(Unit) + value.take() + @Suppress("UNCHECKED_CAST") + return value as ExpiringValue + } + } + + fun consume(expiry: Duration): T? = get(expiry)?.also { take() } + fun take() { + taken = true + } +} \ No newline at end of file diff --git a/mod/src/main/kotlin/moe/nea/ledger/ItemChange.kt b/mod/src/main/kotlin/moe/nea/ledger/ItemChange.kt new file mode 100644 index 0000000..fda709c --- /dev/null +++ b/mod/src/main/kotlin/moe/nea/ledger/ItemChange.kt @@ -0,0 +1,84 @@ +package moe.nea.ledger + +import moe.nea.ledger.database.DBItemEntry +import moe.nea.ledger.database.ResultRow +import net.minecraft.event.HoverEvent +import net.minecraft.util.ChatComponentText +import net.minecraft.util.ChatStyle +import net.minecraft.util.EnumChatFormatting +import net.minecraft.util.IChatComponent + +data class ItemChange( + val itemId: ItemId, + val count: Double, + val direction: ChangeDirection, +) { + fun formatChat(): IChatComponent { + return ChatComponentText(" ") + .appendSibling(direction.chatFormat) + .appendText(" ") + .appendSibling(ChatComponentText("$count").setChatStyle(ChatStyle().setColor(EnumChatFormatting.WHITE))) + .appendSibling(ChatComponentText("x").setChatStyle(ChatStyle().setColor(EnumChatFormatting.DARK_GRAY))) + .appendText(" ") + .appendSibling(ChatComponentText(itemId.string).setChatStyle(ChatStyle().setParentStyle(ChatStyle().setColor( + EnumChatFormatting.WHITE)))) + } + + enum class ChangeDirection { + GAINED, + TRANSFORM, + SYNC, + CATALYST, + LOST; + + val chatFormat by lazy { formatChat0() } + private fun formatChat0(): IChatComponent { + val (text, color) = when (this) { + GAINED -> "+" to EnumChatFormatting.GREEN + TRANSFORM -> "~" to EnumChatFormatting.YELLOW + SYNC -> "=" to EnumChatFormatting.BLUE + CATALYST -> "*" to EnumChatFormatting.DARK_PURPLE + LOST -> "-" to EnumChatFormatting.RED + } + return ChatComponentText(text) + .setChatStyle( + ChatStyle() + .setColor(color) + .setChatHoverEvent(HoverEvent(HoverEvent.Action.SHOW_TEXT, + ChatComponentText(name).setChatStyle(ChatStyle().setColor(color))))) + } + } + + companion object { + fun gainCoins(number: Double): ItemChange { + return gain(ItemId.COINS, number) + } + + fun unpair(direction: ChangeDirection, pair: Pair): ItemChange { + return ItemChange(pair.first, pair.second, direction) + } + + fun unpairGain(pair: Pair) = unpair(ChangeDirection.GAINED, pair) + fun unpairLose(pair: Pair) = unpair(ChangeDirection.LOST, pair) + + fun gain(itemId: ItemId, amount: Number): ItemChange { + return ItemChange(itemId, amount.toDouble(), ChangeDirection.GAINED) + } + + fun lose(itemId: ItemId, amount: Number): ItemChange { + return ItemChange(itemId, amount.toDouble(), ChangeDirection.LOST) + } + + fun loseCoins(number: Double): ItemChange { + return lose(ItemId.COINS, number) + } + + fun from(result: ResultRow): ItemChange { + return ItemChange( + result[DBItemEntry.itemId], + result[DBItemEntry.size], + result[DBItemEntry.mode], + ) + } + } +} \ No newline at end of file diff --git a/mod/src/main/kotlin/moe/nea/ledger/ItemId.kt b/mod/src/main/kotlin/moe/nea/ledger/ItemId.kt new file mode 100644 index 0000000..8211cd3 --- /dev/null +++ b/mod/src/main/kotlin/moe/nea/ledger/ItemId.kt @@ -0,0 +1,35 @@ +package moe.nea.ledger + +import moe.nea.ledger.utils.NoSideEffects + +data class ItemId( + val string: String +) { + @NoSideEffects + fun singleItem(): Pair { + return withStackSize(1) + } + + @NoSideEffects + fun withStackSize(size: Number): Pair { + return Pair(this, size.toDouble()) + } + + + companion object { + + @JvmStatic + @NoSideEffects + fun forName(string: String) = ItemId(string) + fun skill(skill: String) = ItemId("SKYBLOCK_SKILL_$skill") + + val GARDEN = skill("GARDEN") + val FARMING = skill("FARMING") + + + val COINS = ItemId("SKYBLOCK_COIN") + val GEMSTONE_POWDER = ItemId("SKYBLOCK_POWDER_GEMSTONE") + val MITHRIL_POWDER = ItemId("SKYBLOCK_POWDER_MITHRIL") + val NIL = ItemId("SKYBLOCK_NIL") + } +} \ No newline at end of file diff --git a/mod/src/main/kotlin/moe/nea/ledger/ItemIdProvider.kt b/mod/src/main/kotlin/moe/nea/ledger/ItemIdProvider.kt new file mode 100644 index 0000000..0bacf32 --- /dev/null +++ b/mod/src/main/kotlin/moe/nea/ledger/ItemIdProvider.kt @@ -0,0 +1,188 @@ +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.gen.ItemIds +import moe.nea.ledger.modules.ExternalDataProvider +import net.minecraft.client.Minecraft +import net.minecraft.item.ItemStack +import net.minecraft.nbt.NBTTagCompound +import net.minecraftforge.client.event.GuiScreenEvent +import net.minecraftforge.common.MinecraftForge +import net.minecraftforge.fml.common.eventhandler.EventPriority +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent +import org.lwjgl.input.Mouse + +class ItemIdProvider { + + @SubscribeEvent + fun onMouseInput(event: GuiScreenEvent.MouseInputEvent.Pre) { + if (Mouse.getEventButton() == -1) return + MinecraftForge.EVENT_BUS.post(BeforeGuiAction(event.gui)) + } + + @SubscribeEvent + fun onKeyInput(event: GuiScreenEvent.KeyboardInputEvent.Pre) { + MinecraftForge.EVENT_BUS.post(BeforeGuiAction(event.gui)) + } + + private val knownNames = mutableMapOf() + + 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(::saveKnownItem)) + } + + @SubscribeEvent(priority = EventPriority.HIGH) + fun savePlayerInventoryIds(event: BeforeGuiAction) { + val player = Minecraft.getMinecraft().thePlayer ?: return + val inventory = player.inventory ?: return + inventory.mainInventory?.forEach { saveFromSlot(it) } + 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() + val display = nbt.getCompoundTag("display") + var name = display.getString("Name").unformattedString() + name = preprocessName(name) + name = name.trim() + val id = stack.getInternalId() + if (id != null && name.isNotBlank()) { + saveKnownItem(name, id) + } + } + + @SubscribeEvent(priority = EventPriority.HIGH) + fun saveChestInventoryIds(event: BeforeGuiAction) { + val slots = event.chestSlots ?: return + val chestName = slots.lowerChestInventory.name.unformattedString() + val isOrderMenu = chestName == "Your Bazaar Orders" || chestName == "Co-op Bazaar Orders" + val preprocessor: (String) -> String = if (isOrderMenu) { + { it.removePrefix("BUY ").removePrefix("SELL ") } + } else { + { it } + } + slots.inventorySlots.forEach { + saveFromSlot(it?.stack, preprocessor) + } + } + + // TODO: make use of colour + fun findForName(name: String, fallbackToGenerated: Boolean = true): ItemId? { + var id = knownNames[createLookupTagFromDisplayName(name)] + if (id == null && fallbackToGenerated) { + id = generateName(name) + } + return id + } + + fun generateName(name: String): ItemId { + return ItemId(name.uppercase().replace(" ", "_")) + } + + private val coinRegex = "(?$SHORT_NUMBER_PATTERN) Coins?".toPattern() + private val stackedItemRegex = "(?.*) x(?$SHORT_NUMBER_PATTERN)".toPattern() + private val reverseStackedItemRegex = "(?$SHORT_NUMBER_PATTERN)x (?.*)".toPattern() + private val essenceRegex = "(?.*) Essence x(?$SHORT_NUMBER_PATTERN)".toPattern() + private val numberedItemRegex = "(?$SHORT_NUMBER_PATTERN) (?.*)".toPattern() + + fun findCostItemsFromSpan(lore: List): List> { + return lore.iterator().asSequence() + .dropWhile { it.unformattedString() != "Cost" }.drop(1) + .takeWhile { it != "" } + .map { findStackableItemByName(it) ?: Pair(ItemId.NIL, 1.0) } + .toList() + } + + private val etherialRewardPattern = "\\+(?${SHORT_NUMBER_PATTERN})x? (?.*)".toPattern() + + fun findStackableItemByName(name: String, fallbackToGenerated: Boolean = false): Pair? { + 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" -> ItemIds.SKYBLOCK_COPPER + "Bits" -> ItemIds.SKYBLOCK_BIT + "Garden Experience" -> ItemId.GARDEN + "Farming XP" -> ItemId.FARMING + "Gold Essence" -> ItemIds.ESSENCE_GOLD + "Gemstone Powder" -> ItemId.GEMSTONE_POWDER + "Mithril Powder" -> ItemId.MITHRIL_POWDER + "Pelts" -> ItemIds.SKYBLOCK_PELT + "Fine Flour" -> ItemIds.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"))) + } + stackedItemRegex.useMatcher(properName) { + val item = findForName(group("name"), fallbackToGenerated) + if (item != null) { + val count = parseShortNumber(group("count")) + return Pair(item, count) + } + } + reverseStackedItemRegex.useMatcher(properName) { + val item = findForName(group("name"), fallbackToGenerated) + if (item != null) { + val count = parseShortNumber(group("count")) + 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 { + return knownNames.values + } +} \ No newline at end of file diff --git a/mod/src/main/kotlin/moe/nea/ledger/ItemUtil.kt b/mod/src/main/kotlin/moe/nea/ledger/ItemUtil.kt new file mode 100644 index 0000000..a3d8162 --- /dev/null +++ b/mod/src/main/kotlin/moe/nea/ledger/ItemUtil.kt @@ -0,0 +1,90 @@ +package moe.nea.ledger + +import net.minecraft.inventory.IInventory +import net.minecraft.item.ItemStack +import net.minecraft.nbt.NBTTagCompound + + +fun ItemStack.getExtraAttributes(): NBTTagCompound { + val nbt = this.tagCompound ?: return NBTTagCompound() + return nbt.getCompoundTag("ExtraAttributes") +} + +fun ItemStack?.getInternalId(): ItemId? { + if (this == null) return null + val extraAttributes = getExtraAttributes() + var id = extraAttributes.getString("id") + id = id.takeIf { it.isNotBlank() } + if (id == "PET") { + id = getPetId() ?: id + } + if (id == "ENCHANTED_BOOK") { + id = getEnchanments().entries.singleOrNull()?.let { + "${it.key};${it.value}".uppercase() + } + } + return id?.let(::ItemId) +} + +fun ItemStack.getEnchanments(): Map { + val enchantments = getExtraAttributes().getCompoundTag("enchantments") + return enchantments.keySet.associateWith { enchantments.getInteger(it) } +} + +class PetInfo { + var type: String? = null + var tier: String? = null +} + +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 + if (petInfo?.type == null || petInfo.tier == null) return null + return petInfo.type + ";" + rarityToIndex(petInfo.tier ?: "") +} + +fun rarityToIndex(rarity: String): Int { + return when (rarity) { + "COMMON" -> 0 + "UNCOMMON" -> 1 + "RARE" -> 2 + "EPIC" -> 3 + "LEGENDARY" -> 4 + "MYTHIC" -> 5 + else -> -1 + } +} + +fun ItemStack.getLore(): List { + val nbt = this.tagCompound ?: NBTTagCompound() + val extraAttributes = nbt.getCompoundTag("display") + val lore = extraAttributes.getTagList("Lore", 8) + return (0 until lore.tagCount()).map { lore.getStringTagAt(it) } +} + + +fun IInventory.asIterable(): Iterable = object : Iterable { + override fun iterator(): Iterator { + return object : Iterator { + 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") + return extraAttributes.getString("Name") +} + diff --git a/mod/src/main/kotlin/moe/nea/ledger/Ledger.kt b/mod/src/main/kotlin/moe/nea/ledger/Ledger.kt new file mode 100644 index 0000000..bc667e4 --- /dev/null +++ b/mod/src/main/kotlin/moe/nea/ledger/Ledger.kt @@ -0,0 +1,205 @@ +package moe.nea.ledger + +import com.google.gson.Gson +import io.github.notenoughupdates.moulconfig.Config +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 +import moe.nea.ledger.events.RegistrationFinishedEvent +import moe.nea.ledger.events.WorldSwitchEvent +import moe.nea.ledger.gen.BuildConfig +import moe.nea.ledger.modules.AccessorySwapperDetection +import moe.nea.ledger.modules.AllowanceDetection +import moe.nea.ledger.modules.AuctionHouseDetection +import moe.nea.ledger.modules.BankDetection +import moe.nea.ledger.modules.BazaarDetection +import moe.nea.ledger.modules.BazaarOrderDetection +import moe.nea.ledger.modules.BitsDetection +import moe.nea.ledger.modules.BitsShopDetection +import moe.nea.ledger.modules.DragonEyePlacementDetection +import moe.nea.ledger.modules.`DragonSacrificeDetection` +import moe.nea.ledger.modules.DungeonChestDetection +import moe.nea.ledger.modules.ExternalDataProvider +import moe.nea.ledger.modules.EyedropsDetection +import moe.nea.ledger.modules.ForgeDetection +import moe.nea.ledger.modules.GambleDetection +import moe.nea.ledger.modules.GodPotionDetection +import moe.nea.ledger.modules.GodPotionMixinDetection +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.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 +import net.minecraftforge.client.event.ClientChatReceivedEvent +import net.minecraftforge.common.MinecraftForge +import net.minecraftforge.event.entity.EntityJoinWorldEvent +import net.minecraftforge.fml.common.Mod +import net.minecraftforge.fml.common.event.FMLInitializationEvent +import net.minecraftforge.fml.common.eventhandler.EventPriority +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent +import net.minecraftforge.fml.common.gameevent.TickEvent +import net.minecraftforge.fml.common.gameevent.TickEvent.ClientTickEvent +import org.apache.logging.log4j.LogManager +import java.io.File +import java.util.concurrent.ConcurrentLinkedQueue + +@Mod(modid = "ledger", useMetadata = true, version = BuildConfig.VERSION) +class Ledger { + /* + You have withdrawn 1M coins! You now have 518M coins in your account! + You have deposited 519M coins! You now have 519M coins in your account! + + // ORDERS: + + [Bazaar] Buy Order Setup! 160x Wheat for 720.0 coins. + [Bazaar] Claimed 160x Wheat worth 720.0 coins bought for 4.5 each! + + [Bazaar] Sell Offer Setup! 160x Wheat for 933.4 coins. + [Bazaar] Claimed 34,236,799 coins from selling 176x Hyper Catalyst at 196,741 each! + + // INSTABUY: + + [Bazaar] Bought 64x Wheat for 377.6 coins! + [Bazaar] Sold 64x Wheat for 268.8 coins! + + // AUCTION HOUSE: + + You collected 8,712,000 coins from selling Ultimate Carrot Candy Upgrade to [VIP] kodokush in an auction! + You purchased 2x Walnut for 69 coins! + You purchased ◆ Ice Rune I for 4,000 coins! + + // NPC + + // You bought Cactus x32 for 465.6 Coins! + // You sold Cactus x1 for 3 Coins! + // You bought back Potato x3 for 9 Coins! + + TODO: TRADING, FORGE, VISITORS / COPPER, CORPSES ÖFFNEN, HIGH / LOW GAMBLES, MINION ITEMS (maybe inferno refuel) + TODO: PET LEVELING COSTS AT FANN, SLAYER / MOB DROPS, SLAYER START COST + */ + companion object { + val dataFolder = File("money-ledger").apply { mkdirs() } + val logger = LogManager.getLogger("MoneyLedger") + val managedConfig = ManagedConfig.create(File("config/money-ledger/config.json"), LedgerConfig::class.java) { + checkExpose = false + customProcessor { option, ann -> + UpdateUi(option) + } + } + val gson = Gson() + private val tickQueue = ConcurrentLinkedQueue() + fun runLater(runnable: Runnable) { + tickQueue.add(runnable) + } + + val di = DI() + } + + @Mod.EventHandler + fun init(event: FMLInitializationEvent) { + logger.info("Initializing ledger") + + TelemetryProvider.setupFor(di) + di.registerSingleton(this) + di.registerSingleton(Minecraft.getMinecraft()) + di.registerSingleton(gson) + di.register(LedgerConfig::class.java, DIProvider { managedConfig.instance }) + di.register(Config::class.java, DIProvider.fromInheritance(LedgerConfig::class.java)) + di.registerInjectableClasses( + AccessorySwapperDetection::class.java, + AllowanceDetection::class.java, + AuctionHouseDetection::class.java, + BankDetection::class.java, + BazaarDetection::class.java, + BazaarOrderDetection::class.java, + BitsDetection::class.java, + BitsShopDetection::class.java, + ConfigCommand::class.java, + Database::class.java, + DebugDataCommand::class.java, + DragonEyePlacementDetection::class.java, + DragonSacrificeDetection::class.java, + DungeonChestDetection::class.java, + ErrorUtil::class.java, + ExternalDataProvider::class.java, + EyedropsDetection::class.java, + ForgeDetection::class.java, + GambleDetection::class.java, + GodPotionDetection::class.java, + GodPotionMixinDetection::class.java, + ItemIdProvider::class.java, + KatDetection::class.java, + KuudraChestDetection::class.java, + LedgerLogger::class.java, + LogChatCommand::class.java, + MinecraftExecutor::class.java, + MineshaftCorpseDetection::class.java, + MinionDetection::class.java, + NpcDetection::class.java, + QueryCommand::class.java, + RequestUtil::class.java, + TriggerCommand::class.java, + UpdateChecker::class.java, + VisitorDetection::class.java, + ) + val errorUtil = di.provide() + errorUtil.catch { + di.instantiateAll() + di.getAllInstances().forEach(MinecraftForge.EVENT_BUS::register) + di.getAllInstances().filterIsInstance() + .forEach { ClientCommandHandler.instance.registerCommand(it) } + } + + errorUtil.catch { + di.provide().loadAndUpgrade() + } + + MinecraftForge.EVENT_BUS.post(RegistrationFinishedEvent()) + } + + var lastJoin = -1L + + @SubscribeEvent + fun worldSwitchEvent(event: EntityJoinWorldEvent) { + if (event.entity == Minecraft.getMinecraft().thePlayer) { + lastJoin = System.currentTimeMillis() + MinecraftForge.EVENT_BUS.post(WorldSwitchEvent()) + } + } + + @SubscribeEvent + fun tickEvent(event: ClientTickEvent) { + if (event.phase == TickEvent.Phase.END + && lastJoin > 0 + && System.currentTimeMillis() - lastJoin > 10_000 + && Minecraft.getMinecraft().thePlayer != null + ) { + lastJoin = -1 + MinecraftForge.EVENT_BUS.post(LateWorldLoadEvent()) + } + while (true) { + val queued = tickQueue.poll() ?: break + queued.run() + } + } + + @SubscribeEvent(receiveCanceled = true, priority = EventPriority.HIGHEST) + fun onChat(event: ClientChatReceivedEvent) { + if (event.type != 2.toByte()) + MinecraftForge.EVENT_BUS.post(ChatReceived(event)) + } +} diff --git a/mod/src/main/kotlin/moe/nea/ledger/LedgerEntry.kt b/mod/src/main/kotlin/moe/nea/ledger/LedgerEntry.kt new file mode 100644 index 0000000..d4a3932 --- /dev/null +++ b/mod/src/main/kotlin/moe/nea/ledger/LedgerEntry.kt @@ -0,0 +1,29 @@ +package moe.nea.ledger + +import com.google.gson.JsonObject +import moe.nea.ledger.gen.ItemIds +import java.time.Instant +import java.util.UUID + +data class LedgerEntry( + val transactionType: TransactionType, + val timestamp: Instant, + val items: List, +) { + fun intoJson(profileId: UUID?): JsonObject { + val coinAmount = items.find { it.itemId == ItemId.COINS || it.itemId == ItemIds.SKYBLOCK_BIT }?.count + val nonCoins = items.find { it.itemId != ItemId.COINS && it.itemId != ItemIds.SKYBLOCK_BIT } + return JsonObject().apply { + addProperty("transactionType", transactionType.name) + addProperty("timestamp", timestamp.toEpochMilli().toString()) + addProperty("totalTransactionValue", coinAmount) + addProperty("itemId", nonCoins?.itemId?.string ?: "") + addProperty("itemAmount", nonCoins?.count ?: 0.0) + addProperty("profileId", profileId.toString()) + addProperty( + "playerId", + MCUUIDUtil.getPlayerUUID().toString() + ) + } + } +} \ No newline at end of file diff --git a/mod/src/main/kotlin/moe/nea/ledger/LedgerLogger.kt b/mod/src/main/kotlin/moe/nea/ledger/LedgerLogger.kt new file mode 100644 index 0000000..6049aa2 --- /dev/null +++ b/mod/src/main/kotlin/moe/nea/ledger/LedgerLogger.kt @@ -0,0 +1,136 @@ +package moe.nea.ledger + +import com.google.gson.Gson +import com.google.gson.JsonArray +import moe.nea.ledger.database.DBItemEntry +import moe.nea.ledger.database.DBLogEntry +import moe.nea.ledger.database.Database +import moe.nea.ledger.events.ChatReceived +import moe.nea.ledger.utils.ULIDWrapper +import moe.nea.ledger.utils.di.Inject +import net.minecraft.client.Minecraft +import net.minecraft.util.ChatComponentText +import net.minecraft.util.IChatComponent +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent +import net.minecraftforge.fml.common.gameevent.TickEvent.ClientTickEvent +import java.io.File +import java.text.SimpleDateFormat +import java.util.Date +import java.util.UUID + +class LedgerLogger { + fun printOut(text: String) = printOut(ChatComponentText(text)) + fun printOut(comp: IChatComponent) { + Minecraft.getMinecraft().ingameGUI?.chatGUI?.printChatMessage(comp) + } + + val profileIdPattern = + "Profile ID: (?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})".toPattern() + + var currentProfile: UUID? = null + + var shouldLog by Ledger.managedConfig.instance.debug::logEntries + + @Inject + lateinit var database: Database + + @SubscribeEvent + fun onProfileSwitch(event: ChatReceived) { + profileIdPattern.useMatcher(event.message) { + currentProfile = UUID.fromString(group("profile")) + } + } + + + fun printToChat(entry: LedgerEntry) { + val items = entry.items.joinToString("\n§e") { " - ${it.direction} ${it.count}x ${it.itemId}" } + printOut( + """ + §e================= TRANSACTION START + §eTYPE: §a${entry.transactionType} + §eTIMESTAMP: §a${entry.timestamp} + §e%s + §ePROFILE: §a${currentProfile} + §e================= TRANSACTION END + """.trimIndent().replace("%s", items) + ) + } + + val entries = JsonArray() + var hasRecentlyMerged = false + var lastMergeTime = System.currentTimeMillis() + + fun doMerge() { + val allFiles = folder.listFiles()?.toList() ?: emptyList() + val mergedJson = allFiles + .filter { it.name != "merged.json" && it.extension == "json" } + .sortedDescending() + .map { it.readText().trim().removePrefix("[").removeSuffix("]") } + .joinToString(",", "[", "]") + folder.resolve("merged.json").writeText(mergedJson) + hasRecentlyMerged = true + } + + init { + Runtime.getRuntime().addShutdownHook(Thread { doMerge() }) + } + + @SubscribeEvent + fun onTick(event: ClientTickEvent) { + if (!hasRecentlyMerged && (System.currentTimeMillis() - lastMergeTime) > 60_000L) { + lastMergeTime = System.currentTimeMillis() + doMerge() + } + } + + fun logEntry(entry: LedgerEntry) { + if (shouldLog) + printToChat(entry) + Ledger.logger.info("Logging entry of type ${entry.transactionType}") + val transactionId = ULIDWrapper.createULIDAt(entry.timestamp) + DBLogEntry.insert(database.connection) { + it[DBLogEntry.profileId] = currentProfile ?: MCUUIDUtil.NIL_UUID + it[DBLogEntry.playerId] = MCUUIDUtil.getPlayerUUID() + it[DBLogEntry.type] = entry.transactionType + it[DBLogEntry.transactionId] = transactionId + } + entry.items.forEach { change -> + DBItemEntry.insert(database.connection) { + it[DBItemEntry.transactionId] = transactionId + it[DBItemEntry.mode] = change.direction + it[DBItemEntry.size] = change.count + it[DBItemEntry.itemId] = change.itemId + } + } + entries.add(entry.intoJson(currentProfile)) + commit() + } + + fun commit() { + try { + hasRecentlyMerged = false + file.writeText(gson.toJson(entries)) + } catch (ex: Exception) { + Ledger.logger.error("Could not save file", ex) + } + } + + val gson = Gson() + + val folder = Ledger.dataFolder + val file: File = run { + val date = SimpleDateFormat("yyyy.MM.dd").format(Date()) + + generateSequence(0) { it + 1 } + .map { + if (it == 0) + folder.resolve("$date.json") + else + folder.resolve("$date-$it.json") + } + .filter { !it.exists() } + .first() + } +} + + diff --git a/mod/src/main/kotlin/moe/nea/ledger/LogChatCommand.kt b/mod/src/main/kotlin/moe/nea/ledger/LogChatCommand.kt new file mode 100644 index 0000000..90b2545 --- /dev/null +++ b/mod/src/main/kotlin/moe/nea/ledger/LogChatCommand.kt @@ -0,0 +1,27 @@ +package moe.nea.ledger + +import moe.nea.ledger.utils.di.Inject +import net.minecraft.command.CommandBase +import net.minecraft.command.ICommandSender + +class LogChatCommand : CommandBase() { + @Inject + lateinit var logger: LedgerLogger + + override fun getCommandName(): String { + return "ledgerlogchat" + } + + override fun canCommandSenderUseCommand(sender: ICommandSender?): Boolean { + return true + } + + override fun getCommandUsage(sender: ICommandSender?): String { + return "" + } + + override fun processCommand(sender: ICommandSender?, args: Array?) { + logger.shouldLog = !logger.shouldLog + logger.printOut("§eLedger logging toggled " + (if (logger.shouldLog) "§aon" else "§coff") + "§e.") + } +} \ No newline at end of file diff --git a/mod/src/main/kotlin/moe/nea/ledger/MCUUIDUtil.kt b/mod/src/main/kotlin/moe/nea/ledger/MCUUIDUtil.kt new file mode 100644 index 0000000..79068cc --- /dev/null +++ b/mod/src/main/kotlin/moe/nea/ledger/MCUUIDUtil.kt @@ -0,0 +1,22 @@ +package moe.nea.ledger + +import com.mojang.util.UUIDTypeAdapter +import net.minecraft.client.Minecraft +import java.util.UUID + +object MCUUIDUtil { + + fun parseDashlessUuid(string: String) = UUIDTypeAdapter.fromString(string) + val NIL_UUID = UUID(0L, 0L) + fun getPlayerUUID(): UUID { + val currentUUID = Minecraft.getMinecraft().thePlayer?.uniqueID + ?: Minecraft.getMinecraft().session?.playerID?.let(::parseDashlessUuid) + ?: lastKnownUUID + lastKnownUUID = currentUUID + return currentUUID + } + + + private var lastKnownUUID: UUID = NIL_UUID + +} \ No newline at end of file diff --git a/mod/src/main/kotlin/moe/nea/ledger/NumberUtil.kt b/mod/src/main/kotlin/moe/nea/ledger/NumberUtil.kt new file mode 100644 index 0000000..438f342 --- /dev/null +++ b/mod/src/main/kotlin/moe/nea/ledger/NumberUtil.kt @@ -0,0 +1,117 @@ +package moe.nea.ledger + +import net.minecraft.event.ClickEvent +import net.minecraft.event.HoverEvent +import net.minecraft.util.ChatComponentText +import net.minecraft.util.ChatStyle +import net.minecraft.util.EnumChatFormatting +import net.minecraft.util.IChatComponent +import java.time.Instant +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import java.time.format.DateTimeFormatterBuilder +import java.time.temporal.ChronoField +import java.util.regex.Matcher +import java.util.regex.Pattern + +// language=regexp +val SHORT_NUMBER_PATTERN = "[0-9]+(?:,[0-9]+)*(?:\\.[0-9]+)?[kKmMbB]?" + +// language=regexp +val ROMAN_NUMBER_PATTERN = "[IVXLCDM]+" + +val romanNumbers = mapOf( + 'I' to 1, + 'V' to 5, + 'X' to 10, + 'L' to 50, + 'C' to 100, + 'D' to 500, + 'M' to 1000 +) + +fun parseRomanNumber(string: String): Int { + var smallestSeenSoFar = Int.MAX_VALUE + var lastSeenOfSmallest = 0 + var amount = 0 + for (c in string) { + val cV = romanNumbers[c]!! + if (cV == smallestSeenSoFar) { + lastSeenOfSmallest++ + amount += cV + } else if (cV < smallestSeenSoFar) { + smallestSeenSoFar = cV + amount += cV + lastSeenOfSmallest = 1 + } else { + amount -= lastSeenOfSmallest * smallestSeenSoFar * 2 + smallestSeenSoFar = cV + amount += cV + lastSeenOfSmallest = 1 + } + } + return amount +} + +val siScalars = mapOf( + 'k' to 1_000.0, + 'K' to 1_000.0, + 'm' to 1_000_000.0, + 'M' to 1_000_000.0, + 'b' to 1_000_000_000.0, + 'B' to 1_000_000_000.0, +) + +fun parseShortNumber(string: String): Double { + var k = string.replace(",", "") + val scalar = k.last() + var scalarMultiplier = siScalars[scalar] + if (scalarMultiplier == null) { + scalarMultiplier = 1.0 + } else { + k = k.dropLast(1) + } + return k.toDouble() * scalarMultiplier +} + +fun Pattern.matches(string: String): Boolean = matcher(string).matches() +inline fun Pattern.useMatcher(string: String, block: Matcher.() -> T): T? = + matcher(string).takeIf { it.matches() }?.let(block) + +fun String.ifDropLast(suffix: String, block: (String) -> T): T? { + if (endsWith(suffix)) { + return block(dropLast(suffix.length)) + } + return null +} + +fun String.unformattedString(): String = replace("§.".toRegex(), "") + +val timeFormat: DateTimeFormatter = DateTimeFormatterBuilder() + .appendValue(ChronoField.DAY_OF_MONTH, 2) + .appendLiteral(".") + .appendValue(ChronoField.MONTH_OF_YEAR, 2) + .appendLiteral(".") + .appendValue(ChronoField.YEAR, 4) + .appendLiteral(" ") + .appendValue(ChronoField.HOUR_OF_DAY, 2) + .appendLiteral(":") + .appendValue(ChronoField.MINUTE_OF_HOUR, 2) + .appendLiteral(":") + .appendValue(ChronoField.SECOND_OF_MINUTE, 2) + .toFormatter() + +fun Instant.formatChat(): IChatComponent { + val text = ChatComponentText( + LocalDateTime.ofInstant(this, ZoneId.systemDefault()).format(timeFormat) + ) + text.setChatStyle( + ChatStyle() + .setChatClickEvent( + ClickEvent(ClickEvent.Action.OPEN_URL, "https://time.is/${this.epochSecond}")) + .setChatHoverEvent( + HoverEvent(HoverEvent.Action.SHOW_TEXT, ChatComponentText("Click to show on time.is"))) + .setColor(EnumChatFormatting.AQUA)) + return text +} diff --git a/mod/src/main/kotlin/moe/nea/ledger/QueryCommand.kt b/mod/src/main/kotlin/moe/nea/ledger/QueryCommand.kt new file mode 100644 index 0000000..19bd5d0 --- /dev/null +++ b/mod/src/main/kotlin/moe/nea/ledger/QueryCommand.kt @@ -0,0 +1,197 @@ +package moe.nea.ledger + +import moe.nea.ledger.database.sql.ANDExpression +import moe.nea.ledger.database.sql.BooleanExpression +import moe.nea.ledger.database.sql.Clause +import moe.nea.ledger.database.DBItemEntry +import moe.nea.ledger.database.DBLogEntry +import moe.nea.ledger.database.Database +import moe.nea.ledger.utils.ULIDWrapper +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 + +class QueryCommand : CommandBase() { + override fun canCommandSenderUseCommand(sender: ICommandSender?): Boolean { + return true + } + + override fun getCommandName(): String { + return "ledger" + } + + override fun getCommandUsage(sender: ICommandSender?): String { + return "" + } + + override fun getCommandAliases(): List { + return listOf("lgq") + } + + @Inject + lateinit var logger: LedgerLogger + + override fun processCommand(sender: ICommandSender, args: Array) { + if (args.isEmpty()) { + logger.printOut("§eHere is how you can look up transactions:") + logger.printOut("") + logger.printOut("§f- §e/ledger withitem %POTATO%") + logger.printOut(" §aLook up transactions involving potatoes!") + logger.printOut("§f- §e/ledger withitem ENCHANTED_POTATO") + logger.printOut(" §aLook up transactions involving just enchanted potatoes!") + logger.printOut("§f- §e/ledger withitem %POTATO% withitem %CARROT%") + logger.printOut(" §aLook up transactions involving potatoes or carrots!") + logger.printOut("§f- §e/ledger withtype AUCTION_SOLD") + logger.printOut(" §aLook up transactions of sold auctions!") + logger.printOut("§f- §e/ledger withtype AUCTION_SOLD withitem CRIMSON%") + logger.printOut(" §aLook up sold auctions involving crimson armor pieces!") + logger.printOut("") + logger.printOut("§eFilters of the same type apply using §aOR§e and loggers of different types apply using §aAND§e.") + logger.printOut("§eYou can use % as a wildcard!") + return + } + val p = parseArgs(args) + when (p) { + is ParseResult.Success -> { + executeQuery(p) + } + + is ParseResult.UnknownFilter -> { + logger.printOut("§cUnknown filter name ${p.start}. Available filter names are: ${mFilters.keys.joinToString()}") + } + + is ParseResult.MissingArg -> { + logger.printOut("§cFilter ${p.filterM.name} is missing an argument.") + } + } + } + + override fun addTabCompletionOptions( + sender: ICommandSender, + args: Array, + pos: BlockPos + ): MutableList? { + 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) { + val grouped = parse.filters + val query = DBLogEntry.from(database.connection) + .select(DBLogEntry.type, DBLogEntry.transactionId) + .join(DBItemEntry, on = Clause { column(DBLogEntry.transactionId) eq column(DBItemEntry.transactionId) }) + for (value in grouped.values) { + query.where(ANDExpression(value)) + } + query.limit(80u) + val dedup = mutableSetOf() + 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) }) + .map { ItemChange.from(it) } + val text = ChatComponentText("") + .setChatStyle(ChatStyle().setColor(EnumChatFormatting.YELLOW)) + .appendSibling( + ChatComponentText(type.name) + .setChatStyle(ChatStyle().setColor(EnumChatFormatting.GREEN)) + ) + .appendText(" on ") + .appendSibling(timestamp.formatChat()) + .appendText("\n") + .appendSibling( + ChatComponentText(transactionId.wrapped).setChatStyle(ChatStyle().setColor(EnumChatFormatting.DARK_GRAY)) + ) + for (item in items) { + text.appendText("\n") + .appendSibling(item.formatChat()) + } + text.appendText("\n") + logger.printOut(text) + } + } + + sealed interface ParseResult { + data class UnknownFilter(val start: String) : ParseResult + data class MissingArg(val filterM: FilterM) : ParseResult + data class Success(val lastFilterM: FilterM, val filters: Map>) : ParseResult + } + + fun parseArgs(args: Array): ParseResult { + require(args.isNotEmpty()) + val arr = args.iterator() + val filters = mutableMapOf>() + var lastFilterM: FilterM? = null + while (arr.hasNext()) { + val filterName = arr.next() + val filterM = mFilters[filterName] + if (filterM == null) { + return ParseResult.UnknownFilter(filterName) + } + if (!arr.hasNext()) { + return ParseResult.MissingArg(filterM) + } + filters.getOrPut(filterM, ::mutableListOf).add(filterM.getFilter(arr.next())) + lastFilterM = filterM + } + return ParseResult.Success(lastFilterM!!, filters) + } + + + val mFilters = listOf(TypeFilter, ItemFilter).associateBy { it.name } + + object TypeFilter : FilterM { + override val name: String + get() = "withtype" + + override fun getFilter(text: String): BooleanExpression { + val preparedText = "%" + text.trim('%') + "%" + return Clause { column(DBLogEntry.type) like preparedText } + } + + override fun tabComplete(partialArg: String): MutableList { + 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() // TODO: close this escape hatch + override fun getFilter(text: String): BooleanExpression { + return Clause { column(DBItemEntry.itemId) like text } + } + + override fun tabComplete(partialArg: String): MutableList? { + 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? +// fun tabCompleteFilter() TODO + } + +} \ No newline at end of file diff --git a/mod/src/main/kotlin/moe/nea/ledger/ScoreboardUtil.kt b/mod/src/main/kotlin/moe/nea/ledger/ScoreboardUtil.kt new file mode 100644 index 0000000..783664b --- /dev/null +++ b/mod/src/main/kotlin/moe/nea/ledger/ScoreboardUtil.kt @@ -0,0 +1,29 @@ +package moe.nea.ledger + +import net.minecraft.client.Minecraft +import net.minecraft.scoreboard.ScorePlayerTeam + +object ScoreboardUtil { + + val sidebarSlot = 1 + fun getScoreboardStrings(): List { + val scoreboard = Minecraft.getMinecraft().theWorld.scoreboard + val objective = scoreboard.getObjectiveInDisplaySlot(sidebarSlot) + val scoreList = scoreboard.getSortedScores(objective).take(15) + .map { + ScorePlayerTeam.formatPlayerName(scoreboard.getPlayersTeam(it.playerName), it.playerName) + } + .map { stripAlien(it) } + .reversed() + return scoreList + } + + fun stripAlien(string: String): String { + val sb = StringBuilder() + for (c in string) { + if (Minecraft.getMinecraft().fontRendererObj.getCharWidth(c) > 0 || c == '§') + sb.append(c) + } + return sb.toString() + } +} \ No newline at end of file diff --git a/mod/src/main/kotlin/moe/nea/ledger/TelemetryProvider.kt b/mod/src/main/kotlin/moe/nea/ledger/TelemetryProvider.kt new file mode 100644 index 0000000..d9c7108 --- /dev/null +++ b/mod/src/main/kotlin/moe/nea/ledger/TelemetryProvider.kt @@ -0,0 +1,66 @@ +package moe.nea.ledger + +import com.google.gson.JsonArray +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import moe.nea.ledger.gen.BuildConfig +import moe.nea.ledger.utils.di.DI +import moe.nea.ledger.utils.di.DIProvider +import moe.nea.ledger.utils.telemetry.CommonKeys +import moe.nea.ledger.utils.telemetry.ContextValue +import moe.nea.ledger.utils.telemetry.EventRecorder +import moe.nea.ledger.utils.telemetry.JsonElementContext +import moe.nea.ledger.utils.telemetry.LoggingEventRecorder +import moe.nea.ledger.utils.telemetry.Span +import net.minecraft.client.Minecraft +import net.minecraft.util.Session +import net.minecraftforge.fml.common.Loader + +object TelemetryProvider { + fun injectTo(di: DI) { + di.register( + EventRecorder::class.java, + if (DevUtil.isDevEnv) DIProvider.singeleton(LoggingEventRecorder(Ledger.logger, true)) + else DIProvider.singeleton( + LoggingEventRecorder(Ledger.logger, false)) // TODO: replace with upload to server + ) + } + + val USER = "minecraft_user" + val MINECRAFT_VERSION = "minecraft_version" + val MODS = "mods" + + class MinecraftUser(val session: Session) : ContextValue { + override fun serialize(): JsonElement { + val obj = JsonObject() + obj.addProperty("uuid", session.playerID) + obj.addProperty("name", session.username) + return obj + } + } + + fun setupDefaultSpan() { + val sp = Span.current() + sp.add(USER, MinecraftUser(Minecraft.getMinecraft().session)) + sp.add(MINECRAFT_VERSION, ContextValue.compound( + "static" to "1.8.9", + "rt" to Minecraft.getMinecraft().version, + )) + val mods = JsonArray() + Loader.instance().activeModList.map { + val obj = JsonObject() + obj.addProperty("id", it.modId) + obj.addProperty("version", it.version) + obj.addProperty("displayVersion", it.displayVersion) + obj + }.forEach(mods::add) + sp.add(MODS, JsonElementContext(mods)) + sp.add(CommonKeys.VERSION, ContextValue.string(BuildConfig.FULL_VERSION)) + sp.add(CommonKeys.COMMIT_VERSION, ContextValue.string(BuildConfig.GIT_COMMIT)) + } + + fun setupFor(di: DI) { + injectTo(di) + setupDefaultSpan() + } +} \ No newline at end of file diff --git a/mod/src/main/kotlin/moe/nea/ledger/TransactionType.kt b/mod/src/main/kotlin/moe/nea/ledger/TransactionType.kt new file mode 100644 index 0000000..33c633d --- /dev/null +++ b/mod/src/main/kotlin/moe/nea/ledger/TransactionType.kt @@ -0,0 +1,35 @@ +package moe.nea.ledger + +enum class TransactionType { + ACCESSORIES_SWAPPING, + ALLOWANCE_GAIN, + AUCTION_BOUGHT, + AUCTION_LISTING_CHARGE, + AUCTION_SOLD, + AUTOMERCHANT_PROFIT_COLLECT, + BANK_DEPOSIT, + BANK_WITHDRAW, + BAZAAR_BUY_INSTANT, + BAZAAR_BUY_ORDER, + BAZAAR_SELL_INSTANT, + BAZAAR_SELL_ORDER, + BITS_PURSE_STATUS, + BOOSTER_COOKIE_ATE, + CAPSAICIN_EYEDROPS_USED, + COMMUNITY_SHOP_BUY, + CORPSE_DESECRATED, + DIE_ROLLED, + DRACONIC_SACRIFICE, + DUNGEON_CHEST_OPEN, + FORGED, + GOD_POTION_DRANK, + GOD_POTION_MIXIN_DRANK, + KAT_TIMESKIP, + KAT_UPGRADE, + KISMET_REROLL, + KUUDRA_CHEST_OPEN, + NPC_BUY, + NPC_SELL, + VISITOR_BARGAIN, + WYRM_EVOKED, +} \ No newline at end of file diff --git a/mod/src/main/kotlin/moe/nea/ledger/TriggerCommand.kt b/mod/src/main/kotlin/moe/nea/ledger/TriggerCommand.kt new file mode 100644 index 0000000..c97627d --- /dev/null +++ b/mod/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) { + 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/mod/src/main/kotlin/moe/nea/ledger/config/DebugOptions.kt b/mod/src/main/kotlin/moe/nea/ledger/config/DebugOptions.kt new file mode 100644 index 0000000..fd5ed3d --- /dev/null +++ b/mod/src/main/kotlin/moe/nea/ledger/config/DebugOptions.kt @@ -0,0 +1,13 @@ +package moe.nea.ledger.config + +import io.github.notenoughupdates.moulconfig.annotations.ConfigEditorBoolean +import io.github.notenoughupdates.moulconfig.annotations.ConfigOption + +class DebugOptions { + @ConfigOption(name = "Log entries to chat", + desc = "Appends all logged entries into the chat as they are logged. This option does not persist on restarts.") + @Transient + @ConfigEditorBoolean + @JvmField + var logEntries = false +} diff --git a/mod/src/main/kotlin/moe/nea/ledger/config/LedgerConfig.kt b/mod/src/main/kotlin/moe/nea/ledger/config/LedgerConfig.kt new file mode 100644 index 0000000..91ee5c1 --- /dev/null +++ b/mod/src/main/kotlin/moe/nea/ledger/config/LedgerConfig.kt @@ -0,0 +1,35 @@ +package moe.nea.ledger.config + +import io.github.notenoughupdates.moulconfig.Config +import io.github.notenoughupdates.moulconfig.DescriptionRendereringBehaviour +import io.github.notenoughupdates.moulconfig.annotations.Category +import io.github.notenoughupdates.moulconfig.processor.ProcessedOption +import moe.nea.ledger.Ledger + +class LedgerConfig : Config() { + override fun getTitle(): String { + return "§6Ledger §7- §6Hypixel SkyBlock data logger §7by §anea89o" + } + + override fun saveNow() { + super.saveNow() + Ledger.managedConfig.saveToFile() + } + + override fun getDescriptionBehaviour(option: ProcessedOption?): DescriptionRendereringBehaviour { + return DescriptionRendereringBehaviour.EXPAND_PANEL + } + + @Category(name = "Ledger", desc = "") + @JvmField + val main = MainOptions() + + @Category(name = "Synchronization", desc = "") + @JvmField + val synchronization = SynchronizationOptions() + + @Category(name = "Debug", desc = "") + @JvmField + val debug = DebugOptions() + +} \ No newline at end of file diff --git a/mod/src/main/kotlin/moe/nea/ledger/config/MainOptions.kt b/mod/src/main/kotlin/moe/nea/ledger/config/MainOptions.kt new file mode 100644 index 0000000..1efa970 --- /dev/null +++ b/mod/src/main/kotlin/moe/nea/ledger/config/MainOptions.kt @@ -0,0 +1,27 @@ +package moe.nea.ledger.config + +import io.github.notenoughupdates.moulconfig.annotations.Conf