aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/build.yml18
-rw-r--r--.gitignore2
-rw-r--r--basetypes/build.gradle.kts12
-rw-r--r--basetypes/src/main/kotlin/moe/nea/ledger/ItemChange.kt43
-rw-r--r--basetypes/src/main/kotlin/moe/nea/ledger/ItemId.kt35
-rw-r--r--basetypes/src/main/kotlin/moe/nea/ledger/TransactionType.kt (renamed from src/main/kotlin/moe/nea/ledger/TransactionType.kt)8
-rw-r--r--basetypes/src/main/kotlin/moe/nea/ledger/utils/RemoveInRelease.kt4
-rw-r--r--basetypes/src/main/kotlin/moe/nea/ledger/utils/ULIDWrapper.kt35
-rw-r--r--basetypes/src/main/kotlin/moe/nea/ledger/utils/UUIDUtil.kt41
-rw-r--r--build-src/README.md2
-rw-r--r--build-src/build.gradle.kts12
-rw-r--r--build-src/settings.gradle.kts6
-rw-r--r--build-src/src/main/kotlin/GenerateItemIds.kt72
-rw-r--r--build-src/src/main/kotlin/RepoDownload.kt41
-rw-r--r--build-src/src/main/kotlin/helpers.kt17
-rw-r--r--build-src/src/main/kotlin/ledger-globals.gradle.kts25
-rw-r--r--build-src/src/main/kotlin/ledger-repo.gradle.kts1
-rw-r--r--build-src/src/main/kotlin/ledger-staged-proguard.gradle.kts1
-rw-r--r--build.gradle.kts309
-rw-r--r--database/core/build.gradle.kts12
-rw-r--r--database/core/src/main/kotlin/moe/nea/ledger/database/Column.kt31
-rw-r--r--database/core/src/main/kotlin/moe/nea/ledger/database/Constraint.kt6
-rw-r--r--database/core/src/main/kotlin/moe/nea/ledger/database/DBSchema.kt13
-rw-r--r--database/core/src/main/kotlin/moe/nea/ledger/database/DBType.kt43
-rw-r--r--database/core/src/main/kotlin/moe/nea/ledger/database/InsertStatement.kt7
-rw-r--r--database/core/src/main/kotlin/moe/nea/ledger/database/Query.kt111
-rw-r--r--database/core/src/main/kotlin/moe/nea/ledger/database/ResultRow.kt22
-rw-r--r--database/core/src/main/kotlin/moe/nea/ledger/database/Table.kt103
-rw-r--r--database/core/src/main/kotlin/moe/nea/ledger/database/UniqueConstraint.kt14
-rw-r--r--database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBDouble.kt18
-rw-r--r--database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBEnum.kt31
-rw-r--r--database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBInstant.kt19
-rw-r--r--database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBInt.kt18
-rw-r--r--database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBString.kt18
-rw-r--r--database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBUlid.kt21
-rw-r--r--database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBUuid.kt20
-rw-r--r--database/core/src/main/kotlin/moe/nea/ledger/database/sql/ANDExpression.kt26
-rw-r--r--database/core/src/main/kotlin/moe/nea/ledger/database/sql/BooleanExpression.kt3
-rw-r--r--database/core/src/main/kotlin/moe/nea/ledger/database/sql/Clause.kt12
-rw-r--r--database/core/src/main/kotlin/moe/nea/ledger/database/sql/ClauseBuilder.kt25
-rw-r--r--database/core/src/main/kotlin/moe/nea/ledger/database/sql/ColumnOperand.kt18
-rw-r--r--database/core/src/main/kotlin/moe/nea/ledger/database/sql/EqualsClause.kt16
-rw-r--r--database/core/src/main/kotlin/moe/nea/ledger/database/sql/IntoSelectable.kt5
-rw-r--r--database/core/src/main/kotlin/moe/nea/ledger/database/sql/Join.kt19
-rw-r--r--database/core/src/main/kotlin/moe/nea/ledger/database/sql/LessThanEqualsExpression.kt15
-rw-r--r--database/core/src/main/kotlin/moe/nea/ledger/database/sql/LessThanExpression.kt15
-rw-r--r--database/core/src/main/kotlin/moe/nea/ledger/database/sql/LikeClause.kt17
-rw-r--r--database/core/src/main/kotlin/moe/nea/ledger/database/sql/ListClause.kt8
-rw-r--r--database/core/src/main/kotlin/moe/nea/ledger/database/sql/ListExpression.kt22
-rw-r--r--database/core/src/main/kotlin/moe/nea/ledger/database/sql/ORExpression.kt23
-rw-r--r--database/core/src/main/kotlin/moe/nea/ledger/database/sql/Operand.kt10
-rw-r--r--database/core/src/main/kotlin/moe/nea/ledger/database/sql/SQLQueryComponent.kt45
-rw-r--r--database/core/src/main/kotlin/moe/nea/ledger/database/sql/SQLQueryGenerator.kt25
-rw-r--r--database/core/src/main/kotlin/moe/nea/ledger/database/sql/Selectable.kt17
-rw-r--r--database/core/src/main/kotlin/moe/nea/ledger/database/sql/StringOperand.kt17
-rw-r--r--database/core/src/main/kotlin/moe/nea/ledger/database/sql/TypedOperand.kt7
-rw-r--r--database/core/src/main/kotlin/moe/nea/ledger/database/sql/ValuedOperand.kt15
-rw-r--r--database/impl/build.gradle.kts12
-rw-r--r--database/impl/src/main/kotlin/moe/nea/ledger/database/DBLogEntry.kt (renamed from src/main/kotlin/moe/nea/ledger/database/DBLogEntry.kt)13
-rw-r--r--database/impl/src/main/kotlin/moe/nea/ledger/database/DBUpgrade.kt (renamed from src/main/kotlin/moe/nea/ledger/database/DBUpgrade.kt)4
-rw-r--r--database/impl/src/main/kotlin/moe/nea/ledger/database/Database.kt (renamed from src/main/kotlin/moe/nea/ledger/database/Database.kt)9
-rw-r--r--database/impl/src/main/kotlin/moe/nea/ledger/database/Upgrades.kt (renamed from src/main/kotlin/moe/nea/ledger/database/Upgrades.kt)2
-rw-r--r--database/impl/src/main/kotlin/moe/nea/ledger/database/schema.dot (renamed from src/main/kotlin/moe/nea/ledger/database/schema.dot)0
-rw-r--r--dependency-injection/build.gradle.kts8
-rw-r--r--dependency-injection/src/main/kotlin/moe/nea/ledger/utils/di/DI.kt (renamed from src/main/kotlin/moe/nea/ledger/utils/di/DI.kt)5
-rw-r--r--dependency-injection/src/main/kotlin/moe/nea/ledger/utils/di/DIProvider.kt (renamed from src/main/kotlin/moe/nea/ledger/utils/di/DIProvider.kt)6
-rw-r--r--dependency-injection/src/main/kotlin/moe/nea/ledger/utils/di/Inject.kt (renamed from src/main/kotlin/moe/nea/ledger/utils/di/Inject.kt)0
-rw-r--r--gradle.properties5
-rw-r--r--gradle/wrapper/gradle-wrapper.properties2
-rw-r--r--ledger-rules.pro3
-rw-r--r--log4j2.xml5
-rw-r--r--mod/build.gradle.kts175
-rw-r--r--mod/gradle.properties3
-rw-r--r--mod/ledger-rules.pro4
-rw-r--r--mod/log4j2.xml9
-rw-r--r--mod/src/main/java/moe/nea/ledger/init/AutoDiscoveryMixinPlugin.java195
-rw-r--r--mod/src/main/java/moe/nea/ledger/mixin/AccessorContainerDispenser.java12
-rw-r--r--mod/src/main/java/moe/nea/ledger/mixin/AccessorContainerHopper.java13
-rw-r--r--mod/src/main/java/moe/nea/ledger/mixin/AccessorGuiEditSign.java (renamed from src/main/java/moe/nea/ledger/mixin/AccessorGuiEditSign.java)0
-rw-r--r--mod/src/main/java/moe/nea/ledger/mixin/MouseClickEventPatch.java (renamed from src/main/java/moe/nea/ledger/mixin/MouseClickEventPatch.java)0
-rw-r--r--mod/src/main/java/moe/nea/ledger/mixin/OnInitializationCompletePatch.java (renamed from src/main/java/moe/nea/ledger/mixin/OnInitializationCompletePatch.java)0
-rw-r--r--mod/src/main/java/moe/nea/ledger/mixin/devenv/RegisterModResourcesPatch.java66
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/ConfigCommand.kt (renamed from src/main/kotlin/moe/nea/ledger/ConfigCommand.kt)0
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/DebouncedValue.kt (renamed from src/main/kotlin/moe/nea/ledger/DebouncedValue.kt)0
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/DebugDataCommand.kt (renamed from src/main/kotlin/moe/nea/ledger/DebugDataCommand.kt)0
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/DevUtil.kt (renamed from src/main/kotlin/moe/nea/ledger/DevUtil.kt)0
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/ExpiringValue.kt (renamed from src/main/kotlin/moe/nea/ledger/ExpiringValue.kt)0
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/ItemIdProvider.kt (renamed from src/main/kotlin/moe/nea/ledger/ItemIdProvider.kt)15
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/ItemUtil.kt (renamed from src/main/kotlin/moe/nea/ledger/ItemUtil.kt)0
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/Ledger.kt (renamed from src/main/kotlin/moe/nea/ledger/Ledger.kt)29
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/LedgerEntry.kt (renamed from src/main/kotlin/moe/nea/ledger/LedgerEntry.kt)7
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/LedgerLogger.kt (renamed from src/main/kotlin/moe/nea/ledger/LedgerLogger.kt)7
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/LogChatCommand.kt (renamed from src/main/kotlin/moe/nea/ledger/LogChatCommand.kt)0
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/MCUUIDUtil.kt (renamed from src/main/kotlin/moe/nea/ledger/UUIDUtil.kt)23
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/NumberUtil.kt (renamed from src/main/kotlin/moe/nea/ledger/NumberUtil.kt)35
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/QueryCommand.kt (renamed from src/main/kotlin/moe/nea/ledger/QueryCommand.kt)13
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/ScoreboardUtil.kt (renamed from src/main/kotlin/moe/nea/ledger/ScoreboardUtil.kt)0
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/TriggerCommand.kt (renamed from src/main/kotlin/moe/nea/ledger/TriggerCommand.kt)0
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/config/DebugOptions.kt (renamed from src/main/kotlin/moe/nea/ledger/config/DebugOptions.kt)3
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/config/LedgerConfig.kt (renamed from src/main/kotlin/moe/nea/ledger/config/LedgerConfig.kt)0
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/config/MainOptions.kt (renamed from src/main/kotlin/moe/nea/ledger/config/MainOptions.kt)0
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/config/SynchronizationOptions.kt (renamed from src/main/kotlin/moe/nea/ledger/config/SynchronizationOptions.kt)0
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/config/UpdateUi.kt (renamed from src/main/kotlin/moe/nea/ledger/config/UpdateUi.kt)0
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/config/UpdateUiMarker.kt (renamed from src/main/kotlin/moe/nea/ledger/config/UpdateUiMarker.kt)0
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/events/BeforeGuiAction.kt21
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/events/ChatReceived.kt (renamed from src/main/kotlin/moe/nea/ledger/events/ChatReceived.kt)0
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/events/ExtraSupplyIdEvent.kt (renamed from src/main/kotlin/moe/nea/ledger/events/ExtraSupplyIdEvent.kt)0
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/events/GuiClickEvent.kt (renamed from src/main/kotlin/moe/nea/ledger/events/GuiClickEvent.kt)0
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/events/InitializationComplete.kt (renamed from src/main/kotlin/moe/nea/ledger/events/InitializationComplete.kt)0
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/events/LedgerEvent.kt22
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/events/RegistrationFinishedEvent.kt (renamed from src/main/java/moe/nea/ledger/events/RegistrationFinishedEvent.kt)0
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/events/SupplyDebugInfo.kt (renamed from src/main/kotlin/moe/nea/ledger/events/SupplyDebugInfo.kt)0
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/events/TriggerEvent.kt (renamed from src/main/kotlin/moe/nea/ledger/events/TriggerEvent.kt)0
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/events/WorldLoadEvent.kt (renamed from src/main/kotlin/moe/nea/ledger/events/WorldLoadEvent.kt)0
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/events/WorldSwitchEvent.kt (renamed from src/main/kotlin/moe/nea/ledger/events/WorldSwitchEvent.kt)0
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/modules/AccessorySwapperDetection.kt34
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/modules/AllowanceDetection.kt37
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/modules/AuctionHouseDetection.kt (renamed from src/main/kotlin/moe/nea/ledger/modules/AuctionHouseDetection.kt)0
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/modules/BankDetection.kt (renamed from src/main/kotlin/moe/nea/ledger/modules/BankDetection.kt)0
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/modules/BankInterestDetection.kt44
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/modules/BasicReforgeDetection.kt71
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/modules/BazaarDetection.kt (renamed from src/main/kotlin/moe/nea/ledger/modules/BazaarDetection.kt)0
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/modules/BazaarOrderDetection.kt (renamed from src/main/kotlin/moe/nea/ledger/modules/BazaarOrderDetection.kt)0
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/modules/BitsDetection.kt (renamed from src/main/kotlin/moe/nea/ledger/modules/BitsDetection.kt)10
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/modules/BitsShopDetection.kt (renamed from src/main/kotlin/moe/nea/ledger/modules/BitsShopDetection.kt)13
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/modules/CaducousFeederDetection.kt48
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/modules/ChestDetection.kt (renamed from src/main/kotlin/moe/nea/ledger/modules/ChestDetection.kt)0
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/modules/DragonEyePlacementDetection.kt (renamed from src/main/kotlin/moe/nea/ledger/modules/DragonEyePlacementDetection.kt)4
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/modules/DragonSacrificeDetection.kt (renamed from src/main/kotlin/moe/nea/ledger/modules/DragonSacrificeDetection.kt)4
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/modules/DungeonChestDetection.kt (renamed from src/main/kotlin/moe/nea/ledger/modules/DungeonChestDetection.kt)35
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/modules/ExternalDataProvider.kt (renamed from src/main/kotlin/moe/nea/ledger/modules/ExternalDataProvider.kt)5
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/modules/EyedropsDetection.kt (renamed from src/main/kotlin/moe/nea/ledger/modules/EyedropsDetection.kt)10
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/modules/ForgeDetection.kt (renamed from src/main/kotlin/moe/nea/ledger/modules/ForgeDetection.kt)0
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/modules/GambleDetection.kt (renamed from src/main/kotlin/moe/nea/ledger/modules/GambleDetection.kt)3
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/modules/GhostCoinDropDetection.kt38
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/modules/GodPotionDetection.kt (renamed from src/main/kotlin/moe/nea/ledger/modules/GodPotionDetection.kt)10
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/modules/GodPotionMixinDetection.kt (renamed from src/main/kotlin/moe/nea/ledger/modules/GodPotionMixinDetection.kt)6
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/modules/GummyPolarBearDetection.kt34
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/modules/KatDetection.kt (renamed from src/main/kotlin/moe/nea/ledger/modules/KatDetection.kt)0
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/modules/KuudraChestDetection.kt (renamed from src/main/kotlin/moe/nea/ledger/modules/KuudraChestDetection.kt)3
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/modules/MineshaftCorpseDetection.kt (renamed from src/main/kotlin/moe/nea/ledger/modules/MineshaftCorpseDetection.kt)0
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/modules/MinionDetection.kt (renamed from src/main/kotlin/moe/nea/ledger/modules/MinionDetection.kt)0
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/modules/NpcDetection.kt (renamed from src/main/kotlin/moe/nea/ledger/modules/NpcDetection.kt)0
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/modules/PestRepellentDetection.kt47
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/modules/UpdateChecker.kt (renamed from src/main/kotlin/moe/nea/ledger/modules/UpdateChecker.kt)0
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/modules/VisitorDetection.kt (renamed from src/main/kotlin/moe/nea/ledger/modules/VisitorDetection.kt)3
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/telemetry/GuiContextValue.kt16
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/telemetry/TelemetryProvider.kt (renamed from src/main/kotlin/moe/nea/ledger/TelemetryProvider.kt)6
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/utils/BorderedTextTracker.kt (renamed from src/main/kotlin/moe/nea/ledger/utils/BorderedTextTracker.kt)0
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/utils/ErrorUtil.kt (renamed from src/main/kotlin/moe/nea/ledger/utils/ErrorUtil.kt)0
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/utils/GsonUtil.kt (renamed from src/main/kotlin/moe/nea/ledger/utils/GsonUtil.kt)0
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/utils/MinecraftExecutor.kt (renamed from src/main/kotlin/moe/nea/ledger/utils/MinecraftExecutor.kt)0
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/utils/ScreenUtil.kt29
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/utils/network/Request.kt (renamed from src/main/kotlin/moe/nea/ledger/utils/network/Request.kt)0
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/utils/network/RequestTrace.kt21
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/utils/network/RequestUtil.kt (renamed from src/main/kotlin/moe/nea/ledger/utils/network/RequestUtil.kt)9
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/utils/network/Response.kt (renamed from src/main/kotlin/moe/nea/ledger/utils/network/Response.kt)0
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/BooleanContext.kt (renamed from src/main/kotlin/moe/nea/ledger/utils/telemetry/BooleanContext.kt)0
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/CommonKeys.kt (renamed from src/main/kotlin/moe/nea/ledger/utils/telemetry/CommonKeys.kt)0
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/Context.kt (renamed from src/main/kotlin/moe/nea/ledger/utils/telemetry/Context.kt)0
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/ContextValue.kt (renamed from src/main/kotlin/moe/nea/ledger/utils/telemetry/ContextValue.kt)0
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/EventRecorder.kt (renamed from src/main/kotlin/moe/nea/ledger/utils/telemetry/EventRecorder.kt)0
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/ExceptionContextValue.kt (renamed from src/main/kotlin/moe/nea/ledger/utils/telemetry/ExceptionContextValue.kt)0
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/JsonElementContext.kt (renamed from src/main/kotlin/moe/nea/ledger/utils/telemetry/JsonElementContext.kt)0
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/LoggingEventRecorder.kt (renamed from src/main/kotlin/moe/nea/ledger/utils/telemetry/LoggingEventRecorder.kt)0
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/RecordedEvent.kt (renamed from src/main/kotlin/moe/nea/ledger/utils/telemetry/RecordedEvent.kt)0
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/Severity.kt (renamed from src/main/kotlin/moe/nea/ledger/utils/telemetry/Severity.kt)0
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/Span.kt (renamed from src/main/kotlin/moe/nea/ledger/utils/telemetry/Span.kt)3
-rw-r--r--mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/StringContext.kt (renamed from src/main/kotlin/moe/nea/ledger/utils/telemetry/StringContext.kt)0
-rw-r--r--mod/src/main/resources/ledgerkeystore.jks (renamed from src/main/resources/ledgerkeystore.jks)bin104393 -> 104393 bytes
-rw-r--r--mod/src/main/resources/mcmod.info (renamed from src/main/resources/mcmod.info)0
-rw-r--r--mod/src/main/resources/mixins.moneyledger.json (renamed from src/main/resources/mixins.moneyledger.json)1
-rw-r--r--mod/src/test/kotlin/moe/nea/ledger/NumberUtilKtTest.kt (renamed from src/test/kotlin/moe/nea/ledger/NumberUtilKtTest.kt)0
-rw-r--r--server/aio/build.gradle.kts19
-rw-r--r--server/aio/src/main/kotlin/moe/nea/ledger/server/aio/AIO.kt20
-rw-r--r--server/analysis/build.gradle.kts16
-rw-r--r--server/analysis/src/main/kotlin/moe/nea/ledger/analysis/Analysis.kt9
-rw-r--r--server/analysis/src/main/kotlin/moe/nea/ledger/analysis/AnalysisFilter.kt26
-rw-r--r--server/analysis/src/main/kotlin/moe/nea/ledger/analysis/AnalysisResult.kt8
-rw-r--r--server/analysis/src/main/kotlin/moe/nea/ledger/analysis/CoinsSpentOnAuctions.kt49
-rw-r--r--server/analysis/src/main/kotlin/moe/nea/ledger/analysis/Visualization.kt30
-rw-r--r--server/core/build.gradle.kts37
-rw-r--r--server/core/src/main/kotlin/moe/nea/ledger/server/core/Application.kt81
-rw-r--r--server/core/src/main/kotlin/moe/nea/ledger/server/core/api/BaseApi.kt205
-rw-r--r--server/core/src/main/kotlin/moe/nea/ledger/server/core/model.kt30
-rw-r--r--server/core/src/main/resources/application.conf10
-rw-r--r--server/frontend/.gitignore2
-rw-r--r--server/frontend/build.gradle.kts17
-rw-r--r--server/frontend/index.html16
-rw-r--r--server/frontend/package.json37
-rw-r--r--server/frontend/pnpm-lock.yaml1920
-rw-r--r--server/frontend/src/Analysis.tsx84
-rw-r--r--server/frontend/src/App.module.css0
-rw-r--r--server/frontend/src/App.tsx22
-rw-r--r--server/frontend/src/Test.tsx31
-rw-r--r--server/frontend/src/api-schema.d.ts236
-rw-r--r--server/frontend/src/api.ts13
-rw-r--r--server/frontend/src/index.css13
-rw-r--r--server/frontend/src/index.tsx23
-rw-r--r--server/frontend/tsconfig.json19
-rw-r--r--server/frontend/vite.config.ts17
-rw-r--r--server/swagger/build.gradle.kts17
-rw-r--r--server/swagger/src/main/kotlin/moe/nea/ledger/server/core/api/OpenApiModel.kt117
-rw-r--r--server/swagger/src/main/kotlin/moe/nea/ledger/server/core/api/docs.kt323
-rw-r--r--settings.gradle.kts18
-rw-r--r--src/main/java/moe/nea/ledger/init/AutoDiscoveryMixinPlugin.java188
-rw-r--r--src/main/kotlin/moe/nea/ledger/ItemChange.kt84
-rw-r--r--src/main/kotlin/moe/nea/ledger/ItemId.kt46
-rw-r--r--src/main/kotlin/moe/nea/ledger/database/DBSchema.kt545
-rw-r--r--src/main/kotlin/moe/nea/ledger/events/BeforeGuiAction.kt11
-rw-r--r--test.pngbin6844 -> 0 bytes
211 files changed, 5877 insertions, 1283 deletions
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 0b2ab06..fd9ec1e 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -15,18 +15,30 @@ jobs:
with:
distribution: temurin
java-version: 17
-
+ - name: Setup PNPM
+ uses: pnpm/action-setup@v4
+ with:
+ package_json_file: 'server/frontend/package.json'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
- name: Execute Gradle build
run: ./gradlew build
-
+ - name: Show build directory
+ if: runner.debug == '1'
+ run: |
+ ls -lahR mod/build
- name: Upload built mod JAR
uses: actions/upload-artifact@v4.3.0
with:
name: mod-jar
- path: build/libs/*.jar
+ path: mod/build/libs/*.jar
+ - name: Upload partial JARs
+ if: runner.debug == '1'
+ uses: actions/upload-artifact@v4.3.0
+ with:
+ name: extras
+ path: mod/build/badjars/*.jar
release:
runs-on: ubuntu-latest
needs: gradle
diff --git a/.gitignore b/.gitignore
index 69fa48a..52f30f2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,4 +3,4 @@
run/
build/
.gradle/
-
+.kotlin/
diff --git a/basetypes/build.gradle.kts b/basetypes/build.gradle.kts
new file mode 100644
index 0000000..f4b1a8b
--- /dev/null
+++ b/basetypes/build.gradle.kts
@@ -0,0 +1,12 @@
+plugins {
+ `java-library`
+ kotlin("jvm")
+}
+
+java {
+ toolchain.languageVersion.set(JavaLanguageVersion.of(8))
+}
+
+dependencies {
+ implementation("io.azam.ulidj:ulidj:1.0.4")
+}
diff --git a/basetypes/src/main/kotlin/moe/nea/ledger/ItemChange.kt b/basetypes/src/main/kotlin/moe/nea/ledger/ItemChange.kt
new file mode 100644
index 0000000..6cadf27
--- /dev/null
+++ b/basetypes/src/main/kotlin/moe/nea/ledger/ItemChange.kt
@@ -0,0 +1,43 @@
+package moe.nea.ledger
+
+data class ItemChange(
+ val itemId: ItemId,
+ val count: Double,
+ val direction: ChangeDirection,
+) {
+
+ enum class ChangeDirection {
+ GAINED,
+ TRANSFORM,
+ SYNC,
+ CATALYST,
+ LOST;
+
+ }
+
+ companion object {
+ fun gainCoins(number: Double): 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)
+ }
+
+ fun lose(itemId: ItemId, amount: Number): ItemChange {
+ return ItemChange(itemId, amount.toDouble(), ChangeDirection.LOST)
+ }
+
+ fun loseCoins(number: Double): ItemChange {
+ return lose(ItemId.COINS, number)
+ }
+
+ }
+} \ No newline at end of file
diff --git a/basetypes/src/main/kotlin/moe/nea/ledger/ItemId.kt b/basetypes/src/main/kotlin/moe/nea/ledger/ItemId.kt
new file mode 100644
index 0000000..8dcfa27
--- /dev/null
+++ b/basetypes/src/main/kotlin/moe/nea/ledger/ItemId.kt
@@ -0,0 +1,35 @@
+package moe.nea.ledger
+
+import moe.nea.ledger.utils.RemoveInRelease
+
+data class ItemId(
+ val string: String
+) {
+ @RemoveInRelease
+ fun singleItem(): Pair<ItemId, Double> {
+ return withStackSize(1)
+ }
+
+ @RemoveInRelease
+ fun withStackSize(size: Number): Pair<ItemId, Double> {
+ return Pair(this, size.toDouble())
+ }
+
+
+ companion object {
+
+ @JvmStatic
+ @RemoveInRelease
+ 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/src/main/kotlin/moe/nea/ledger/TransactionType.kt b/basetypes/src/main/kotlin/moe/nea/ledger/TransactionType.kt
index 26749d6..527b6cd 100644
--- a/src/main/kotlin/moe/nea/ledger/TransactionType.kt
+++ b/basetypes/src/main/kotlin/moe/nea/ledger/TransactionType.kt
@@ -1,18 +1,23 @@
package moe.nea.ledger
enum class TransactionType {
+ ACCESSORIES_SWAPPING,
+ ALLOWANCE_GAIN,
AUCTION_BOUGHT,
AUCTION_LISTING_CHARGE,
AUCTION_SOLD,
AUTOMERCHANT_PROFIT_COLLECT,
BANK_DEPOSIT,
+ BANK_INTEREST,
BANK_WITHDRAW,
+ BASIC_REFORGE,
BAZAAR_BUY_INSTANT,
BAZAAR_BUY_ORDER,
BAZAAR_SELL_INSTANT,
BAZAAR_SELL_ORDER,
BITS_PURSE_STATUS,
BOOSTER_COOKIE_ATE,
+ CADUCOUS_FEEDER_USED,
CAPSAICIN_EYEDROPS_USED,
COMMUNITY_SHOP_BUY,
CORPSE_DESECRATED,
@@ -20,14 +25,17 @@ enum class TransactionType {
DRACONIC_SACRIFICE,
DUNGEON_CHEST_OPEN,
FORGED,
+ GHOST_COIN_DROP,
GOD_POTION_DRANK,
GOD_POTION_MIXIN_DRANK,
+ GUMMY_POLAR_BEAR_ATE,
KAT_TIMESKIP,
KAT_UPGRADE,
KISMET_REROLL,
KUUDRA_CHEST_OPEN,
NPC_BUY,
NPC_SELL,
+ PEST_REPELLENT_USED,
VISITOR_BARGAIN,
WYRM_EVOKED,
} \ No newline at end of file
diff --git a/basetypes/src/main/kotlin/moe/nea/ledger/utils/RemoveInRelease.kt b/basetypes/src/main/kotlin/moe/nea/ledger/utils/RemoveInRelease.kt
new file mode 100644
index 0000000..319fb63
--- /dev/null
+++ b/basetypes/src/main/kotlin/moe/nea/ledger/utils/RemoveInRelease.kt
@@ -0,0 +1,4 @@
+package moe.nea.ledger.utils
+
+@Retention(AnnotationRetention.BINARY)
+annotation class RemoveInRelease
diff --git a/basetypes/src/main/kotlin/moe/nea/ledger/utils/ULIDWrapper.kt b/basetypes/src/main/kotlin/moe/nea/ledger/utils/ULIDWrapper.kt
new file mode 100644
index 0000000..29d5e31
--- /dev/null
+++ b/basetypes/src/main/kotlin/moe/nea/ledger/utils/ULIDWrapper.kt
@@ -0,0 +1,35 @@
+package moe.nea.ledger.utils
+
+import io.azam.ulidj.ULID
+import java.time.Instant
+import kotlin.random.Random
+
+@JvmInline
+value class ULIDWrapper(
+ val wrapped: String
+) {
+ companion object {
+ fun lowerBound(timestamp: Instant): ULIDWrapper {
+ return ULIDWrapper(ULID.generate(timestamp.toEpochMilli(), ByteArray(10)))
+ }
+
+ fun upperBound(timestamp: Instant): ULIDWrapper {
+ return ULIDWrapper(ULID.generate(timestamp.toEpochMilli(), ByteArray(10) { -1 }))
+ }
+
+ fun createULIDAt(timestamp: Instant): ULIDWrapper {
+ return ULIDWrapper(ULID.generate(
+ timestamp.toEpochMilli(),
+ Random.nextBytes(10)
+ ))
+ }
+ }
+
+ fun getTimestamp(): Instant {
+ return Instant.ofEpochMilli(ULID.getTimestamp(wrapped))
+ }
+
+ init {
+ require(ULID.isValid(wrapped))
+ }
+} \ No newline at end of file
diff --git a/basetypes/src/main/kotlin/moe/nea/ledger/utils/UUIDUtil.kt b/basetypes/src/main/kotlin/moe/nea/ledger/utils/UUIDUtil.kt
new file mode 100644
index 0000000..92a29f7
--- /dev/null
+++ b/basetypes/src/main/kotlin/moe/nea/ledger/utils/UUIDUtil.kt
@@ -0,0 +1,41 @@
+package moe.nea.ledger.utils
+
+import java.util.UUID
+import kotlin.uuid.ExperimentalUuidApi
+import kotlin.uuid.Uuid
+import kotlin.uuid.toJavaUuid
+
+object UUIDUtil {
+ @OptIn(ExperimentalUuidApi::class)
+ fun parsePotentiallyDashlessUUID(str: String): UUID {
+ val bytes = ByteArray(16)
+ var i = -1
+ var bi = 0
+ while (++i < str.length) {
+ val char = str[i]
+ if (char == '-') {
+ if (bi != 4 && bi != 6 && bi != 8 && bi != 10) {
+ error("Unexpected dash in UUID: $str")
+ }
+ continue
+ }
+ val current = parseHexDigit(str, char)
+ ++i
+ if (i >= str.length)
+ error("Unexpectedly short UUID: $str")
+ val next = parseHexDigit(str, str[i])
+ bytes[bi++] = (current * 16 or next).toByte()
+ }
+ if (bi != 16)
+ error("Unexpectedly short UUID: $str")
+ return Uuid.fromByteArray(bytes).toJavaUuid()
+ }
+
+ private fun parseHexDigit(str: String, char: Char): Int {
+ val d = char - '0'
+ if (d < 10) return d
+ val h = char - 'a'
+ if (h < 6) return 10 + h
+ error("Unexpected hex digit $char in UUID: $str")
+ }
+} \ No newline at end of file
diff --git a/build-src/README.md b/build-src/README.md
new file mode 100644
index 0000000..66d45bc
--- /dev/null
+++ b/build-src/README.md
@@ -0,0 +1,2 @@
+
+Intentionally not using `buildSrc` over an `includeBuild("build-src")` due to better performance of composite builds.
diff --git a/build-src/build.gradle.kts b/build-src/build.gradle.kts
new file mode 100644
index 0000000..b06addb
--- /dev/null
+++ b/build-src/build.gradle.kts
@@ -0,0 +1,12 @@
+plugins {
+ kotlin("jvm") version "2.0.20"
+ `kotlin-dsl`
+}
+repositories {
+ mavenCentral()
+}
+dependencies {
+ implementation("com.google.code.gson:gson:2.9.1") // Match loom :)
+ implementation(gradleApi())
+ api("com.guardsquare:proguard-gradle:7.6.1")
+}
diff --git a/build-src/settings.gradle.kts b/build-src/settings.gradle.kts
new file mode 100644
index 0000000..f9db621
--- /dev/null
+++ b/build-src/settings.gradle.kts
@@ -0,0 +1,6 @@
+pluginManagement {
+ repositories {
+ mavenCentral()
+ gradlePluginPortal()
+ }
+} \ No newline at end of file
diff --git a/build-src/src/main/kotlin/GenerateItemIds.kt b/build-src/src/main/kotlin/GenerateItemIds.kt
new file mode 100644
index 0000000..24f2f62
--- /dev/null
+++ b/build-src/src/main/kotlin/GenerateItemIds.kt
@@ -0,0 +1,72 @@
+import com.google.gson.Gson
+import org.gradle.api.DefaultTask
+import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputDirectory
+import org.gradle.api.tasks.Internal
+import org.gradle.api.tasks.OutputDirectory
+import org.gradle.api.tasks.TaskAction
+import java.io.File
+
+abstract class GenerateItemIds : DefaultTask() {
+ @get: OutputDirectory
+ abstract val outputDirectory: DirectoryProperty
+
+ @get: InputDirectory
+ abstract val repoFiles: DirectoryProperty
+
+ @get: Input
+ abstract val repoHash: Property<String>
+
+ @get: Input
+ abstract val packageName: Property<String>
+
+ @get:Internal
+ val outputFile get() = outputDirectory.asFile.get().resolve(packageName.get().replace(".", "/") + "/ItemIds.java")
+
+ init {
+ repoHash.convention("unknown-repo-git-hash")
+ }
+
+ @TaskAction
+ fun generateItemIds() {
+ val nonIdName = "[^A-Z0-9_]".toRegex()
+
+ data class Item(val id: String, val file: File) {
+ val javaName get() = id.replace(nonIdName, { "__" + it.value.single().code })
+ }
+
+ val items = mutableListOf<Item>()
+ for (listFile in repoFiles.asFile.get().resolve("items").listFiles() ?: emptyArray()) {
+ listFile ?: continue
+ if (listFile.extension != "json") {
+ error("Unknown file $listFile")
+ }
+ items.add(Item(listFile.nameWithoutExtension, listFile))
+ }
+ items.sortedBy { it.id }
+ outputFile.parentFile.mkdirs()
+ val writer = outputFile.writer().buffered()
+ writer.appendLine("// @generated from " + repoHash.get())
+ writer.appendLine("package " + packageName.get() + ";")
+ writer.appendLine()
+ writer.appendLine("import moe.nea.ledger.ItemId;")
+ writer.appendLine()
+ writer.appendLine("/**")
+ writer.appendLine(" * Automatically generated {@link ItemId} list.")
+ writer.appendLine(" */")
+ writer.appendLine("@org.jspecify.annotations.NullMarked")
+ writer.appendLine("public interface ItemIds {")
+ val gson = Gson()
+ for (item in items) {
+ writer.appendLine("\t/**")
+ writer.appendLine("\t * @see <a href=${gson.toJson("https://github.com/NotEnoughUpdates/NotEnoughUpdates-REPO/blob/${repoHash.get()}/items/${item.id}.json")}>JSON definition</a>")
+ writer.appendLine("\t */")
+ writer.appendLine("\tItemId ${item.javaName} =" +
+ " ItemId.forName(${gson.toJson(item.id)});")
+ }
+ writer.appendLine("}")
+ writer.close()
+ }
+}
diff --git a/build-src/src/main/kotlin/RepoDownload.kt b/build-src/src/main/kotlin/RepoDownload.kt
new file mode 100644
index 0000000..182c66f
--- /dev/null
+++ b/build-src/src/main/kotlin/RepoDownload.kt
@@ -0,0 +1,41 @@
+import org.gradle.api.DefaultTask
+import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.OutputDirectory
+import org.gradle.api.tasks.TaskAction
+import java.net.URI
+import java.util.zip.ZipInputStream
+
+abstract class RepoDownload : DefaultTask() {
+ @get:Input
+ abstract val hash: Property<String>
+
+ @get:OutputDirectory
+ abstract val outputDirectory: DirectoryProperty
+
+ init {
+ outputDirectory.convention(project.layout.buildDirectory.dir("extracted-test-repo"))
+ }
+
+ @TaskAction
+ fun performDownload() {
+ val outputDir = outputDirectory.asFile.get().absoluteFile
+ outputDir.mkdirs()
+ URI("https://github.com/notEnoughUpdates/notEnoughUpdates-rEPO/archive/${hash.get()}.zip").toURL().openStream()
+ .let(::ZipInputStream)
+ .use { zipInput ->
+ while (true) {
+ val entry = zipInput.nextEntry ?: break
+ val destination = outputDir.resolve(
+ entry.name.substringAfter('/')).absoluteFile
+ require(outputDir in generateSequence(destination) { it.parentFile })
+ if (entry.isDirectory) continue
+ destination.parentFile.mkdirs()
+ destination.outputStream().use { output ->
+ zipInput.copyTo(output)
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/build-src/src/main/kotlin/helpers.kt b/build-src/src/main/kotlin/helpers.kt
new file mode 100644
index 0000000..48c230e
--- /dev/null
+++ b/build-src/src/main/kotlin/helpers.kt
@@ -0,0 +1,17 @@
+import org.gradle.api.plugins.ExtensionAware
+import org.gradle.kotlin.dsl.DependencyHandlerScope
+import org.gradle.kotlin.dsl.configure
+import org.gradle.kotlin.dsl.findByType
+
+
+inline fun <reified T : Any> ExtensionAware.configureIf(crossinline block: T.() -> Unit) {
+ if (extensions.findByType<T>() != null) {
+ extensions.configure<T> { block() }
+ }
+}
+
+val ktor_version = "3.0.3"
+
+fun DependencyHandlerScope.declareKtorVersion() {
+ "implementation"(platform("io.ktor:ktor-bom:$ktor_version"))
+}
diff --git a/build-src/src/main/kotlin/ledger-globals.gradle.kts b/build-src/src/main/kotlin/ledger-globals.gradle.kts
new file mode 100644
index 0000000..4238322
--- /dev/null
+++ b/build-src/src/main/kotlin/ledger-globals.gradle.kts
@@ -0,0 +1,25 @@
+apply(plugin = "org.gradle.base")
+
+repositories {
+ mavenCentral()
+ maven("https://repo.nea.moe/releases/")
+ maven("https://repo.spongepowered.org/maven/")
+ maven("https://maven.notenoughupdates.org/releases")
+ maven("https://pkgs.dev.azure.com/djtheredstoner/DevAuth/_packaging/public/maven/v1")
+}
+
+tasks.withType<AbstractArchiveTask> {
+ this.isPreserveFileTimestamps = false
+ this.isReproducibleFileOrder = true
+ this.archiveBaseName.set("ledger-" + project.path.replace(":", "-").trim('-'))
+}
+
+tasks.withType<Test> {
+ useJUnitPlatform()
+}
+
+tasks.withType<JavaCompile> {
+ options.encoding = "UTF-8"
+}
+
+
diff --git a/build-src/src/main/kotlin/ledger-repo.gradle.kts b/build-src/src/main/kotlin/ledger-repo.gradle.kts
new file mode 100644
index 0000000..1a9be63
--- /dev/null
+++ b/build-src/src/main/kotlin/ledger-repo.gradle.kts
@@ -0,0 +1 @@
+tasks.register("downloadRepo", RepoDownload::class)
diff --git a/build-src/src/main/kotlin/ledger-staged-proguard.gradle.kts b/build-src/src/main/kotlin/ledger-staged-proguard.gradle.kts
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/build-src/src/main/kotlin/ledger-staged-proguard.gradle.kts
@@ -0,0 +1 @@
+
diff --git a/build.gradle.kts b/build.gradle.kts
index 269f000..4c6ee45 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,29 +1,18 @@
import com.github.gmazzo.buildconfig.BuildConfigExtension
-import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
-import com.google.gson.Gson
-import org.apache.commons.lang3.SystemUtils
-import proguard.gradle.ProGuardTask
import java.io.ByteArrayOutputStream
-import java.net.URI
-import java.util.zip.ZipInputStream
-buildscript {
- repositories {
- mavenCentral()
- }
- dependencies {
- classpath("com.guardsquare:proguard-gradle:7.6.1")
- }
+plugins {
+ val kotlinVersion = "2.0.21"
+ kotlin("jvm") version kotlinVersion apply false
+ kotlin("plugin.serialization") version kotlinVersion apply false
+ id("com.github.gmazzo.buildconfig") version "5.5.0" apply false
+ id("ledger-globals")
+ id("com.google.devtools.ksp") version "2.0.21-1.0.26" apply false
+ id("com.github.johnrengelman.shadow") version "8.1.1" apply false
}
-plugins {
- idea
- java
- id("gg.essential.loom") version "0.10.0.+"
- id("dev.architectury.architectury-pack200") version "0.1.3"
- id("com.github.johnrengelman.shadow") version "8.1.1"
- id("com.github.gmazzo.buildconfig") version "5.5.0"
- kotlin("jvm") version "2.0.20"
+allprojects {
+ apply(plugin = "ledger-globals")
}
fun cmd(vararg args: String): String {
@@ -35,278 +24,16 @@ fun cmd(vararg args: String): String {
return baos.toByteArray().decodeToString().trim()
}
-
-val baseGroup: String by project
-val mcVersion: String by project
val gitVersion = cmd("git", "rev-parse", "--short", "HEAD")
val fullVersion = project.property("mod_version").toString()
-val version: String = "$fullVersion-$gitVersion"
-val mixinGroup = "$baseGroup.mixin"
-project.version = version
-val modid: String by project
-
-// Toolchains:
-java {
- toolchain.languageVersion.set(JavaLanguageVersion.of(8))
-}
-
-// Minecraft configuration:
-loom {
- log4jConfigs.from(file("log4j2.xml"))
- launchConfigs {
- "client" {
- property("mixin.debug", "true")
- arg("--tweakClass", "org.spongepowered.asm.launch.MixinTweaker")
- arg("--tweakClass", "io.github.notenoughupdates.moulconfig.tweaker.DevelopmentResourceTweaker")
- }
- }
- runConfigs {
- "client" {
- if (SystemUtils.IS_OS_MAC_OSX) {
- // This argument causes a crash on macOS
- vmArgs.remove("-XstartOnFirstThread")
- }
- }
- remove(getByName("server"))
- }
- forge {
- pack200Provider.set(dev.architectury.pack200.java.Pack200Adapter())
- mixinConfig("mixins.$modid.json")
- }
- mixin {
- defaultRefmapName.set("mixins.$modid.refmap.json")
- }
-}
-
-tasks.compileJava {
- dependsOn(tasks.processResources)
-}
-
-sourceSets.main {
- output.setResourcesDir(sourceSets.main.flatMap { it.java.classesDirectory })
- java.srcDir(layout.projectDirectory.dir("src/main/kotlin"))
- kotlin.destinationDirectory.set(java.destinationDirectory)
-}
-
-repositories {
- mavenCentral()
- maven("https://repo.nea.moe/releases/")
- maven("https://repo.spongepowered.org/maven/")
- maven("https://maven.notenoughupdates.org/releases")
- maven("https://pkgs.dev.azure.com/djtheredstoner/DevAuth/_packaging/public/maven/v1")
-}
-
-val shadowImpl: Configuration by configurations.creating {
- configurations.implementation.get().extendsFrom(this)
-}
-
-dependencies {
- minecraft("com.mojang:minecraft:1.8.9")
- mappings("de.oceanlabs.mcp:mcp_stable:22-1.8.9")
- forge("net.minecraftforge:forge:1.8.9-11.15.1.2318-1.8.9")
-
- shadowImpl(kotlin("stdlib-jdk8"))
-
- shadowImpl("org.spongepowered:mixin:0.7.11-SNAPSHOT") {
- isTransitive = false
- }
- annotationProcessor("org.spongepowered:mixin:0.8.5-SNAPSHOT")
-
- shadowImpl("org.xerial:sqlite-jdbc:3.45.3.0")
- shadowImpl("org.notenoughupdates.moulconfig:legacy:3.0.0-beta.9")
- shadowImpl("io.azam.ulidj:ulidj:1.0.4")
- shadowImpl("moe.nea:libautoupdate:1.3.1")
- runtimeOnly("me.djtheredstoner:DevAuth-forge-legacy:1.1.2")
- testImplementation("org.junit.jupiter:junit-jupiter:5.9.2")
-}
-
-// Tasks:
-
-// Delete default shadow configuration
-tasks.shadowJar {
- doFirst { error("Incorrect shadow JAR built!") }
-}
-
-tasks.test {
- useJUnitPlatform()
-}
-
-tasks.withType(JavaCompile::class) {
- options.encoding = "UTF-8"
-}
-
-abstract class GenerateItemIds : DefaultTask() {
- @get: OutputDirectory
- abstract val outputDirectory: DirectoryProperty
-
- @get: InputDirectory
- abstract val repoFiles: DirectoryProperty
-
- @get: Input
- abstract val repoHash: Property<String>
-
- @get: Input
- abstract val packageName: Property<String>
-
- @get:Internal
- val outputFile get() = outputDirectory.asFile.get().resolve(packageName.get().replace(".", "/") + "/ItemIds.java")
-
- @TaskAction
- fun generateItemIds() {
- val nonIdName = "[^A-Z0-9_]".toRegex()
-
- data class Item(val id: String) {
- val javaName get() = id.replace(nonIdName, { "__" + it.value.single().code })
- }
-
- val items = mutableListOf<Item>()
- for (listFile in repoFiles.asFile.get().resolve("items").listFiles() ?: emptyArray()) {
- listFile ?: continue
- if (listFile.extension != "json") {
- error("Unknown file $listFile")
- }
- items.add(Item(listFile.nameWithoutExtension))
+val versionName: String = "$fullVersion-$gitVersion"
+allprojects {
+ version = versionName
+ afterEvaluate {
+ configureIf<BuildConfigExtension> {
+ buildConfigField<String>("VERSION", versionName)
+ buildConfigField<String>("FULL_VERSION", fullVersion)
+ buildConfigField<String>("GIT_COMMIT", gitVersion)
}
- items.sortedBy { it.id }
- outputFile.parentFile.mkdirs()
- val writer = outputFile.writer().buffered()
- writer.appendLine("// @generated from " + repoHash.get())
- writer.appendLine("package " + packageName.get() + ";")
- writer.appendLine()
- writer.appendLine("import moe.nea.ledger.ItemId;")
- writer.appendLine()
- writer.appendLine("/**")
- writer.appendLine(" * Automatically generated {@link ItemId} list.")
- writer.appendLine(" */")
- writer.appendLine("public class ItemIds {")
- val gson = Gson()
- for (item in items) {
- writer.appendLine("\t/**")
- writer.appendLine("\t * @see <a href=${gson.toJson("https://github.com/NotEnoughUpdates/NotEnoughUpdates-REPO/blob/${repoHash.get()}/items/${item.id}.json")}>JSON definition</a>")
- writer.appendLine("\t */")
- writer.appendLine("\tpublic static final ItemId ${item.javaName} =" +
- " ItemId.forName(${gson.toJson(item.id)});")
- }
- writer.appendLine("}")
- writer.close()
- }
-}
-
-abstract class RepoDownload : DefaultTask() {
- @get:Input
- abstract val hash: Property<String>
-
- @get:OutputDirectory
- abstract val outputDirectory: DirectoryProperty
-
- init {
- outputDirectory.convention(project.layout.buildDirectory.dir("extracted-test-repo"))
- }
-
- @TaskAction
- fun performDownload() {
- val outputDir = outputDirectory.asFile.get().absoluteFile
- outputDir.mkdirs()
- URI("https://github.com/notEnoughUpdates/notEnoughUpdates-rEPO/archive/${hash.get()}.zip").toURL().openStream()
- .let(::ZipInputStream)
- .use { zipInput ->
- while (true) {
- val entry = zipInput.nextEntry ?: break
- val destination = outputDir.resolve(
- entry.name.substringAfter('/')).absoluteFile
- require(outputDir in generateSequence(destination) { it.parentFile })
- if (entry.isDirectory) continue
- destination.parentFile.mkdirs()
- destination.outputStream().use { output ->
- zipInput.copyTo(output)
- }
- }
- }
- }
-}
-
-val downloadRepo by tasks.register("downloadRepo", RepoDownload::class) {
- hash.set("725ddb8")
-}
-
-val generateItemIds by tasks.register("generateItemIds", GenerateItemIds::class) {
- repoHash.set(downloadRepo.hash)
- packageName.set("moe.nea.ledger.gen")
- outputDirectory.set(layout.buildDirectory.dir("generated/sources/itemIds"))
- repoFiles.set(downloadRepo.outputDirectory)
-}
-sourceSets.main {
- java.srcDir(generateItemIds)
-}
-
-tasks.withType(Jar::class) {
- archiveBaseName.set(modid)
- manifest.attributes.run {
- this["FMLCorePluginContainsFMLMod"] = "true"
- this["ForceLoadAsMod"] = "true"
-
- // If you don't want mixins, remove these lines
- this["TweakClass"] = "org.spongepowered.asm.launch.MixinTweaker"
- this["MixinConfigs"] = "mixins.$modid.json"
- }
-}
-
-tasks.processResources {
- inputs.property("version", project.version)
- inputs.property("mcversion", mcVersion)
- inputs.property("modid", modid)
- inputs.property("basePackage", baseGroup)
-
- filesMatching(listOf("mcmod.info", "mixins.$modid.json")) {
- expand(inputs.properties)
}
-
- rename("(.+_at.cfg)", "META-INF/$1")
-}
-
-
-val proguardOutJar = project.layout.buildDirectory.file("badjars/stripped.jar")
-val proguard = tasks.register("proguard", ProGuardTask::class) {
- dependsOn(tasks.jar)
- injars(tasks.jar.map { it.archiveFile })
- outjars(proguardOutJar)
- configuration(file("ledger-rules.pro"))
- verbose()
- val libJava = javaToolchains.launcherFor(java.toolchain)
- .get()
- .metadata.installationPath.file("jre/lib/rt.jar")
- println(libJava)
- libraryjars(libJava)
- libraryjars(configurations.compileClasspath)
}
-
-val shadowJar2 = tasks.register("shadowJar2", ShadowJar::class) {
- destinationDirectory.set(layout.buildDirectory.dir("badjars"))
- archiveClassifier.set("all-dev")
- from(proguardOutJar)
- dependsOn(proguard)
- configurations = listOf(shadowImpl)
- relocate("moe.nea.libautoupdate", "moe.nea.ledger.deps.libautoupdate")
- mergeServiceFiles()
-}
-val remapJar by tasks.named<net.fabricmc.loom.task.RemapJarTask>("remapJar") {
- archiveClassifier.set("")
- from(shadowJar2)
- input.set(shadowJar2.get().archiveFile)
-}
-
-tasks.jar {
- archiveClassifier.set("without-deps")
- destinationDirectory.set(layout.buildDirectory.dir("badjars"))
-}
-
-
-tasks.assemble.get().dependsOn(tasks.remapJar)
-
-configure<BuildConfigExtension> {
- packageName("moe.nea.ledger.gen")
- buildConfigField<String>("VERSION", version)
- buildConfigField<String>("FULL_VERSION", fullVersion)
- buildConfigField<String>("GIT_COMMIT", gitVersion)
-}
-
diff --git a/database/core/build.gradle.kts b/database/core/build.gradle.kts
new file mode 100644
index 0000000..a4390fc
--- /dev/null
+++ b/database/core/build.gradle.kts
@@ -0,0 +1,12 @@
+plugins {
+ `java-library`
+ kotlin("jvm")
+}
+
+dependencies {
+ api(project(":basetypes"))
+}
+
+java {
+ toolchain.languageVersion.set(JavaLanguageVersion.of(8))
+}
diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/Column.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/Column.kt
new file mode 100644
index 0000000..c21a159
--- /dev/null
+++ b/database/core/src/main/kotlin/moe/nea/ledger/database/Column.kt
@@ -0,0 +1,31 @@
+package moe.nea.ledger.database
+
+import moe.nea.ledger.database.sql.IntoSelectable
+import moe.nea.ledger.database.sql.Selectable
+import java.sql.PreparedStatement
+
+class Column<T, Raw> @Deprecated("Use Table.column instead") constructor(
+ val table: Table,
+ val name: String,
+ val type: DBType<T, Raw>
+) : IntoSelectable<T> {
+ override fun asSelectable() = object : Selectable<T, Raw> {
+ override fun asSql(): String {
+ return qualifiedSqlName
+ }
+
+ override val dbType: DBType<T, Raw>
+ get() = this@Column.type
+
+ override fun guessColumn(): Column<T, Raw> {
+ return this@Column
+ }
+
+ override fun appendToStatement(stmt: PreparedStatement, startIndex: Int): Int {
+ return startIndex
+ }
+ }
+
+ val sqlName get() = "`$name`"
+ val qualifiedSqlName get() = table.sqlName + "." + sqlName
+} \ No newline at end of file
diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/Constraint.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/Constraint.kt
new file mode 100644
index 0000000..729c6b8
--- /dev/null
+++ b/database/core/src/main/kotlin/moe/nea/ledger/database/Constraint.kt
@@ -0,0 +1,6 @@
+package moe.nea.ledger.database
+
+interface Constraint {
+ val affectedColumns: Collection<Column<*, *>>
+ fun asSQL(): String
+} \ No newline at end of file
diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/DBSchema.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/DBSchema.kt
new file mode 100644
index 0000000..764cd26
--- /dev/null
+++ b/database/core/src/main/kotlin/moe/nea/ledger/database/DBSchema.kt
@@ -0,0 +1,13 @@
+package moe.nea.ledger.database
+
+import java.sql.Connection
+import java.sql.PreparedStatement
+
+fun Connection.prepareAndLog(statement: String): PreparedStatement {
+ println("Preparing to execute $statement")
+ return prepareStatement(statement)
+}
+
+
+
+
diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/DBType.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/DBType.kt
new file mode 100644
index 0000000..622aff3
--- /dev/null
+++ b/database/core/src/main/kotlin/moe/nea/ledger/database/DBType.kt
@@ -0,0 +1,43 @@
+package moe.nea.ledger.database
+
+import moe.nea.ledger.database.sql.ClauseBuilder
+import java.sql.PreparedStatement
+import java.sql.ResultSet
+
+
+interface DBType<
+ /**
+ * Mapped type of this db type. Represents the Java type this db type accepts for saving to the database.
+ */
+ T,
+ /**
+ * Phantom marker type representing how this db type is presented to the actual DB. Is used by APIs such as [ClauseBuilder] to allow for rough typechecking.
+ */
+ RawType> {
+ val dbType: String
+
+ fun get(result: ResultSet, index: Int): T
+ fun set(stmt: PreparedStatement, index: Int, value: T)
+ fun getName(): String = javaClass.simpleName
+ fun <R> mapped(
+ from: (R) -> T,
+ to: (T) -> R,
+ ): DBType<R, RawType> {
+ return object : DBType<R, RawType> {
+ override fun getName(): String {
+ return "Mapped(${this@DBType.getName()})"
+ }
+
+ override val dbType: String
+ get() = this@DBType.dbType
+
+ override fun get(result: ResultSet, index: Int): R {
+ return to(this@DBType.get(result, index))
+ }
+
+ override fun set(stmt: PreparedStatement, index: Int, value: R) {
+ this@DBType.set(stmt, index, from(value))
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/InsertStatement.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/InsertStatement.kt
new file mode 100644
index 0000000..25bef22
--- /dev/null
+++ b/database/core/src/main/kotlin/moe/nea/ledger/database/InsertStatement.kt
@@ -0,0 +1,7 @@
+package moe.nea.ledger.database
+
+class InsertStatement(val properties: MutableMap<Column<*, *>, Any>) {
+ operator fun <T : Any> set(key: Column<T, *>, value: T) {
+ properties[key] = value
+ }
+} \ No newline at end of file
diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/Query.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/Query.kt
new file mode 100644
index 0000000..a23c878
--- /dev/null
+++ b/database/core/src/main/kotlin/moe/nea/ledger/database/Query.kt
@@ -0,0 +1,111 @@
+package moe.nea.ledger.database
+
+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.sql.IntoSelectable
+import moe.nea.ledger.database.sql.Join
+import moe.nea.ledger.database.sql.SQLQueryComponent
+import moe.nea.ledger.database.sql.SQLQueryGenerator.concatToFilledPreparedStatement
+import moe.nea.ledger.database.sql.Selectable
+import java.sql.Connection
+
+class Query(
+ val connection: Connection,
+ val selectedColumns: MutableList<Selectable<*, *>>,
+ var table: Table,
+ var limit: UInt? = null,
+ var skip: UInt? = null,
+ val joins: MutableList<Join> = mutableListOf(),
+ val conditions: MutableList<BooleanExpression> = mutableListOf(),
+ var distinct: Boolean = false,
+// var order: OrderClause?= null,
+) : Iterable<ResultRow> {
+ fun join(table: Table, on: Clause): Query {
+ joins.add(Join(table, on))
+ return this
+ }
+
+ fun where(binOp: BooleanExpression): Query {
+ conditions.add(binOp)
+ return this
+ }
+
+ fun select(vararg columns: IntoSelectable<*>): Query {
+ for (column in columns) {
+ this.selectedColumns.add(column.asSelectable())
+ }
+ return this
+ }
+
+ fun skip(skip: UInt): Query {
+ require(limit != null)
+ this.skip = skip
+ return this
+ }
+
+ fun distinct(): Query {
+ this.distinct = true
+ return this
+ }
+
+ fun limit(limit: UInt): Query {
+ this.limit = limit
+ return this
+ }
+
+ override fun iterator(): Iterator<ResultRow> {
+ val elements = mutableListOf(
+ SQLQueryComponent.standalone("SELECT"),
+ )
+ if (distinct)
+ elements.add(SQLQueryComponent.standalone("DISTINCT"))
+ selectedColumns.forEachIndexed { idx, it ->
+ elements.add(it)
+ if (idx != selectedColumns.lastIndex) {
+ elements.add(SQLQueryComponent.standalone(","))
+ }
+ }
+ elements.add(SQLQueryComponent.standalone("FROM ${table.sqlName}"))
+ elements.addAll(joins)
+ if (conditions.any()) {
+ elements.add(SQLQueryComponent.standalone("WHERE"))
+ elements.add(ANDExpression(conditions))
+ }
+ if (limit != null) {
+ elements.add(SQLQueryComponent.standalone("LIMIT $limit"))
+ if (skip != null) {
+ elements.add(SQLQueryComponent.standalone("OFFSET $skip"))
+ }
+ }
+ val prepared = elements.concatToFilledPreparedStatement(connection)
+ val results = prepared.executeQuery()
+ return object : Iterator<ResultRow> {
+ var hasAdvanced = false
+ var hasEnded = false
+ override fun hasNext(): Boolean {
+ if (hasEnded) return false
+ if (hasAdvanced) return true
+ if (results.next()) {
+ hasAdvanced = true
+ return true
+ } else {
+ results.close() // TODO: somehow enforce closing this
+ hasEnded = true
+ return false
+ }
+ }
+
+ override fun next(): ResultRow {
+ if (!hasNext()) {
+ throw NoSuchElementException()
+ }
+ hasAdvanced = false
+ return ResultRow(selectedColumns.withIndex().associate {
+ it.value to it.value.dbType.get(results, it.index + 1)
+ })
+ }
+
+ }
+ }
+} \ No newline at end of file
diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/ResultRow.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/ResultRow.kt
new file mode 100644
index 0000000..7b57abd
--- /dev/null
+++ b/database/core/src/main/kotlin/moe/nea/ledger/database/ResultRow.kt
@@ -0,0 +1,22 @@
+package moe.nea.ledger.database
+
+import moe.nea.ledger.database.sql.Selectable
+
+class ResultRow(val selectableValues: Map<Selectable<*, *>, *>) {
+ val columnValues = selectableValues.mapNotNull {
+ val col = it.key.guessColumn() ?: return@mapNotNull null
+ col to it.value
+ }.toMap()
+
+ operator fun <T> get(column: Column<T, *>): T {
+ val value = columnValues[column]
+ ?: error("Invalid column ${column.name}. Only ${columnValues.keys.joinToString { it.name }} are available.")
+ return value as T
+ }
+
+ operator fun <T> get(column: Selectable<T, *>): T {
+ val value = selectableValues[column]
+ ?: error("Invalid selectable ${column}. Only ${selectableValues.keys} are available.")
+ return value as T
+ }
+} \ No newline at end of file
diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/Table.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/Table.kt
new file mode 100644
index 0000000..a462813
--- /dev/null
+++ b/database/core/src/main/kotlin/moe/nea/ledger/database/Table.kt
@@ -0,0 +1,103 @@
+package moe.nea.ledger.database
+
+import java.sql.Connection
+
+abstract class Table(val name: String) {
+ val sqlName get() = "`$name`"
+ protected val _mutable_columns: MutableList<Column<*, *>> = mutableListOf()
+ protected val _mutable_constraints: MutableList<Constraint> = mutableListOf()
+ val columns: List<Column<*, *>> get() = _mutable_columns
+ val constraints get() = _mutable_constraints
+ protected fun unique(vararg columns: Column<*, *>) {
+ _mutable_constraints.add(UniqueConstraint(columns.toList()))
+ }
+
+ protected fun <T, R> column(name: String, type: DBType<T, R>): Column<T, R> {
+ @Suppress("DEPRECATION") val column = Column(this, name, type)
+ _mutable_columns.add(column)
+ return column
+ }
+
+ fun debugSchema() {
+ val nameWidth = columns.maxOf { it.name.length }
+ val typeWidth = columns.maxOf { it.type.getName().length }
+ val totalWidth = maxOf(2 + nameWidth + 3 + typeWidth + 2, name.length + 4)
+ val adjustedTypeWidth = totalWidth - nameWidth - 2 - 3 - 2
+
+ var string = "\n"
+ string += ("+" + "-".repeat(totalWidth - 2) + "+\n")
+ string += ("| $name${" ".repeat(totalWidth - 4 - name.length)} |\n")
+ string += ("+" + "-".repeat(totalWidth - 2) + "+\n")
+ for (column in columns) {
+ string += ("| ${column.name}${" ".repeat(nameWidth - column.name.length)} |")
+ string += (" ${column.type.getName()}" +
+ "${" ".repeat(adjustedTypeWidth - column.type.getName().length)} |\n")
+ }
+ string += ("+" + "-".repeat(totalWidth - 2) + "+")
+ println(string)
+ }
+
+ fun createIfNotExists(
+ connection: Connection,
+ filteredColumns: List<Column<*, *>> = columns
+ ) {
+ val properties = mutableListOf<String>()
+ for (column in filteredColumns) {
+ properties.add("${column.sqlName} ${column.type.dbType}")
+ }
+ val columnSet = filteredColumns.toSet()
+ for (constraint in constraints) {
+ if (columnSet.containsAll(constraint.affectedColumns)) {
+ properties.add(constraint.asSQL())
+ }
+ }
+ connection.prepareAndLog("CREATE TABLE IF NOT EXISTS $sqlName (" + properties.joinToString() + ")")
+ .execute()
+ }
+
+ fun alterTableAddColumns(
+ connection: Connection,
+ newColumns: List<Column<*, *>>
+ ) {
+ for (column in newColumns) {
+ connection.prepareAndLog("ALTER TABLE $sqlName ADD ${column.sqlName} ${column.type.dbType}")
+ .execute()
+ }
+ for (constraint in constraints) {
+ // TODO: automatically add constraints, maybe (or maybe move constraints into the upgrade schema)
+ }
+ }
+
+ enum class OnConflict {
+ FAIL,
+ IGNORE,
+ REPLACE,
+ ;
+
+ fun asSql(): String {
+ return name
+ }
+ }
+
+ fun insert(connection: Connection, onConflict: OnConflict = OnConflict.FAIL, block: (InsertStatement) -> Unit) {
+ val insert = InsertStatement(HashMap())
+ block(insert)
+ require(insert.properties.keys == columns.toSet())
+ val columnNames = columns.joinToString { it.sqlName }
+ val valueNames = columns.joinToString { "?" }
+ val statement =
+ connection.prepareAndLog("INSERT OR ${onConflict.asSql()} INTO $sqlName ($columnNames) VALUES ($valueNames)")
+ for ((index, column) in columns.withIndex()) {
+ (column as Column<Any, *>).type.set(statement, index + 1, insert.properties[column]!!)
+ }
+ statement.execute()
+ }
+
+ fun from(connection: Connection): Query {
+ return Query(connection, mutableListOf(), this)
+ }
+
+ fun selectAll(connection: Connection): Query {
+ return Query(connection, columns.mapTo(mutableListOf()) { it.asSelectable() }, this)
+ }
+} \ No newline at end of file
diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/UniqueConstraint.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/UniqueConstraint.kt
new file mode 100644
index 0000000..31ef06c
--- /dev/null
+++ b/database/core/src/main/kotlin/moe/nea/ledger/database/UniqueConstraint.kt
@@ -0,0 +1,14 @@
+package moe.nea.ledger.database
+
+class UniqueConstraint(val columns: List<Column<*, *>>) : Constraint {
+ init {
+ require(columns.isNotEmpty())
+ }
+
+ override val affectedColumns: Collection<Column<*, *>>
+ get() = columns
+
+ override fun asSQL(): String {
+ return "UNIQUE (${columns.joinToString() { it.sqlName }})"
+ }
+} \ No newline at end of file
diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBDouble.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBDouble.kt
new file mode 100644
index 0000000..9840df2
--- /dev/null
+++ b/database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBDouble.kt
@@ -0,0 +1,18 @@
+package moe.nea.ledger.database.columns
+
+import moe.nea.ledger.database.DBType
+import java.sql.PreparedStatement
+import java.sql.ResultSet
+
+object DBDouble : DBType<Double, Double> {
+ override val dbType: String
+ get() = "DOUBLE"
+
+ override fun get(result: ResultSet, index: Int): Double {
+ return result.getDouble(index)
+ }
+
+ override fun set(stmt: PreparedStatement, index: Int, value: Double) {
+ stmt.setDouble(index, value)
+ }
+} \ No newline at end of file
diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBEnum.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBEnum.kt
new file mode 100644
index 0000000..78ac578
--- /dev/null
+++ b/database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBEnum.kt
@@ -0,0 +1,31 @@
+package moe.nea.ledger.database.columns
+
+import moe.nea.ledger.database.DBType
+import java.sql.PreparedStatement
+import java.sql.ResultSet
+
+class DBEnum<T : Enum<T>>(
+ val type: Class<T>,
+) : DBType<T, String> {
+ companion object {
+ inline operator fun <reified T : Enum<T>> invoke(): DBEnum<T> {
+ return DBEnum(T::class.java)
+ }
+ }
+
+ override val dbType: String
+ get() = "TEXT"
+
+ override fun getName(): String {
+ return "DBEnum(${type.simpleName})"
+ }
+
+ override fun set(stmt: PreparedStatement, index: Int, value: T) {
+ stmt.setString(index, value.name)
+ }
+
+ override fun get(result: ResultSet, index: Int): T {
+ val name = result.getString(index)
+ return java.lang.Enum.valueOf(type, name)
+ }
+} \ No newline at end of file
diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBInstant.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBInstant.kt
new file mode 100644
index 0000000..2cf1882
--- /dev/null
+++ b/database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBInstant.kt
@@ -0,0 +1,19 @@
+package moe.nea.ledger.database.columns
+
+import moe.nea.ledger.database.DBType
+import java.sql.PreparedStatement
+import java.sql.ResultSet
+import java.time.Instant
+
+object DBInstant : DBType<Instant, Long> {
+ override val dbType: String
+ get() = "INTEGER"
+
+ override fun set(stmt: PreparedStatement, index: Int, value: Instant) {
+ stmt.setLong(index, value.toEpochMilli())
+ }
+
+ override fun get(result: ResultSet, index: Int): Instant {
+ return Instant.ofEpochMilli(result.getLong(index))
+ }
+} \ No newline at end of file
diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBInt.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBInt.kt
new file mode 100644
index 0000000..b5406e1
--- /dev/null
+++ b/database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBInt.kt
@@ -0,0 +1,18 @@
+package moe.nea.ledger.database.columns
+
+import moe.nea.ledger.database.DBType
+import java.sql.PreparedStatement
+import java.sql.ResultSet
+
+object DBInt : DBType<Long, Long> {
+ override val dbType: String
+ get() = "INTEGER"
+
+ override fun get(result: ResultSet, index: Int): Long {
+ return result.getLong(index)
+ }
+
+ override fun set(stmt: PreparedStatement, index: Int, value: Long) {
+ stmt.setLong(index, value)
+ }
+} \ No newline at end of file
diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBString.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBString.kt
new file mode 100644
index 0000000..4406627
--- /dev/null
+++ b/database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBString.kt
@@ -0,0 +1,18 @@
+package moe.nea.ledger.database.columns
+
+import moe.nea.ledger.database.DBType
+import java.sql.PreparedStatement
+import java.sql.ResultSet
+
+object DBString : DBType<String, String> {
+ override val dbType: String
+ get() = "TEXT"
+
+ override fun get(result: ResultSet, index: Int): String {
+ return result.getString(index)
+ }
+
+ override fun set(stmt: PreparedStatement, index: Int, value: String) {
+ stmt.setString(index, value)
+ }
+} \ No newline at end of file
diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBUlid.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBUlid.kt
new file mode 100644
index 0000000..1fcc9d8
--- /dev/null
+++ b/database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBUlid.kt
@@ -0,0 +1,21 @@
+package moe.nea.ledger.database.columns
+
+import moe.nea.ledger.utils.ULIDWrapper
+import moe.nea.ledger.database.DBType
+import java.sql.PreparedStatement
+import java.sql.ResultSet
+
+object DBUlid : DBType<ULIDWrapper, String> {
+ override val dbType: String
+ get() = "TEXT"
+
+ override fun get(result: ResultSet, index: Int): ULIDWrapper {
+ val text = result.getString(index)
+ return ULIDWrapper(text)
+ }
+
+ override fun set(stmt: PreparedStatement, index: Int, value: ULIDWrapper) {
+ stmt.setString(index, value.wrapped)
+ }
+}
+
diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBUuid.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBUuid.kt
new file mode 100644
index 0000000..eaea440
--- /dev/null
+++ b/database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBUuid.kt
@@ -0,0 +1,20 @@
+package moe.nea.ledger.database.columns
+
+import moe.nea.ledger.database.DBType
+import moe.nea.ledger.utils.UUIDUtil
+import java.sql.PreparedStatement
+import java.sql.ResultSet
+import java.util.UUID
+
+object DBUuid : DBType<UUID, String> {
+ override val dbType: String
+ get() = "TEXT"
+
+ override fun get(result: ResultSet, index: Int): UUID {
+ return UUIDUtil.parsePotentiallyDashlessUUID(result.getString(index))
+ }
+
+ override fun set(stmt: PreparedStatement, index: Int, value: UUID) {
+ stmt.setString(index, value.toString())
+ }
+} \ No newline at end of file
diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/ANDExpression.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/ANDExpression.kt
new file mode 100644
index 0000000..43d5a53
--- /dev/null
+++ b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/ANDExpression.kt
@@ -0,0 +1,26 @@
+package moe.nea.ledger.database.sql
+
+import java.sql.PreparedStatement
+
+data class ANDExpression(
+ val elements: List<BooleanExpression>
+) : BooleanExpression {
+ init {
+ require(elements.isNotEmpty())
+ }
+
+ override fun asSql(): String {
+ elements.singleOrNull()?.let {
+ return "(" + it.asSql() + ")"
+ }
+ return elements.joinToString(" AND ", "(", ")") { it.asSql() }
+ }
+
+ override fun appendToStatement(stmt: PreparedStatement, startIndex: Int): Int {
+ var index = startIndex
+ for (element in elements) {
+ index = element.appendToStatement(stmt, index)
+ }
+ return index
+ }
+} \ No newline at end of file
diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/BooleanExpression.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/BooleanExpression.kt
new file mode 100644
index 0000000..5f2fba5
--- /dev/null
+++ b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/BooleanExpression.kt
@@ -0,0 +1,3 @@
+package moe.nea.ledger.database.sql
+
+interface BooleanExpression : SQLQueryComponent \ No newline at end of file
diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/Clause.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/Clause.kt
new file mode 100644
index 0000000..205e566
--- /dev/null
+++ b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/Clause.kt
@@ -0,0 +1,12 @@
+package moe.nea.ledger.database.sql
+
+/**
+ * Directly constructing [clauses][Clause] is discouraged. Instead [Clause.invoke] should be used.
+ */
+interface Clause : BooleanExpression {
+ companion object {
+ operator fun <T> invoke(builder: ClauseBuilder.() -> T): T {
+ return builder(ClauseBuilder())
+ }
+ }
+} \ No newline at end of file
diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/ClauseBuilder.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/ClauseBuilder.kt
new file mode 100644
index 0000000..cb0ddfc
--- /dev/null
+++ b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/ClauseBuilder.kt
@@ -0,0 +1,25 @@
+package moe.nea.ledger.database.sql
+
+import moe.nea.ledger.database.Column
+import moe.nea.ledger.database.DBType
+
+class ClauseBuilder {
+ // TODO: should we match on T AND R? maybe allow explicit upcasting
+ fun <T, R> column(column: Column<T, R>): ColumnOperand<T, R> = ColumnOperand(column)
+ fun string(string: String): StringOperand = StringOperand(string)
+ fun <T, R> value(dbType: DBType<T, R>, value: T): Operand<T, R> = ValuedOperand(dbType, value)
+ infix fun <T> Operand<*, T>.eq(operand: Operand<*, T>): Clause = EqualsClause(this, operand)
+ infix fun <T, R> TypedOperand<T, R>.eq(value: T): Clause = EqualsClause(this, value(dbType, value))
+ infix fun Operand<*, String>.like(op: StringOperand): Clause = LikeClause(this, op)
+ infix fun Operand<*, String>.like(op: String): Clause = LikeClause(this, string(op))
+ infix fun <T> Operand<*, T>.lt(op: Operand<*, T>): BooleanExpression = LessThanExpression(this, op)
+ infix fun <T> Operand<*, T>.le(op: Operand<*, T>): BooleanExpression = LessThanEqualsExpression(this, op)
+ infix fun <T> Operand<*, T>.gt(op: Operand<*, T>): BooleanExpression = op lt this
+ infix fun <T> Operand<*, T>.ge(op: Operand<*, T>): BooleanExpression = op le this
+ infix fun <T> Operand<*, T>.inList(list: ListExpression<*, T>): Clause = ListClause(this, list)
+ infix fun <T, R> TypedOperand<T, R>.inList(list: List<T>): Clause = this inList list(dbType, list)
+ fun <T, R> list(dbType: DBType<T, R>, vararg values: T): ListExpression<T, R> = list(dbType, values.toList())
+ fun <T, R> list(dbType: DBType<T, R>, values: List<T>): ListExpression<T, R> = ListExpression(values, dbType)
+ infix fun BooleanExpression.and(clause: BooleanExpression): BooleanExpression = ANDExpression(listOf(this, clause))
+ infix fun BooleanExpression.or(clause: BooleanExpression): BooleanExpression = ORExpression(listOf(this, clause))
+} \ No newline at end of file
diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/ColumnOperand.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/ColumnOperand.kt
new file mode 100644
index 0000000..430d592
--- /dev/null
+++ b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/ColumnOperand.kt
@@ -0,0 +1,18 @@
+package moe.nea.ledger.database.sql
+
+import moe.nea.ledger.database.Column
+import moe.nea.ledger.database.DBType
+import java.sql.PreparedStatement
+
+data class ColumnOperand<T, Raw>(val column: Column<T, Raw>) : TypedOperand<T, Raw> {
+ override val dbType: DBType<T, Raw>
+ get() = column.type
+
+ override fun asSql(): String {
+ return column.qualifiedSqlName
+ }
+
+ override fun appendToStatement(stmt: PreparedStatement, startIndex: Int): Int {
+ return startIndex
+ }
+} \ No newline at end of file
diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/EqualsClause.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/EqualsClause.kt
new file mode 100644
index 0000000..cfe72ef
--- /dev/null
+++ b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/EqualsClause.kt
@@ -0,0 +1,16 @@
+package moe.nea.ledger.database.sql
+
+import java.sql.PreparedStatement
+
+data class EqualsClause(val left: Operand<*, *>, val right: Operand<*, *>) : Clause {
+ override fun asSql(): String {
+ return left.asSql() + " = " + right.asSql()
+ }
+
+ override fun appendToStatement(stmt: PreparedStatement, startIndex: Int): Int {
+ var index = startIndex
+ index = left.appendToStatement(stmt, index)
+ index = right.appendToStatement(stmt, index)
+ return index
+ }
+} \ No newline at end of file
diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/IntoSelectable.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/IntoSelectable.kt
new file mode 100644
index 0000000..3775387
--- /dev/null
+++ b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/IntoSelectable.kt
@@ -0,0 +1,5 @@
+package moe.nea.ledger.database.sql
+
+interface IntoSelectable<T> {
+ fun asSelectable(): Selectable<T, *>
+} \ No newline at end of file
diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/Join.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/Join.kt
new file mode 100644
index 0000000..621aa05
--- /dev/null
+++ b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/Join.kt
@@ -0,0 +1,19 @@
+package moe.nea.ledger.database.sql
+
+import moe.nea.ledger.database.Table
+import java.sql.PreparedStatement
+
+data class Join(
+ val table: Table,
+//TODO: aliased columns val tableAlias: String,
+ val filter: Clause,
+) : SQLQueryComponent {
+ // JOIN ItemEntry on LogEntry.transactionId = ItemEntry.transactionId
+ override fun asSql(): String {
+ return "JOIN ${table.sqlName} ON ${filter.asSql()}"
+ }
+
+ override fun appendToStatement(stmt: PreparedStatement, startIndex: Int): Int {
+ return filter.appendToStatement(stmt, startIndex)
+ }
+} \ No newline at end of file
diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/LessThanEqualsExpression.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/LessThanEqualsExpression.kt
new file mode 100644
index 0000000..4820c97
--- /dev/null
+++ b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/LessThanEqualsExpression.kt
@@ -0,0 +1,15 @@
+package moe.nea.ledger.database.sql
+
+import java.sql.PreparedStatement
+
+class LessThanEqualsExpression(val lhs: Operand<*, *>, val rhs: Operand<*, *>) :
+ BooleanExpression {
+ override fun asSql(): String {
+ return "${lhs.asSql()} <= ${rhs.asSql()}"
+ }
+
+ override fun appendToStatement(stmt: PreparedStatement, startIndex: Int): Int {
+ val next = lhs.appendToStatement(stmt, startIndex)
+ return rhs.appendToStatement(stmt, next)
+ }
+}
diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/LessThanExpression.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/LessThanExpression.kt
new file mode 100644
index 0000000..4609ac1
--- /dev/null
+++ b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/LessThanExpression.kt
@@ -0,0 +1,15 @@
+package moe.nea.ledger.database.sql
+
+import java.sql.PreparedStatement
+
+class LessThanExpression(val lhs: Operand<*, *>, val rhs: Operand<*, *>) :
+ BooleanExpression {
+ override fun asSql(): String {
+ return "${lhs.asSql()} < ${rhs.asSql()}"
+ }
+
+ override fun appendToStatement(stmt: PreparedStatement, startIndex: Int): Int {
+ val next = lhs.appendToStatement(stmt, startIndex)
+ return rhs.appendToStatement(stmt, next)
+ }
+}
diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/LikeClause.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/LikeClause.kt
new file mode 100644
index 0000000..1122329
--- /dev/null
+++ b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/LikeClause.kt
@@ -0,0 +1,17 @@
+package moe.nea.ledger.database.sql
+
+import java.sql.PreparedStatement
+
+data class LikeClause<T>(val left: Operand<T, String>, val right: StringOperand) : Clause {
+ //TODO: check type safety with this one
+ override fun asSql(): String {
+ return "(" + left.asSql() + " LIKE " + right.asSql() + ")"
+ }
+
+ override fun appendToStatement(stmt: PreparedStatement, startIndex: Int): Int {
+ var index = startIndex
+ index = left.appendToStatement(stmt, index)
+ index = right.appendToStatement(stmt, index)
+ return index
+ }
+} \ No newline at end of file
diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/ListClause.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/ListClause.kt
new file mode 100644
index 0000000..d240472
--- /dev/null
+++ b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/ListClause.kt
@@ -0,0 +1,8 @@
+package moe.nea.ledger.database.sql
+
+class ListClause<R>(
+ val lhs: Operand<*, R>,
+ val list: ListExpression<*, R>,
+) : Clause, SQLQueryComponent by SQLQueryComponent.composite(
+ lhs, SQLQueryComponent.standalone("IN"), list
+) \ No newline at end of file
diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/ListExpression.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/ListExpression.kt
new file mode 100644
index 0000000..e1522d0
--- /dev/null
+++ b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/ListExpression.kt
@@ -0,0 +1,22 @@
+package moe.nea.ledger.database.sql
+
+import moe.nea.ledger.database.DBType
+import java.sql.PreparedStatement
+
+data class ListExpression<T, R>(
+ val elements: List<T>,
+ val dbType: DBType<T, R>
+) : Operand<List<T>, List<R>> {
+ override fun asSql(): String {
+ return elements.joinToString(prefix = "(", postfix = ")") { "?" }
+ }
+
+ override fun appendToStatement(stmt: PreparedStatement, startIndex: Int): Int {
+ var index = startIndex
+ for (element in elements) {
+ dbType.set(stmt, index, element)
+ index++
+ }
+ return index
+ }
+}
diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/ORExpression.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/ORExpression.kt
new file mode 100644
index 0000000..637963d
--- /dev/null
+++ b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/ORExpression.kt
@@ -0,0 +1,23 @@
+package moe.nea.ledger.database.sql
+
+import java.sql.PreparedStatement
+
+data class ORExpression(
+ val elements: List<BooleanExpression>
+) : BooleanExpression {
+ init {
+ require(elements.isNotEmpty())
+ }
+
+ override fun asSql(): String {
+ return (elements + SQLQueryComponent.standalone("FALSE")).joinToString(" OR ", "(", ")") { it.asSql() }
+ }
+
+ override fun appendToStatement(stmt: PreparedStatement, startIndex: Int): Int {
+ var index = startIndex
+ for (element in elements) {
+ index = element.appendToStatement(stmt, index)
+ }
+ return index
+ }
+} \ No newline at end of file
diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/Operand.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/Operand.kt
new file mode 100644
index 0000000..b085103
--- /dev/null
+++ b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/Operand.kt
@@ -0,0 +1,10 @@
+package moe.nea.ledger.database.sql
+
+interface Operand<T,
+ /**
+ * The db sided type (or a rough equivalence).
+ * @see moe.nea.ledger.database.DBType Raw type parameter
+ */
+ Raw> : SQLQueryComponent {
+
+} \ No newline at end of file
diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/SQLQueryComponent.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/SQLQueryComponent.kt
new file mode 100644
index 0000000..77d63d3
--- /dev/null
+++ b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/SQLQueryComponent.kt
@@ -0,0 +1,45 @@
+package moe.nea.ledger.database.sql
+
+import java.sql.PreparedStatement
+
+interface SQLQueryComponent {
+ fun asSql(): String
+
+ /**
+ * @return the next writable index (should equal to the amount of `?` in [asSql] + [startIndex])
+ */
+ fun appendToStatement(stmt: PreparedStatement, startIndex: Int): Int
+
+ companion object {
+ fun composite(vararg elements: SQLQueryComponent): SQLQueryComponent {
+ return object : SQLQueryComponent {
+ override fun asSql(): String {
+ return elements.joinToString(" ") { it.asSql() }
+ }
+
+ override fun appendToStatement(stmt: PreparedStatement, startIndex: Int): Int {
+ var index = startIndex
+ for (element in elements) {
+ val lastIndex = index
+ index = element.appendToStatement(stmt, index)
+ require(lastIndex <= index) { "$element just tried to go back in time $index < $lastIndex" }
+ }
+ return index
+
+ }
+ }
+ }
+
+ fun standalone(sql: String): SQLQueryComponent {
+ return object : SQLQueryComponent {
+ override fun asSql(): String {
+ return sql
+ }
+
+ override fun appendToStatement(stmt: PreparedStatement, startIndex: Int): Int {
+ return startIndex
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/SQLQueryGenerator.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/SQLQueryGenerator.kt
new file mode 100644
index 0000000..2eb54fd
--- /dev/null
+++ b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/SQLQueryGenerator.kt
@@ -0,0 +1,25 @@
+package moe.nea.ledger.database.sql
+
+import moe.nea.ledger.database.prepareAndLog
+import java.sql.Connection
+import java.sql.PreparedStatement
+
+object SQLQueryGenerator {
+ fun List<SQLQueryComponent>.concatToFilledPreparedStatement(connection: Connection): PreparedStatement {
+ val query = StringBuilder()
+ for (element in this) {
+ if (query.isNotEmpty()) {
+ query.append(" ")
+ }
+ query.append(element.asSql())
+ }
+ val statement = connection.prepareAndLog(query.toString())
+ var index = 1
+ for (element in this) {
+ val nextIndex = element.appendToStatement(statement, index)
+ if (nextIndex < index) error("$element went back in time")
+ index = nextIndex
+ }
+ return statement
+ }
+}
diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/Selectable.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/Selectable.kt
new file mode 100644
index 0000000..8241a9d
--- /dev/null
+++ b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/Selectable.kt
@@ -0,0 +1,17 @@
+package moe.nea.ledger.database.sql
+
+import moe.nea.ledger.database.Column
+import moe.nea.ledger.database.DBType
+
+/**
+ * Something that can be selected. Like a column, or an expression thereof
+ */
+interface Selectable<T, Raw> : SQLQueryComponent, IntoSelectable<T> {
+ override fun asSelectable(): Selectable<T, Raw> {
+ return this
+ }
+
+ val dbType: DBType<T, Raw>
+ fun guessColumn(): Column<T, *>?
+}
+
diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/StringOperand.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/StringOperand.kt
new file mode 100644
index 0000000..b8d3690
--- /dev/null
+++ b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/StringOperand.kt
@@ -0,0 +1,17 @@
+package moe.nea.ledger.database.sql
+
+import java.sql.PreparedStatement
+
+/**
+ * As opposed to just any [Operand<*, String>][Operand], this string operand represents a string operand that is part of the query, as opposed to potentially the state of a column.
+ */
+data class StringOperand(val value: String) : Operand<String, String> {
+ override fun asSql(): String {
+ return "?"
+ }
+
+ override fun appendToStatement(stmt: PreparedStatement, startIndex: Int): Int {
+ stmt.setString(startIndex, value)
+ return 1 + startIndex
+ }
+} \ No newline at end of file
diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/TypedOperand.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/TypedOperand.kt
new file mode 100644
index 0000000..8a1f723
--- /dev/null
+++ b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/TypedOperand.kt
@@ -0,0 +1,7 @@
+package moe.nea.ledger.database.sql
+
+import moe.nea.ledger.database.DBType
+
+interface TypedOperand<T, Raw> : Operand<T, Raw> {
+ val dbType: DBType<T, Raw>
+}
diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/ValuedOperand.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/ValuedOperand.kt
new file mode 100644
index 0000000..714b4b5
--- /dev/null
+++ b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/ValuedOperand.kt
@@ -0,0 +1,15 @@
+package moe.nea.ledger.database.sql
+
+import moe.nea.ledger.database.DBType
+import java.sql.PreparedStatement
+
+class ValuedOperand<T, R>(val dbType: DBType<T, R>, val value: T) : Operand<T, R> {
+ override fun asSql(): String {
+ return "?"
+ }
+
+ override fun appendToStatement(stmt: PreparedStatement, startIndex: Int): Int {
+ dbType.set(stmt, startIndex, value)
+ return startIndex + 1
+ }
+}
diff --git a/database/impl/build.gradle.kts b/database/impl/build.gradle.kts
new file mode 100644
index 0000000..17a7a5a
--- /dev/null
+++ b/database/impl/build.gradle.kts
@@ -0,0 +1,12 @@
+plugins {
+ `java-library`
+ kotlin("jvm")
+}
+
+dependencies {
+ api(project(":database:core"))
+}
+
+java {
+ toolchain.languageVersion.set(JavaLanguageVersion.of(8))
+}
diff --git a/src/main/kotlin/moe/nea/ledger/database/DBLogEntry.kt b/database/impl/src/main/kotlin/moe/nea/ledger/database/DBLogEntry.kt
index 77ac215..e2530cc 100644
--- a/src/main/kotlin/moe/nea/ledger/database/DBLogEntry.kt
+++ b/database/impl/src/main/kotlin/moe/nea/ledger/database/DBLogEntry.kt
@@ -3,6 +3,11 @@ package moe.nea.ledger.database
import moe.nea.ledger.ItemChange
import moe.nea.ledger.ItemId
import moe.nea.ledger.TransactionType
+import moe.nea.ledger.database.columns.DBDouble
+import moe.nea.ledger.database.columns.DBEnum
+import moe.nea.ledger.database.columns.DBString
+import moe.nea.ledger.database.columns.DBUlid
+import moe.nea.ledger.database.columns.DBUuid
object DBLogEntry : Table("LogEntry") {
val transactionId = column("transactionId", DBUlid)
@@ -16,4 +21,12 @@ object DBItemEntry : Table("ItemEntry") {
val mode = column("mode", DBEnum<ItemChange.ChangeDirection>())
val itemId = column("item", DBString.mapped(ItemId::string, ::ItemId))
val size = column("size", DBDouble)
+
+ fun objMap(result: ResultRow): ItemChange {
+ return ItemChange(
+ result[itemId],
+ result[size],
+ result[mode],
+ )
+ }
}
diff --git a/src/main/kotlin/moe/nea/ledger/database/DBUpgrade.kt b/database/impl/src/main/kotlin/moe/nea/ledger/database/DBUpgrade.kt
index 7d1782a..9739978 100644
--- a/src/main/kotlin/moe/nea/ledger/database/DBUpgrade.kt
+++ b/database/impl/src/main/kotlin/moe/nea/ledger/database/DBUpgrade.kt
@@ -37,14 +37,14 @@ interface DBUpgrade {
return upgrades.groupBy { it.toVersion }
}
- fun createTable(to: Long, table: Table, vararg columns: Column<*>): DBUpgrade {
+ fun createTable(to: Long, table: Table, vararg columns: Column<*, *>): DBUpgrade {
require(columns.all { it in table.columns })
return of("Create table ${table}", to) {
table.createIfNotExists(it, columns.toList())
}
}
- fun addColumns(to: Long, table: Table, vararg columns: Column<*>): DBUpgrade {
+ fun addColumns(to: Long, table: Table, vararg columns: Column<*, *>): DBUpgrade {
return of("Add columns to table $table", to) {
table.alterTableAddColumns(it, columns.toList())
}
diff --git a/src/main/kotlin/moe/nea/ledger/database/Database.kt b/database/impl/src/main/kotlin/moe/nea/ledger/database/Database.kt
index a77ea30..e4b34c8 100644
--- a/src/main/kotlin/moe/nea/ledger/database/Database.kt
+++ b/database/impl/src/main/kotlin/moe/nea/ledger/database/Database.kt
@@ -1,10 +1,11 @@
package moe.nea.ledger.database
-import moe.nea.ledger.Ledger
+import moe.nea.ledger.database.columns.DBString
+import java.io.File
import java.sql.Connection
import java.sql.DriverManager
-class Database {
+class Database(val dataFolder: File) {
lateinit var connection: Connection
object MetaTable : Table("LedgerMeta") {
@@ -33,7 +34,7 @@ class Database {
val databaseVersion: Long = 1
fun loadAndUpgrade() {
- connection = DriverManager.getConnection("jdbc:sqlite:${Ledger.dataFolder.resolve("database.db")}")
+ connection = DriverManager.getConnection("jdbc:sqlite:${dataFolder.resolve("database.db")}")
MetaTable.createIfNotExists(connection)
val meta = MetaTable.selectAll(connection).associate { MetaKey(it[MetaTable.key]) to it[MetaTable.value] }
val lastLaunch = meta[MetaKey.LAST_LAUNCH]?.toLong() ?: 0L
@@ -42,6 +43,8 @@ class Database {
val oldVersion = meta[MetaKey.DATABASE_VERSION]?.toLong() ?: -1
println("Old Database Version: $oldVersion; Current version: $databaseVersion")
+ if (oldVersion > databaseVersion)
+ error("Outdated software. Database is newer than me!")
// TODO: create a backup if there is a db version upgrade happening
DBUpgrade.performUpgradeChain(
connection, oldVersion, databaseVersion,
diff --git a/src/main/kotlin/moe/nea/ledger/database/Upgrades.kt b/database/impl/src/main/kotlin/moe/nea/ledger/database/Upgrades.kt
index e83abe7..76dfb5d 100644
--- a/src/main/kotlin/moe/nea/ledger/database/Upgrades.kt
+++ b/database/impl/src/main/kotlin/moe/nea/ledger/database/Upgrades.kt
@@ -15,6 +15,4 @@ class Upgrades {
DBItemEntry.itemId, DBItemEntry.size, DBItemEntry.mode, DBItemEntry.transactionId
))
}
-
-
} \ No newline at end of file
diff --git a/src/main/kotlin/moe/nea/ledger/database/schema.dot b/database/impl/src/main/kotlin/moe/nea/ledger/database/schema.dot
index d932f6a..d932f6a 100644
--- a/src/main/kotlin/moe/nea/ledger/database/schema.dot
+++ b/database/impl/src/main/kotlin/moe/nea/ledger/database/schema.dot
diff --git a/dependency-injection/build.gradle.kts b/dependency-injection/build.gradle.kts
new file mode 100644
index 0000000..5a51941
--- /dev/null
+++ b/dependency-injection/build.gradle.kts
@@ -0,0 +1,8 @@
+plugins {
+ `java-library`
+ kotlin("jvm")
+}
+
+java {
+ toolchain.languageVersion.set(JavaLanguageVersion.of(8))
+}
diff --git a/src/main/kotlin/moe/nea/ledger/utils/di/DI.kt b/dependency-injection/src/main/kotlin/moe/nea/ledger/utils/di/DI.kt
index a9061d7..0683063 100644
--- a/src/main/kotlin/moe/nea/ledger/utils/di/DI.kt
+++ b/dependency-injection/src/main/kotlin/moe/nea/ledger/utils/di/DI.kt
@@ -47,6 +47,11 @@ class DI {
providers[type] = provider
}
+ fun <I : Any, T : I> registerInjectableInterface(parent: Class<I>, type: Class<T>) {
+ internalRegisterInjectableClass(type)
+ register(parent, DIProvider.fromInheritance(type))
+ }
+
fun registerInjectableClasses(vararg type: Class<*>) {
type.forEach { internalRegisterInjectableClass(it) }
}
diff --git a/src/main/kotlin/moe/nea/ledger/utils/di/DIProvider.kt b/dependency-injection/src/main/kotlin/moe/nea/ledger/utils/di/DIProvider.kt
index bd5b9ef..8a54d5f 100644
--- a/src/main/kotlin/moe/nea/ledger/utils/di/DIProvider.kt
+++ b/dependency-injection/src/main/kotlin/moe/nea/ledger/utils/di/DIProvider.kt
@@ -18,7 +18,7 @@ fun interface DIProvider<T : Any> : BaseDIProvider<T, Unit> {
companion object {
- fun <T : Any> fromInjectableClass(clazz: Class<T>): DIProvider<T> {
+ fun <T : Any> fromInjectableClass(clazz: Class<out T>): DIProvider<T> {
@Suppress("UNCHECKED_CAST")
val cons = (clazz.constructors.find { it.getAnnotation(Inject::class.java) != null }
?: clazz.constructors.find { it.parameterCount == 0 }
@@ -41,6 +41,10 @@ fun interface DIProvider<T : Any> : BaseDIProvider<T, Unit> {
fun <T : Any> singeleton(value: T): DIProvider<T> {
return DIProvider { _ -> value }
}
+
+ fun <I : Any> fromInheritance(type: Class<out I>): DIProvider<I> {
+ return DIProvider { it.provide(type) }
+ }
}
}
diff --git a/src/main/kotlin/moe/nea/ledger/utils/di/Inject.kt b/dependency-injection/src/main/kotlin/moe/nea/ledger/utils/di/Inject.kt
index a8fdd87..a8fdd87 100644
--- a/src/main/kotlin/moe/nea/ledger/utils/di/Inject.kt
+++ b/dependency-injection/src/main/kotlin/moe/nea/ledger/utils/di/Inject.kt
diff --git a/gradle.properties b/gradle.properties
index 38adc2e..6e2027c 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,6 +1,3 @@
-loom.platform=forge
-org.gradle.jvmargs=-Xmx2g
+org.gradle.jvmargs = -Xmx2g
baseGroup = moe.nea.ledger
-mcVersion = 1.8.9
-modid = moneyledger
mod_version = 2.0.0
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index a80b22c..b82aa23 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
diff --git a/ledger-rules.pro b/ledger-rules.pro
deleted file mode 100644
index 2d8459e..0000000
--- a/ledger-rules.pro
+++ /dev/null
@@ -1,3 +0,0 @@
--keep class !moe.nea.ledger.gen.** {*;}
--dontobfuscate
--assumenosideeffects class moe.nea.ledger.ItemId { *; } \ No newline at end of file
diff --git a/log4j2.xml b/log4j2.xml
deleted file mode 100644
index af9b1b7..0000000
--- a/log4j2.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<Configuration status="WARN">
- <!-- Filter out Hypixel scoreboard and sound errors -->
- <RegexFilter regex="Error executing task.*|Unable to play unknown soundEvent.*" onMatch="DENY" onMismatch="NEUTRAL"/>
-</Configuration> \ No newline at end of file
diff --git a/mod/build.gradle.kts b/mod/build.gradle.kts
new file mode 100644
index 0000000..0ad4aa6
--- /dev/null
+++ b/mod/build.gradle.kts
@@ -0,0 +1,175 @@
+import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
+import proguard.gradle.ProGuardTask
+
+plugins {
+ idea
+ java
+ id("gg.essential.loom") version "1.6.+"
+ id("dev.architectury.architectury-pack200") version "0.1.3"
+ id("com.github.johnrengelman.shadow") version "8.1.1"
+ id("com.github.gmazzo.buildconfig")
+ kotlin("jvm")
+ id("ledger-repo")
+}
+val baseGroup: String by project
+val mcVersion: String by project
+val mixinGroup = "$baseGroup.mixin"
+val modid: String by project
+
+// Toolchains:
+java {
+ toolchain.languageVersion.set(JavaLanguageVersion.of(8))
+}
+
+// Minecraft configuration:
+loom {
+ forge {
+ pack200Provider.set(dev.architectury.pack200.java.Pack200Adapter())
+ mixinConfig("mixins.$modid.json")
+ }
+ log4jConfigs.from(file("log4j2.xml"))
+ runConfigs {
+ "client" {
+ isIdeConfigGenerated = true
+ property("ledger.bonusresourcemod", sourceSets.main.get().output.resourcesDir!!.absolutePath)
+ property("mixin.debug", "true")
+ programArgs("--tweakClass", "org.spongepowered.asm.launch.MixinTweaker")
+ programArgs("--tweakClass", "io.github.notenoughupdates.moulconfig.tweaker.DevelopmentResourceTweaker")
+ }
+ remove(getByName("server"))
+ }
+ mixin.useLegacyMixinAp.set(false)
+}
+
+// TODO: Add an extra shadow configuration for optimizable jars
+//val optShadowImpl: Configuration by configurations.creating {
+//
+//}
+
+val shadowImpl: Configuration by configurations.creating {
+ configurations.implementation.get().extendsFrom(this)
+}
+
+dependencies {
+ minecraft("com.mojang:minecraft:1.8.9")
+ mappings("de.oceanlabs.mcp:mcp_stable:22-1.8.9")
+ forge("net.minecraftforge:forge:1.8.9-11.15.1.2318-1.8.9")
+
+ shadowImpl(kotlin("stdlib-jdk8"))
+ implementation("org.jspecify:jspecify:1.0.0")
+
+ shadowImpl("org.spongepowered:mixin:0.7.11-SNAPSHOT") {
+ isTransitive = false
+ }
+
+ shadowImpl("org.xerial:sqlite-jdbc:3.45.3.0")
+ shadowImpl("org.notenoughupdates.moulconfig:legacy:3.0.0-beta.9")
+ shadowImpl("io.azam.ulidj:ulidj:1.0.4")
+ shadowImpl(project(":dependency-injection"))
+ shadowImpl(project(":database:impl"))
+ shadowImpl("moe.nea:libautoupdate:1.3.1") {
+ exclude(module = "gson")
+ }
+ runtimeOnly("me.djtheredstoner:DevAuth-forge-legacy:1.2.1")
+ testImplementation("org.junit.jupiter:junit-jupiter:5.9.2")
+}
+
+// Tasks:
+
+// Delete default shadow configuration
+tasks.shadowJar {
+ doFirst { error("Incorrect shadow JAR built!") }
+}
+
+tasks.downloadRepo {
+ hash.set("dcf1dbc")
+}
+
+val generateItemIds by tasks.register("generateItemIds", GenerateItemIds::class) {
+ repoHash.set(tasks.downloadRepo.get().hash)
+ packageName.set("moe.nea.ledger.gen")
+ outputDirectory.set(layout.buildDirectory.dir("generated/sources/itemIds"))
+ repoFiles.set(tasks.downloadRepo.get().outputDirectory)
+}
+sourceSets.main {
+ java.srcDir(generateItemIds)
+}
+tasks.withType<AbstractArchiveTask> {
+ archiveBaseName.set(modid)
+}
+tasks.withType<Jar> {
+ manifest.attributes.run {
+ this["FMLCorePluginContainsFMLMod"] = "true"
+ this["ForceLoadAsMod"] = "true"
+ this["TweakClass"] = "org.spongepowered.asm.launch.MixinTweaker"
+ this["MixinConfigs"] = "mixins.$modid.json"
+ }
+}
+
+tasks.processResources {
+ inputs.property("version", project.version)
+ inputs.property("mcversion", mcVersion)
+ inputs.property("modid", modid)
+ inputs.property("basePackage", baseGroup)
+
+ filesMatching(listOf("mcmod.info", "mixins.$modid.json")) {
+ expand(inputs.properties)
+ }
+}
+
+
+val proguardOutJar = project.layout.buildDirectory.file("badjars/stripped.jar")
+val proguard = tasks.register("proguard", ProGuardTask::class) {
+ dependsOn(tasks.jar)
+ injars(tasks.jar.map { it.archiveFile })
+ outjars(proguardOutJar)
+ configuration(file("ledger-rules.pro"))
+ val libJava = javaToolchains.launcherFor(java.toolchain)
+ .get()
+ .metadata.installationPath.file("jre/lib/rt.jar")
+ libraryjars(libJava)
+ libraryjars(configurations.compileClasspath)
+}
+
+val shadowJar2 = tasks.register("shadowJar2", ShadowJar::class) {
+ destinationDirectory.set(layout.buildDirectory.dir("badjars"))
+ archiveClassifier.set("all-dev")
+ from(proguardOutJar)
+ dependsOn(proguard)
+ configurations = listOf(shadowImpl)
+ relocate("moe.nea.libautoupdate", "moe.nea.ledger.deps.libautoupdate")
+ relocate("io.github.notenoughupdates.moulconfig", "moe.nea.ledger.deps.moulconfig")
+ relocate("io.azam.ulidj", "moe.nea.ledger.deps.ulid")
+ mergeServiceFiles()
+ exclude(
+ // Signatures
+ "META-INF/INDEX.LIST",
+ "META-INF/*.SF",
+ "META-INF/*.DSA",
+ "META-INF/*.RSA",
+ "module-info.class",
+
+ "META-INF/*.kotlin_module",
+ "META-INF/versions/**"
+ )
+}
+tasks.remapJar {
+ archiveClassifier.set("")
+ inputFile.set(shadowJar2.flatMap { it.archiveFile })
+}
+
+tasks.jar {
+ archiveClassifier.set("without-deps")
+ destinationDirectory.set(layout.buildDirectory.dir("badjars"))
+}
+
+tasks.runClient {
+ javaLauncher.set(javaToolchains.launcherFor(java.toolchain))
+}
+
+tasks.assemble.get().dependsOn(tasks.remapJar)
+
+buildConfig {
+ packageName("moe.nea.ledger.gen")
+}
+
diff --git a/mod/gradle.properties b/mod/gradle.properties
new file mode 100644
index 0000000..28f0604
--- /dev/null
+++ b/mod/gradle.properties
@@ -0,0 +1,3 @@
+loom.platform = forge
+mcVersion = 1.8.9
+modid = moneyledger
diff --git a/mod/ledger-rules.pro b/mod/ledger-rules.pro
new file mode 100644
index 0000000..faa10c2
--- /dev/null
+++ b/mod/ledger-rules.pro
@@ -0,0 +1,4 @@
+-keep class !moe.nea.ledger.gen.** {*;}
+-dontobfuscate
+-assumenosideeffects class ** { @moe.nea.ledger.utils.RemoveInRelease <methods>; }
+#-dontoptimize
diff --git a/mod/log4j2.xml b/mod/log4j2.xml
new file mode 100644
index 0000000..ff7a816
--- /dev/null
+++ b/mod/log4j2.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Configuration status="WARN">
+ <!-- Filter out Hypixel scoreboard and sound errors -->
+ <RegexFilter regex="Unable to play unknown soundEvent.*" onMatch="DENY" onMismatch="NEUTRAL"/>
+ <RegexFilter regex="Zip file .* failed to read properly, it will be ignored.*" onMatch="DENY" onMismatch="NEUTRAL"/>
+ <RegexFilter regex="There was a problem reading the entry META-INF/versions/9/.* in the jar .* - probably a corrupt zip" onMatch="DENY" onMismatch="NEUTRAL"/>
+ <RegexFilter regex="Unable to read a class file correctly" onMatch="DENY" onMismatch="NEUTRAL"/>
+ <RegexFilter regex="Error executing task" onMatch="DENY" onMismatch="NEUTRAL"/>
+</Configuration> \ No newline at end of file
diff --git a/mod/src/main/java/moe/nea/ledger/init/AutoDiscoveryMixinPlugin.java b/mod/src/main/java/moe/nea/ledger/init/AutoDiscoveryMixinPlugin.java
new file mode 100644
index 0000000..64fa6c2
--- /dev/null
+++ b/mod/src/main/java/moe/nea/ledger/init/AutoDiscoveryMixinPlugin.java
@@ -0,0 +1,195 @@
+package moe.nea.ledger.init;
+
+import net.minecraft.launchwrapper.Launch;
+import org.spongepowered.asm.lib.tree.ClassNode;
+import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin;
+import org.spongepowered.asm.mixin.extensibility.IMixinInfo;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Stream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+/**
+ * A mixin plugin to automatically discover all mixins in the current JAR.
+ * <p>
+ * This mixin plugin automatically scans your entire JAR (or class directory, in case of an in-IDE launch) for classes inside of your
+ * mixin package and registers those. It does this recursively for sub packages of the mixin package as well. This means you will need
+ * to only have mixin classes inside of your mixin package, which is good style anyway.
+ *
+ * @author Linnea Gräf
+ */
+public class AutoDiscoveryMixinPlugin implements IMixinConfigPlugin {
+ private static final List<AutoDiscoveryMixinPlugin> mixinPlugins = new ArrayList<>();
+
+ public static List<AutoDiscoveryMixinPlugin> getMixinPlugins() {
+ return mixinPlugins;
+ }
+
+ private String mixinPackage;
+
+ @Override
+ public void onLoad(String mixinPackage) {
+ this.mixinPackage = mixinPackage;
+ mixinPlugins.add(this);
+ }
+
+ /**
+ * Resolves the base class root for a given class URL. This resolves either the JAR root, or the class file root.
+ * In either case the return value of this + the class name will resolve back to the original class url, or to other
+ * class urls for other classes.
+ */
+ public URL getBaseUrlForClassUrl(URL classUrl) {
+ String string = classUrl.toString();
+ if (classUrl.getProtocol().equals("jar")) {
+ try {
+ return new URL(string.substring(4).split("!")[0]);
+ } catch (MalformedURLException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ if (string.endsWith(".class")) {
+ try {
+ return new URL(string.replace("\\", "/")
+ .replace(getClass().getCanonicalName()
+ .replace(".", "/") + ".class", ""));
+ } catch (MalformedURLException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ return classUrl;
+ }
+
+ /**
+ * Get the package that contains all the mixins. This value is set by mixin itself using {@link #onLoad}.
+ */
+ public String getMixinPackage() {
+ return mixinPackage;
+ }
+
+ /**
+ * Get the path inside the class root to the mixin package
+ */
+ public String getMixinBaseDir() {
+ return mixinPackage.replace(".", "/");
+ }
+
+ /**
+ * A list of all discovered mixins.
+ */
+ private List<String> mixins = null;
+
+ /**
+ * Try to add mixin class ot the mixins based on the filepath inside of the class root.
+ * Removes the {@code .class} file suffix, as well as the base mixin package.
+ * <p><b>This method cannot be called after mixin initialization.</p>
+ *
+ * @param className the name or path of a class to be registered as a mixin.
+ */
+ public void tryAddMixinClass(String className) {
+ if (!className.endsWith(".class")) return;
+ if (className.indexOf('$') >= 0) return;
+ String norm = (className.endsWith(".class") ? className.substring(0, className.length() - ".class".length()) : className)
+ .replace("\\", "/")
+ .replace("/", ".");
+ if (norm.startsWith(getMixinPackage() + ".") && !norm.endsWith(".")) {
+ mixins.add(norm.substring(getMixinPackage().length() + 1));
+ }
+ }
+
+ /**
+ * Search through the JAR or class directory to find mixins contained in {@link #getMixinPackage()}
+ */
+ @Override
+ public List<String> getMixins() {
+ if (mixins != null) return mixins;
+ System.out.println("Trying to discover mixins");
+ mixins = new ArrayList<>();
+ URL classUrl = getClass().getProtectionDomain().getCodeSource().getLocation();
+ System.out.println("Found classes at " + classUrl);
+ Path file;
+ try {
+ file = Paths.get(getBaseUrlForClassUrl(classUrl).toURI());
+ } catch (URISyntaxException e) {
+ throw new RuntimeException(e);
+ }
+ System.out.println("Base directory found at " + file);
+ if (Files.isDirectory(file)) {
+ walkDir(file);
+ } else {
+ walkJar(file);
+ }
+ System.out.println("Found mixins: " + mixins);
+
+ if (!(Boolean) Launch.blackboard.get("fml.deobfuscatedEnvironment")) {
+ mixins.removeIf(it -> it.contains("devenv"));
+ }
+
+ return mixins;
+ }
+
+ /**
+ * Search through directory for mixin classes based on {@link #getMixinBaseDir}.
+ *
+ * @param classRoot The root directory in which classes are stored for the default package.
+ */
+ private void walkDir(Path classRoot) {
+ System.out.println("Trying to find mixins from directory");
+ try (Stream<Path> classes = Files.walk(classRoot.resolve(getMixinBaseDir()))) {
+ classes.map(it -> classRoot.relativize(it).toString())
+ .forEach(this::tryAddMixinClass);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Read through a JAR file, trying to find all mixins inside.
+ */
+ private void walkJar(Path file) {
+ System.out.println("Trying to find mixins from jar file");
+ try (ZipInputStream zis = new ZipInputStream(Files.newInputStream(file))) {
+ ZipEntry next;
+ while ((next = zis.getNextEntry()) != null) {
+ tryAddMixinClass(next.getName());
+ zis.closeEntry();
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {
+
+ }
+
+ @Override
+ public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {
+
+ }
+
+ @Override
+ public String getRefMapperConfig() {
+ return null;
+ }
+
+ @Override
+ public boolean shouldApplyMixin(String targetClassName, String mixinClassName) {
+ return true;
+ }
+
+ @Override
+ public void acceptTargets(Set<String> myTargets, Set<String> otherTargets) {
+
+ }
+}
diff --git a/mod/src/main/java/moe/nea/ledger/mixin/AccessorContainerDispenser.java b/mod/src/main/java/moe/nea/ledger/mixin/AccessorContainerDispenser.java
new file mode 100644
index 0000000..a3d32c4
--- /dev/null
+++ b/mod/src/main/java/moe/nea/ledger/mixin/AccessorContainerDispenser.java
@@ -0,0 +1,12 @@
+package moe.nea.ledger.mixin;
+
+import net.minecraft.inventory.ContainerDispenser;
+import net.minecraft.inventory.IInventory;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.gen.Accessor;
+
+@Mixin(ContainerDispenser.class)
+public interface AccessorContainerDispenser {
+ @Accessor("dispenserInventory")
+ IInventory getDispenserInventory_ledger();
+}
diff --git a/mod/src/main/java/moe/nea/ledger/mixin/AccessorContainerHopper.java b/mod/src/main/java/moe/nea/ledger/mixin/AccessorContainerHopper.java
new file mode 100644
index 0000000..ee29d4f
--- /dev/null
+++ b/mod/src/main/java/moe/nea/ledger/mixin/AccessorContainerHopper.java
@@ -0,0 +1,13 @@
+package moe.nea.ledger.mixin;
+
+import net.minecraft.inventory.ContainerDispenser;
+import net.minecraft.inventory.ContainerHopper;
+import net.minecraft.inventory.IInventory;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.gen.Accessor;
+
+@Mixin(ContainerHopper.class)
+public interface AccessorContainerHopper {
+ @Accessor("hopperInventory")
+ IInventory getHopperInventory_ledger();
+}
diff --git a/src/main/java/moe/nea/ledger/mixin/AccessorGuiEditSign.java b/mod/src/main/java/moe/nea/ledger/mixin/AccessorGuiEditSign.java
index 52b8911..52b8911 100644
--- a/src/main/java/moe/nea/ledger/mixin/AccessorGuiEditSign.java
+++ b/mod/src/main/java/moe/nea/ledger/mixin/AccessorGuiEditSign.java
diff --git a/src/main/java/moe/nea/ledger/mixin/MouseClickEventPatch.java b/mod/src/main/java/moe/nea/ledger/mixin/MouseClickEventPatch.java
index 4e6e360..4e6e360 100644
--- a/src/main/java/moe/nea/ledger/mixin/MouseClickEventPatch.java
+++ b/mod/src/main/java/moe/nea/ledger/mixin/MouseClickEventPatch.java
diff --git a/src/main/java/moe/nea/ledger/mixin/OnInitializationCompletePatch.java b/mod/src/main/java/moe/nea/ledger/mixin/OnInitializationCompletePatch.java
index fc9afb7..fc9afb7 100644
--- a/src/main/java/moe/nea/ledger/mixin/OnInitializationCompletePatch.java
+++ b/mod/src/main/java/moe/nea/ledger/mixin/OnInitializationCompletePatch.java
diff --git a/mod/src/main/java/moe/nea/ledger/mixin/devenv/RegisterModResourcesPatch.java b/mod/src/main/java/moe/nea/ledger/mixin/devenv/RegisterModResourcesPatch.java
new file mode 100644
index 0000000..88e8364
--- /dev/null
+++ b/mod/src/main/java/moe/nea/ledger/mixin/devenv/RegisterModResourcesPatch.java
@@ -0,0 +1,66 @@
+package moe.nea.ledger.mixin.devenv;
+
+import com.google.common.eventbus.EventBus;
+import net.minecraftforge.fml.client.FMLFileResourcePack;
+import net.minecraftforge.fml.common.DummyModContainer;
+import net.minecraftforge.fml.common.LoadController;
+import net.minecraftforge.fml.common.ModContainer;
+import net.minecraftforge.fml.common.ModMetadata;
+import net.minecraftforge.fml.common.discovery.ASMDataTable;
+import net.minecraftforge.fml.common.discovery.ContainerType;
+import net.minecraftforge.fml.common.discovery.ModCandidate;
+import net.minecraftforge.fml.common.discovery.ModDiscoverer;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.List;
+
+@Mixin(value = ModDiscoverer.class, remap = false)
+public class RegisterModResourcesPatch {
+ @Shadow
+ private List<ModCandidate> candidates;
+
+ @Inject(method = "identifyMods", at = @At("HEAD"), remap = false)
+ private void addCandidate(CallbackInfoReturnable<List<ModContainer>> cir) {
+ String bonusResourceMod = System.getProperty("ledger.bonusresourcemod");
+ if (bonusResourceMod == null) return;
+ File file = new File(bonusResourceMod);
+ if (!file.isDirectory()) return;
+ ModMetadata modMetadata = new ModMetadata();
+ modMetadata.modId = "ledger-bonus";
+ modMetadata.name = "Ledger Bonus Resources";
+ modMetadata.autogenerated = true;
+ ModContainer container = new DummyModContainer(modMetadata) {
+ @Override
+ public Object getMod() {
+ return new Object();
+ }
+
+ @Override
+ public boolean registerBus(EventBus bus, LoadController controller) {
+ return true;
+ }
+
+ @Override
+ public File getSource() {
+ return file;
+ }
+
+ @Override
+ public Class<?> getCustomResourcePackClass() {
+ return FMLFileResourcePack.class;
+ }
+ };
+ candidates.add(new ModCandidate(file, file, ContainerType.DIR) {
+ @Override
+ public List<ModContainer> explore(ASMDataTable table) {
+ return Collections.singletonList(container);
+ }
+ });
+ }
+}
diff --git a/src/main/kotlin/moe/nea/ledger/ConfigCommand.kt b/mod/src/main/kotlin/moe/nea/ledger/ConfigCommand.kt
index 5b964c8..5b964c8 100644
--- a/src/main/kotlin/moe/nea/ledger/ConfigCommand.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/ConfigCommand.kt
diff --git a/src/main/kotlin/moe/nea/ledger/DebouncedValue.kt b/mod/src/main/kotlin/moe/nea/ledger/DebouncedValue.kt
index 66fba8d..66fba8d 100644
--- a/src/main/kotlin/moe/nea/ledger/DebouncedValue.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/DebouncedValue.kt
diff --git a/src/main/kotlin/moe/nea/ledger/DebugDataCommand.kt b/mod/src/main/kotlin/moe/nea/ledger/DebugDataCommand.kt
index bab0a78..bab0a78 100644
--- a/src/main/kotlin/moe/nea/ledger/DebugDataCommand.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/DebugDataCommand.kt
diff --git a/src/main/kotlin/moe/nea/ledger/DevUtil.kt b/mod/src/main/kotlin/moe/nea/ledger/DevUtil.kt
index d0dd653..d0dd653 100644
--- a/src/main/kotlin/moe/nea/ledger/DevUtil.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/DevUtil.kt
diff --git a/src/main/kotlin/moe/nea/ledger/ExpiringValue.kt b/mod/src/main/kotlin/moe/nea/ledger/ExpiringValue.kt
index b50b14e..b50b14e 100644
--- a/src/main/kotlin/moe/nea/ledger/ExpiringValue.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/ExpiringValue.kt
diff --git a/src/main/kotlin/moe/nea/ledger/ItemIdProvider.kt b/mod/src/main/kotlin/moe/nea/ledger/ItemIdProvider.kt
index 4d85713..ff2c691 100644
--- a/src/main/kotlin/moe/nea/ledger/ItemIdProvider.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/ItemIdProvider.kt
@@ -4,6 +4,7 @@ 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
@@ -19,12 +20,12 @@ class ItemIdProvider {
@SubscribeEvent
fun onMouseInput(event: GuiScreenEvent.MouseInputEvent.Pre) {
if (Mouse.getEventButton() == -1) return
- MinecraftForge.EVENT_BUS.post(BeforeGuiAction(event.gui))
+ BeforeGuiAction(event.gui).post()
}
@SubscribeEvent
fun onKeyInput(event: GuiScreenEvent.KeyboardInputEvent.Pre) {
- MinecraftForge.EVENT_BUS.post(BeforeGuiAction(event.gui))
+ BeforeGuiAction(event.gui).post()
}
private val knownNames = mutableMapOf<String, ItemId>()
@@ -129,15 +130,15 @@ class ItemIdProvider {
}
etherialRewardPattern.useMatcher(properName) {
val id = when (val id = group("what")) {
- "Copper" -> ItemId.COPPER
- "Bits" -> ItemId.BITS
+ "Copper" -> ItemIds.SKYBLOCK_COPPER
+ "Bits" -> ItemIds.SKYBLOCK_BIT
"Garden Experience" -> ItemId.GARDEN
"Farming XP" -> ItemId.FARMING
- "Gold Essence" -> ItemId.GOLD_ESSENCE
+ "Gold Essence" -> ItemIds.ESSENCE_GOLD
"Gemstone Powder" -> ItemId.GEMSTONE_POWDER
"Mithril Powder" -> ItemId.MITHRIL_POWDER
- "Pelts" -> ItemId.PELT
- "Fine Flour" -> ItemId.FINE_FLOUR
+ "Pelts" -> ItemIds.SKYBLOCK_PELT
+ "Fine Flour" -> ItemIds.FINE_FLOUR
else -> {
id.ifDropLast(" Experience") {
ItemId.skill(generateName(it).string)
diff --git a/src/main/kotlin/moe/nea/ledger/ItemUtil.kt b/mod/src/main/kotlin/moe/nea/ledger/ItemUtil.kt
index a3d8162..a3d8162 100644
--- a/src/main/kotlin/moe/nea/ledger/ItemUtil.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/ItemUtil.kt
diff --git a/src/main/kotlin/moe/nea/ledger/Ledger.kt b/mod/src/main/kotlin/moe/nea/ledger/Ledger.kt
index 5682797..6d3c592 100644
--- a/src/main/kotlin/moe/nea/ledger/Ledger.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/Ledger.kt
@@ -1,6 +1,7 @@
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
@@ -11,28 +12,37 @@ 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.BankInterestDetection
+import moe.nea.ledger.modules.BasicReforgeDetection
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.CaducousFeederDetection
import moe.nea.ledger.modules.DragonEyePlacementDetection
-import moe.nea.ledger.modules.`DragonSacrificeDetection`
+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.GhostCoinDropDetection
import moe.nea.ledger.modules.GodPotionDetection
import moe.nea.ledger.modules.GodPotionMixinDetection
+import moe.nea.ledger.modules.GummyPolarBearDetection
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.PestRepellentDetection
import moe.nea.ledger.modules.UpdateChecker
import moe.nea.ledger.modules.VisitorDetection
+import moe.nea.ledger.telemetry.TelemetryProvider
import moe.nea.ledger.utils.ErrorUtil
import moe.nea.ledger.utils.MinecraftExecutor
import moe.nea.ledger.utils.di.DI
@@ -85,7 +95,7 @@ class Ledger {
// 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: TRADING, FORGE, MINION ITEMS (maybe inferno refuel)
TODO: PET LEVELING COSTS AT FANN, SLAYER / MOB DROPS, SLAYER START COST
*/
companion object {
@@ -103,7 +113,9 @@ class Ledger {
tickQueue.add(runnable)
}
- val di = DI()
+ private val di = DI()
+
+ fun leakDI() = di
}
@Mod.EventHandler
@@ -115,15 +127,21 @@ class Ledger {
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.register(Database::class.java, DIProvider { Database(dataFolder) })
di.registerInjectableClasses(
+ AccessorySwapperDetection::class.java,
+ AllowanceDetection::class.java,
AuctionHouseDetection::class.java,
BankDetection::class.java,
+ BankInterestDetection::class.java,
+ BasicReforgeDetection::class.java,
BazaarDetection::class.java,
BazaarOrderDetection::class.java,
BitsDetection::class.java,
BitsShopDetection::class.java,
+ CaducousFeederDetection::class.java,
ConfigCommand::class.java,
- Database::class.java,
DebugDataCommand::class.java,
DragonEyePlacementDetection::class.java,
DragonSacrificeDetection::class.java,
@@ -133,8 +151,10 @@ class Ledger {
EyedropsDetection::class.java,
ForgeDetection::class.java,
GambleDetection::class.java,
+ GhostCoinDropDetection::class.java,
GodPotionDetection::class.java,
GodPotionMixinDetection::class.java,
+ GummyPolarBearDetection::class.java,
ItemIdProvider::class.java,
KatDetection::class.java,
KuudraChestDetection::class.java,
@@ -144,6 +164,7 @@ class Ledger {
MineshaftCorpseDetection::class.java,
MinionDetection::class.java,
NpcDetection::class.java,
+ PestRepellentDetection::class.java,
QueryCommand::class.java,
RequestUtil::class.java,
TriggerCommand::class.java,
diff --git a/src/main/kotlin/moe/nea/ledger/LedgerEntry.kt b/mod/src/main/kotlin/moe/nea/ledger/LedgerEntry.kt
index dec0727..d4a3932 100644
--- a/src/main/kotlin/moe/nea/ledger/LedgerEntry.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/LedgerEntry.kt
@@ -1,6 +1,7 @@
package moe.nea.ledger
import com.google.gson.JsonObject
+import moe.nea.ledger.gen.ItemIds
import java.time.Instant
import java.util.UUID
@@ -10,8 +11,8 @@ data class LedgerEntry(
val items: List<ItemChange>,
) {
fun intoJson(profileId: UUID?): JsonObject {
- val coinAmount = items.find { it.itemId == ItemId.COINS || it.itemId == ItemId.BITS }?.count
- val nonCoins = items.find { it.itemId != ItemId.COINS && it.itemId != ItemId.BITS }
+ 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())
@@ -21,7 +22,7 @@ data class LedgerEntry(
addProperty("profileId", profileId.toString())
addProperty(
"playerId",
- UUIDUtil.getPlayerUUID().toString()
+ MCUUIDUtil.getPlayerUUID().toString()
)
}
}
diff --git a/src/main/kotlin/moe/nea/ledger/LedgerLogger.kt b/mod/src/main/kotlin/moe/nea/ledger/LedgerLogger.kt
index 913d1b5..6049aa2 100644
--- a/src/main/kotlin/moe/nea/ledger/LedgerLogger.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/LedgerLogger.kt
@@ -6,6 +6,7 @@ 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
@@ -86,10 +87,10 @@ class LedgerLogger {
if (shouldLog)
printToChat(entry)
Ledger.logger.info("Logging entry of type ${entry.transactionType}")
- val transactionId = UUIDUtil.createULIDAt(entry.timestamp)
+ val transactionId = ULIDWrapper.createULIDAt(entry.timestamp)
DBLogEntry.insert(database.connection) {
- it[DBLogEntry.profileId] = currentProfile ?: UUIDUtil.NIL_UUID
- it[DBLogEntry.playerId] = UUIDUtil.getPlayerUUID()
+ it[DBLogEntry.profileId] = currentProfile ?: MCUUIDUtil.NIL_UUID
+ it[DBLogEntry.playerId] = MCUUIDUtil.getPlayerUUID()
it[DBLogEntry.type] = entry.transactionType
it[DBLogEntry.transactionId] = transactionId
}
diff --git a/src/main/kotlin/moe/nea/ledger/LogChatCommand.kt b/mod/src/main/kotlin/moe/nea/ledger/LogChatCommand.kt
index 90b2545..90b2545 100644
--- a/src/main/kotlin/moe/nea/ledger/LogChatCommand.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/LogChatCommand.kt
diff --git a/src/main/kotlin/moe/nea/ledger/UUIDUtil.kt b/mod/src/main/kotlin/moe/nea/ledger/MCUUIDUtil.kt
index 5549908..79068cc 100644
--- a/src/main/kotlin/moe/nea/ledger/UUIDUtil.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/MCUUIDUtil.kt
@@ -1,25 +1,10 @@
package moe.nea.ledger
import com.mojang.util.UUIDTypeAdapter
-import io.azam.ulidj.ULID
import net.minecraft.client.Minecraft
-import java.time.Instant
import java.util.UUID
-import kotlin.random.Random
-object UUIDUtil {
- @JvmInline
- value class ULIDWrapper(
- val wrapped: String
- ) {
- fun getTimestamp(): Instant {
- return Instant.ofEpochMilli(ULID.getTimestamp(wrapped))
- }
-
- init {
- require(ULID.isValid(wrapped))
- }
- }
+object MCUUIDUtil {
fun parseDashlessUuid(string: String) = UUIDTypeAdapter.fromString(string)
val NIL_UUID = UUID(0L, 0L)
@@ -31,12 +16,6 @@ object UUIDUtil {
return currentUUID
}
- fun createULIDAt(timestamp: Instant): ULIDWrapper {
- return ULIDWrapper(ULID.generate(
- timestamp.toEpochMilli(),
- Random.nextBytes(10)
- ))
- }
private var lastKnownUUID: UUID = NIL_UUID
diff --git a/src/main/kotlin/moe/nea/ledger/NumberUtil.kt b/mod/src/main/kotlin/moe/nea/ledger/NumberUtil.kt
index 438f342..fa295b0 100644
--- a/src/main/kotlin/moe/nea/ledger/NumberUtil.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/NumberUtil.kt
@@ -115,3 +115,38 @@ fun Instant.formatChat(): IChatComponent {
.setColor(EnumChatFormatting.AQUA))
return text
}
+
+private val formatChatDirection = run {
+ fun ItemChange.ChangeDirection.formatChat0(): IChatComponent {
+ val (text, color) = when (this) {
+ ItemChange.ChangeDirection.GAINED -> "+" to EnumChatFormatting.GREEN
+ ItemChange.ChangeDirection.TRANSFORM -> "~" to EnumChatFormatting.YELLOW
+ ItemChange.ChangeDirection.SYNC -> "=" to EnumChatFormatting.BLUE
+ ItemChange.ChangeDirection.CATALYST -> "*" to EnumChatFormatting.DARK_PURPLE
+ ItemChange.ChangeDirection.LOST -> "-" to EnumChatFormatting.RED
+ }
+ return ChatComponentText(text)
+ .setChatStyle(
+ ChatStyle()
+ .setColor(color)
+ .setChatHoverEvent(HoverEvent(HoverEvent.Action.SHOW_TEXT,
+ ChatComponentText(name).setChatStyle(ChatStyle().setColor(color)))))
+ }
+ ItemChange.ChangeDirection.entries.associateWith { it.formatChat0() }
+}
+
+fun ItemChange.ChangeDirection.formatChat(): IChatComponent {
+ return formatChatDirection[this]!!
+}
+
+fun ItemChange.formatChat(): IChatComponent {
+ return ChatComponentText(" ")
+ .appendSibling(direction.formatChat())
+ .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))))
+}
+
diff --git a/src/main/kotlin/moe/nea/ledger/QueryCommand.kt b/mod/src/main/kotlin/moe/nea/ledger/QueryCommand.kt
index 9967a4a..80dd54c 100644
--- a/src/main/kotlin/moe/nea/ledger/QueryCommand.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/QueryCommand.kt
@@ -1,11 +1,12 @@
package moe.nea.ledger
-import moe.nea.ledger.database.ANDExpression
-import moe.nea.ledger.database.BooleanExpression
-import moe.nea.ledger.database.Clause
+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
@@ -92,7 +93,7 @@ class QueryCommand : CommandBase() {
query.where(ANDExpression(value))
}
query.limit(80u)
- val dedup = mutableSetOf<UUIDUtil.ULIDWrapper>()
+ val dedup = mutableSetOf<ULIDWrapper>()
query.forEach {
val type = it[DBLogEntry.type]
val transactionId = it[DBLogEntry.transactionId]
@@ -102,7 +103,7 @@ class QueryCommand : CommandBase() {
val timestamp = transactionId.getTimestamp()
val items = DBItemEntry.selectAll(database.connection)
.where(Clause { column(DBItemEntry.transactionId) eq string(transactionId.wrapped) })
- .map { ItemChange.from(it) }
+ .map { DBItemEntry.objMap(it) }
val text = ChatComponentText("")
.setChatStyle(ChatStyle().setColor(EnumChatFormatting.YELLOW))
.appendSibling(
@@ -171,7 +172,7 @@ class QueryCommand : CommandBase() {
override val name: String
get() = "withitem"
- private val itemIdProvider = Ledger.di.provide<ItemIdProvider>() // TODO: close this escape hatch
+ private val itemIdProvider = Ledger.leakDI().provide<ItemIdProvider>() // TODO: close this escape hatch
override fun getFilter(text: String): BooleanExpression {
return Clause { column(DBItemEntry.itemId) like text }
}
diff --git a/src/main/kotlin/moe/nea/ledger/ScoreboardUtil.kt b/mod/src/main/kotlin/moe/nea/ledger/ScoreboardUtil.kt
index 783664b..783664b 100644
--- a/src/main/kotlin/moe/nea/ledger/ScoreboardUtil.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/ScoreboardUtil.kt
diff --git a/src/main/kotlin/moe/nea/ledger/TriggerCommand.kt b/mod/src/main/kotlin/moe/nea/ledger/TriggerCommand.kt
index c97627d..c97627d 100644
--- a/src/main/kotlin/moe/nea/ledger/TriggerCommand.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/TriggerCommand.kt
diff --git a/src/main/kotlin/moe/nea/ledger/config/DebugOptions.kt b/mod/src/main/kotlin/moe/nea/ledger/config/DebugOptions.kt
index fd5ed3d..6b4e51c 100644
--- a/src/main/kotlin/moe/nea/ledger/config/DebugOptions.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/config/DebugOptions.kt
@@ -2,6 +2,7 @@ package moe.nea.ledger.config
import io.github.notenoughupdates.moulconfig.annotations.ConfigEditorBoolean
import io.github.notenoughupdates.moulconfig.annotations.ConfigOption
+import moe.nea.ledger.DevUtil
class DebugOptions {
@ConfigOption(name = "Log entries to chat",
@@ -9,5 +10,5 @@ class DebugOptions {
@Transient
@ConfigEditorBoolean
@JvmField
- var logEntries = false
+ var logEntries = DevUtil.isDevEnv
}
diff --git a/src/main/kotlin/moe/nea/ledger/config/LedgerConfig.kt b/mod/src/main/kotlin/moe/nea/ledger/config/LedgerConfig.kt
index 91ee5c1..91ee5c1 100644
--- a/src/main/kotlin/moe/nea/ledger/config/LedgerConfig.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/config/LedgerConfig.kt
diff --git a/src/main/kotlin/moe/nea/ledger/config/MainOptions.kt b/mod/src/main/kotlin/moe/nea/ledger/config/MainOptions.kt
index 1efa970..1efa970 100644
--- a/src/main/kotlin/moe/nea/ledger/config/MainOptions.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/config/MainOptions.kt
diff --git a/src/main/kotlin/moe/nea/ledger/config/SynchronizationOptions.kt b/mod/src/main/kotlin/moe/nea/ledger/config/SynchronizationOptions.kt
index b8c740b..b8c740b 100644
--- a/src/main/kotlin/moe/nea/ledger/config/SynchronizationOptions.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/config/SynchronizationOptions.kt
diff --git a/src/main/kotlin/moe/nea/ledger/config/UpdateUi.kt b/mod/src/main/kotlin/moe/nea/ledger/config/UpdateUi.kt
index 86ccbf7..86ccbf7 100644
--- a/src/main/kotlin/moe/nea/ledger/config/UpdateUi.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/config/UpdateUi.kt
diff --git a/src/main/kotlin/moe/nea/ledger/config/UpdateUiMarker.kt b/mod/src/main/kotlin/moe/nea/ledger/config/UpdateUiMarker.kt
index 7a0466a..7a0466a 100644
--- a/src/main/kotlin/moe/nea/ledger/config/UpdateUiMarker.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/config/UpdateUiMarker.kt
diff --git a/mod/src/main/kotlin/moe/nea/ledger/events/BeforeGuiAction.kt b/mod/src/main/kotlin/moe/nea/ledger/events/BeforeGuiAction.kt
new file mode 100644
index 0000000..7f6eae9
--- /dev/null
+++ b/mod/src/main/kotlin/moe/nea/ledger/events/BeforeGuiAction.kt
@@ -0,0 +1,21 @@
+package moe.nea.ledger.events
+
+import com.google.gson.JsonElement
+import com.google.gson.JsonObject
+import moe.nea.ledger.telemetry.GuiContextValue
+import moe.nea.ledger.utils.telemetry.ContextValue
+import net.minecraft.client.gui.GuiScreen
+import net.minecraft.client.gui.inventory.GuiChest
+import net.minecraft.client.gui.inventory.GuiContainer
+import net.minecraft.inventory.ContainerChest
+import net.minecraftforge.fml.common.eventhandler.Event
+
+data class BeforeGuiAction(val gui: GuiScreen) : LedgerEvent() {
+ val chest = gui as? GuiChest
+ val chestSlots = chest?.inventorySlots as ContainerChest?
+ override fun serialize(): JsonElement {
+ return JsonObject().apply {
+ add("gui", GuiContextValue(gui).serialize())
+ }
+ }
+}
diff --git a/src/main/kotlin/moe/nea/ledger/events/ChatReceived.kt b/mod/src/main/kotlin/moe/nea/ledger/events/ChatReceived.kt
index a352c27..a352c27 100644
--- a/src/main/kotlin/moe/nea/ledger/events/ChatReceived.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/events/ChatReceived.kt
diff --git a/src/main/kotlin/moe/nea/ledger/events/ExtraSupplyIdEvent.kt b/mod/src/main/kotlin/moe/nea/ledger/events/ExtraSupplyIdEvent.kt
index d040961..d040961 100644
--- a/src/main/kotlin/moe/nea/ledger/events/ExtraSupplyIdEvent.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/events/ExtraSupplyIdEvent.kt
diff --git a/src/main/kotlin/moe/nea/ledger/events/GuiClickEvent.kt b/mod/src/main/kotlin/moe/nea/ledger/events/GuiClickEvent.kt
index 9e057dd..9e057dd 100644
--- a/src/main/kotlin/moe/nea/ledger/events/GuiClickEvent.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/events/GuiClickEvent.kt
diff --git a/src/main/kotlin/moe/nea/ledger/events/InitializationComplete.kt b/mod/src/main/kotlin/moe/nea/ledger/events/InitializationComplete.kt
index d917039..d917039 100644
--- a/src/main/kotlin/moe/nea/ledger/events/InitializationComplete.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/events/InitializationComplete.kt
diff --git a/mod/src/main/kotlin/moe/nea/ledger/events/LedgerEvent.kt b/mod/src/main/kotlin/moe/nea/ledger/events/LedgerEvent.kt
new file mode 100644
index 0000000..cbb3f81
--- /dev/null
+++ b/mod/src/main/kotlin/moe/nea/ledger/events/LedgerEvent.kt
@@ -0,0 +1,22 @@
+package moe.nea.ledger.events
+
+import moe.nea.ledger.Ledger
+import moe.nea.ledger.utils.ErrorUtil
+import moe.nea.ledger.utils.telemetry.CommonKeys
+import moe.nea.ledger.utils.telemetry.ContextValue
+import net.minecraftforge.common.MinecraftForge
+import net.minecraftforge.fml.common.eventhandler.Event
+
+abstract class LedgerEvent : Event(), ContextValue {
+ fun post() {
+ Ledger.leakDI()
+ .provide<ErrorUtil>()
+ .catch(
+ CommonKeys.EVENT_MESSAGE to ContextValue.string("Error during event execution"),
+ "event_instance" to this,
+ "event_type" to ContextValue.string(javaClass.name)
+ ) {
+ MinecraftForge.EVENT_BUS.post(this)
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/java/moe/nea/ledger/events/RegistrationFinishedEvent.kt b/mod/src/main/kotlin/moe/nea/ledger/events/RegistrationFinishedEvent.kt
index d36e0c7..d36e0c7 100644
--- a/src/main/java/moe/nea/ledger/events/RegistrationFinishedEvent.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/events/RegistrationFinishedEvent.kt
diff --git a/src/main/kotlin/moe/nea/ledger/events/SupplyDebugInfo.kt b/mod/src/main/kotlin/moe/nea/ledger/events/SupplyDebugInfo.kt
index cab0a20..cab0a20 100644
--- a/src/main/kotlin/moe/nea/ledger/events/SupplyDebugInfo.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/events/SupplyDebugInfo.kt
diff --git a/src/main/kotlin/moe/nea/ledger/events/TriggerEvent.kt b/mod/src/main/kotlin/moe/nea/ledger/events/TriggerEvent.kt
index 3751f43..3751f43 100644
--- a/src/main/kotlin/moe/nea/ledger/events/TriggerEvent.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/events/TriggerEvent.kt
diff --git a/src/main/kotlin/moe/nea/ledger/events/WorldLoadEvent.kt b/mod/src/main/kotlin/moe/nea/ledger/events/WorldLoadEvent.kt
index d60f3a4..d60f3a4 100644
--- a/src/main/kotlin/moe/nea/ledger/events/WorldLoadEvent.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/events/WorldLoadEvent.kt
diff --git a/src/main/kotlin/moe/nea/ledger/events/WorldSwitchEvent.kt b/mod/src/main/kotlin/moe/nea/ledger/events/WorldSwitchEvent.kt
index 22a97f7..22a97f7 100644
--- a/src/main/kotlin/moe/nea/ledger/events/WorldSwitchEvent.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/events/WorldSwitchEvent.kt
diff --git a/mod/src/main/kotlin/moe/nea/ledger/modules/AccessorySwapperDetection.kt b/mod/src/main/kotlin/moe/nea/ledger/modules/AccessorySwapperDetection.kt
new file mode 100644
index 0000000..808ac5c
--- /dev/null
+++ b/mod/src/main/kotlin/moe/nea/ledger/modules/AccessorySwapperDetection.kt
@@ -0,0 +1,34 @@
+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.ChatReceived
+import moe.nea.ledger.gen.ItemIds
+import moe.nea.ledger.useMatcher
+import moe.nea.ledger.utils.di.Inject
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+
+class AccessorySwapperDetection {
+
+ val swapperUsed = "Swapped .* enrichments to .*!".toPattern()
+
+ @Inject
+ lateinit var logger: LedgerLogger
+
+ @SubscribeEvent
+ fun onChat(event: ChatReceived) {
+ swapperUsed.useMatcher(event.message) {
+ logger.logEntry(
+ LedgerEntry(
+ TransactionType.ACCESSORIES_SWAPPING,
+ event.timestamp,
+ listOf(
+ ItemChange.lose(ItemIds.TALISMAN_ENRICHMENT_SWAPPER, 1)
+ )
+ )
+ )
+ }
+ }
+} \ No newline at end of file
diff --git a/mod/src/main/kotlin/moe/nea/ledger/modules/AllowanceDetection.kt b/mod/src/main/kotlin/moe/nea/ledger/modules/AllowanceDetection.kt
new file mode 100644
index 0000000..cd48d45
--- /dev/null
+++ b/mod/src/main/kotlin/moe/nea/ledger/modules/AllowanceDetection.kt
@@ -0,0 +1,37 @@
+package moe.nea.ledger.modules
+
+import moe.nea.ledger.ItemChange
+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.events.ChatReceived
+import moe.nea.ledger.parseShortNumber
+import moe.nea.ledger.useMatcher
+import moe.nea.ledger.utils.di.Inject
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+import java.util.regex.Pattern
+
+class AllowanceDetection {
+
+ val allowancePattern =
+ Pattern.compile("ALLOWANCE! You earned (?<coins>$SHORT_NUMBER_PATTERN) coins!")
+
+ @Inject
+ lateinit var logger: LedgerLogger
+
+ @SubscribeEvent
+ fun onAllowanceGain(event: ChatReceived) {
+ allowancePattern.useMatcher(event.message) {
+ logger.logEntry(
+ LedgerEntry(
+ TransactionType.ALLOWANCE_GAIN,
+ event.timestamp,
+ listOf(
+ ItemChange.gainCoins(parseShortNumber(group("coins"))),
+ )
+ )
+ )
+ }
+ }
+}
diff --git a/src/main/kotlin/moe/nea/ledger/modules/AuctionHouseDetection.kt b/mod/src/main/kotlin/moe/nea/ledger/modules/AuctionHouseDetection.kt
index d02095d..d02095d 100644
--- a/src/main/kotlin/moe/nea/ledger/modules/AuctionHouseDetection.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/modules/AuctionHouseDetection.kt
diff --git a/src/main/kotlin/moe/nea/ledger/modules/BankDetection.kt b/mod/src/main/kotlin/moe/nea/ledger/modules/BankDetection.kt
index 928d30c..928d30c 100644
--- a/src/main/kotlin/moe/nea/ledger/modules/BankDetection.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/modules/BankDetection.kt
diff --git a/mod/src/main/kotlin/moe/nea/ledger/modules/BankInterestDetection.kt b/mod/src/main/kotlin/moe/nea/ledger/modules/BankInterestDetection.kt
new file mode 100644
index 0000000..5069930
--- /dev/null
+++ b/mod/src/main/kotlin/moe/nea/ledger/modules/BankInterestDetection.kt
@@ -0,0 +1,44 @@
+package moe.nea.ledger.modules
+
+import moe.nea.ledger.ItemChange
+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.events.ChatReceived
+import moe.nea.ledger.parseShortNumber
+import moe.nea.ledger.useMatcher
+import moe.nea.ledger.utils.di.Inject
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+import java.util.regex.Matcher
+import java.util.regex.Pattern
+
+class BankInterestDetection {
+
+ val bankInterestPattern =
+ Pattern.compile("You have just received (?<coins>$SHORT_NUMBER_PATTERN) coins as interest in your (co-op|personal) bank account!")
+ val offlineBankInterestPattern =
+ Pattern.compile("Since you've been away you earned (?<coins>$SHORT_NUMBER_PATTERN) coins as interest in your personal bank account!")
+
+ @Inject
+ lateinit var logger: LedgerLogger
+
+
+ @SubscribeEvent
+ fun onChat(event: ChatReceived) {
+ fun Matcher.logInterest() {
+ logger.logEntry(
+ LedgerEntry(
+ TransactionType.BANK_INTEREST,
+ event.timestamp,
+ listOf(
+ ItemChange.gainCoins(parseShortNumber(group("coins"))),
+ )
+ )
+ )
+ }
+
+ bankInterestPattern.useMatcher(event.message) { logInterest() }
+ offlineBankInterestPattern.useMatcher(event.message) { logInterest() }
+ }
+}
diff --git a/mod/src/main/kotlin/moe/nea/ledger/modules/BasicReforgeDetection.kt b/mod/src/main/kotlin/moe/nea/ledger/modules/BasicReforgeDetection.kt
new file mode 100644
index 0000000..17e2983
--- /dev/null
+++ b/mod/src/main/kotlin/moe/nea/ledger/modules/BasicReforgeDetection.kt
@@ -0,0 +1,71 @@
+package moe.nea.ledger.modules
+
+import moe.nea.ledger.ExpiringValue
+import moe.nea.ledger.ItemChange
+import moe.nea.ledger.ItemId
+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.events.ChatReceived
+import moe.nea.ledger.events.GuiClickEvent
+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.di.Inject
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+import kotlin.time.Duration.Companion.seconds
+
+class BasicReforgeDetection {
+
+ var costPattern = "(?<cost>$SHORT_NUMBER_PATTERN) Coins".toPattern()
+
+ @Inject
+ lateinit var logger: LedgerLogger
+
+ data class ReforgeInstance(
+ val price: Double,
+ val item: ItemId,
+ )
+
+ var lastReforge = ExpiringValue.empty<ReforgeInstance>()
+
+ @SubscribeEvent
+ fun onReforgeClick(event: GuiClickEvent) {
+ val slot = event.slotIn ?: return
+ val displayName = slot.inventory.displayName.unformattedText
+ if (!displayName.unformattedString().contains("Reforge Item") &&
+ !displayName.unformattedString().startsWith("The Hex")
+ ) return
+ val stack = slot.stack ?: return
+ val cost = stack.getLore()
+ .firstNotNullOfOrNull { costPattern.useMatcher(it.unformattedString()) { parseShortNumber(group("cost")) } }
+ ?: return
+
+ if (stack.getDisplayNameU() == "§aReforge Item" || stack.getDisplayNameU() == "§aRandom Basic Reforge") {
+ lastReforge = ExpiringValue(ReforgeInstance(cost, ItemId.NIL /*TODO: read out item stack that is being reforged to save it as a transformed item!*/))
+ }
+ }
+
+ val reforgeChatNotification = "You reforged your .* into a .*!".toPattern()
+
+ @SubscribeEvent
+ fun onReforgeChat(event: ChatReceived) {
+ reforgeChatNotification.useMatcher(event.message) {
+ val reforge = lastReforge.get(3.seconds) ?: return
+ logger.logEntry(
+ LedgerEntry(
+ TransactionType.BASIC_REFORGE,
+ event.timestamp,
+ listOf(
+ ItemChange.loseCoins(reforge.price),
+ ItemChange(reforge.item, 1.0, ItemChange.ChangeDirection.TRANSFORM)
+ )
+ )
+ )
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/kotlin/moe/nea/ledger/modules/BazaarDetection.kt b/mod/src/main/kotlin/moe/nea/ledger/modules/BazaarDetection.kt
index 0f1fc2c..0f1fc2c 100644
--- a/src/main/kotlin/moe/nea/ledger/modules/BazaarDetection.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/modules/BazaarDetection.kt
diff --git a/src/main/kotlin/moe/nea/ledger/modules/BazaarOrderDetection.kt b/mod/src/main/kotlin/moe/nea/ledger/modules/BazaarOrderDetection.kt
index 330ee1d..330ee1d 100644
--- a/src/main/kotlin/moe/nea/ledger/modules/BazaarOrderDetection.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/modules/BazaarOrderDetection.kt
diff --git a/src/main/kotlin/moe/nea/ledger/modules/BitsDetection.kt b/mod/src/main/kotlin/moe/nea/ledger/modules/BitsDetection.kt
index e4c3c98..f6dad12 100644
--- a/src/main/kotlin/moe/nea/ledger/modules/BitsDetection.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/modules/BitsDetection.kt
@@ -1,7 +1,6 @@
package moe.nea.ledger.modules
import moe.nea.ledger.ItemChange
-import moe.nea.ledger.ItemId
import moe.nea.ledger.events.ChatReceived
import moe.nea.ledger.events.LateWorldLoadEvent
import moe.nea.ledger.LedgerEntry
@@ -9,6 +8,7 @@ import moe.nea.ledger.LedgerLogger
import moe.nea.ledger.SHORT_NUMBER_PATTERN
import moe.nea.ledger.ScoreboardUtil
import moe.nea.ledger.TransactionType
+import moe.nea.ledger.gen.ItemIds
import moe.nea.ledger.parseShortNumber
import moe.nea.ledger.unformattedString
import moe.nea.ledger.useMatcher
@@ -33,7 +33,7 @@ class BitsDetection @Inject constructor(val ledger: LedgerLogger) {
TransactionType.BITS_PURSE_STATUS,
Instant.now(),
listOf(
- ItemChange(ItemId.BITS, bits.toDouble(), ItemChange.ChangeDirection.SYNC)
+ ItemChange(ItemIds.SKYBLOCK_BIT, bits.toDouble(), ItemChange.ChangeDirection.SYNC)
)
)
)
@@ -50,9 +50,9 @@ class BitsDetection @Inject constructor(val ledger: LedgerLogger) {
ledger.logEntry(
LedgerEntry(
TransactionType.BOOSTER_COOKIE_ATE,
- Instant.now(),
- listOf(
- ItemChange.lose(ItemId.BOOSTER_COOKIE, 1)
+ Instant.now(),
+ listOf(
+ ItemChange.lose(ItemIds.BOOSTER_COOKIE, 1)
)
)
)
diff --git a/src/main/kotlin/moe/nea/ledger/modules/BitsShopDetection.kt b/mod/src/main/kotlin/moe/nea/ledger/modules/BitsShopDetection.kt
index d7e0a0d..84185bf 100644
--- a/src/main/kotlin/moe/nea/ledger/modules/BitsShopDetection.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/modules/BitsShopDetection.kt
@@ -8,6 +8,7 @@ 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.gen.ItemIds
import moe.nea.ledger.getInternalId
import moe.nea.ledger.getLore
import moe.nea.ledger.parseShortNumber
@@ -21,10 +22,10 @@ class BitsShopDetection @Inject constructor(val ledger: LedgerLogger) {
data class BitShopEntry(
- val id: ItemId,
- val stackSize: Int,
- val bitPrice: Int,
- val timestamp: Long = System.currentTimeMillis()
+ val id: ItemId,
+ val stackSize: Int,
+ val bitPrice: Int,
+ val timestamp: Long = System.currentTimeMillis()
)
var lastClickedBitShopItem: BitShopEntry? = null
@@ -54,8 +55,8 @@ class BitsShopDetection @Inject constructor(val ledger: LedgerLogger) {
TransactionType.COMMUNITY_SHOP_BUY,
Instant.now(),
listOf(
- ItemChange.lose(ItemId.BITS, lastBit.bitPrice.toDouble()),
- ItemChange.gain(lastBit.id, lastBit.stackSize)
+ ItemChange.lose(ItemIds.SKYBLOCK_BIT, lastBit.bitPrice.toDouble()),
+ ItemChange.gain(lastBit.id, lastBit.stackSize)
)
)
)
diff --git a/mod/src/main/kotlin/moe/nea/ledger/modules/CaducousFeederDetection.kt b/mod/src/main/kotlin/moe/nea/ledger/modules/CaducousFeederDetection.kt
new file mode 100644
index 0000000..b64c7e5
--- /dev/null
+++ b/mod/src/main/kotlin/moe/nea/ledger/modules/CaducousFeederDetection.kt
@@ -0,0 +1,48 @@
+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.GuiClickEvent
+import moe.nea.ledger.gen.ItemIds
+import moe.nea.ledger.getDisplayNameU
+import moe.nea.ledger.getInternalId
+import moe.nea.ledger.unformattedString
+import moe.nea.ledger.utils.di.Inject
+import net.minecraft.client.Minecraft
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+import java.time.Instant
+
+class CaducousFeederDetection {
+
+ @Inject
+ lateinit var logger: LedgerLogger
+
+ @Inject
+ lateinit var minecraft: Minecraft
+
+ @SubscribeEvent
+ fun onFeederClick(event: GuiClickEvent) {
+ val slot = event.slotIn ?: return
+ val displayName = slot.inventory.displayName.unformattedText
+ if (!displayName.unformattedString().contains("Confirm Caducous Feeder")) return
+ val stack = slot.stack ?: return
+ val player = minecraft.thePlayer ?: return
+ if (!player.inventory.mainInventory.any { it?.getInternalId() == ItemIds.ULTIMATE_CARROT_CANDY }) return
+ if (stack.getDisplayNameU() != "§aUse Caducous Feeder") return
+ val petId = slot.inventory.getStackInSlot(13)?.getInternalId() ?: ItemId.NIL
+
+ logger.logEntry(
+ LedgerEntry(
+ TransactionType.CADUCOUS_FEEDER_USED,
+ Instant.now(),
+ listOf(
+ ItemChange.lose(ItemIds.ULTIMATE_CARROT_CANDY, 1),
+ ItemChange(petId, 1.0, ItemChange.ChangeDirection.TRANSFORM),
+ )
+ )
+ )
+ }
+} \ No newline at end of file
diff --git a/src/main/kotlin/moe/nea/ledger/modules/ChestDetection.kt b/mod/src/main/kotlin/moe/nea/ledger/modules/ChestDetection.kt
index cca02e1..cca02e1 100644
--- a/src/main/kotlin/moe/nea/ledger/modules/ChestDetection.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/modules/ChestDetection.kt
diff --git a/src/main/kotlin/moe/nea/ledger/modules/DragonEyePlacementDetection.kt b/mod/src/main/kotlin/moe/nea/ledger/modules/DragonEyePlacementDetection.kt
index b9f70c4..b7b9de1 100644
--- a/src/main/kotlin/moe/nea/ledger/modules/DragonEyePlacementDetection.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/modules/DragonEyePlacementDetection.kt
@@ -1,12 +1,12 @@
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.events.WorldSwitchEvent
+import moe.nea.ledger.gen.ItemIds
import moe.nea.ledger.useMatcher
import moe.nea.ledger.utils.di.Inject
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
@@ -34,7 +34,7 @@ class DragonEyePlacementDetection {
TransactionType.WYRM_EVOKED,
event.timestamp,
listOf(
- ItemChange.lose(ItemId.SUMMONING_EYE, eyeCount)
+ ItemChange.lose(ItemIds.SUMMONING_EYE, eyeCount)
)
))
eyeCount = 0
diff --git a/src/main/kotlin/moe/nea/ledger/modules/DragonSacrificeDetection.kt b/mod/src/main/kotlin/moe/nea/ledger/modules/DragonSacrificeDetection.kt
index 20934d2..3bf36f9 100644
--- a/src/main/kotlin/moe/nea/ledger/modules/DragonSacrificeDetection.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/modules/DragonSacrificeDetection.kt
@@ -2,13 +2,13 @@ package moe.nea.ledger.modules
import moe.nea.ledger.DebouncedValue
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.SHORT_NUMBER_PATTERN
import moe.nea.ledger.TransactionType
import moe.nea.ledger.events.ChatReceived
+import moe.nea.ledger.gen.ItemIds
import moe.nea.ledger.parseShortNumber
import moe.nea.ledger.useMatcher
import moe.nea.ledger.utils.di.Inject
@@ -43,7 +43,7 @@ class DragonSacrificeDetection {
event.timestamp,
listOf(
ItemChange.lose(sacrifice, 1),
- ItemChange.gain(ItemId.DRAGON_ESSENCE, lootEssence)
+ ItemChange.gain(ItemIds.ESSENCE_DRAGON, lootEssence)
)
))
}
diff --git a/src/main/kotlin/moe/nea/ledger/modules/DungeonChestDetection.kt b/mod/src/main/kotlin/moe/nea/ledger/modules/DungeonChestDetection.kt
index feb452e..37d0e9c 100644
--- a/src/main/kotlin/moe/nea/ledger/modules/DungeonChestDetection.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/modules/DungeonChestDetection.kt
@@ -2,19 +2,20 @@ package moe.nea.ledger.modules
import moe.nea.ledger.ExpiringValue
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.events.ExtraSupplyIdEvent
import moe.nea.ledger.events.GuiClickEvent
+import moe.nea.ledger.gen.ItemIds
import moe.nea.ledger.getDisplayNameU
import moe.nea.ledger.unformattedString
import moe.nea.ledger.useMatcher
import moe.nea.ledger.utils.di.Inject
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
import java.time.Instant
+import java.util.concurrent.locks.ReentrantLock
import kotlin.time.Duration.Companion.seconds
class DungeonChestDetection @Inject constructor(val logger: LedgerLogger) : ChestDetection() {
@@ -30,7 +31,7 @@ class DungeonChestDetection @Inject constructor(val logger: LedgerLogger) : Ches
TransactionType.KISMET_REROLL,
Instant.now(),
listOf(
- ItemChange.lose(ItemId.KISMET_FEATHER, 1)
+ ItemChange.lose(ItemIds.KISMET_FEATHER, 1)
)
)
)
@@ -42,8 +43,8 @@ class DungeonChestDetection @Inject constructor(val logger: LedgerLogger) : Ches
@SubscribeEvent
fun supplyExtraIds(event: ExtraSupplyIdEvent) {
- event.store("Dungeon Chest Key", ItemId("DUNGEON_CHEST_KEY"))
- event.store("Kismet Feather", ItemId("KISMET_FEATHER"))
+ event.store("Dungeon Chest Key", ItemIds.DUNGEON_CHEST_KEY)
+ event.store("Kismet Feather", ItemIds.KISMET_FEATHER)
}
@SubscribeEvent
@@ -51,7 +52,31 @@ class DungeonChestDetection @Inject constructor(val logger: LedgerLogger) : Ches
lastOpenedChest = ExpiringValue(scrapeChestReward(event.slotIn ?: return) ?: return)
}
- val rewardMessage = " .* CHEST REWARDS".toPattern()
+ class Mutex<T>(defaultValue: T) {
+ private var value: T = defaultValue
+ val lock = ReentrantLock()
+
+ fun getUnsafeLockedValue(): T {
+ if (!lock.isHeldByCurrentThread)
+ error("Accessed unsafe locked value, without holding the lock.")
+ return value
+ }
+
+ fun <R> withLock(func: (T) -> R): R {
+ lock.lockInterruptibly()
+ try {
+ val ret = func(value)
+ if (ret === value) {
+ error("Please don't smuggle out the locked value. If this is unintentional, please append a `Unit` instruction to the end of your `withLock` call: `.withLock { /* your existing code */; Unit }`.")
+ }
+ return ret
+ } finally {
+ lock.unlock()
+ }
+ }
+ }
+
+ val rewardMessage = " *(WOOD|GOLD|DIAMOND|EMERALD|OBSIDIAN|BEDROCK) CHEST REWARDS".toPattern()
@SubscribeEvent
fun onChatMessage(event: ChatReceived) {
diff --git a/src/main/kotlin/moe/nea/ledger/modules/ExternalDataProvider.kt b/mod/src/main/kotlin/moe/nea/ledger/modules/ExternalDataProvider.kt
index 93bb453..42a1f42 100644
--- a/src/main/kotlin/moe/nea/ledger/modules/ExternalDataProvider.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/modules/ExternalDataProvider.kt
@@ -14,6 +14,7 @@ import java.util.concurrent.CompletableFuture
class ExternalDataProvider @Inject constructor(
val requestUtil: RequestUtil
) {
+ // TODO: Save all the data locally, so in case of a failed request older versions can be used
fun createAuxillaryDataRequest(path: String): Request {
return requestUtil.createRequest("https://github.com/nea89o/ledger-auxiliary-data/raw/refs/heads/master/$path")
@@ -22,7 +23,9 @@ class ExternalDataProvider @Inject constructor(
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>>())
+ val nameMap =
+ response?.json(GsonUtil.typeToken<Map<String, String>>())
+ ?: mapOf()
return@supplyAsync nameMap
}
diff --git a/src/main/kotlin/moe/nea/ledger/modules/EyedropsDetection.kt b/mod/src/main/kotlin/moe/nea/ledger/modules/EyedropsDetection.kt
index 2b1a8cd..c90f8d9 100644
--- a/src/main/kotlin/moe/nea/ledger/modules/EyedropsDetection.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/modules/EyedropsDetection.kt
@@ -1,11 +1,11 @@
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.gen.ItemIds
import moe.nea.ledger.useMatcher
import moe.nea.ledger.utils.di.Inject
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
@@ -22,10 +22,10 @@ class EyedropsDetection {
capsaicinEyedropsUsed.useMatcher(event.message) {
logger.logEntry(
LedgerEntry(
- TransactionType.CAPSAICIN_EYEDROPS_USED,
- event.timestamp,
- listOf(
- ItemChange.lose(ItemId.CAP_EYEDROPS, 1)
+ TransactionType.CAPSAICIN_EYEDROPS_USED,
+ event.timestamp,
+ listOf(
+ ItemChange.lose(ItemIds.CAPSAICIN_EYEDROPS_NO_CHARGES, 1)
)
)
)
diff --git a/src/main/kotlin/moe/nea/ledger/modules/ForgeDetection.kt b/mod/src/main/kotlin/moe/nea/ledger/modules/ForgeDetection.kt
index 95811ed..95811ed 100644
--- a/src/main/kotlin/moe/nea/ledger/modules/ForgeDetection.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/modules/ForgeDetection.kt
diff --git a/src/main/kotlin/moe/nea/ledger/modules/GambleDetection.kt b/mod/src/main/kotlin/moe/nea/ledger/modules/GambleDetection.kt
index 0ef43a2..9149e14 100644
--- a/src/main/kotlin/moe/nea/ledger/modules/GambleDetection.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/modules/GambleDetection.kt
@@ -1,7 +1,6 @@
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
@@ -24,7 +23,7 @@ class GambleDetection {
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 item = if (isLowClass) ItemIds.ARCHFIEND_DICE else ItemIds.HIGH_CLASS_ARCHFIEND_DICE
val face = group("face")
val rollCost = if (isLowClass) 666_000.0 else 6_600_000.0
if (face == "7") {
diff --git a/mod/src/main/kotlin/moe/nea/ledger/modules/GhostCoinDropDetection.kt b/mod/src/main/kotlin/moe/nea/ledger/modules/GhostCoinDropDetection.kt
new file mode 100644
index 0000000..42084e2
--- /dev/null
+++ b/mod/src/main/kotlin/moe/nea/ledger/modules/GhostCoinDropDetection.kt
@@ -0,0 +1,38 @@
+package moe.nea.ledger.modules
+
+import moe.nea.ledger.ItemChange
+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.events.ChatReceived
+import moe.nea.ledger.parseShortNumber
+import moe.nea.ledger.useMatcher
+import moe.nea.ledger.utils.di.Inject
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+import java.util.regex.Pattern
+
+class GhostCoinDropDetection {
+
+ val ghostCoinPattern =
+ Pattern.compile("The ghost's death materialized (?<coins>$SHORT_NUMBER_PATTERN) coins from the mists!")
+
+ @Inject
+ lateinit var logger: LedgerLogger
+
+ @SubscribeEvent
+ fun onGhostCoinDrop(event: ChatReceived) {
+ ghostCoinPattern.useMatcher(event.message) {
+ logger.logEntry(
+ LedgerEntry(
+ // TODO: merge this into a generic mob drop tt
+ TransactionType.GHOST_COIN_DROP,
+ event.timestamp,
+ listOf(
+ ItemChange.gainCoins(parseShortNumber(group("coins"))),
+ )
+ )
+ )
+ }
+ }
+}
diff --git a/src/main/kotlin/moe/nea/ledger/modules/GodPotionDetection.kt b/mod/src/main/kotlin/moe/nea/ledger/modules/GodPotionDetection.kt
index 806feb0..56e2b69 100644
--- a/src/main/kotlin/moe/nea/ledger/modules/GodPotionDetection.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/modules/GodPotionDetection.kt
@@ -1,11 +1,11 @@
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.gen.ItemIds
import moe.nea.ledger.useMatcher
import moe.nea.ledger.utils.di.Inject
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
@@ -22,10 +22,10 @@ class GodPotionDetection {
godPotionDrank.useMatcher(event.message) {
logger.logEntry(
LedgerEntry(
- TransactionType.GOD_POTION_DRANK,
- event.timestamp,
- listOf(
- ItemChange.lose(ItemId.GOD_POTION, 1)
+ TransactionType.GOD_POTION_DRANK,
+ event.timestamp,
+ listOf(
+ ItemChange.lose(ItemIds.GOD_POTION_2, 1)
)
)
)
diff --git a/src/main/kotlin/moe/nea/ledger/modules/GodPotionMixinDetection.kt b/mod/src/main/kotlin/moe/nea/ledger/modules/GodPotionMixinDetection.kt
index b96a24a..072503f 100644
--- a/src/main/kotlin/moe/nea/ledger/modules/GodPotionMixinDetection.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/modules/GodPotionMixinDetection.kt
@@ -26,9 +26,9 @@ class GodPotionMixinDetection {
godPotionMixinDrank.useMatcher(event.message) {
logger.logEntry(
LedgerEntry(
- TransactionType.GOD_POTION_MIXIN_DRANK,
- event.timestamp,
- listOf(
+ TransactionType.GOD_POTION_MIXIN_DRANK,
+ event.timestamp,
+ listOf(
ItemChange.lose(itemIdProvider.findForName(group("what")) ?: ItemId.NIL, 1)
)
)
diff --git a/mod/src/main/kotlin/moe/nea/ledger/modules/GummyPolarBearDetection.kt b/mod/src/main/kotlin/moe/nea/ledger/modules/GummyPolarBearDetection.kt
new file mode 100644
index 0000000..d69df83
--- /dev/null
+++ b/mod/src/main/kotlin/moe/nea/ledger/modules/GummyPolarBearDetection.kt
@@ -0,0 +1,34 @@
+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.ChatReceived
+import moe.nea.ledger.gen.ItemIds
+import moe.nea.ledger.useMatcher
+import moe.nea.ledger.utils.di.Inject
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+
+class GummyPolarBearDetection {
+
+ val ateGummyPolarBear = "You ate a Re-heated Gummy Polar Bear!".toPattern()
+
+ @Inject
+ lateinit var logger: LedgerLogger
+
+ @SubscribeEvent
+ fun onChat(event: ChatReceived) {
+ ateGummyPolarBear.useMatcher(event.message) {
+ logger.logEntry(
+ LedgerEntry(
+ TransactionType.GUMMY_POLAR_BEAR_ATE,
+ event.timestamp,
+ listOf(
+ ItemChange.lose(ItemIds.REHEATED_GUMMY_POLAR_BEAR, 1)
+ )
+ )
+ )
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/kotlin/moe/nea/ledger/modules/KatDetection.kt b/mod/src/main/kotlin/moe/nea/ledger/modules/KatDetection.kt
index eda5aba..eda5aba 100644
--- a/src/main/kotlin/moe/nea/ledger/modules/KatDetection.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/modules/KatDetection.kt
diff --git a/src/main/kotlin/moe/nea/ledger/modules/KuudraChestDetection.kt b/mod/src/main/kotlin/moe/nea/ledger/modules/KuudraChestDetection.kt
index e0e9322..88c45d2 100644
--- a/src/main/kotlin/moe/nea/ledger/modules/KuudraChestDetection.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/modules/KuudraChestDetection.kt
@@ -36,6 +36,9 @@ class KuudraChestDetection : ChestDetection() {
if (requiredKey != null && !hasKey(requiredKey)) {
return
}
+ if (requiredKey == null && event.slotIn.inventory.name != "Free Chest") {
+ return
+ }
log.logEntry(LedgerEntry(
TransactionType.KUUDRA_CHEST_OPEN,
diffs.timestamp,
diff --git a/src/main/kotlin/moe/nea/ledger/modules/MineshaftCorpseDetection.kt b/mod/src/main/kotlin/moe/nea/ledger/modules/MineshaftCorpseDetection.kt
index 60b06ae..60b06ae 100644
--- a/src/main/kotlin/moe/nea/ledger/modules/MineshaftCorpseDetection.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/modules/MineshaftCorpseDetection.kt
diff --git a/src/main/kotlin/moe/nea/ledger/modules/MinionDetection.kt b/mod/src/main/kotlin/moe/nea/ledger/modules/MinionDetection.kt
index 6999c7f..6999c7f 100644
--- a/src/main/kotlin/moe/nea/ledger/modules/MinionDetection.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/modules/MinionDetection.kt
diff --git a/src/main/kotlin/moe/nea/ledger/modules/NpcDetection.kt b/mod/src/main/kotlin/moe/nea/ledger/modules/NpcDetection.kt
index 95b8aa5..95b8aa5 100644
--- a/src/main/kotlin/moe/nea/ledger/modules/NpcDetection.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/modules/NpcDetection.kt
diff --git a/mod/src/main/kotlin/moe/nea/ledger/modules/PestRepellentDetection.kt b/mod/src/main/kotlin/moe/nea/ledger/modules/PestRepellentDetection.kt
new file mode 100644
index 0000000..f627393
--- /dev/null
+++ b/mod/src/main/kotlin/moe/nea/ledger/modules/PestRepellentDetection.kt
@@ -0,0 +1,47 @@
+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.ChatReceived
+import moe.nea.ledger.gen.ItemIds
+import moe.nea.ledger.useMatcher
+import moe.nea.ledger.utils.di.Inject
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+
+class PestRepellentDetection {
+
+ val pestRepellent = "YUM! Pests will now spawn (?<reduction>[2-4])x less while you break crops for the next 60m!".toPattern()
+
+ @Inject
+ lateinit var logger: LedgerLogger
+
+ @SubscribeEvent
+ fun onChat(event: ChatReceived) {
+ pestRepellent.useMatcher(event.message) {
+ val reductionAmount = group("reduction")
+ if (reductionAmount == "2") {
+ logger.logEntry(
+ LedgerEntry(
+ TransactionType.PEST_REPELLENT_USED,
+ event.timestamp,
+ listOf(
+ ItemChange.lose(ItemIds.PEST_REPELLENT, 1),
+ )
+ )
+ )
+ } else if (reductionAmount == "4"){
+ logger.logEntry(
+ LedgerEntry(
+ TransactionType.PEST_REPELLENT_USED,
+ event.timestamp,
+ listOf(
+ ItemChange.lose(ItemIds.PEST_REPELLENT_MAX, 1),
+ )
+ )
+ )
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/kotlin/moe/nea/ledger/modules/UpdateChecker.kt b/mod/src/main/kotlin/moe/nea/ledger/modules/UpdateChecker.kt
index 0d89ca1..0d89ca1 100644
--- a/src/main/kotlin/moe/nea/ledger/modules/UpdateChecker.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/modules/UpdateChecker.kt
diff --git a/src/main/kotlin/moe/nea/ledger/modules/VisitorDetection.kt b/mod/src/main/kotlin/moe/nea/ledger/modules/VisitorDetection.kt
index f457ae4..5178e9f 100644
--- a/src/main/kotlin/moe/nea/ledger/modules/VisitorDetection.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/modules/VisitorDetection.kt
@@ -5,15 +5,12 @@ import moe.nea.ledger.ItemId
import moe.nea.ledger.ItemIdProvider
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.events.ExtraSupplyIdEvent
import moe.nea.ledger.events.GuiClickEvent
import moe.nea.ledger.getDisplayNameU
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.di.Inject
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
import java.time.Instant
diff --git a/mod/src/main/kotlin/moe/nea/ledger/telemetry/GuiContextValue.kt b/mod/src/main/kotlin/moe/nea/ledger/telemetry/GuiContextValue.kt
new file mode 100644
index 0000000..2d7db39
--- /dev/null
+++ b/mod/src/main/kotlin/moe/nea/ledger/telemetry/GuiContextValue.kt
@@ -0,0 +1,16 @@
+package moe.nea.ledger.telemetry
+
+import com.google.gson.JsonElement
+import com.google.gson.JsonObject
+import moe.nea.ledger.utils.ScreenUtil
+import moe.nea.ledger.utils.telemetry.ContextValue
+import net.minecraft.client.gui.GuiScreen
+
+class GuiContextValue(val gui: GuiScreen) : ContextValue {
+ override fun serialize(): JsonElement {
+ return JsonObject().apply {
+ addProperty("class", gui.javaClass.name)
+ addProperty("name", ScreenUtil.estimateName(gui))
+ }
+ }
+}
diff --git a/src/main/kotlin/moe/nea/ledger/TelemetryProvider.kt b/mod/src/main/kotlin/moe/nea/ledger/telemetry/TelemetryProvider.kt
index d9c7108..c2fff23 100644
--- a/src/main/kotlin/moe/nea/ledger/TelemetryProvider.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/telemetry/TelemetryProvider.kt
@@ -1,8 +1,10 @@
-package moe.nea.ledger
+package moe.nea.ledger.telemetry
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonObject
+import moe.nea.ledger.DevUtil
+import moe.nea.ledger.Ledger
import moe.nea.ledger.gen.BuildConfig
import moe.nea.ledger.utils.di.DI
import moe.nea.ledger.utils.di.DIProvider
@@ -40,7 +42,7 @@ object TelemetryProvider {
}
fun setupDefaultSpan() {
- val sp = Span.current()
+ val sp = Span.rootSpan
sp.add(USER, MinecraftUser(Minecraft.getMinecraft().session))
sp.add(MINECRAFT_VERSION, ContextValue.compound(
"static" to "1.8.9",
diff --git a/src/main/kotlin/moe/nea/ledger/utils/BorderedTextTracker.kt b/mod/src/main/kotlin/moe/nea/ledger/utils/BorderedTextTracker.kt
index 9e621e8..9e621e8 100644
--- a/src/main/kotlin/moe/nea/ledger/utils/BorderedTextTracker.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/utils/BorderedTextTracker.kt
diff --git a/src/main/kotlin/moe/nea/ledger/utils/ErrorUtil.kt b/mod/src/main/kotlin/moe/nea/ledger/utils/ErrorUtil.kt
index e0c83f9..e0c83f9 100644
--- a/src/main/kotlin/moe/nea/ledger/utils/ErrorUtil.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/utils/ErrorUtil.kt
diff --git a/src/main/kotlin/moe/nea/ledger/utils/GsonUtil.kt b/mod/src/main/kotlin/moe/nea/ledger/utils/GsonUtil.kt
index d3c1f6e..d3c1f6e 100644
--- a/src/main/kotlin/moe/nea/ledger/utils/GsonUtil.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/utils/GsonUtil.kt
diff --git a/src/main/kotlin/moe/nea/ledger/utils/MinecraftExecutor.kt b/mod/src/main/kotlin/moe/nea/ledger/utils/MinecraftExecutor.kt
index affd86c..affd86c 100644
--- a/src/main/kotlin/moe/nea/ledger/utils/MinecraftExecutor.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/utils/MinecraftExecutor.kt
diff --git a/mod/src/main/kotlin/moe/nea/ledger/utils/ScreenUtil.kt b/mod/src/main/kotlin/moe/nea/ledger/utils/ScreenUtil.kt
new file mode 100644
index 0000000..0305126
--- /dev/null
+++ b/mod/src/main/kotlin/moe/nea/ledger/utils/ScreenUtil.kt
@@ -0,0 +1,29 @@
+package moe.nea.ledger.utils
+
+import moe.nea.ledger.mixin.AccessorContainerDispenser
+import moe.nea.ledger.mixin.AccessorContainerHopper
+import net.minecraft.client.gui.GuiScreen
+import net.minecraft.client.gui.inventory.GuiContainer
+import net.minecraft.inventory.ContainerChest
+import net.minecraft.inventory.IInventory
+
+object ScreenUtil {
+ fun estimateInventory(screen: GuiScreen?): IInventory? {
+ if (screen !is GuiContainer) {
+ return null
+ }
+ val container = screen.inventorySlots ?: return null
+ if (container is ContainerChest)
+ return container.lowerChestInventory
+ if (container is AccessorContainerDispenser)
+ return container.dispenserInventory_ledger
+ if (container is AccessorContainerHopper)
+ return container.hopperInventory_ledger
+ return null
+
+ }
+
+ fun estimateName(screen: GuiScreen?): String? {
+ return estimateInventory(screen)?.name
+ }
+} \ No newline at end of file
diff --git a/src/main/kotlin/moe/nea/ledger/utils/network/Request.kt b/mod/src/main/kotlin/moe/nea/ledger/utils/network/Request.kt
index ddf2fcc..ddf2fcc 100644
--- a/src/main/kotlin/moe/nea/ledger/utils/network/Request.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/utils/network/Request.kt
diff --git a/mod/src/main/kotlin/moe/nea/ledger/utils/network/RequestTrace.kt b/mod/src/main/kotlin/moe/nea/ledger/utils/network/RequestTrace.kt
new file mode 100644
index 0000000..3953e09
--- /dev/null
+++ b/mod/src/main/kotlin/moe/nea/ledger/utils/network/RequestTrace.kt
@@ -0,0 +1,21 @@
+package moe.nea.ledger.utils.network
+
+import com.google.gson.JsonElement
+import com.google.gson.JsonObject
+import moe.nea.ledger.utils.telemetry.ContextValue
+
+class RequestTrace(val request: Request) : ContextValue {
+ override fun serialize(): JsonElement {
+ return JsonObject().apply {
+ addProperty("url", request.url.toString())
+ addProperty("method", request.method.name)
+ addProperty("content-type", request.headers["content-type"])
+ addProperty("accept", request.headers["accept"])
+ }
+ }
+
+ companion object {
+ val KEY = "http_request"
+ fun createTrace(request: Request): Pair<String, RequestTrace> = KEY to RequestTrace(request)
+ }
+} \ No newline at end of file
diff --git a/src/main/kotlin/moe/nea/ledger/utils/network/RequestUtil.kt b/mod/src/main/kotlin/moe/nea/ledger/utils/network/RequestUtil.kt
index a49c65a..8101527 100644
--- a/src/main/kotlin/moe/nea/ledger/utils/network/RequestUtil.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/utils/network/RequestUtil.kt
@@ -2,6 +2,8 @@ package moe.nea.ledger.utils.network
import moe.nea.ledger.utils.ErrorUtil
import moe.nea.ledger.utils.di.Inject
+import moe.nea.ledger.utils.telemetry.CommonKeys
+import moe.nea.ledger.utils.telemetry.ContextValue
import java.net.URL
import java.net.URLConnection
import java.security.KeyStore
@@ -38,7 +40,10 @@ class RequestUtil @Inject constructor(val errorUtil: ErrorUtil) {
fun createRequest(url: String) = createRequest(URL(url))
fun createRequest(url: URL) = Request(url, Request.Method.GET, null, mapOf())
- fun executeRequest(request: Request): Response {
+ fun executeRequest(request: Request): Response? = errorUtil.catch(
+ CommonKeys.EVENT_MESSAGE to ContextValue.string("Failed to execute request"),
+ RequestTrace.createTrace(request)
+ ) {
val connection = request.url.openConnection()
enhanceConnection(connection)
connection.setRequestProperty("accept-encoding", "gzip")
@@ -56,7 +61,7 @@ class RequestUtil @Inject constructor(val errorUtil: ErrorUtil) {
val text = stream.bufferedReader().readText()
stream.close()
// Do NOT call connection.disconnect() to allow for connection reuse
- return Response(request, text, connection.headerFields)
+ return@catch Response(request, text, connection.headerFields)
}
diff --git a/src/main/kotlin/moe/nea/ledger/utils/network/Response.kt b/mod/src/main/kotlin/moe/nea/ledger/utils/network/Response.kt
index daae7f7..daae7f7 100644
--- a/src/main/kotlin/moe/nea/ledger/utils/network/Response.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/utils/network/Response.kt
diff --git a/src/main/kotlin/moe/nea/ledger/utils/telemetry/BooleanContext.kt b/mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/BooleanContext.kt
index 5f4ccdf..5f4ccdf 100644
--- a/src/main/kotlin/moe/nea/ledger/utils/telemetry/BooleanContext.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/BooleanContext.kt
diff --git a/src/main/kotlin/moe/nea/ledger/utils/telemetry/CommonKeys.kt b/mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/CommonKeys.kt
index 004ae9c..004ae9c 100644
--- a/src/main/kotlin/moe/nea/ledger/utils/telemetry/CommonKeys.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/CommonKeys.kt
diff --git a/src/main/kotlin/moe/nea/ledger/utils/telemetry/Context.kt b/mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/Context.kt
index 3c30a52..3c30a52 100644
--- a/src/main/kotlin/moe/nea/ledger/utils/telemetry/Context.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/Context.kt
diff --git a/src/main/kotlin/moe/nea/ledger/utils/telemetry/ContextValue.kt b/mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/ContextValue.kt
index b5891fc..b5891fc 100644
--- a/src/main/kotlin/moe/nea/ledger/utils/telemetry/ContextValue.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/ContextValue.kt
diff --git a/src/main/kotlin/moe/nea/ledger/utils/telemetry/EventRecorder.kt b/mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/EventRecorder.kt
index 28b1ab5..28b1ab5 100644
--- a/src/main/kotlin/moe/nea/ledger/utils/telemetry/EventRecorder.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/EventRecorder.kt
diff --git a/src/main/kotlin/moe/nea/ledger/utils/telemetry/ExceptionContextValue.kt b/mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/ExceptionContextValue.kt
index 96b70ec..96b70ec 100644
--- a/src/main/kotlin/moe/nea/ledger/utils/telemetry/ExceptionContextValue.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/ExceptionContextValue.kt
diff --git a/src/main/kotlin/moe/nea/ledger/utils/telemetry/JsonElementContext.kt b/mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/JsonElementContext.kt
index 1601f56..1601f56 100644
--- a/src/main/kotlin/moe/nea/ledger/utils/telemetry/JsonElementContext.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/JsonElementContext.kt
diff --git a/src/main/kotlin/moe/nea/ledger/utils/telemetry/LoggingEventRecorder.kt b/mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/LoggingEventRecorder.kt
index 82a76ed..82a76ed 100644
--- a/src/main/kotlin/moe/nea/ledger/utils/telemetry/LoggingEventRecorder.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/LoggingEventRecorder.kt
diff --git a/src/main/kotlin/moe/nea/ledger/utils/telemetry/RecordedEvent.kt b/mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/RecordedEvent.kt
index 346417d..346417d 100644
--- a/src/main/kotlin/moe/nea/ledger/utils/telemetry/RecordedEvent.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/RecordedEvent.kt
diff --git a/src/main/kotlin/moe/nea/ledger/utils/telemetry/Severity.kt b/mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/Severity.kt
index e9a3b79..e9a3b79 100644
--- a/src/main/kotlin/moe/nea/ledger/utils/telemetry/Severity.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/Severity.kt
diff --git a/src/main/kotlin/moe/nea/ledger/utils/telemetry/Span.kt b/mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/Span.kt
index 0d680a9..8b8e284 100644
--- a/src/main/kotlin/moe/nea/ledger/utils/telemetry/Span.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/Span.kt
@@ -2,9 +2,10 @@ package moe.nea.ledger.utils.telemetry
class Span(val parent: Span?) : AutoCloseable {
companion object {
+ val rootSpan = Span(null)
private val _current = object : InheritableThreadLocal<Span>() {
override fun initialValue(): Span {
- return Span(null)
+ return Span(rootSpan)
}
override fun childValue(parentValue: Span?): Span {
diff --git a/src/main/kotlin/moe/nea/ledger/utils/telemetry/StringContext.kt b/mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/StringContext.kt
index 2d33075..2d33075 100644
--- a/src/main/kotlin/moe/nea/ledger/utils/telemetry/StringContext.kt
+++ b/mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/StringContext.kt
diff --git a/src/main/resources/ledgerkeystore.jks b/mod/src/main/resources/ledgerkeystore.jks
index b71185a..b71185a 100644
--- a/src/main/resources/ledgerkeystore.jks
+++ b/mod/src/main/resources/ledgerkeystore.jks
Binary files differ
diff --git a/src/main/resources/mcmod.info b/mod/src/main/resources/mcmod.info
index fdeffd8..fdeffd8 100644
--- a/src/main/resources/mcmod.info
+++ b/mod/src/main/resources/mcmod.info
diff --git a/src/main/resources/mixins.moneyledger.json b/mod/src/main/resources/mixins.moneyledger.json
index 5ea0c57..fa6482e 100644
--- a/src/main/resources/mixins.moneyledger.json
+++ b/mod/src/main/resources/mixins.moneyledger.json
@@ -1,7 +1,6 @@
{
"package": "${basePackage}.mixin",
"plugin": "${basePackage}.init.AutoDiscoveryMixinPlugin",
- "refmap": "mixins.${modid}.refmap.json",
"minVersion": "0.7",
"compatibilityLevel": "JAVA_8",
"__comment": "You do not need to manually register mixins in this template. Check the auto discovery mixin plugin for more info."
diff --git a/src/test/kotlin/moe/nea/ledger/NumberUtilKtTest.kt b/mod/src/test/kotlin/moe/nea/ledger/NumberUtilKtTest.kt
index 4068a42..4068a42 100644
--- a/src/test/kotlin/moe/nea/ledger/NumberUtilKtTest.kt
+++ b/mod/src/test/kotlin/moe/nea/ledger/NumberUtilKtTest.kt
diff --git a/server/aio/build.gradle.kts b/server/aio/build.gradle.kts
new file mode 100644
index 0000000..22819f0
--- /dev/null
+++ b/server/aio/build.gradle.kts
@@ -0,0 +1,19 @@
+plugins {
+ kotlin("jvm")
+ application
+}
+
+dependencies {
+ declareKtorVersion()
+ implementation(project(":server:core"))
+ implementation(project(":server:frontend"))
+}
+
+java {
+ toolchain.languageVersion.set(JavaLanguageVersion.of(21))
+}
+
+application {
+ mainClass.set("moe.nea.ledger.server.core.ApplicationKt")
+}
+
diff --git a/server/aio/src/main/kotlin/moe/nea/ledger/server/aio/AIO.kt b/server/aio/src/main/kotlin/moe/nea/ledger/server/aio/AIO.kt
new file mode 100644
index 0000000..e59301f
--- /dev/null
+++ b/server/aio/src/main/kotlin/moe/nea/ledger/server/aio/AIO.kt
@@ -0,0 +1,20 @@
+package moe.nea.ledger.server.aio
+
+import io.ktor.server.application.Application
+import io.ktor.server.http.content.singlePageApplication
+import io.ktor.server.routing.Routing
+import moe.nea.ledger.server.core.AIOProvider
+
+
+class AIO : AIOProvider {
+ override fun Routing.installExtraRouting() {
+ singlePageApplication {
+ useResources = true
+ filesPath = "ledger-web-dist"
+ defaultPage = "index.html"
+ }
+ }
+
+ override fun Application.module() {
+ }
+} \ No newline at end of file
diff --git a/server/analysis/build.gradle.kts b/server/analysis/build.gradle.kts
new file mode 100644
index 0000000..d5d48a0
--- /dev/null
+++ b/server/analysis/build.gradle.kts
@@ -0,0 +1,16 @@
+plugins {
+ kotlin("jvm")
+ kotlin("plugin.serialization")
+ id("com.google.devtools.ksp")
+}
+
+dependencies {
+ api("org.jetbrains.kotlinx:kotlinx-serialization-core:1.8.0")
+ ksp("dev.zacsweers.autoservice:auto-service-ksp:1.2.0")
+ implementation("com.google.auto.service:auto-service-annotations:1.1.1")
+ implementation(project(":database:impl"))
+}
+
+java {
+ toolchain.languageVersion.set(JavaLanguageVersion.of(21))
+}
diff --git a/server/analysis/src/main/kotlin/moe/nea/ledger/analysis/Analysis.kt b/server/analysis/src/main/kotlin/moe/nea/ledger/analysis/Analysis.kt
new file mode 100644
index 0000000..abcf8ed
--- /dev/null
+++ b/server/analysis/src/main/kotlin/moe/nea/ledger/analysis/Analysis.kt
@@ -0,0 +1,9 @@
+package moe.nea.ledger.analysis
+
+import java.sql.Connection
+
+interface Analysis {
+ val id: String
+ val name: String
+ fun perform(database: Connection, filter: AnalysisFilter): AnalysisResult
+} \ No newline at end of file
diff --git a/server/analysis/src/main/kotlin/moe/nea/ledger/analysis/AnalysisFilter.kt b/server/analysis/src/main/kotlin/moe/nea/ledger/analysis/AnalysisFilter.kt
new file mode 100644
index 0000000..10d9b9c
--- /dev/null
+++ b/server/analysis/src/main/kotlin/moe/nea/ledger/analysis/AnalysisFilter.kt
@@ -0,0 +1,26 @@
+package moe.nea.ledger.analysis
+
+import moe.nea.ledger.database.DBLogEntry
+import moe.nea.ledger.database.Query
+import moe.nea.ledger.database.columns.DBUlid
+import moe.nea.ledger.database.sql.Clause
+import moe.nea.ledger.utils.ULIDWrapper
+import java.time.Instant
+import java.time.ZoneId
+import java.util.UUID
+
+interface AnalysisFilter {
+ fun applyTo(query: Query) {
+ query.where(Clause { column(DBLogEntry.transactionId) ge value(DBUlid, ULIDWrapper.lowerBound(startWindow)) })
+ .where(Clause { column(DBLogEntry.transactionId) le value(DBUlid, ULIDWrapper.upperBound(endWindow)) })
+//TODO: .where(Clause { column(DBLogEntry.profileId) inList profiles })
+ }
+
+ fun timeZone(): ZoneId {
+ return ZoneId.systemDefault()
+ }
+
+ val startWindow: Instant
+ val endWindow: Instant
+ val profiles: List<UUID>
+}
diff --git a/server/analysis/src/main/kotlin/moe/nea/ledger/analysis/AnalysisResult.kt b/server/analysis/src/main/kotlin/moe/nea/ledger/analysis/AnalysisResult.kt
new file mode 100644
index 0000000..4ad47f7
--- /dev/null
+++ b/server/analysis/src/main/kotlin/moe/nea/ledger/analysis/AnalysisResult.kt
@@ -0,0 +1,8 @@
+package moe.nea.ledger.analysis
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class AnalysisResult(
+ val visualizations: List<Visualization>
+) \ No newline at end of file
diff --git a/server/analysis/src/main/kotlin/moe/nea/ledger/analysis/CoinsSpentOnAuctions.kt b/server/analysis/src/main/kotlin/moe/nea/ledger/analysis/CoinsSpentOnAuctions.kt
new file mode 100644
index 0000000..d1ce52b
--- /dev/null
+++ b/server/analysis/src/main/kotlin/moe/nea/ledger/analysis/CoinsSpentOnAuctions.kt
@@ -0,0 +1,49 @@
+package moe.nea.ledger.analysis
+
+import com.google.auto.service.AutoService
+import moe.nea.ledger.ItemChange
+import moe.nea.ledger.ItemId
+import moe.nea.ledger.TransactionType
+import moe.nea.ledger.database.DBItemEntry
+import moe.nea.ledger.database.DBLogEntry
+import moe.nea.ledger.database.sql.Clause
+import java.sql.Connection
+import java.time.LocalDate
+
+@AutoService(Analysis::class)
+class CoinsSpentOnAuctions : Analysis {
+ override val name: String
+ get() = "Shopping Costs"
+ override val id: String
+ get() = "coins-spent-on-auctions"
+
+ override fun perform(database: Connection, filter: AnalysisFilter): AnalysisResult {
+ val query = DBLogEntry.from(database)
+ .join(DBItemEntry, Clause { column(DBItemEntry.transactionId) eq column(DBLogEntry.transactionId) })
+ .where(Clause { column(DBItemEntry.itemId) eq ItemId.COINS })
+ .where(Clause { column(DBItemEntry.mode) eq ItemChange.ChangeDirection.LOST })
+ .where(Clause { column(DBLogEntry.type) eq TransactionType.AUCTION_BOUGHT })
+ .select(DBItemEntry.size, DBLogEntry.transactionId)
+ filter.applyTo(query)
+ val spentThatDay = mutableMapOf<LocalDate, Double>()
+ for (resultRow in query) {
+ val timestamp = resultRow[DBLogEntry.transactionId].getTimestamp()
+ val damage = resultRow[DBItemEntry.size]
+ val localZone = filter.timeZone()
+ val localDate = timestamp.atZone(localZone).toLocalDate()
+ spentThatDay.merge(localDate, damage) { a, b -> a + b }
+ }
+ return AnalysisResult(
+ listOf(
+ Visualization(
+ "Coins spent on auctions",
+ xLabel = "Time",
+ yLabel = "Coins Spent that day",
+ dataPoints = spentThatDay.entries.map { (k, v) ->
+ DataPoint(k.atTime(12, 0).atZone(filter.timeZone()).toInstant(), v)
+ }
+ )
+ )
+ )
+ }
+} \ No newline at end of file
diff --git a/server/analysis/src/main/kotlin/moe/nea/ledger/analysis/Visualization.kt b/server/analysis/src/main/kotlin/moe/nea/ledger/analysis/Visualization.kt
new file mode 100644
index 0000000..d0c0d56
--- /dev/null
+++ b/server/analysis/src/main/kotlin/moe/nea/ledger/analysis/Visualization.kt
@@ -0,0 +1,30 @@
+package moe.nea.ledger.analysis
+
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.descriptors.PrimitiveKind
+import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import java.time.Instant
+
+@Serializable
+data class Visualization(
+ val label: String,
+ val xLabel: String,
+ val yLabel: String,
+ val dataPoints: List<DataPoint>
+)
+
+@Serializable
+data class DataPoint(
+ val time: @Serializable(InstantSerializer::class) Instant,
+ val value: Double,
+)
+
+object InstantSerializer : KSerializer<Instant> {
+ override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("java.time.Instant", PrimitiveKind.LONG)
+ override fun serialize(encoder: Encoder, value: Instant) = encoder.encodeLong(value.toEpochMilli())
+ override fun deserialize(decoder: Decoder): Instant = Instant.ofEpochMilli(decoder.decodeLong())
+} \ No newline at end of file
diff --git a/server/core/build.gradle.kts b/server/core/build.gradle.kts
new file mode 100644
index 0000000..b2a3222
--- /dev/null
+++ b/server/core/build.gradle.kts
@@ -0,0 +1,37 @@
+plugins {
+ kotlin("jvm")
+ kotlin("plugin.serialization")
+ application
+ id("com.github.gmazzo.buildconfig")
+}
+
+
+dependencies {
+ declareKtorVersion()
+ api("io.ktor:ktor-server-netty")
+ api("io.ktor:ktor-server-status-pages")
+ api("io.ktor:ktor-server-content-negotiation")
+ api("io.ktor:ktor-serialization-kotlinx-json")
+ api("io.ktor:ktor-server-compression")
+ api("io.ktor:ktor-server-cors")
+ api("sh.ondr:kotlin-json-schema:0.1.1")
+ api(project(":server:analysis"))
+ api(project(":database:impl"))
+ api(project(":server:swagger"))
+
+ runtimeOnly("ch.qos.logback:logback-classic:1.5.16")
+ runtimeOnly("org.xerial:sqlite-jdbc:3.45.3.0")
+}
+
+java {
+ toolchain.languageVersion.set(JavaLanguageVersion.of(21))
+}
+application {
+ val isDevelopment: Boolean = project.ext.has("development")
+ applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment",
+ "-Dledger.databasefolder=${project(":mod").file("run/money-ledger").absoluteFile}")
+ mainClass.set("moe.nea.ledger.server.core.ApplicationKt")
+}
+buildConfig {
+ packageName("moe.nea.ledger.gen")
+}
diff --git a/server/core/src/main/kotlin/moe/nea/ledger/server/core/Application.kt b/server/core/src/main/kotlin/moe/nea/ledger/server/core/Application.kt
new file mode 100644
index 0000000..23b2a6a
--- /dev/null
+++ b/server/core/src/main/kotlin/moe/nea/ledger/server/core/Application.kt
@@ -0,0 +1,81 @@
+package moe.nea.ledger.server.core
+
+import io.ktor.serialization.kotlinx.json.json
+import io.ktor.server.application.Application
+import io.ktor.server.application.install
+import io.ktor.server.netty.EngineMain
+import io.ktor.server.plugins.compression.Compression
+import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
+import io.ktor.server.plugins.cors.routing.CORS
+import io.ktor.server.response.respondRedirect
+import io.ktor.server.routing.Routing
+import io.ktor.server.routing.get
+import io.ktor.server.routing.route
+import io.ktor.server.routing.routing
+import kotlinx.serialization.json.Json
+import moe.nea.ledger.database.Database
+import moe.nea.ledger.gen.BuildConfig
+import moe.nea.ledger.server.core.api.Documentation
+import moe.nea.ledger.server.core.api.Info
+import moe.nea.ledger.server.core.api.Server
+import moe.nea.ledger.server.core.api.apiRouting
+import moe.nea.ledger.server.core.api.openApiDocsJson
+import moe.nea.ledger.server.core.api.openApiUi
+import moe.nea.ledger.server.core.api.setApiRoot
+import java.io.File
+
+fun main(args: Array<String>) {
+ EngineMain.main(args)
+}
+
+interface AIOProvider {
+ fun Routing.installExtraRouting()
+ fun Application.module()
+}
+
+fun Application.module() {
+ val aio = runCatching {
+ Class.forName("moe.nea.ledger.server.aio.AIO")
+ .newInstance() as AIOProvider
+ }.getOrNull()
+ aio?.run { module() }
+ install(Compression)
+ install(Documentation) {
+ info = Info(
+ "Ledger Analysis Server",
+ "Your local API for loading ledger data",
+ BuildConfig.VERSION
+ )
+ servers.add(
+ Server("http://localhost:8080/api", "Your Local Server")
+ )
+ }
+ install(ContentNegotiation) {
+ json(Json {
+ this.explicitNulls = false
+ this.encodeDefaults = true
+ })
+// cbor()
+ }
+ install(CORS) {
+ anyHost()
+ }
+ val database = Database(File(System.getProperty("ledger.databasefolder",
+ "/home/nea/.local/share/PrismLauncher/instances/Skyblock/.minecraft/money-ledger")))
+ database.loadAndUpgrade()
+ routing {
+ route("/api") {
+ setApiRoot()
+ get { call.respondRedirect("/openapi/") }
+ apiRouting(database)
+ }
+ route("/api.json") {
+ openApiDocsJson()
+ }
+ route("/openapi") {
+ openApiUi("/api.json")
+ }
+ aio?.run { installExtraRouting() }
+ }
+}
+
diff --git a/server/core/src/main/kotlin/moe/nea/ledger/server/core/api/BaseApi.kt b/server/core/src/main/kotlin/moe/nea/ledger/server/core/api/BaseApi.kt
new file mode 100644
index 0000000..3240a65
--- /dev/null
+++ b/server/core/src/main/kotlin/moe/nea/ledger/server/core/api/BaseApi.kt
@@ -0,0 +1,205 @@
+package moe.nea.ledger.server.core.api
+
+import io.ktor.http.Url
+import io.ktor.http.toURI
+import io.ktor.server.response.respond
+import io.ktor.server.routing.Route
+import io.ktor.server.routing.get
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.async
+import kotlinx.coroutines.withContext
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.descriptors.PrimitiveKind
+import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.decodeFromStream
+import moe.nea.ledger.ItemChange
+import moe.nea.ledger.TransactionType
+import moe.nea.ledger.analysis.Analysis
+import moe.nea.ledger.analysis.AnalysisFilter
+import moe.nea.ledger.analysis.AnalysisResult
+import moe.nea.ledger.database.DBItemEntry
+import moe.nea.ledger.database.DBLogEntry
+import moe.nea.ledger.database.Database
+import moe.nea.ledger.database.sql.Clause
+import moe.nea.ledger.server.core.Profile
+import moe.nea.ledger.utils.ULIDWrapper
+import java.time.Instant
+import java.util.ServiceLoader
+import java.util.UUID
+
+fun Route.apiRouting(database: Database) {
+ val allOfferedAnalysisServices: Map<String, Analysis> = run {
+ val serviceLoader = ServiceLoader.load(Analysis::class.java, environment.classLoader)
+ val map = mutableMapOf<String, Analysis>()
+ serviceLoader.forEach {
+ map[it.id] = it
+ }
+ map
+ }
+
+ get("/profiles") {
+ val profiles = DBLogEntry.from(database.connection)
+ .select(DBLogEntry.playerId, DBLogEntry.profileId)
+ .distinct()
+ .map {
+ Profile(it[DBLogEntry.playerId], it[DBLogEntry.profileId])
+ }
+ call.respond(profiles)
+ }.docs {
+ summary = "List all profiles and players known to ledger"
+ operationId = "listProfiles"
+ tag(Tags.PROFILE)
+ respondsOk {
+ schema<List<Profile>>()
+ }
+ }
+ @OptIn(DelicateCoroutinesApi::class)
+ val itemNames = GlobalScope.async {
+ val itemNamesUrl =
+ Url("https://github.com/nea89o/ledger-auxiliary-data/raw/refs/heads/master/data/item_names.json")
+ Json.decodeFromStream<Map<String, String>>(itemNamesUrl.toURI().toURL().openStream())
+ }
+ get("/analysis/execute") {
+ val analysis = allOfferedAnalysisServices[call.queryParameters["analysis"]] ?: TODO()
+ val start = call.queryParameters["tStart"]?.toLongOrNull()?.let { Instant.ofEpochMilli(it) } ?: TODO()
+ val end = call.queryParameters["tEnd"]?.toLongOrNull()?.let { Instant.ofEpochMilli(it) } ?: TODO()
+ val analysisResult = withContext(Dispatchers.IO) {
+ analysis.perform(
+ database.connection,
+ object : AnalysisFilter {
+ override val startWindow: Instant
+ get() = start
+ override val endWindow: Instant
+ get() = end
+ override val profiles: List<UUID>
+ get() = listOf()
+ }
+ )
+ }
+ call.respond(analysisResult)
+ }.docs {
+ summary = "Execute an analysis on a given timeframe"
+ operationId = "executeAnalysis"
+ queryParameter<String>("analysis", description = "An analysis id obtained from getAnalysis")
+ queryParameter<Long>("tStart", description = "The start of the timeframe to analyze")
+ queryParameter<Long>("tEnd",
+ description = "The end of the timeframe to analyze. Make sure to use the end of the day if you want the entire day included.")
+ tag(Tags.DATA)
+ respondsOk {
+ schema<AnalysisResult>()
+ }
+ }
+ get("/analysis/list") {
+ call.respond(allOfferedAnalysisServices.values.map {
+ AnalysisListing(it.name, it.id)
+ })
+ }.docs {
+ summary = "List all installed analysis"
+ operationId = "getAnalysis"
+ tag(Tags.DATA)
+ respondsOk {
+ schema<List<AnalysisListing>>()
+ }
+ }
+ get("/item") {
+ val itemIds = call.queryParameters.getAll("itemId")?.toSet() ?: emptySet()
+ val itemNameMap = itemNames.await()
+ call.respond(itemIds.associateWith { itemNameMap[it] })
+ }.docs {
+ summary = "Get item names for item ids"
+ operationId = "getItemNames"
+ tag(Tags.HYPIXEL)
+ queryParameter<List<String>>("itemId")
+ respondsOk {
+ schema<Map<String, String?>>()
+ }
+ }
+ get("/entries") {
+ val logs = mutableMapOf<ULIDWrapper, LogEntry>()
+ val items = mutableMapOf<ULIDWrapper, MutableList<SerializableItemChange>>()
+ withContext(Dispatchers.IO) {
+ DBLogEntry.from(database.connection)
+ .join(DBItemEntry, Clause { column(DBItemEntry.transactionId) eq column(DBLogEntry.transactionId) })
+ .select(DBLogEntry.profileId,
+ DBLogEntry.playerId,
+ DBLogEntry.transactionId,
+ DBLogEntry.type,
+ DBItemEntry.mode,
+ DBItemEntry.itemId,
+ DBItemEntry.size)
+ .forEach { row ->
+ logs.getOrPut(row[DBLogEntry.transactionId]) {
+ LogEntry(row[DBLogEntry.type],
+ row[DBLogEntry.transactionId],
+ listOf())
+ }
+ items.getOrPut(row[DBLogEntry.transactionId]) { mutableListOf() }
+ .add(SerializableItemChange(
+ row[DBItemEntry.itemId].string,
+ row[DBItemEntry.mode],
+ row[DBItemEntry.size],
+ ))
+ }
+ }
+ val compiled = logs.values.map { it.copy(items = items[it.id]!!) }
+ call.respond(compiled)
+ }.docs {
+ summary = "Get all log entries"
+ operationId = "getLogEntries"
+ tag(Tags.DATA)
+ respondsOk {
+ schema<List<LogEntry>>()
+ }
+ }
+}
+
+@Serializable
+data class AnalysisListing(
+ val name: String,
+ val id: String,
+)
+
+@Serializable
+data class LogEntry(
+ val type: TransactionType,
+ val id: @Serializable(ULIDSerializer::class) ULIDWrapper,
+ val items: List<SerializableItemChange>,
+)
+
+@Serializable
+data class SerializableItemChange(
+ val itemId: String,
+ val direction: ItemChange.ChangeDirection,
+ val amount: Double,
+)
+
+object ULIDSerializer : KSerializer<ULIDWrapper> {
+ override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("ULID", PrimitiveKind.STRING)
+
+ override fun deserialize(decoder: Decoder): ULIDWrapper {
+ return ULIDWrapper(decoder.decodeString())
+ }
+
+ override fun serialize(encoder: Encoder, value: ULIDWrapper) {
+ encoder.encodeString(value.wrapped)
+ }
+}
+
+enum class Tags : IntoTag {
+ PROFILE,
+ HYPIXEL,
+ MANAGEMENT,
+ DATA,
+ ;
+
+ override fun intoTag(): String {
+ return name
+ }
+} \ No newline at end of file
diff --git a/server/core/src/main/kotlin/moe/nea/ledger/server/core/model.kt b/server/core/src/main/kotlin/moe/nea/ledger/server/core/model.kt
new file mode 100644
index 0000000..a27a729
--- /dev/null
+++ b/server/core/src/main/kotlin/moe/nea/ledger/server/core/model.kt
@@ -0,0 +1,30 @@
+@file:UseSerializers(UUIDSerializer::class)
+
+package moe.nea.ledger.server.core
+
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.UseSerializers
+import kotlinx.serialization.descriptors.PrimitiveKind
+import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
+import kotlinx.serialization.descriptors.SerialDescriptor
+import java.util.UUID
+
+object UUIDSerializer : KSerializer<UUID> {
+ override val descriptor: SerialDescriptor
+ get() = PrimitiveSerialDescriptor("LedgerUUID", PrimitiveKind.STRING)
+
+ override fun deserialize(decoder: kotlinx.serialization.encoding.Decoder): UUID {
+ return UUID.fromString(decoder.decodeString())
+ }
+
+ override fun serialize(encoder: kotlinx.serialization.encoding.Encoder, value: UUID) {
+ encoder.encodeString(value.toString())
+ }
+}
+
+@Serializable
+data class Profile(
+ val playerId: UUID,
+ val profileId: UUID,
+) \ No newline at end of file
diff --git a/server/core/src/main/resources/application.conf b/server/core/src/main/resources/application.conf
new file mode 100644
index 0000000..386ffd3
--- /dev/null
+++ b/server/core/src/main/resources/application.conf
@@ -0,0 +1,10 @@
+ktor {
+ application {
+ modules = [
+ moe.nea.ledger.server.core.ApplicationKt.module
+ ]
+ }
+ deployment {
+ port = 8080
+ }
+} \ No newline at end of file
diff --git a/server/frontend/.gitignore b/server/frontend/.gitignore
new file mode 100644
index 0000000..76add87
--- /dev/null
+++ b/server/frontend/.gitignore
@@ -0,0 +1,2 @@
+node_modules
+dist \ No newline at end of file
diff --git a/server/frontend/build.gradle.kts b/server/frontend/build.gradle.kts
new file mode 100644
index 0000000..72fe6ea
--- /dev/null
+++ b/server/frontend/build.gradle.kts
@@ -0,0 +1,17 @@
+import com.github.gradle.node.pnpm.task.PnpmTask
+
+plugins {
+ id("com.github.node-gradle.node") version "7.1.0"
+ `java-library`
+}
+
+val webDist by tasks.register("webDist", PnpmTask::class) {
+ dependsOn(tasks.pnpmInstall)
+ args.addAll("build")
+ outputs.dir("dist")
+}
+tasks.jar {
+ from(webDist) {
+ into("ledger-web-dist/")
+ }
+}
diff --git a/server/frontend/index.html b/server/frontend/index.html
new file mode 100644
index 0000000..48c59fc
--- /dev/null
+++ b/server/frontend/index.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <meta name="theme-color" content="#000000" />
+ <link rel="shortcut icon" type="image/ico" href="/src/assets/favicon.ico" />
+ <title>Solid App</title>
+ </head>
+ <body>
+ <noscript>You need to enable JavaScript to run this app.</noscript>
+ <div id="root"></div>
+
+ <script src="/src/index.tsx" type="module"></script>
+ </body>
+</html>
diff --git a/server/frontend/package.json b/server/frontend/package.json
new file mode 100644
index 0000000..a8a8880
--- /dev/null
+++ b/server/frontend/package.json
@@ -0,0 +1,37 @@
+{
+ "name": "ledger-frontend",
+ "version": "0.0.0",
+ "description": "",
+ "type": "module",
+ "scripts": {
+ "start": "vite",
+ "dev": "vite",
+ "build": "vite build",
+ "serve": "vite preview",
+ "test:ts": "tsc --noEmit",
+ "genApi": "openapi-typescript http://localhost:8080/api.json -o src/api-schema.d.ts"
+ },
+ "license": "MIT",
+ "devDependencies": {
+ "openapi-typescript": "^7.5.2",
+ "solid-devtools": "^0.33.0",
+ "typescript": "^5.7.2",
+ "vite": "^6.0.0",
+ "vite-plugin-solid": "^2.11.0"
+ },
+ "dependencies": {
+ "@solidjs/router": "^0.15.3",
+ "apexcharts": "^4.3.0",
+ "moment": "^2.30.1",
+ "openapi-fetch": "^0.13.4",
+ "solid-apexcharts": "^0.4.0",
+ "solid-js": "^1.9.3"
+ },
+ "devEngines": {
+ "packageManager": {
+ "name": "pnpm",
+ "onFail": "error"
+ }
+ },
+ "packageManager": "pnpm@^9.3.0"
+}
diff --git a/server/frontend/pnpm-lock.yaml b/server/frontend/pnpm-lock.yaml
new file mode 100644
index 0000000..6483404
--- /dev/null
+++ b/server/frontend/pnpm-lock.yaml
@@ -0,0 +1,1920 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ dependencies:
+ '@solidjs/router':
+ specifier: ^0.15.3
+ version: 0.15.3(solid-js@1.9.4)
+ apexcharts:
+ specifier: ^4.3.0
+ version: 4.3.0
+ moment:
+ specifier: ^2.30.1
+ version: 2.30.1
+ openapi-fetch:
+ specifier: ^0.13.4
+ version: 0.13.4
+ solid-apexcharts:
+ specifier: ^0.4.0
+ version: 0.4.0(apexcharts@4.3.0)(solid-js@1.9.4)
+ solid-js:
+ specifier: ^1.9.3
+ version: 1.9.4
+ devDependencies:
+ openapi-typescript:
+ specifier: ^7.5.2
+ version: 7.5.2(typescript@5.7.3)
+ solid-devtools:
+ specifier: ^0.33.0
+ version: 0.33.0(solid-js@1.9.4)(vite@6.0.7(sass@1.83.4))
+ typescript:
+ specifier: ^5.7.2
+ version: 5.7.3
+ vite:
+ specifier: ^6.0.0
+ version: 6.0.7(sass@1.83.4)
+ vite-plugin-solid:
+ specifier: ^2.11.0
+ version: 2.11.0(solid-js@1.9.4)(vite@6.0.7(sass@1.83.4))
+
+packages:
+
+ '@ampproject/remapping@2.3.0':
+ resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
+ engines: {node: '>=6.0.0'}
+
+ '@babel/code-frame@7.26.2':
+ resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/compat-data@7.26.5':
+ resolution: {integrity: sha512-XvcZi1KWf88RVbF9wn8MN6tYFloU5qX8KjuF3E1PVBmJ9eypXfs4GRiJwLuTZL0iSnJUKn1BFPa5BPZZJyFzPg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/core@7.26.0':
+ resolution: {integrity: sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/generator@7.26.5':
+ resolution: {integrity: sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-compilation-targets@7.26.5':
+ resolution: {integrity: sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-imports@7.18.6':
+ resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-imports@7.25.9':
+ resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-transforms@7.26.0':
+ resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-plugin-utils@7.26.5':
+ resolution: {integrity: sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-string-parser@7.25.9':
+ resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-identifier@7.25.9':
+ resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-option@7.25.9':
+ resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helpers@7.26.0':
+ resolution: {integrity: sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/parser@7.26.5':
+ resolution: {integrity: sha512-SRJ4jYmXRqV1/Xc+TIVG84WjHBXKlxO9sHQnA2Pf12QQEAp1LOh6kDzNHXcUnbH1QI0FDoPPVOt+vyUDucxpaw==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ '@babel/plugin-syntax-jsx@7.25.9':
+ resolution: {integrity: sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-typescript@7.25.9':
+ resolution: {integrity: sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/template@7.25.9':
+ resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/traverse@7.26.5':
+ resolution: {integrity: sha512-rkOSPOw+AXbgtwUga3U4u8RpoK9FEFWBNAlTpcnkLFjL5CT+oyHNuUUC/xx6XefEJ16r38r8Bc/lfp6rYuHeJQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/types@7.26.5':
+ resolution: {integrity: sha512-L6mZmwFDK6Cjh1nRCLXpa6no13ZIioJDz7mdkzHv399pThrTa/k0nUlNaenOeh2kWu/iaOQYElEpKPUswUa9Vg==}
+ engines: {node: '>=6.9.0'}
+
+ '@esbuild/aix-ppc64@0.24.2':
+ resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [aix]
+
+ '@esbuild/android-arm64@0.24.2':
+ resolution: {integrity: sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [android]
+
+ '@esbuild/android-arm@0.24.2':
+ resolution: {integrity: sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [android]
+
+ '@esbuild/android-x64@0.24.2':
+ resolution: {integrity: sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [android]
+
+ '@esbuild/darwin-arm64@0.24.2':
+ resolution: {integrity: sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@esbuild/darwin-x64@0.24.2':
+ resolution: {integrity: sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@esbuild/freebsd-arm64@0.24.2':
+ resolution: {integrity: sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@esbuild/freebsd-x64@0.24.2':
+ resolution: {integrity: sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@esbuild/linux-arm64@0.24.2':
+ resolution: {integrity: sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@esbuild/linux-arm@0.24.2':
+ resolution: {integrity: sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [linux]
+
+ '@esbuild/linux-ia32@0.24.2':
+ resolution: {integrity: sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [linux]
+
+ '@esbuild/linux-loong64@0.24.2':
+ resolution: {integrity: sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==}
+ engines: {node: '>=18'}
+ cpu: [loong64]
+ os: [linux]
+
+ '@esbuild/linux-mips64el@0.24.2':
+ resolution: {integrity: sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==}
+ engines: {node: '>=18'}
+ cpu: [mips64el]
+ os: [linux]
+
+ '@esbuild/linux-ppc64@0.24.2':
+ resolution: {integrity: sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@esbuild/linux-riscv64@0.24.2':
+ resolution: {integrity: sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==}
+ engines: {node: '>=18'}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@esbuild/linux-s390x@0.24.2':
+ resolution: {integrity: sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==}
+ engines: {node: '>=18'}
+ cpu: [s390x]
+ os: [linux]
+
+ '@esbuild/linux-x64@0.24.2':
+ resolution: {integrity: sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [linux]
+
+ '@esbuild/netbsd-arm64@0.24.2':
+ resolution: {integrity: sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [netbsd]
+
+ '@esbuild/netbsd-x64@0.24.2':
+ resolution: {integrity: sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [netbsd]
+
+ '@esbuild/openbsd-arm64@0.24.2':
+ resolution: {integrity: sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openbsd]
+
+ '@esbuild/openbsd-x64@0.24.2':
+ resolution: {integrity: sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@esbuild/sunos-x64@0.24.2':
+ resolution: {integrity: sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [sunos]
+
+ '@esbuild/win32-arm64@0.24.2':
+ resolution: {integrity: sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@esbuild/win32-ia32@0.24.2':
+ resolution: {integrity: sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@esbuild/win32-x64@0.24.2':
+ resolution: {integrity: sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [win32]
+
+ '@jridgewell/gen-mapping@0.3.8':
+ resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==}
+ engines: {node: '>=6.0.0'}
+
+ '@jridgewell/resolve-uri@3.1.2':
+ resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
+ engines: {node: '>=6.0.0'}
+
+ '@jridgewell/set-array@1.2.1':
+ resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==}
+ engines: {node: '>=6.0.0'}
+
+ '@jridgewell/sourcemap-codec@1.5.0':
+ resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
+
+ '@jridgewell/trace-mapping@0.3.25':
+ resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
+
+ '@nothing-but/utils@0.17.0':
+ resolution: {integrity: sha512-TuCHcHLOqDL0SnaAxACfuRHBNRgNJcNn9X0GiH5H3YSDBVquCr3qEIG3FOQAuMyZCbu9w8nk2CHhOsn7IvhIwQ==}
+
+ '@parcel/watcher-android-arm64@2.5.0':
+ resolution: {integrity: sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [arm64]
+ os: [android]
+
+ '@parcel/watcher-darwin-arm64@2.5.0':
+ resolution: {integrity: sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@parcel/watcher-darwin-x64@2.5.0':
+ resolution: {integrity: sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@parcel/watcher-freebsd-x64@2.5.0':
+ resolution: {integrity: sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@parcel/watcher-linux-arm-glibc@2.5.0':
+ resolution: {integrity: sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [arm]
+ os: [linux]
+
+ '@parcel/watcher-linux-arm-musl@2.5.0':
+ resolution: {integrity: sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [arm]
+ os: [linux]
+
+ '@parcel/watcher-linux-arm64-glibc@2.5.0':
+ resolution: {integrity: sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@parcel/watcher-linux-arm64-musl@2.5.0':
+ resolution: {integrity: sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@parcel/watcher-linux-x64-glibc@2.5.0':
+ resolution: {integrity: sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [x64]
+ os: [linux]
+
+ '@parcel/watcher-linux-x64-musl@2.5.0':
+ resolution: {integrity: sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [x64]
+ os: [linux]
+
+ '@parcel/watcher-win32-arm64@2.5.0':
+ resolution: {integrity: sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@parcel/watcher-win32-ia32@2.5.0':
+ resolution: {integrity: sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@parcel/watcher-win32-x64@2.5.0':
+ resolution: {integrity: sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [x64]
+ os: [win32]
+
+ '@parcel/watcher@2.5.0':
+ resolution: {integrity: sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ==}
+ engines: {node: '>= 10.0.0'}
+
+ '@redocly/ajv@8.11.2':
+ resolution: {integrity: sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==}
+
+ '@redocly/config@0.20.1':
+ resolution: {integrity: sha512-TYiTDtuItiv95YMsrRxyCs1HKLrDPtTvpaD3+kDKXBnFDeJuYKZ+eHXpCr6YeN4inxfVBs7DLhHsQcs9srddyQ==}
+
+ '@redocly/openapi-core@1.27.2':
+ resolution: {integrity: sha512-qVrDc27DHpeO2NRCMeRdb4299nijKQE3BY0wrA+WUHlOLScorIi/y7JzammLk22IaTvjR9Mv9aTAdjE1aUwJnA==}
+ engines: {node: '>=14.19.0', npm: '>=7.0.0'}
+
+ '@rollup/rollup-android-arm-eabi@4.30.1':
+ resolution: {integrity: sha512-pSWY+EVt3rJ9fQ3IqlrEUtXh3cGqGtPDH1FQlNZehO2yYxCHEX1SPsz1M//NXwYfbTlcKr9WObLnJX9FsS9K1Q==}
+ cpu: [arm]
+ os: [android]
+
+ '@rollup/rollup-android-arm64@4.30.1':
+ resolution: {integrity: sha512-/NA2qXxE3D/BRjOJM8wQblmArQq1YoBVJjrjoTSBS09jgUisq7bqxNHJ8kjCHeV21W/9WDGwJEWSN0KQ2mtD/w==}
+ cpu: [arm64]
+ os: [android]
+
+ '@rollup/rollup-darwin-arm64@4.30.1':
+ resolution: {integrity: sha512-r7FQIXD7gB0WJ5mokTUgUWPl0eYIH0wnxqeSAhuIwvnnpjdVB8cRRClyKLQr7lgzjctkbp5KmswWszlwYln03Q==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rollup/rollup-darwin-x64@4.30.1':
+ resolution: {integrity: sha512-x78BavIwSH6sqfP2xeI1hd1GpHL8J4W2BXcVM/5KYKoAD3nNsfitQhvWSw+TFtQTLZ9OmlF+FEInEHyubut2OA==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rollup/rollup-freebsd-arm64@4.30.1':
+ resolution: {integrity: sha512-HYTlUAjbO1z8ywxsDFWADfTRfTIIy/oUlfIDmlHYmjUP2QRDTzBuWXc9O4CXM+bo9qfiCclmHk1x4ogBjOUpUQ==}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@rollup/rollup-freebsd-x64@4.30.1':
+ resolution: {integrity: sha512-1MEdGqogQLccphhX5myCJqeGNYTNcmTyaic9S7CG3JhwuIByJ7J05vGbZxsizQthP1xpVx7kd3o31eOogfEirw==}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.30.1':
+ resolution: {integrity: sha512-PaMRNBSqCx7K3Wc9QZkFx5+CX27WFpAMxJNiYGAXfmMIKC7jstlr32UhTgK6T07OtqR+wYlWm9IxzennjnvdJg==}
+ cpu: [arm]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm-musleabihf@4.30.1':
+ resolution: {integrity: sha512-B8Rcyj9AV7ZlEFqvB5BubG5iO6ANDsRKlhIxySXcF1axXYUyqwBok+XZPgIYGBgs7LDXfWfifxhw0Ik57T0Yug==}
+ cpu: [arm]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm64-gnu@4.30.1':
+ resolution: {integrity: sha512-hqVyueGxAj3cBKrAI4aFHLV+h0Lv5VgWZs9CUGqr1z0fZtlADVV1YPOij6AhcK5An33EXaxnDLmJdQikcn5NEw==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm64-musl@4.30.1':
+ resolution: {integrity: sha512-i4Ab2vnvS1AE1PyOIGp2kXni69gU2DAUVt6FSXeIqUCPIR3ZlheMW3oP2JkukDfu3PsexYRbOiJrY+yVNSk9oA==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rollup/rollup-linux-loongarch64-gnu@4.30.1':
+ resolution: {integrity: sha512-fARcF5g296snX0oLGkVxPmysetwUk2zmHcca+e9ObOovBR++9ZPOhqFUM61UUZ2EYpXVPN1redgqVoBB34nTpQ==}
+ cpu: [loong64]
+ os: [linux]
+
+ '@rollup/rollup-linux-powerpc64le-gnu@4.30.1':
+ resolution: {integrity: sha512-GLrZraoO3wVT4uFXh67ElpwQY0DIygxdv0BNW9Hkm3X34wu+BkqrDrkcsIapAY+N2ATEbvak0XQ9gxZtCIA5Rw==}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@rollup/rollup-linux-riscv64-gnu@4.30.1':
+ resolution: {integrity: sha512-0WKLaAUUHKBtll0wvOmh6yh3S0wSU9+yas923JIChfxOaaBarmb/lBKPF0w/+jTVozFnOXJeRGZ8NvOxvk/jcw==}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@rollup/rollup-linux-s390x-gnu@4.30.1':
+ resolution: {integrity: sha512-GWFs97Ruxo5Bt+cvVTQkOJ6TIx0xJDD/bMAOXWJg8TCSTEK8RnFeOeiFTxKniTc4vMIaWvCplMAFBt9miGxgkA==}
+ cpu: [s390x]
+ os: [linux]
+
+ '@rollup/rollup-linux-x64-gnu@4.30.1':
+ resolution: {integrity: sha512-UtgGb7QGgXDIO+tqqJ5oZRGHsDLO8SlpE4MhqpY9Llpzi5rJMvrK6ZGhsRCST2abZdBqIBeXW6WPD5fGK5SDwg==}
+ cpu: [x64]
+ os: [linux]
+
+ '@rollup/rollup-linux-x64-musl@4.30.1':
+ resolution: {integrity: sha512-V9U8Ey2UqmQsBT+xTOeMzPzwDzyXmnAoO4edZhL7INkwQcaW1Ckv3WJX3qrrp/VHaDkEWIBWhRwP47r8cdrOow==}
+ cpu: [x64]
+ os: [linux]
+
+ '@rollup/rollup-win32-arm64-msvc@4.30.1':
+ resolution: {integrity: sha512-WabtHWiPaFF47W3PkHnjbmWawnX/aE57K47ZDT1BXTS5GgrBUEpvOzq0FI0V/UYzQJgdb8XlhVNH8/fwV8xDjw==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@rollup/rollup-win32-ia32-msvc@4.30.1':
+ resolution: {integrity: sha512-pxHAU+Zv39hLUTdQQHUVHf4P+0C47y/ZloorHpzs2SXMRqeAWmGghzAhfOlzFHHwjvgokdFAhC4V+6kC1lRRfw==}
+ cpu: [ia32]
+ os: [win32]
+
+ '@rollup/rollup-win32-x64-msvc@4.30.1':
+ resolution: {integrity: sha512-D6qjsXGcvhTjv0kI4fU8tUuBDF/Ueee4SVX79VfNDXZa64TfCW1Slkb6Z7O1p7vflqZjcmOVdZlqf8gvJxc6og==}
+ cpu: [x64]
+ os: [win32]
+
+ '@solid-devtools/debugger@0.26.0':
+ resolution: {integrity: sha512-36QxZ+s/lY60E+Pb9q0eTsdqgaog4c823WIj5dC2LFdGrGXbVGBQEj6k7CgvMnEETdwndrd0Fm72fQyYPlZrVA==}
+ peerDependencies:
+ solid-js: ^1.9.0
+
+ '@solid-devtools/shared@0.19.0':
+ resolution: {integrity: sha512-OGo6l84f9X5YEAqSEM4Xl94+xKXSqmACMzKWsAqO0BStLBMVL0vIVu286AQk5XkNxn11/EB9wrdkZc9GUzKlxA==}
+ peerDependencies:
+ solid-js: ^1.9.0
+
+ '@solid-primitives/bounds@0.0.122':
+ resolution: {integrity: sha512-kUq/IprOdFr/rg2upon5lQGOoTnDAmxQS4ASKK2l+VwoKSctdPwgu/4qJxEITZikL+nB0myYZzBZWptySV0cRg==}
+ peerDependencies:
+ solid-js: ^1.6.12
+
+ '@solid-primitives/cursor@0.0.115':
+ resolution: {integrity: sha512-8nEmUN/sacXPChwuJOAi6Yi6VnxthW/Jk8VGvvcF38AenjUvOA6FHI6AkJILuFXjQw1PGxia1YbH/Mn77dPiOA==}
+ peerDependencies:
+ solid-js: ^1.6.12
+
+ '@solid-primitives/event-listener@2.3.3':
+ resolution: {integrity: sha512-DAJbl+F0wrFW2xmcV8dKMBhk9QLVLuBSW+TR4JmIfTaObxd13PuL7nqaXnaYKDWOYa6otB00qcCUIGbuIhSUgQ==}
+ peerDependencies:
+ solid-js: ^1.6.12
+
+ '@solid-primitives/keyboard@1.2.8':
+ resolution: {integrity: sha512-pJtcbkjozS6L1xvTht9rPpyPpX55nAkfBzbFWdf3y0Suwh6qClTibvvObzKOf7uzQ+8aZRDH4LsoGmbTKXtJjQ==}
+ peerDependencies:
+ solid-js: ^1.6.12
+
+ '@solid-primitives/media@2.2.10':
+ resolution: {integrity: sha512-zICx9lXvevycyHmzUp1AfrxmUsF27JGvDygf51mHUpvy/Y2SmxkM6UHKstBDlRSpLUhPTnF0iHCfdfne6g4Fow==}
+ peerDependencies:
+ solid-js: ^1.6.12
+
+ '@solid-primitives/platform@0.1.2':
+ resolution: {integrity: sha512-sSxcZfuUrtxcwV0vdjmGnZQcflACzMfLriVeIIWXKp8hzaS3Or3tO6EFQkTd3L8T5dTq+kTtLvPscXIpL0Wzdg==}
+ peerDependencies:
+ solid-js: ^1.6.12
+
+ '@solid-primitives/refs@1.0.8':
+ resolution: {integrity: sha512-+jIsWG8/nYvhaCoG2Vg6CJOLgTmPKFbaCrNQKWfChalgUf9WrVxWw0CdJb3yX15n5lUcQ0jBo6qYtuVVmBLpBw==}
+ peerDependencies:
+ solid-js: ^1.6.12
+
+ '@solid-primitives/resize-observer@2.0.27':
+ resolution: {integrity: sha512-RmusjHqoA4U6MKI/T9yBJVDttASHpWBki1+YwM9zGXEDBqbysTa3lZpnlB244LzphQmobgeXVS78v0KtXVsF9g==}
+ peerDependencies:
+ solid-js: ^1.6.12
+
+ '@solid-primitives/rootless@1.4.5':
+ resolution: {integrity: sha512-GFJE9GC3ojx0aUKqAUZmQPyU8fOVMtnVNrkdk2yS4kd17WqVSpXpoTmo9CnOwA+PG7FTzdIkogvfLQSLs4lrww==}
+ peerDependencies:
+ solid-js: ^1.6.12
+
+ '@solid-primitives/scheduled@1.4.4':
+ resolution: {integrity: sha512-BTGdFP7t+s7RSak+s1u0eTix4lHP23MrbGkgQTFlt1E+4fmnD/bEx3ZfNW7Grylz3GXgKyXrgDKA7jQ/wuWKgA==}
+ peerDependencies:
+ solid-js: ^1.6.12
+
+ '@solid-primitives/static-store@0.0.8':
+ resolution: {integrity: sha512-ZecE4BqY0oBk0YG00nzaAWO5Mjcny8Fc06CdbXadH9T9lzq/9GefqcSe/5AtdXqjvY/DtJ5C6CkcjPZO0o/eqg==}
+ peerDependencies:
+ solid-js: ^1.6.12
+
+ '@solid-primitives/static-store@0.0.9':
+ resolution: {integrity: sha512-8zaTXTEnQFqdwfkqWmGVb/OYgSTbRgxJSWQNfLuA+KnuW4RzTRQE2jzgnNJjJjaloruv9EHGvikmJzQJ5aOrEw==}
+ peerDependencies:
+ solid-js: ^1.6.12
+
+ '@solid-primitives/styles@0.0.114':
+ resolution: {integrity: sha512-SFXr16mgr6LvZAIj6L7i59HHg+prAmIF8VP/U3C6jSHz68Eh1G71vaWr9vlJVpy/j6bh1N8QUzu5CgtvIC92OQ==}
+ peerDependencies:
+ solid-js: ^1.6.12
+
+ '@solid-primitives/utils@6.2.3':
+ resolution: {integrity: sha512-CqAwKb2T5Vi72+rhebSsqNZ9o67buYRdEJrIFzRXz3U59QqezuuxPsyzTSVCacwS5Pf109VRsgCJQoxKRoECZQ==}
+ peerDependencies:
+ solid-js: ^1.6.12
+
+ '@solidjs/router@0.15.3':
+ resolution: {integrity: sha512-iEbW8UKok2Oio7o6Y4VTzLj+KFCmQPGEpm1fS3xixwFBdclFVBvaQVeibl1jys4cujfAK5Kn6+uG2uBm3lxOMw==}
+ peerDependencies:
+ solid-js: ^1.8.6
+
+ '@svgdotjs/svg.draggable.js@3.0.5':
+ resolution: {integrity: sha512-ljL/fB0tAjRfFOJGhXpr7rEx9DJ6D7Pxt3AXvgxjEM17g6wK3Ho9nXhntraOMx8JLZdq4NBMjokeXMvnQzJVYA==}
+ peerDependencies:
+ '@svgdotjs/svg.js': ^3.2.4
+
+ '@svgdotjs/svg.filter.js@3.0.8':
+ resolution: {integrity: sha512-YshF2YDaeRA2StyzAs5nUPrev7npQ38oWD0eTRwnsciSL2KrRPMoUw8BzjIXItb3+dccKGTX3IQOd2NFzmHkog==}
+ engines: {node: '>= 0.8.0'}
+
+ '@svgdotjs/svg.js@3.2.4':
+ resolution: {integrity: sha512-BjJ/7vWNowlX3Z8O4ywT58DqbNRyYlkk6Yz/D13aB7hGmfQTvGX4Tkgtm/ApYlu9M7lCQi15xUEidqMUmdMYwg==}
+
+ '@svgdotjs/svg.resize.js@2.0.5':
+ resolution: {integrity: sha512-4heRW4B1QrJeENfi7326lUPYBCevj78FJs8kfeDxn5st0IYPIRXoTtOSYvTzFWgaWWXd3YCDE6ao4fmv91RthA==}
+ engines: {node: '>= 14.18'}
+ peerDependencies:
+ '@svgdotjs/svg.js': ^3.2.4
+ '@svgdotjs/svg.select.js': ^4.0.1
+
+ '@svgdotjs/svg.select.js@4.0.2':
+ resolution: {integrity: sha512-5gWdrvoQX3keo03SCmgaBbD+kFftq0F/f2bzCbNnpkkvW6tk4rl4MakORzFuNjvXPWwB4az9GwuvVxQVnjaK2g==}
+ engines: {node: '>= 14.18'}
+ peerDependencies:
+ '@svgdotjs/svg.js': ^3.2.4
+
+ '@types/babel__core@7.20.5':
+ resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
+
+ '@types/babel__generator@7.6.8':
+ resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==}
+
+ '@types/babel__template@7.4.4':
+ resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==}
+
+ '@types/babel__traverse@7.20.6':
+ resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==}
+
+ '@types/estree@1.0.6':
+ resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==}
+
+ '@yr/monotone-cubic-spline@1.0.3':
+ resolution: {integrity: sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==}
+
+ agent-base@7.1.3:
+ resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==}
+ engines: {node: '>= 14'}
+
+ ansi-colors@4.1.3:
+ resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
+ engines: {node: '>=6'}
+
+ apexcharts@4.3.0:
+ resolution: {integrity: sha512-PfvZQpv91T68hzry9l5zP3Gip7sQvF0nFK91uCBrswIKX7rbIdbVNS4fOks9m9yP3Ppgs6LHgU2M/mjoG4NM0A==}
+
+ argparse@2.0.1:
+ resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+
+ babel-plugin-jsx-dom-expressions@0.39.5:
+ resolution: {integrity: sha512-dwyVkszHRsZCXfFusu3xq1DJS7twhgLrjEpMC1gtTfJG1xSrMMKWWhdl1SFFFNXrvYDsoHiRxSbku/TzLxHNxg==}
+ peerDependencies:
+ '@babel/core': ^7.20.12
+
+ babel-preset-solid@1.9.3:
+ resolution: {integrity: sha512-jvlx5wDp8s+bEF9sGFw/84SInXOA51ttkUEroQziKMbxplXThVKt83qB6bDTa1HuLNatdU9FHpFOiQWs1tLQIg==}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ balanced-match@1.0.2:
+ resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
+
+ brace-expansion@2.0.1:
+ resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
+
+ braces@3.0.3:
+ resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
+ engines: {node: '>=8'}
+
+ browserslist@4.24.4:
+ resolution: {integrity: sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==}
+ engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
+ hasBin: true
+
+ caniuse-lite@1.0.30001692:
+ resolution: {integrity: sha512-A95VKan0kdtrsnMubMKxEKUKImOPSuCpYgxSQBo036P5YYgVIcOYJEgt/txJWqObiRQeISNCfef9nvlQ0vbV7A==}
+
+ change-case@5.4.4:
+ resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==}
+
+ chokidar@4.0.3:
+ resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
+ engines: {node: '>= 14.16.0'}
+
+ colorette@1.4.0:
+ resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==}
+
+ convert-source-map@2.0.0:
+ resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+
+ csstype@3.1.3:
+ resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
+
+ debug@4.4.0:
+ resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
+ defu@6.1.4:
+ resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
+
+ detect-libc@1.0.3:
+ resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==}
+ engines: {node: '>=0.10'}
+ hasBin: true
+
+ electron-to-chromium@1.5.83:
+ resolution: {integrity: sha512-LcUDPqSt+V0QmI47XLzZrz5OqILSMGsPFkDYus22rIbgorSvBYEFqq854ltTmUdHkY92FSdAAvsh4jWEULMdfQ==}
+
+ entities@4.5.0:
+ resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
+ engines: {node: '>=0.12'}
+
+ esbuild@0.24.2:
+ resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ escalade@3.2.0:
+ resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
+ engines: {node: '>=6'}
+
+ fast-deep-equal@3.1.3:
+ resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
+
+ fill-range@7.1.1:
+ resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
+ engines: {node: '>=8'}
+
+ fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
+ gensync@1.0.0-beta.2:
+ resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
+ engines: {node: '>=6.9.0'}
+
+ globals@11.12.0:
+ resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==}
+ engines: {node: '>=4'}
+
+ html-entities@2.3.3:
+ resolution: {integrity: sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==}
+
+ https-proxy-agent@7.0.6:
+ resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
+ engines: {node: '>= 14'}
+
+ immutable@5.0.3:
+ resolution: {integrity: sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==}
+
+ index-to-position@0.1.2:
+ resolution: {integrity: sha512-MWDKS3AS1bGCHLBA2VLImJz42f7bJh8wQsTGCzI3j519/CASStoDONUBVz2I/VID0MpiX3SGSnbOD2xUalbE5g==}
+ engines: {node: '>=18'}
+
+ is-extglob@2.1.1:
+ resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
+ engines: {node: '>=0.10.0'}
+
+ is-glob@4.0.3:
+ resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
+ engines: {node: '>=0.10.0'}
+
+ is-number@7.0.0:
+ resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
+ engines: {node: '>=0.12.0'}
+
+ is-what@4.1.16:
+ resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==}
+ engines: {node: '>=12.13'}
+
+ js-levenshtein@1.1.6:
+ resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==}
+ engines: {node: '>=0.10.0'}
+
+ js-tokens@4.0.0:
+ resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+
+ js-yaml@4.1.0:
+ resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
+ hasBin: true
+
+ jsesc@3.1.0:
+ resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ json-schema-traverse@1.0.0:
+ resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
+
+ json5@2.2.3:
+ resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ lru-cache@5.1.1:
+ resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
+
+ merge-anything@5.1.7:
+ resolution: {integrity: sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==}
+ engines: {node: '>=12.13'}
+
+ micromatch@4.0.8:
+ resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
+ engines: {node: '>=8.6'}
+
+ minimatch@5.1.6:
+ resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==}
+ engines: {node: '>=10'}
+
+ moment@2.30.1:
+ resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==}
+
+ ms@2.1.3:
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
+ nanoid@3.3.8:
+ resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
+ node-addon-api@7.1.1:
+ resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
+
+ node-fetch@2.7.0:
+ resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
+ engines: {node: 4.x || >=6.0.0}
+ peerDependencies:
+ encoding: ^0.1.0
+ peerDependenciesMeta:
+ encoding:
+ optional: true
+
+ node-releases@2.0.19:
+ resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
+
+ openapi-fetch@0.13.4:
+ resolution: {integrity: sha512-JHX7UYjLEiHuQGCPxa3CCCIqe/nc4bTIF9c4UYVC8BegAbWoS3g4gJxKX5XcG7UtYQs2060kY6DH64KkvNZahg==}
+
+ openapi-typescript-helpers@0.0.15:
+ resolution: {integrity: sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==}
+
+ openapi-typescript@7.5.2:
+ resolution: {integrity: sha512-W/QXuQz0Fa3bGY6LKoqTCgrSX+xI/ST+E5RXo2WBmp3WwgXCWKDJPHv5GZmElF4yLCccnqYsakBDOJikHZYGRw==}
+ hasBin: true
+ peerDependencies:
+ typescript: ^5.x
+
+ parse-json@8.1.0:
+ resolution: {integrity: sha512-rum1bPifK5SSar35Z6EKZuYPJx85pkNaFrxBK3mwdfSJ1/WKbYrjoW/zTPSjRRamfmVX1ACBIdFAO0VRErW/EA==}
+ engines: {node: '>=18'}
+
+ parse5@7.2.1:
+ resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==}
+
+ picocolors@1.1.1:
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+ picomatch@2.3.1:
+ resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
+ engines: {node: '>=8.6'}
+
+ pluralize@8.0.0:
+ resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
+ engines: {node: '>=4'}
+
+ postcss@8.5.1:
+ resolution: {integrity: sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==}
+ engines: {node: ^10 || ^12 || >=14}
+
+ readdirp@4.1.1:
+ resolution: {integrity: sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==}
+ engines: {node: '>= 14.18.0'}
+
+ require-from-string@2.0.2:
+ resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
+ engines: {node: '>=0.10.0'}
+
+ rollup@4.30.1:
+ resolution: {integrity: sha512-mlJ4glW020fPuLi7DkM/lN97mYEZGWeqBnrljzN0gs7GLctqX3lNWxKQ7Gl712UAX+6fog/L3jh4gb7R6aVi3w==}
+ engines: {node: '>=18.0.0', npm: '>=8.0.0'}
+ hasBin: true
+
+ sass@1.83.4:
+ resolution: {integrity: sha512-B1bozCeNQiOgDcLd33e2Cs2U60wZwjUUXzh900ZyQF5qUasvMdDZYbQ566LJu7cqR+sAHlAfO6RMkaID5s6qpA==}
+ engines: {node: '>=14.0.0'}
+ hasBin: true
+
+ semver@6.3.1:
+ resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
+ hasBin: true
+
+ seroval-plugins@1.2.0:
+ resolution: {integrity: sha512-hULTbfzSe81jGWLH8TAJjkEvw6JWMqOo9Uq+4V4vg+HNq53hyHldM9ZOfjdzokcFysiTp9aFdV2vJpZFqKeDjQ==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ seroval: ^1.0
+
+ seroval@1.2.0:
+ resolution: {integrity: sha512-GURoU99ko2UiAgUC3qDCk59Jb3Ss4Po8VIMGkG8j5PFo2Q7y0YSMP8QG9NuL/fJCoTz9V1XZUbpNIMXPOfaGpA==}
+ engines: {node: '>=10'}
+
+ solid-apexcharts@0.4.0:
+ resolution: {integrity: sha512-b3tjFaYNF2ggvFq+VSaxxj2ocxfKKkB7jnWL60uDaokv2A3RwFXXzmPYZMkN80FQaLnxeNzkUdre/S9HgshLnA==}
+ engines: {node: '>=18', pnpm: '>=8.6.0'}
+ peerDependencies:
+ apexcharts: ^4.0.0
+ solid-js: ^1.6.0
+
+ solid-devtools@0.33.0:
+ resolution: {integrity: sha512-xRB4Jhgns3dBuM/s0j70BpXKy77sNjISud9xXBv60qC4cnJ/TcuVHI1t+05luj1BEKJVQSokqIaVoZWcjqA9yw==}
+ peerDependencies:
+ solid-js: ^1.9.0
+ vite: ^2.2.3 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0
+ peerDependenciesMeta:
+ vite:
+ optional: true
+
+ solid-js@1.9.4:
+ resolution: {integrity: sha512-ipQl8FJ31bFUoBNScDQTG3BjN6+9Rg+Q+f10bUbnO6EOTTf5NGerJeHc7wyu5I4RMHEl/WwZwUmy/PTRgxxZ8g==}
+
+ solid-refresh@0.6.3:
+ resolution: {integrity: sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA==}
+ peerDependencies:
+ solid-js: ^1.3
+
+ source-map-js@1.2.1:
+ resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ engines: {node: '>=0.10.0'}
+
+ supports-color@9.4.0:
+ resolution: {integrity: sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==}
+ engines: {node: '>=12'}
+
+ to-regex-range@5.0.1:
+ resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
+ engines: {node: '>=8.0'}
+
+ tr46@0.0.3:
+ resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
+
+ type-fest@4.32.0:
+ resolution: {integrity: sha512-rfgpoi08xagF3JSdtJlCwMq9DGNDE0IMh3Mkpc1wUypg9vPi786AiqeBBKcqvIkq42azsBM85N490fyZjeUftw==}
+ engines: {node: '>=16'}
+
+ typescript@5.7.3:
+ resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
+ update-browserslist-db@1.1.2:
+ resolution: {integrity: sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==}
+ hasBin: true
+ peerDependencies:
+ browserslist: '>= 4.21.0'
+
+ uri-js-replace@1.0.1:
+ resolution: {integrity: sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==}
+
+ validate-html-nesting@1.2.2:
+ resolution: {integrity: sha512-hGdgQozCsQJMyfK5urgFcWEqsSSrK63Awe0t/IMR0bZ0QMtnuaiHzThW81guu3qx9abLi99NEuiaN6P9gVYsNg==}
+
+ vite-plugin-solid@2.11.0:
+ resolution: {integrity: sha512-G+NiwDj4EAeUE0wt3Ur9f+Lt9oMUuLd0FIxYuqwJSqRacKQRteCwUFzNy8zMEt88xWokngQhiFjfJMhjc1fDXw==}
+ peerDependencies:
+ '@testing-library/jest-dom': ^5.16.6 || ^5.17.0 || ^6.*
+ solid-js: ^1.7.2
+ vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0
+ peerDependenciesMeta:
+ '@testing-library/jest-dom':
+ optional: true
+
+ vite@6.0.7:
+ resolution: {integrity: sha512-RDt8r/7qx9940f8FcOIAH9PTViRrghKaK2K1jY3RaAURrEUbm9Du1mJ72G+jlhtG3WwodnfzY8ORQZbBavZEAQ==}
+ engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
+ jiti: '>=1.21.0'
+ less: '*'
+ lightningcss: ^1.21.0
+ sass: '*'
+ sass-embedded: '*'
+ stylus: '*'
+ sugarss: '*'
+ terser: ^5.16.0
+ tsx: ^4.8.1
+ yaml: ^2.4.2
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ jiti:
+ optional: true
+ less:
+ optional: true
+ lightningcss:
+ optional: true
+ sass:
+ optional: true
+ sass-embedded:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+ tsx:
+ optional: true
+ yaml:
+ optional: true
+
+ vitefu@1.0.5:
+ resolution: {integrity: sha512-h4Vflt9gxODPFNGPwp4zAMZRpZR7eslzwH2c5hn5kNZ5rhnKyRJ50U+yGCdc2IRaBs8O4haIgLNGrV5CrpMsCA==}
+ peerDependencies:
+ vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0
+ peerDependenciesMeta:
+ vite:
+ optional: true
+
+ webidl-conversions@3.0.1:
+ resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
+
+ whatwg-url@5.0.0:
+ resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
+
+ yallist@3.1.1:
+ resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
+
+ yaml-ast-parser@0.0.43:
+ resolution: {integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==}
+
+ yargs-parser@21.1.1:
+ resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
+ engines: {node: '>=12'}
+
+snapshots:
+
+ '@ampproject/remapping@2.3.0':
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.8
+ '@jridgewell/trace-mapping': 0.3.25
+
+ '@babel/code-frame@7.26.2':
+ dependencies:
+ '@babel/helper-validator-identifier': 7.25.9
+ js-tokens: 4.0.0
+ picocolors: 1.1.1
+
+ '@babel/compat-data@7.26.5': {}
+
+ '@babel/core@7.26.0':
+ dependencies:
+ '@ampproject/remapping': 2.3.0
+ '@babel/code-frame': 7.26.2
+ '@babel/generator': 7.26.5
+ '@babel/helper-compilation-targets': 7.26.5
+ '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0)
+ '@babel/helpers': 7.26.0
+ '@babel/parser': 7.26.5
+ '@babel/template': 7.25.9
+ '@babel/traverse': 7.26.5
+ '@babel/types': 7.26.5
+ convert-source-map: 2.0.0
+ debug: 4.4.0(supports-color@9.4.0)
+ gensync: 1.0.0-beta.2
+ json5: 2.2.3
+ semver: 6.3.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/generator@7.26.5':
+ dependencies:
+ '@babel/parser': 7.26.5
+ '@babel/types': 7.26.5
+ '@jridgewell/gen-mapping': 0.3.8
+ '@jridgewell/trace-mapping': 0.3.25
+ jsesc: 3.1.0
+
+ '@babel/helper-compilation-targets@7.26.5':
+ dependencies:
+ '@babel/compat-data': 7.26.5
+ '@babel/helper-validator-option': 7.25.9
+ browserslist: 4.24.4
+ lru-cache: 5.1.1
+ semver: 6.3.1
+
+ '@babel/helper-module-imports@7.18.6':
+ dependencies:
+ '@babel/types': 7.26.5
+
+ '@babel/helper-module-imports@7.25.9':
+ dependencies:
+ '@babel/traverse': 7.26.5
+ '@babel/types': 7.26.5
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-module-imports': 7.25.9
+ '@babel/helper-validator-identifier': 7.25.9
+ '@babel/traverse': 7.26.5
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-plugin-utils@7.26.5': {}
+
+ '@babel/helper-string-parser@7.25.9': {}
+
+ '@babel/helper-validator-identifier@7.25.9': {}
+
+ '@babel/helper-validator-option@7.25.9': {}
+
+ '@babel/helpers@7.26.0':
+ dependencies:
+ '@babel/template': 7.25.9
+ '@babel/types': 7.26.5
+
+ '@babel/parser@7.26.5':
+ dependencies:
+ '@babel/types': 7.26.5
+
+ '@babel/plugin-syntax-jsx@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-plugin-utils': 7.26.5
+
+ '@babel/plugin-syntax-typescript@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-plugin-utils': 7.26.5
+
+ '@babel/template@7.25.9':
+ dependencies:
+ '@babel/code-frame': 7.26.2
+ '@babel/parser': 7.26.5
+ '@babel/types': 7.26.5
+
+ '@babel/traverse@7.26.5':
+ dependencies:
+ '@babel/code-frame': 7.26.2
+ '@babel/generator': 7.26.5
+ '@babel/parser': 7.26.5
+ '@babel/template': 7.25.9
+ '@babel/types': 7.26.5
+ debug: 4.4.0(supports-color@9.4.0)
+ globals: 11.12.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/types@7.26.5':
+ dependencies:
+ '@babel/helper-string-parser': 7.25.9
+ '@babel/helper-validator-identifier': 7.25.9
+
+ '@esbuild/aix-ppc64@0.24.2':
+ optional: true
+
+ '@esbuild/android-arm64@0.24.2':
+ optional: true
+
+ '@esbuild/android-arm@0.24.2':
+ optional: true
+
+ '@esbuild/android-x64@0.24.2':
+ optional: true
+
+ '@esbuild/darwin-arm64@0.24.2':
+ optional: true
+
+ '@esbuild/darwin-x64@0.24.2':
+ optional: true
+
+ '@esbuild/freebsd-arm64@0.24.2':
+ optional: true
+
+ '@esbuild/freebsd-x64@0.24.2':
+ optional: true
+
+ '@esbuild/linux-arm64@0.24.2':
+ optional: true
+
+ '@esbuild/linux-arm@0.24.2':
+ optional: true
+
+ '@esbuild/linux-ia32@0.24.2':
+ optional: true
+
+ '@esbuild/linux-loong64@0.24.2':
+ optional: true
+
+ '@esbuild/linux-mips64el@0.24.2':
+ optional: true
+
+ '@esbuild/linux-ppc64@0.24.2':
+ optional: true
+
+ '@esbuild/linux-riscv64@0.24.2':
+ optional: true
+
+ '@esbuild/linux-s390x@0.24.2':
+ optional: true
+
+ '@esbuild/linux-x64@0.24.2':
+ optional: true
+
+ '@esbuild/netbsd-arm64@0.24.2':
+ optional: true
+
+ '@esbuild/netbsd-x64@0.24.2':
+ optional: true
+
+ '@esbuild/openbsd-arm64@0.24.2':
+ optional: true
+
+ '@esbuild/openbsd-x64@0.24.2':
+ optional: true
+
+ '@esbuild/sunos-x64@0.24.2':
+ optional: true
+
+ '@esbuild/win32-arm64@0.24.2':
+ optional: true
+
+ '@esbuild/win32-ia32@0.24.2':
+ optional: true
+
+ '@esbuild/win32-x64@0.24.2':
+ optional: true
+
+ '@jridgewell/gen-mapping@0.3.8':
+ dependencies:
+ '@jridgewell/set-array': 1.2.1
+ '@jridgewell/sourcemap-codec': 1.5.0
+ '@jridgewell/trace-mapping': 0.3.25
+
+ '@jridgewell/resolve-uri@3.1.2': {}
+
+ '@jridgewell/set-array@1.2.1': {}
+
+ '@jridgewell/sourcemap-codec@1.5.0': {}
+
+ '@jridgewell/trace-mapping@0.3.25':
+ dependencies:
+ '@jridgewell/resolve-uri': 3.1.2
+ '@jridgewell/sourcemap-codec': 1.5.0
+
+ '@nothing-but/utils@0.17.0': {}
+
+ '@parcel/watcher-android-arm64@2.5.0':
+ optional: true
+
+ '@parcel/watcher-darwin-arm64@2.5.0':
+ optional: true
+
+ '@parcel/watcher-darwin-x64@2.5.0':
+ optional: true
+
+ '@parcel/watcher-freebsd-x64@2.5.0':
+ optional: true
+
+ '@parcel/watcher-linux-arm-glibc@2.5.0':
+ optional: true
+
+ '@parcel/watcher-linux-arm-musl@2.5.0':
+ optional: true
+
+ '@parcel/watcher-linux-arm64-glibc@2.5.0':
+ optional: true
+
+ '@parcel/watcher-linux-arm64-musl@2.5.0':
+ optional: true
+
+ '@parcel/watcher-linux-x64-glibc@2.5.0':
+ optional: true
+
+ '@parcel/watcher-linux-x64-musl@2.5.0':
+ optional: true
+
+ '@parcel/watcher-win32-arm64@2.5.0':
+ optional: true
+
+ '@parcel/watcher-win32-ia32@2.5.0':
+ optional: true
+
+ '@parcel/watcher-win32-x64@2.5.0':
+ optional: true
+
+ '@parcel/watcher@2.5.0':
+ dependencies:
+ detect-libc: 1.0.3
+ is-glob: 4.0.3
+ micromatch: 4.0.8
+ node-addon-api: 7.1.1
+ optionalDependencies:
+ '@parcel/watcher-android-arm64': 2.5.0
+ '@parcel/watcher-darwin-arm64': 2.5.0
+ '@parcel/watcher-darwin-x64': 2.5.0
+ '@parcel/watcher-freebsd-x64': 2.5.0
+ '@parcel/watcher-linux-arm-glibc': 2.5.0
+ '@parcel/watcher-linux-arm-musl': 2.5.0
+ '@parcel/watcher-linux-arm64-glibc': 2.5.0
+ '@parcel/watcher-linux-arm64-musl': 2.5.0
+ '@parcel/watcher-linux-x64-glibc': 2.5.0
+ '@parcel/watcher-linux-x64-musl': 2.5.0
+ '@parcel/watcher-win32-arm64': 2.5.0
+ '@parcel/watcher-win32-ia32': 2.5.0
+ '@parcel/watcher-win32-x64': 2.5.0
+ optional: true
+
+ '@redocly/ajv@8.11.2':
+ dependencies:
+ fast-deep-equal: 3.1.3
+ json-schema-traverse: 1.0.0
+ require-from-string: 2.0.2
+ uri-js-replace: 1.0.1
+
+ '@redocly/config@0.20.1': {}
+
+ '@redocly/openapi-core@1.27.2(supports-color@9.4.0)':
+ dependencies:
+ '@redocly/ajv': 8.11.2
+ '@redocly/config': 0.20.1
+ colorette: 1.4.0
+ https-proxy-agent: 7.0.6(supports-color@9.4.0)
+ js-levenshtein: 1.1.6
+ js-yaml: 4.1.0
+ minimatch: 5.1.6
+ node-fetch: 2.7.0
+ pluralize: 8.0.0
+ yaml-ast-parser: 0.0.43
+ transitivePeerDependencies:
+ - encoding
+ - supports-color
+
+ '@rollup/rollup-android-arm-eabi@4.30.1':
+ optional: true
+
+ '@rollup/rollup-android-arm64@4.30.1':
+ optional: true
+
+ '@rollup/rollup-darwin-arm64@4.30.1':
+ optional: true
+
+ '@rollup/rollup-darwin-x64@4.30.1':
+ optional: true
+
+ '@rollup/rollup-freebsd-arm64@4.30.1':
+ optional: true
+
+ '@rollup/rollup-freebsd-x64@4.30.1':
+ optional: true
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.30.1':
+ optional: true
+
+ '@rollup/rollup-linux-arm-musleabihf@4.30.1':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-gnu@4.30.1':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-musl@4.30.1':
+ optional: true
+
+ '@rollup/rollup-linux-loongarch64-gnu@4.30.1':
+ optional: true
+
+ '@rollup/rollup-linux-powerpc64le-gnu@4.30.1':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-gnu@4.30.1':
+ optional: true
+
+ '@rollup/rollup-linux-s390x-gnu@4.30.1':
+ optional: true
+
+ '@rollup/rollup-linux-x64-gnu@4.30.1':
+ optional: true
+
+ '@rollup/rollup-linux-x64-musl@4.30.1':
+ optional: true
+
+ '@rollup/rollup-win32-arm64-msvc@4.30.1':
+ optional: true
+
+ '@rollup/rollup-win32-ia32-msvc@4.30.1':
+ optional: true
+
+ '@rollup/rollup-win32-x64-msvc@4.30.1':
+ optional: true
+
+ '@solid-devtools/debugger@0.26.0(solid-js@1.9.4)':
+ dependencies:
+ '@nothing-but/utils': 0.17.0
+ '@solid-devtools/shared': 0.19.0(solid-js@1.9.4)
+ '@solid-primitives/bounds': 0.0.122(solid-js@1.9.4)
+ '@solid-primitives/cursor': 0.0.115(solid-js@1.9.4)
+ '@solid-primitives/event-listener': 2.3.3(solid-js@1.9.4)
+ '@solid-primitives/keyboard': 1.2.8(solid-js@1.9.4)
+ '@solid-primitives/platform': 0.1.2(solid-js@1.9.4)
+ '@solid-primitives/rootless': 1.4.5(solid-js@1.9.4)
+ '@solid-primitives/scheduled': 1.4.4(solid-js@1.9.4)
+ '@solid-primitives/static-store': 0.0.8(solid-js@1.9.4)
+ '@solid-primitives/utils': 6.2.3(solid-js@1.9.4)
+ solid-js: 1.9.4
+
+ '@solid-devtools/shared@0.19.0(solid-js@1.9.4)':
+ dependencies:
+ '@nothing-but/utils': 0.17.0
+ '@solid-primitives/event-listener': 2.3.3(solid-js@1.9.4)
+ '@solid-primitives/media': 2.2.10(solid-js@1.9.4)
+ '@solid-primitives/refs': 1.0.8(solid-js@1.9.4)
+ '@solid-primitives/rootless': 1.4.5(solid-js@1.9.4)
+ '@solid-primitives/scheduled': 1.4.4(solid-js@1.9.4)
+ '@solid-primitives/static-store': 0.0.8(solid-js@1.9.4)
+ '@solid-primitives/styles': 0.0.114(solid-js@1.9.4)
+ '@solid-primitives/utils': 6.2.3(solid-js@1.9.4)
+ solid-js: 1.9.4
+
+ '@solid-primitives/bounds@0.0.122(solid-js@1.9.4)':
+ dependencies:
+ '@solid-primitives/event-listener': 2.3.3(solid-js@1.9.4)
+ '@solid-primitives/resize-observer': 2.0.27(solid-js@1.9.4)
+ '@solid-primitives/static-store': 0.0.8(solid-js@1.9.4)
+ '@solid-primitives/utils': 6.2.3(solid-js@1.9.4)
+ solid-js: 1.9.4
+
+ '@solid-primitives/cursor@0.0.115(solid-js@1.9.4)':
+ dependencies:
+ '@solid-primitives/utils': 6.2.3(solid-js@1.9.4)
+ solid-js: 1.9.4
+
+ '@solid-primitives/event-listener@2.3.3(solid-js@1.9.4)':
+ dependencies:
+ '@solid-primitives/utils': 6.2.3(solid-js@1.9.4)
+ solid-js: 1.9.4
+
+ '@solid-primitives/keyboard@1.2.8(solid-js@1.9.4)':
+ dependencies:
+ '@solid-primitives/event-listener': 2.3.3(solid-js@1.9.4)
+ '@solid-primitives/rootless': 1.4.5(solid-js@1.9.4)
+ '@solid-primitives/utils': 6.2.3(solid-js@1.9.4)
+ solid-js: 1.9.4
+
+ '@solid-primitives/media@2.2.10(solid-js@1.9.4)':
+ dependencies:
+ '@solid-primitives/event-listener': 2.3.3(solid-js@1.9.4)
+ '@solid-primitives/rootless': 1.4.5(solid-js@1.9.4)
+ '@solid-primitives/static-store': 0.0.9(solid-js@1.9.4)
+ '@solid-primitives/utils': 6.2.3(solid-js@1.9.4)
+ solid-js: 1.9.4
+
+ '@solid-primitives/platform@0.1.2(solid-js@1.9.4)':
+ dependencies:
+ solid-js: 1.9.4
+
+ '@solid-primitives/refs@1.0.8(solid-js@1.9.4)':
+ dependencies:
+ '@solid-primitives/utils': 6.2.3(solid-js@1.9.4)
+ solid-js: 1.9.4
+
+ '@solid-primitives/resize-observer@2.0.27(solid-js@1.9.4)':
+ dependencies:
+ '@solid-primitives/event-listener': 2.3.3(solid-js@1.9.4)
+ '@solid-primitives/rootless': 1.4.5(solid-js@1.9.4)
+ '@solid-primitives/static-store': 0.0.9(solid-js@1.9.4)
+ '@solid-primitives/utils': 6.2.3(solid-js@1.9.4)
+ solid-js: 1.9.4
+
+ '@solid-primitives/rootless@1.4.5(solid-js@1.9.4)':
+ dependencies:
+ '@solid-primitives/utils': 6.2.3(solid-js@1.9.4)
+ solid-js: 1.9.4
+
+ '@solid-primitives/scheduled@1.4.4(solid-js@1.9.4)':
+ dependencies:
+ solid-js: 1.9.4
+
+ '@solid-primitives/static-store@0.0.8(solid-js@1.9.4)':
+ dependencies:
+ '@solid-primitives/utils': 6.2.3(solid-js@1.9.4)
+ solid-js: 1.9.4
+
+ '@solid-primitives/static-store@0.0.9(solid-js@1.9.4)':
+ dependencies:
+ '@solid-primitives/utils': 6.2.3(solid-js@1.9.4)
+ solid-js: 1.9.4
+
+ '@solid-primitives/styles@0.0.114(solid-js@1.9.4)':
+ dependencies:
+ '@solid-primitives/rootless': 1.4.5(solid-js@1.9.4)
+ '@solid-primitives/utils': 6.2.3(solid-js@1.9.4)
+ solid-js: 1.9.4
+
+ '@solid-primitives/utils@6.2.3(solid-js@1.9.4)':
+ dependencies:
+ solid-js: 1.9.4
+
+ '@solidjs/router@0.15.3(solid-js@1.9.4)':
+ dependencies:
+ solid-js: 1.9.4
+
+ '@svgdotjs/svg.draggable.js@3.0.5(@svgdotjs/svg.js@3.2.4)':
+ dependencies:
+ '@svgdotjs/svg.js': 3.2.4
+
+ '@svgdotjs/svg.filter.js@3.0.8':
+ dependencies:
+ '@svgdotjs/svg.js': 3.2.4
+
+ '@svgdotjs/svg.js@3.2.4': {}
+
+ '@svgdotjs/svg.resize.js@2.0.5(@svgdotjs/svg.js@3.2.4)(@svgdotjs/svg.select.js@4.0.2(@svgdotjs/svg.js@3.2.4))':
+ dependencies:
+ '@svgdotjs/svg.js': 3.2.4
+ '@svgdotjs/svg.select.js': 4.0.2(@svgdotjs/svg.js@3.2.4)
+
+ '@svgdotjs/svg.select.js@4.0.2(@svgdotjs/svg.js@3.2.4)':
+ dependencies:
+ '@svgdotjs/svg.js': 3.2.4
+
+ '@types/babel__core@7.20.5':
+ dependencies:
+ '@babel/parser': 7.26.5
+ '@babel/types': 7.26.5
+ '@types/babel__generator': 7.6.8
+ '@types/babel__template': 7.4.4
+ '@types/babel__traverse': 7.20.6
+
+ '@types/babel__generator@7.6.8':
+ dependencies:
+ '@babel/types': 7.26.5
+
+ '@types/babel__template@7.4.4':
+ dependencies:
+ '@babel/parser': 7.26.5
+ '@babel/types': 7.26.5
+
+ '@types/babel__traverse@7.20.6':
+ dependencies:
+ '@babel/types': 7.26.5
+
+ '@types/estree@1.0.6': {}
+
+ '@yr/monotone-cubic-spline@1.0.3': {}
+
+ agent-base@7.1.3: {}
+
+ ansi-colors@4.1.3: {}
+
+ apexcharts@4.3.0:
+ dependencies:
+ '@svgdotjs/svg.draggable.js': 3.0.5(@svgdotjs/svg.js@3.2.4)
+ '@svgdotjs/svg.filter.js': 3.0.8
+ '@svgdotjs/svg.js': 3.2.4
+ '@svgdotjs/svg.resize.js': 2.0.5(@svgdotjs/svg.js@3.2.4)(@svgdotjs/svg.select.js@4.0.2(@svgdotjs/svg.js@3.2.4))
+ '@svgdotjs/svg.select.js': 4.0.2(@svgdotjs/svg.js@3.2.4)
+ '@yr/monotone-cubic-spline': 1.0.3
+
+ argparse@2.0.1: {}
+
+ babel-plugin-jsx-dom-expressions@0.39.5(@babel/core@7.26.0):
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-module-imports': 7.18.6
+ '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.0)
+ '@babel/types': 7.26.5
+ html-entities: 2.3.3
+ parse5: 7.2.1
+ validate-html-nesting: 1.2.2
+
+ babel-preset-solid@1.9.3(@babel/core@7.26.0):
+ dependencies:
+ '@babel/core': 7.26.0
+ babel-plugin-jsx-dom-expressions: 0.39.5(@babel/core@7.26.0)
+
+ balanced-match@1.0.2: {}
+
+ brace-expansion@2.0.1:
+ dependencies:
+ balanced-match: 1.0.2
+
+ braces@3.0.3:
+ dependencies:
+ fill-range: 7.1.1
+ optional: true
+
+ browserslist@4.24.4:
+ dependencies:
+ caniuse-lite: 1.0.30001692
+ electron-to-chromium: 1.5.83
+ node-releases: 2.0.19
+ update-browserslist-db: 1.1.2(browserslist@4.24.4)
+
+ caniuse-lite@1.0.30001692: {}
+
+ change-case@5.4.4: {}
+
+ chokidar@4.0.3:
+ dependencies:
+ readdirp: 4.1.1
+ optional: true
+
+ colorette@1.4.0: {}
+
+ convert-source-map@2.0.0: {}
+
+ csstype@3.1.3: {}
+
+ debug@4.4.0(supports-color@9.4.0):
+ dependencies:
+ ms: 2.1.3
+ optionalDependencies:
+ supports-color: 9.4.0
+
+ defu@6.1.4: {}
+
+ detect-libc@1.0.3:
+ optional: true
+
+ electron-to-chromium@1.5.83: {}
+
+ entities@4.5.0: {}
+
+ esbuild@0.24.2:
+ optionalDependencies:
+ '@esbuild/aix-ppc64': 0.24.2
+ '@esbuild/android-arm': 0.24.2
+ '@esbuild/android-arm64': 0.24.2
+ '@esbuild/android-x64': 0.24.2
+ '@esbuild/darwin-arm64': 0.24.2
+ '@esbuild/darwin-x64': 0.24.2
+ '@esbuild/freebsd-arm64': 0.24.2
+ '@esbuild/freebsd-x64': 0.24.2
+ '@esbuild/linux-arm': 0.24.2
+ '@esbuild/linux-arm64': 0.24.2
+ '@esbuild/linux-ia32': 0.24.2
+ '@esbuild/linux-loong64': 0.24.2
+ '@esbuild/linux-mips64el': 0.24.2
+ '@esbuild/linux-ppc64': 0.24.2
+ '@esbuild/linux-riscv64': 0.24.2
+ '@esbuild/linux-s390x': 0.24.2
+ '@esbuild/linux-x64': 0.24.2
+ '@esbuild/netbsd-arm64': 0.24.2
+ '@esbuild/netbsd-x64': 0.24.2
+ '@esbuild/openbsd-arm64': 0.24.2
+ '@esbuild/openbsd-x64': 0.24.2
+ '@esbuild/sunos-x64': 0.24.2
+ '@esbuild/win32-arm64': 0.24.2
+ '@esbuild/win32-ia32': 0.24.2
+ '@esbuild/win32-x64': 0.24.2
+
+ escalade@3.2.0: {}
+
+ fast-deep-equal@3.1.3: {}
+
+ fill-range@7.1.1:
+ dependencies:
+ to-regex-range: 5.0.1
+ optional: true
+
+ fsevents@2.3.3:
+ optional: true
+
+ gensync@1.0.0-beta.2: {}
+
+ globals@11.12.0: {}
+
+ html-entities@2.3.3: {}
+
+ https-proxy-agent@7.0.6(supports-color@9.4.0):
+ dependencies:
+ agent-base: 7.1.3
+ debug: 4.4.0(supports-color@9.4.0)
+ transitivePeerDependencies:
+ - supports-color
+
+ immutable@5.0.3:
+ optional: true
+
+ index-to-position@0.1.2: {}
+
+ is-extglob@2.1.1:
+ optional: true
+
+ is-glob@4.0.3:
+ dependencies:
+ is-extglob: 2.1.1
+ optional: true
+
+ is-number@7.0.0:
+ optional: true
+
+ is-what@4.1.16: {}
+
+ js-levenshtein@1.1.6: {}
+
+ js-tokens@4.0.0: {}
+
+ js-yaml@4.1.0:
+ dependencies:
+ argparse: 2.0.1
+
+ jsesc@3.1.0: {}
+
+ json-schema-traverse@1.0.0: {}
+
+ json5@2.2.3: {}
+
+ lru-cache@5.1.1:
+ dependencies:
+ yallist: 3.1.1
+
+ merge-anything@5.1.7:
+ dependencies:
+ is-what: 4.1.16
+
+ micromatch@4.0.8:
+ dependencies:
+ braces: 3.0.3
+ picomatch: 2.3.1
+ optional: true
+
+ minimatch@5.1.6:
+ dependencies:
+ brace-expansion: 2.0.1
+
+ moment@2.30.1: {}
+
+ ms@2.1.3: {}
+
+ nanoid@3.3.8: {}
+
+ node-addon-api@7.1.1:
+ optional: true
+
+ node-fetch@2.7.0:
+ dependencies:
+ whatwg-url: 5.0.0
+
+ node-releases@2.0.19: {}
+
+ openapi-fetch@0.13.4:
+ dependencies:
+ openapi-typescript-helpers: 0.0.15
+
+ openapi-typescript-helpers@0.0.15: {}
+
+ openapi-typescript@7.5.2(typescript@5.7.3):
+ dependencies:
+ '@redocly/openapi-core': 1.27.2(supports-color@9.4.0)
+ ansi-colors: 4.1.3
+ change-case: 5.4.4
+ parse-json: 8.1.0
+ supports-color: 9.4.0
+ typescript: 5.7.3
+ yargs-parser: 21.1.1
+ transitivePeerDependencies:
+ - encoding
+
+ parse-json@8.1.0:
+ dependencies:
+ '@babel/code-frame': 7.26.2
+ index-to-position: 0.1.2
+ type-fest: 4.32.0
+
+ parse5@7.2.1:
+ dependencies:
+ entities: 4.5.0
+
+ picocolors@1.1.1: {}
+
+ picomatch@2.3.1:
+ optional: true
+
+ pluralize@8.0.0: {}
+
+ postcss@8.5.1:
+ dependencies:
+ nanoid: 3.3.8
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
+ readdirp@4.1.1:
+ optional: true
+
+ require-from-string@2.0.2: {}
+
+ rollup@4.30.1:
+ dependencies:
+ '@types/estree': 1.0.6
+ optionalDependencies:
+ '@rollup/rollup-android-arm-eabi': 4.30.1
+ '@rollup/rollup-android-arm64': 4.30.1
+ '@rollup/rollup-darwin-arm64': 4.30.1
+ '@rollup/rollup-darwin-x64': 4.30.1
+ '@rollup/rollup-freebsd-arm64': 4.30.1
+ '@rollup/rollup-freebsd-x64': 4.30.1
+ '@rollup/rollup-linux-arm-gnueabihf': 4.30.1
+ '@rollup/rollup-linux-arm-musleabihf': 4.30.1
+ '@rollup/rollup-linux-arm64-gnu': 4.30.1
+ '@rollup/rollup-linux-arm64-musl': 4.30.1
+ '@rollup/rollup-linux-loongarch64-gnu': 4.30.1
+ '@rollup/rollup-linux-powerpc64le-gnu': 4.30.1
+ '@rollup/rollup-linux-riscv64-gnu': 4.30.1
+ '@rollup/rollup-linux-s390x-gnu': 4.30.1
+ '@rollup/rollup-linux-x64-gnu': 4.30.1
+ '@rollup/rollup-linux-x64-musl': 4.30.1
+ '@rollup/rollup-win32-arm64-msvc': 4.30.1
+ '@rollup/rollup-win32-ia32-msvc': 4.30.1
+ '@rollup/rollup-win32-x64-msvc': 4.30.1
+ fsevents: 2.3.3
+
+ sass@1.83.4:
+ dependencies:
+ chokidar: 4.0.3
+ immutable: 5.0.3
+ source-map-js: 1.2.1
+ optionalDependencies:
+ '@parcel/watcher': 2.5.0
+ optional: true
+
+ semver@6.3.1: {}
+
+ seroval-plugins@1.2.0(seroval@1.2.0):
+ dependencies:
+ seroval: 1.2.0
+
+ seroval@1.2.0: {}
+
+ solid-apexcharts@0.4.0(apexcharts@4.3.0)(solid-js@1.9.4):
+ dependencies:
+ apexcharts: 4.3.0
+ defu: 6.1.4
+ solid-js: 1.9.4
+
+ solid-devtools@0.33.0(solid-js@1.9.4)(vite@6.0.7(sass@1.83.4)):
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.26.0)
+ '@babel/types': 7.26.5
+ '@solid-devtools/debugger': 0.26.0(solid-js@1.9.4)
+ '@solid-devtools/shared': 0.19.0(solid-js@1.9.4)
+ solid-js: 1.9.4
+ optionalDependencies:
+ vite: 6.0.7(sass@1.83.4)
+ transitivePeerDependencies:
+ - supports-color
+
+ solid-js@1.9.4:
+ dependencies:
+ csstype: 3.1.3
+ seroval: 1.2.0
+ seroval-plugins: 1.2.0(seroval@1.2.0)
+
+ solid-refresh@0.6.3(solid-js@1.9.4):
+ dependencies:
+ '@babel/generator': 7.26.5
+ '@babel/helper-module-imports': 7.25.9
+ '@babel/types': 7.26.5
+ solid-js: 1.9.4
+ transitivePeerDependencies:
+ - supports-color
+
+ source-map-js@1.2.1: {}
+
+ supports-color@9.4.0: {}
+
+ to-regex-range@5.0.1:
+ dependencies:
+ is-number: 7.0.0
+ optional: true
+
+ tr46@0.0.3: {}
+
+ type-fest@4.32.0: {}
+
+ typescript@5.7.3: {}
+
+ update-browserslist-db@1.1.2(browserslist@4.24.4):
+ dependencies:
+ browserslist: 4.24.4
+ escalade: 3.2.0
+ picocolors: 1.1.1
+
+ uri-js-replace@1.0.1: {}
+
+ validate-html-nesting@1.2.2: {}
+
+ vite-plugin-solid@2.11.0(solid-js@1.9.4)(vite@6.0.7(sass@1.83.4)):
+ dependencies:
+ '@babel/core': 7.26.0
+ '@types/babel__core': 7.20.5
+ babel-preset-solid: 1.9.3(@babel/core@7.26.0)
+ merge-anything: 5.1.7
+ solid-js: 1.9.4
+ solid-refresh: 0.6.3(solid-js@1.9.4)
+ vite: 6.0.7(sass@1.83.4)
+ vitefu: 1.0.5(vite@6.0.7(sass@1.83.4))
+ transitivePeerDependencies:
+ - supports-color
+
+ vite@6.0.7(sass@1.83.4):
+ dependencies:
+ esbuild: 0.24.2
+ postcss: 8.5.1
+ rollup: 4.30.1
+ optionalDependencies:
+ fsevents: 2.3.3
+ sass: 1.83.4
+
+ vitefu@1.0.5(vite@6.0.7(sass@1.83.4)):
+ optionalDependencies:
+ vite: 6.0.7(sass@1.83.4)
+
+ webidl-conversions@3.0.1: {}
+
+ whatwg-url@5.0.0:
+ dependencies:
+ tr46: 0.0.3
+ webidl-conversions: 3.0.1
+
+ yallist@3.1.1: {}
+
+ yaml-ast-parser@0.0.43: {}
+
+ yargs-parser@21.1.1: {}
diff --git a/server/frontend/src/Analysis.tsx b/server/frontend/src/Analysis.tsx
new file mode 100644
index 0000000..3bf9c13
--- /dev/null
+++ b/server/frontend/src/Analysis.tsx
@@ -0,0 +1,84 @@
+import { createAsync, useParams } from "@solidjs/router"
+import { client, getAnalysisList, paths } from "./api.ts";
+import { createSignal, For, onMount, Show, Suspense } from "solid-js";
+import { SolidApexCharts } from "solid-apexcharts";
+
+type AnalysisResult =
+ { status: 'not requested' }
+ | { status: 'loading' }
+ | { status: 'loaded', result: paths['/analysis/execute']['get']['responses'][200]['content']['application/json'] }
+
+export default function Analysis() {
+ const pathParams = useParams();
+ const analysisId = pathParams.id!;
+ let analysis = createAsync(() => getAnalysisList());
+ const analysisName = () => analysis()?.data?.find(it => it.id == analysisId)?.name
+ const [startTimestamp, setStartTimestamp] = createSignal(new Date().getTime() - 1000 * 60 * 60 * 24 * 30);
+ const [endTimestamp, setEndTimestamp] = createSignal(new Date().getTime());
+ const [analysisResult, setAnalysisResult] = createSignal<AnalysisResult>({ status: 'not requested' });
+ return <>
+ <h1><Suspense fallback="Name not loaded...">{analysisName()}</Suspense></h1>
+ <p>
+ <label>
+ Start:
+ <input type="date" value={new Date(startTimestamp()).toISOString().substring(0, 10)} onInput={it => setStartTimestamp(it.target.valueAsNumber)}></input>
+ </label>
+ <label>
+ End:
+ <input type="date" value={new Date(endTimestamp()).toISOString().substring(0, 10)} onInput={it => setEndTimestamp(it.target.valueAsNumber)}></input>
+ </label>
+ <button disabled={analysisResult().status === 'loading'} onClick={() => {
+ setAnalysisResult({ status: 'loading' });
+ (async () => {
+ const result = await client.GET('/analysis/execute', {
+ params: {
+ query: {
+ analysis: analysisId,
+ tEnd: endTimestamp(),
+ tStart: startTimestamp()
+ }
+ }
+ });
+ setAnalysisResult({
+ status: "loaded",
+ result: result.data!
+ });
+ })();
+ }}>
+ Refresh
+ </button>
+
+ <Show when={takeIf(analysisResult(), it => it.status == 'loaded')}>
+ {element =>
+ <For each={element().result.visualizations}>
+ {item =>
+ <div>
+ <SolidApexCharts
+ width={1200}
+ type="bar"
+ options={{
+ xaxis: {
+ type: 'numeric'
+ }
+ }}
+ series={[
+ {
+ name: item.label,
+ data: item.dataPoints.map(it => ([it.time, it.value]))
+ }
+ ]}
+ ></SolidApexCharts>
+ </div>
+ }
+ </For>}
+ </Show>
+ </p >
+ </>
+}
+
+function takeIf<T extends P, P>(
+ obj: P,
+ condition: (arg: P) => arg is T,
+): T | false {
+ return condition(obj) ? obj : false;
+} \ No newline at end of file
diff --git a/server/frontend/src/App.module.css b/server/frontend/src/App.module.css
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/server/frontend/src/App.module.css
diff --git a/server/frontend/src/App.tsx b/server/frontend/src/App.tsx
new file mode 100644
index 0000000..bdc1007
--- /dev/null
+++ b/server/frontend/src/App.tsx
@@ -0,0 +1,22 @@
+import { For, Suspense, type Component } from "solid-js";
+import { A, createAsync } from "@solidjs/router";
+import { client, getAnalysisList } from "./api.ts";
+
+const App: Component = () => {
+ let analysis = createAsync(() => getAnalysisList());
+ return (
+ <>
+ <Suspense fallback="Loading analysis...">
+ <ul>
+ <For each={analysis()?.data}>
+ {item =>
+ <li><A href={`/analysis/${item.id}`}>{item.name}</A></li>
+ }
+ </For>
+ </ul>
+ </Suspense>
+ </>
+ );
+};
+
+export default App;
diff --git a/server/frontend/src/Test.tsx b/server/frontend/src/Test.tsx
new file mode 100644
index 0000000..15d2f73
--- /dev/null
+++ b/server/frontend/src/Test.tsx
@@ -0,0 +1,31 @@
+import { A, createAsync } from "@solidjs/router";
+import { client } from "./api.js";
+import { For, Suspense } from "solid-js";
+
+export default function Test() {
+ let items = createAsync(() =>
+ client.GET("/item", {
+ params: {
+ query: {
+ itemId: ["HYPERION", "BAT_WAND"],
+ },
+ },
+ })
+ );
+ return (
+ <>
+ Test page <A href={"/"}>Back to main</A>
+ <hr />
+ <Suspense fallback={"Loading items..."}>
+ <p>Here are all Items:</p>
+ <For each={Object.entries(items()?.data || {})}>
+ {([id, name]) => (
+ <li>
+ <code>{id}</code>: {name}
+ </li>
+ )}
+ </For>
+ </Suspense>
+ </>
+ );
+}
diff --git a/server/frontend/src/api-schema.d.ts b/server/frontend/src/api-schema.d.ts
new file mode 100644
index 0000000..7ba1db4
--- /dev/null
+++ b/server/frontend/src/api-schema.d.ts
@@ -0,0 +1,236 @@
+/**
+ * This file was auto-generated by openapi-typescript.
+ * Do not make direct changes to the file.
+ */
+
+export interface paths {
+ "/profiles": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** List all profiles and players known to ledger */
+ get: operations["listProfiles"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/analysis/execute": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** Execute an analysis on a given timeframe */
+ get: operations["executeAnalysis"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/analysis/list": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** List all installed analysis */
+ get: operations["getAnalysis"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/item": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** Get item names for item ids */
+ get: operations["getItemNames"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/entries": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** Get all log entries */
+ get: operations["getLogEntries"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+}
+export type webhooks = Record<string, never>;
+export interface components {
+ schemas: never;
+ responses: never;
+ parameters: never;
+ requestBodies: never;
+ headers: never;
+ pathItems: never;
+}
+export type $defs = Record<string, never>;
+export interface operations {
+ listProfiles: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ playerId: string;
+ profileId: string;
+ }[];
+ };
+ };
+ };
+ };
+ executeAnalysis: {
+ parameters: {
+ query: {
+ /** @description An analysis id obtained from getAnalysis */
+ analysis: string;
+ /** @description The start of the timeframe to analyze */
+ tStart: number;
+ /** @description The end of the timeframe to analyze. Make sure to use the end of the day if you want the entire day included. */
+ tEnd: number;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ visualizations: {
+ label: string;
+ xLabel: string;
+ yLabel: string;
+ dataPoints: {
+ time: number;
+ value: number;
+ }[];
+ }[];
+ };
+ };
+ };
+ };
+ };
+ getAnalysis: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ name: string;
+ id: string;
+ }[];
+ };
+ };
+ };
+ };
+ getItemNames: {
+ parameters: {
+ query: {
+ itemId: string[];
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ [key: string]: string;
+ };
+ };
+ };
+ };
+ };
+ getLogEntries: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ /** @enum {string} */
+ type: "ACCESSORIES_SWAPPING" | "ALLOWANCE_GAIN" | "AUCTION_BOUGHT" | "AUCTION_LISTING_CHARGE" | "AUCTION_SOLD" | "AUTOMERCHANT_PROFIT_COLLECT" | "BANK_DEPOSIT" | "BANK_INTEREST" | "BANK_WITHDRAW" | "BASIC_REFORGE" | "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" | "GUMMY_POLAR_BEAR_ATE" | "KAT_TIMESKIP" | "KAT_UPGRADE" | "KISMET_REROLL" | "KUUDRA_CHEST_OPEN" | "NPC_BUY" | "NPC_SELL" | "PEST_REPELLENT_USED" | "VISITOR_BARGAIN" | "WYRM_EVOKED";
+ id: string;
+ items: {
+ itemId: string;
+ /** @enum {string} */
+ direction: "GAINED" | "TRANSFORM" | "SYNC" | "CATALYST" | "LOST";
+ amount: number;
+ }[];
+ }[];
+ };
+ };
+ };
+ };
+}
diff --git a/server/frontend/src/api.ts b/server/frontend/src/api.ts
new file mode 100644
index 0000000..8ab6272
--- /dev/null
+++ b/server/frontend/src/api.ts
@@ -0,0 +1,13 @@
+import createClient from "openapi-fetch";
+import type { paths } from "./api-schema.js";
+import { query } from "@solidjs/router";
+export { type paths };
+
+const apiRoot = import.meta.env.DEV ? "//localhost:8080/api" : "/api";
+
+export const client = createClient<paths>({ baseUrl: apiRoot });
+
+export const getAnalysisList = query(
+ () => client.GET("/analysis/list"),
+ "getAnalysisList"
+)
diff --git a/server/frontend/src/index.css b/server/frontend/src/index.css
new file mode 100644
index 0000000..4a1df4d
--- /dev/null
+++ b/server/frontend/src/index.css
@@ -0,0 +1,13 @@
+body {
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
+ "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
+ sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+code {
+ font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
+ monospace;
+}
diff --git a/server/frontend/src/index.tsx b/server/frontend/src/index.tsx
new file mode 100644
index 0000000..610a78b
--- /dev/null
+++ b/server/frontend/src/index.tsx
@@ -0,0 +1,23 @@
+/* @refresh reload */
+import { render } from "solid-js/web";
+import 'solid-devtools';
+
+import "./index.css";
+import type { RouteDefinition } from "@solidjs/router";
+import { Router } from "@solidjs/router";
+import { lazy } from "solid-js";
+
+const root = document.getElementById("root");
+
+if (!(root instanceof HTMLElement)) {
+ throw new Error(
+ "Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?"
+ );
+}
+const routes: Array<RouteDefinition> = [
+ { path: "/", component: lazy(() => import("./App.tsx")) },
+ { path: "/test/", component: lazy(() => import("./Test.tsx")) },
+ { path: "/analysis/:id", component: lazy(() => import("./Analysis.tsx")) },
+];
+
+render(() => <Router>{routes}</Router>, root!);
diff --git a/server/frontend/tsconfig.json b/server/frontend/tsconfig.json
new file mode 100644
index 0000000..548d331
--- /dev/null
+++ b/server/frontend/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "compilerOptions": {
+ "allowImportingTsExtensions": true,
+ "allowSyntheticDefaultImports": true,
+ "esModuleInterop": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "jsxImportSource": "solid-js",
+ "module": "Preserve",
+ "moduleResolution": "bundler",
+ "noEmit": true,
+ "noUncheckedIndexedAccess": true,
+ "strict": true,
+ "target": "ESNext",
+ "types": [
+ "vite/client"
+ ]
+ }
+}
diff --git a/server/frontend/vite.config.ts b/server/frontend/vite.config.ts
new file mode 100644
index 0000000..4d3fdd1
--- /dev/null
+++ b/server/frontend/vite.config.ts
@@ -0,0 +1,17 @@
+import { defineConfig } from 'vite';
+import solidPlugin from 'vite-plugin-solid';
+import devtools from 'solid-devtools/vite';
+export default defineConfig({
+ plugins: [
+ solidPlugin(),
+ devtools({
+ autoname: true
+ })
+ ],
+ server: {
+ port: 3000,
+ },
+ build: {
+ target: 'esnext',
+ },
+});
diff --git a/server/swagger/build.gradle.kts b/server/swagger/build.gradle.kts
new file mode 100644
index 0000000..76e5f78
--- /dev/null
+++ b/server/swagger/build.gradle.kts
@@ -0,0 +1,17 @@
+plugins {
+ `java-library`
+ kotlin("jvm")
+ kotlin("plugin.serialization")
+}
+
+
+dependencies {
+ declareKtorVersion()
+ api("io.ktor:ktor-server-core")
+ api("sh.ondr:kotlin-json-schema:0.1.1")
+ implementation("org.webjars:swagger-ui:5.18.2")
+}
+
+java {
+ toolchain.languageVersion.set(JavaLanguageVersion.of(21))
+}
diff --git a/server/swagger/src/main/kotlin/moe/nea/ledger/server/core/api/OpenApiModel.kt b/server/swagger/src/main/kotlin/moe/nea/ledger/server/core/api/OpenApiModel.kt
new file mode 100644
index 0000000..af9d3f4
--- /dev/null
+++ b/server/swagger/src/main/kotlin/moe/nea/ledger/server/core/api/OpenApiModel.kt
@@ -0,0 +1,117 @@
+package moe.nea.ledger.server.core.api
+
+import io.ktor.http.ContentType
+import io.ktor.http.HttpStatusCode
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.descriptors.PrimitiveKind
+import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import sh.ondr.jsonschema.JsonSchema
+
+@Serializable
+data class OpenApiModel(
+ val openapi: String = "3.0.0",
+ val info: Info,
+ val servers: List<Server>,
+ val paths: Map<OpenApiPath, OpenApiRoute>,
+)
+
+@Serializable // TODO: custom serializer
+@JvmInline
+value class OpenApiPath(val name: String)
+
+@Serializable
+data class OpenApiRoute(
+ val summary: String,
+ val description: String,
+ val get: OpenApiOperation?,
+ val put: OpenApiOperation?,
+ val patch: OpenApiOperation?,
+ val post: OpenApiOperation?,
+ val delete: OpenApiOperation?,
+)
+
+@Serializable
+data class OpenApiOperation(
+ val tags: List<Tag>,
+ val summary: String,
+ val description: String,
+ val operationId: String,
+ val deprecated: Boolean,
+ val parameters: List<OpenApiParameter>,
+ val responses: Map<@Serializable(HttpStatusCodeIntAsString::class) HttpStatusCode, OpenApiResponse>
+)
+
+@Serializable
+data class OpenApiParameter(
+ @SerialName("in") val location: ParameterLocation,
+ val name: String,
+ val description: String,
+ val schema: JsonSchema?,
+ val required: Boolean = true,
+)
+
+@Serializable
+enum class ParameterLocation {
+ @SerialName("query")
+ QUERY,
+ @SerialName("path")
+ PATH,
+}
+
+object HttpStatusCodeIntAsString : KSerializer<HttpStatusCode> {
+ override val descriptor: SerialDescriptor =
+ PrimitiveSerialDescriptor("HttpStatusCodeIntAsString", PrimitiveKind.STRING)
+
+ override fun deserialize(decoder: Decoder): HttpStatusCode {
+ return HttpStatusCode.fromValue(decoder.decodeString().toInt())
+ }
+
+ override fun serialize(encoder: Encoder, value: HttpStatusCode) {
+ encoder.encodeString(value.value.toString())
+ }
+}
+
+object ContentTypeSerializer : KSerializer<ContentType> {
+ override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("ContentTypeSerializer", PrimitiveKind.STRING)
+
+ override fun deserialize(decoder: Decoder): ContentType {
+ return ContentType.parse(decoder.decodeString())
+ }
+
+ override fun serialize(encoder: Encoder, value: ContentType) {
+ encoder.encodeString(value.contentType + "/" + value.contentSubtype)
+ }
+}
+
+@Serializable
+data class OpenApiResponse(
+ val description: String,
+ val content: Map<@Serializable(ContentTypeSerializer::class) ContentType, OpenApiResponseContentType>
+)
+
+@Serializable
+data class OpenApiResponseContentType(
+ val schema: JsonSchema?
+)
+
+@Serializable
+@JvmInline
+value class Tag(val name: String)
+
+@Serializable
+data class Info(
+ val title: String,
+ val description: String,
+ val version: String,
+)
+
+@Serializable
+data class Server(
+ val url: String,
+ val description: String,
+)
diff --git a/server/swagger/src/main/kotlin/moe/nea/ledger/server/core/api/docs.kt b/server/swagger/src/main/kotlin/moe/nea/ledger/server/core/api/docs.kt
new file mode 100644
index 0000000..c1b550d
--- /dev/null
+++ b/server/swagger/src/main/kotlin/moe/nea/ledger/server/core/api/docs.kt
@@ -0,0 +1,323 @@
+package moe.nea.ledger.server.core.api
+
+import io.ktor.http.ContentType
+import io.ktor.http.HttpMethod
+import io.ktor.http.HttpStatusCode
+import io.ktor.http.content.OutgoingContent
+import io.ktor.http.defaultForFilePath
+import io.ktor.server.application.ApplicationCallPipeline
+import io.ktor.server.application.BaseApplicationPlugin
+import io.ktor.server.application.host
+import io.ktor.server.application.port
+import io.ktor.server.response.respond
+import io.ktor.server.response.respondText
+import io.ktor.server.routing.HttpMethodRouteSelector
+import io.ktor.server.routing.PathSegmentConstantRouteSelector
+import io.ktor.server.routing.RootRouteSelector
+import io.ktor.server.routing.Route
+import io.ktor.server.routing.RoutingNode
+import io.ktor.server.routing.TrailingSlashRouteSelector
+import io.ktor.server.routing.get
+import io.ktor.server.routing.route
+import io.ktor.util.AttributeKey
+import io.ktor.util.cio.KtorDefaultPool
+import io.ktor.utils.io.ByteReadChannel
+import io.ktor.utils.io.jvm.javaio.toByteReadChannel
+import kotlinx.serialization.json.JsonPrimitive
+import sh.ondr.jsonschema.JsonSchema
+import sh.ondr.jsonschema.jsonSchema
+import java.io.File
+import java.io.InputStream
+
+
+fun Route.openApiDocsJson() {
+ get {
+ val docs = plugin(Documentation)
+ val model = docs.finalizeJson()
+ call.respond(model)
+ }
+}
+
+fun Route.openApiUi(apiJsonUrl: String) {
+ get("swagger-initializer.js") {
+ call.respondText(
+ //language=JavaScript
+ """
+ window.onload = function() {
+ //<editor-fold desc="Changeable Configuration Block">
+
+ // the following lines will be replaced by docker/configurator, when it runs in a docker-container
+ window.ui = SwaggerUIBundle({
+ url: ${JsonPrimitive(apiJsonUrl)},
+ dom_id: '#swagger-ui',
+ deepLinking: true,
+ presets: [
+ SwaggerUIBundle.presets.apis,
+ SwaggerUIStandalonePreset
+ ],
+ plugins: [
+ SwaggerUIBundle.plugins.DownloadUrl
+ ],
+ layout: "StandaloneLayout"
+ });
+
+ //</editor-fold>
+ };
+ """.trimIndent())
+ }
+// val swaggerUiProperties =
+// environment.classLoader.getResource("/META-INF/maven/org.webjars/swagger-ui/pom.properties")
+// ?: error("Could not find swagger webjar")
+// val swaggerUiZip = swaggerUiProperties.toString().substringBefore("!")
+ val pathParameterName = "static-content-path-parameter"
+ route("{$pathParameterName...}") {
+ get {
+ var requestedPath = call.parameters.getAll(pathParameterName)?.joinToString(File.separator) ?: ""
+ requestedPath = requestedPath.replace("\\", "/")
+ if (requestedPath.isEmpty()) requestedPath = "index.html"
+ if (requestedPath.contains("..")) {
+ call.respondText("Forbidden", status = HttpStatusCode.Forbidden)
+ return@get
+ }
+ //TODO: I mean i should read out the version properties but idc
+ val version = "5.18.2"
+ val resource =
+ environment.classLoader.getResourceAsStream("META-INF/resources/webjars/swagger-ui/$version/$requestedPath")
+
+ if (resource == null) {
+ call.respondText("Not Found", status = HttpStatusCode.NotFound)
+ return@get
+ }
+
+ call.respond(InputStreamContent(resource, ContentType.defaultForFilePath(requestedPath)))
+ }
+ }
+}
+
+internal class InputStreamContent(
+ private val input: InputStream,
+ override val contentType: ContentType
+) : OutgoingContent.ReadChannelContent() {
+
+ override fun readFrom(): ByteReadChannel = input.toByteReadChannel(pool = KtorDefaultPool)
+}
+
+class DocumentationPath(val path: String)
+
+class DocumentationEndpoint private constructor() {
+ var method: HttpMethod = HttpMethod.Get
+ private set
+ lateinit var path: DocumentationPath
+ private set
+
+ private fun initFromPath(
+ baseRoute: Route?,
+ route: RoutingNode,
+ ) {
+ path = DocumentationPath(createRoutePath(baseRoute, route))
+ }
+
+ private fun createRoutePath(
+ baseRoute: Route?,
+ route: RoutingNode,
+ ): String {
+ if (baseRoute == route)
+ return "/"
+ val parent = route.parent
+ if (parent == null) {
+ if (baseRoute != null)
+ error("Could not find $route in $baseRoute")
+ return "/"
+ }
+ val parentPath = createRoutePath(baseRoute, parent)
+ var parentPathAppendable = parentPath
+ if (!parentPathAppendable.endsWith("/"))
+ parentPathAppendable += "/"
+ return when (val selector = route.selector) {
+ is TrailingSlashRouteSelector -> parentPathAppendable
+ is RootRouteSelector -> parentPath
+ is PathSegmentConstantRouteSelector -> parentPathAppendable + selector.value
+ is HttpMethodRouteSelector -> {
+ method = selector.method
+ parentPath
+ }
+
+ else -> error("Could not comprehend $selector (${selector.javaClass})")
+ }
+ }
+
+ companion object {
+ fun createDocumentationPath(baseRoute: Route?, route: Route): DocumentationEndpoint {
+ val path = DocumentationEndpoint()
+ path.initFromPath(baseRoute, route as RoutingNode)
+ return path
+ }
+ }
+}
+
+class Response {
+ var schema: JsonSchema? = null
+ inline fun <reified T : Any> schema() {
+ schema = jsonSchema<T>()
+ }
+
+ fun intoJson(): OpenApiResponse {
+ return OpenApiResponse(
+ "",
+ mapOf(
+ ContentType.Application.Json to OpenApiResponseContentType(schema)
+ )
+ )
+ }
+}
+
+interface IntoTag {
+ fun intoTag(): String
+}
+
+class DocumentationOperationContext(val route: DocumentationContext) {
+ val responses = mutableMapOf<HttpStatusCode, Response>()
+ fun responds(statusCode: HttpStatusCode, block: Response.() -> Unit) {
+ responses.getOrPut(statusCode) { Response() }.also(block)
+ }
+
+ fun respondsOk(block: Response.() -> Unit) {
+ responds(HttpStatusCode.OK, block)
+ }
+
+ var summary: String = ""
+ var description: String = ""
+ var deprecated: Boolean = false
+ var operationId: String = ""
+ val tags: MutableList<String> = mutableListOf()
+ val parameters: MutableList<OpenApiParameter> = mutableListOf()
+ fun tag(vararg tag: String) {
+ tags.addAll(tag)
+ }
+
+ fun tag(vararg tag: IntoTag) {
+ tag.mapTo(tags) { it.intoTag() }
+ }
+
+ inline fun <reified T : Any> queryParameter(name: String, description: String = "") {
+ parameter(ParameterLocation.QUERY, name, description, jsonSchema<T>())
+ }
+
+ fun parameter(
+ location: ParameterLocation, name: String,
+ description: String = "", schema: JsonSchema? = null
+ ) {
+ parameters.add(OpenApiParameter(
+ location, name, description,
+ schema
+ ))
+ }
+
+ fun intoJson(): OpenApiOperation {
+ return OpenApiOperation(
+ tags = tags.map { Tag(it) },
+ summary = summary,
+ description = description,
+ operationId = operationId,
+ deprecated = deprecated,
+ parameters = parameters,
+ responses = responses.mapValues {
+ it.value.intoJson()
+ }
+ )
+
+ }
+}
+
+class DocumentationContext(val path: DocumentationPath) {
+ val ops: MutableMap<HttpMethod, DocumentationOperationContext> = mutableMapOf()
+ var summary: String = ""
+ var description = ""
+ fun intoJson(): OpenApiRoute {
+ return OpenApiRoute(
+ summary,
+ description,
+ get = ops[HttpMethod.Get]?.intoJson(),
+ put = ops[HttpMethod.Put]?.intoJson(),
+ post = ops[HttpMethod.Post]?.intoJson(),
+ patch = ops[HttpMethod.Patch]?.intoJson(),
+ delete = ops[HttpMethod.Delete]?.intoJson(),
+ )
+ }
+
+ fun createOperationNode(method: HttpMethod): DocumentationOperationContext {
+ return ops.getOrPut(method) { DocumentationOperationContext(this) }
+ }
+}
+
+
+class Documentation(config: Configuration) {
+ companion object Plugin : BaseApplicationPlugin<ApplicationCallPipeline, Configuration, Documentation> {
+ override val key: AttributeKey<Documentation> = AttributeKey("LedgerDocumentation")
+
+ override fun install(pipeline: ApplicationCallPipeline, configure: Configuration.() -> Unit): Documentation {
+ val config = Configuration().also(configure)
+ if (config.servers.isEmpty()) {
+ config.servers.add(Server(
+ "http://${pipeline.environment.config.host}:${pipeline.environment.config.port}",
+ "Server",
+ ))
+ }
+ val plugin = Documentation(config)
+ return plugin
+ }
+ }
+
+ val info = config.info
+ var root: RoutingNode? = null
+ private set
+ val servers: List<Server> = config.servers
+
+ private val documentationNodes = mutableMapOf<DocumentationPath, DocumentationContext>()
+ fun createDocumentationNode(endpoint: DocumentationEndpoint) =
+ documentationNodes.getOrPut(endpoint.path) { DocumentationContext(endpoint.path) }
+ .createOperationNode(endpoint.method)
+
+ private val openApiJson by lazy {
+ OpenApiModel(
+ info = info,
+ servers = servers,
+ paths = documentationNodes.map {
+ OpenApiPath(it.key.path) to it.value.intoJson()
+ }.toMap()
+ )
+ }
+
+ fun finalizeJson(): OpenApiModel {
+ return openApiJson
+ }
+
+ fun setRootNode(routingNode: RoutingNode) {
+ require(documentationNodes.isEmpty()) { "Cannot set API root node after routes have been documented: ${documentationNodes.keys}" }
+ this.root = routingNode
+ }
+
+ class Configuration {
+ var info: Info = Info(
+ title = "Example API Docs",
+ description = "Missing description",
+ version = "0.0.0"
+ )
+ val servers: MutableList<Server> = mutableListOf()
+ }
+}
+
+fun Route.docs(block: DocumentationOperationContext.() -> Unit) {
+ val documentation = plugin(Documentation)
+ val documentationPath = DocumentationEndpoint.createDocumentationPath(documentation.root, this)
+ val node = documentation.createDocumentationNode(documentationPath)
+ block(node)
+}
+
+/**
+ * Mark this current routing node as API route. Note that this will not apply retroactively and all api requests must be declared relative to this one.
+ */
+fun Route.setApiRoot() {
+ plugin(Documentation).setRootNode(this as RoutingNode)
+}
+
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 82d99b6..cab8376 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -1,10 +1,13 @@
pluginManagement {
repositories {
+ maven("https://maven.fabricmc.net")
+ maven("https://jitpack.io")
+ mavenCentral()
+ google()
mavenCentral()
gradlePluginPortal()
maven("https://oss.sonatype.org/content/repositories/snapshots")
maven("https://maven.architectury.dev/")
- maven("https://maven.fabricmc.net")
maven("https://maven.minecraftforge.net/")
maven("https://repo.spongepowered.org/maven/")
maven("https://repo.sk1er.club/repository/maven-releases/")
@@ -19,8 +22,19 @@ pluginManagement {
}
plugins {
- id("org.gradle.toolchains.foojay-resolver-convention") version ("0.6.0")
+ id("org.gradle.toolchains.foojay-resolver-convention") version ("0.8.0")
}
rootProject.name = "ledger"
+include("dependency-injection")
+include("database:core")
+include("database:impl")
+include("basetypes")
+include("mod")
+include("server:swagger")
+include("server:core")
+include("server:frontend")
+include("server:aio")
+include("server:analysis")
+includeBuild("build-src")
diff --git a/src/main/java/moe/nea/ledger/init/AutoDiscoveryMixinPlugin.java b/src/main/java/moe/nea/ledger/init/AutoDiscoveryMixinPlugin.java
deleted file mode 100644
index 2c3a9c7..0000000
--- a/src/main/java/moe/nea/ledger/init/AutoDiscoveryMixinPlugin.java
+++ /dev/null
@@ -1,188 +0,0 @@
-package moe.nea.ledger.init;
-
-import org.spongepowered.asm.lib.tree.ClassNode;
-import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin;
-import org.spongepowered.asm.mixin.extensibility.IMixinInfo;
-
-import java.io.IOException;
-import java.net.MalformedURLException;
-import java.net.URISyntaxException;
-import java.net.URL;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Set;
-import java.util.stream.Stream;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipInputStream;
-
-/**
- * A mixin plugin to automatically discover all mixins in the current JAR.
- * <p>
- * This mixin plugin automatically scans your entire JAR (or class directory, in case of an in-IDE launch) for classes inside of your
- * mixin package and registers those. It does this recursively for sub packages of the mixin package as well. This means you will need
- * to only have mixin classes inside of your mixin package, which is good style anyway.
- *
- * @author Linnea Gräf
- */
-public class AutoDiscoveryMixinPlugin implements IMixinConfigPlugin {
- private static final List<AutoDiscoveryMixinPlugin> mixinPlugins = new ArrayList<>();
-
- public static List<AutoDiscoveryMixinPlugin> getMixinPlugins() {
- return mixinPlugins;
- }
-
- private String mixinPackage;
-
- @Override
- public void onLoad(String mixinPackage) {
- this.mixinPackage = mixinPackage;
- mixinPlugins.add(this);
- }
-
- /**
- * Resolves the base class root for a given class URL. This resolves either the JAR root, or the class file root.
- * In either case the return value of this + the class name will resolve back to the original class url, or to other
- * class urls for other classes.
- */
- public URL getBaseUrlForClassUrl(URL classUrl) {
- String string = classUrl.toString();
- if (classUrl.getProtocol().equals("jar")) {
- try {
- return new URL(string.substring(4).split("!")[0]);
- } catch (MalformedURLException e) {
- throw new RuntimeException(e);
- }
- }
- if (string.endsWith(".class")) {
- try {
- return new URL(string.replace("\\", "/")
- .replace(getClass().getCanonicalName()
- .replace(".", "/") + ".class", ""));
- } catch (MalformedURLException e) {
- throw new RuntimeException(e);
- }
- }
- return classUrl;
- }
-
- /**
- * Get the package that contains all the mixins. This value is set by mixin itself using {@link #onLoad}.
- */
- public String getMixinPackage() {
- return mixinPackage;
- }
-
- /**
- * Get the path inside the class root to the mixin package
- */
- public String getMixinBaseDir() {
- return mixinPackage.replace(".", "/");
- }
-
- /**
- * A list of all discovered mixins.
- */
- private List<String> mixins = null;
-
- /**
- * Try to add mixin class ot the mixins based on the filepath inside of the class root.
- * Removes the {@code .class} file suffix, as well as the base mixin package.
- * <p><b>This method cannot be called after mixin initialization.</p>
- *
- * @param className the name or path of a class to be registered as a mixin.
- */
- public void tryAddMixinClass(String className) {
- String norm = (className.endsWith(".class") ? className.substring(0, className.length() - ".class".length()) : className)
- .replace("\\", "/")
- .replace("/", ".");
- if (norm.startsWith(getMixinPackage() + ".") && !norm.endsWith(".")) {
- mixins.add(norm.substring(getMixinPackage().length() + 1));
- }
- }
-
- /**
- * Search through the JAR or class directory to find mixins contained in {@link #getMixinPackage()}
- */
- @Override
- public List<String> getMixins() {
- if (mixins != null) return mixins;
- System.out.println("Trying to discover mixins");
- mixins = new ArrayList<>();
- URL classUrl = getClass().getProtectionDomain().getCodeSource().getLocation();
- System.out.println("Found classes at " + classUrl);
- Path file;
- try {
- file = Paths.get(getBaseUrlForClassUrl(classUrl).toURI());
- } catch (URISyntaxException e) {
- throw new RuntimeException(e);
- }
- System.out.println("Base directory found at " + file);
- if (Files.isDirectory(file)) {
- walkDir(file);
- } else {
- walkJar(file);
- }
- System.out.println("Found mixins: " + mixins);
-
- return mixins;
- }
-
- /**
- * Search through directory for mixin classes based on {@link #getMixinBaseDir}.
- *
- * @param classRoot The root directory in which classes are stored for the default package.
- */
- private void walkDir(Path classRoot) {
- System.out.println("Trying to find mixins from directory");
- try (Stream<Path> classes = Files.walk(classRoot.resolve(getMixinBaseDir()))) {
- classes.map(it -> classRoot.relativize(it).toString())
- .forEach(this::tryAddMixinClass);
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
-
- /**
- * Read through a JAR file, trying to find all mixins inside.
- */
- private void walkJar(Path file) {
- System.out.println("Trying to find mixins from jar file");
- try (ZipInputStream zis = new ZipInputStream(Files.newInputStream(file))) {
- ZipEntry next;
- while ((next = zis.getNextEntry()) != null) {
- tryAddMixinClass(next.getName());
- zis.closeEntry();
- }
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
-
- @Override
- public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {
-
- }
-
- @Override
- public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {
-
- }
-
- @Override
- public String getRefMapperConfig() {
- return null;
- }
-
- @Override
- public boolean shouldApplyMixin(String targetClassName, String mixinClassName) {
- return true;
- }
-
- @Override
- public void acceptTargets(Set<String> myTargets, Set<String> otherTargets) {
-
- }
-}
diff --git a/src/main/kotlin/moe/nea/ledger/ItemChange.kt b/src/main/kotlin/moe/nea/ledger/ItemChange.kt
deleted file mode 100644
index fda709c..0000000
--- a/src/main/kotlin/moe/nea/ledger/ItemChange.kt
+++ /dev/null
@@ -1,84 +0,0 @@
-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<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)
- }
-
- 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/src/main/kotlin/moe/nea/ledger/ItemId.kt b/src/main/kotlin/moe/nea/ledger/ItemId.kt
deleted file mode 100644
index f4786cd..0000000
--- a/src/main/kotlin/moe/nea/ledger/ItemId.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-package moe.nea.ledger
-
-data class ItemId(
- val string: String
-) {
- fun singleItem(): Pair<ItemId, Double> {
- return withStackSize(1)
- }
-
- fun withStackSize(size: Number): Pair<ItemId, Double> {
- return Pair(this, size.toDouble())
- }
-
-
- companion object {
-
- @JvmStatic
- fun forName(string: String) = ItemId(string)
- fun skill(skill: String) = ItemId("SKYBLOCK_SKILL_$skill")
-
- val GARDEN = skill("GARDEN")
- val FARMING = skill("FARMING")
-
-
- val ARCHFIEND_DYE = ItemId("DYE_ARCHFIEND")
- val ARCHFIEND_HIGH_CLASS = ItemId("HIGH_CLASS_ARCHFIEND_DICE")
- val ARCHFIEND_LOW_CLASS = ItemId("ARCHFIEND_DICE")
- val BITS = ItemId("SKYBLOCK_BIT")
- val BOOSTER_COOKIE = ItemId("BOOSTER_COOKIE")
- val CAP_EYEDROPS = ItemId("CAPSAICIN_EYEDROPS_NO_CHARGES")
- val COINS = ItemId("SKYBLOCK_COIN")
- val COPPER = ItemId("SKYBLOCK_COPPER")
- val DRAGON_ESSENCE = ItemId("ESSENCE_DRAGON")
- val DUNGEON_CHEST_KEY = ItemId("DUNGEON_CHEST_KEY")
- val FINE_FLOUR = ItemId("FINE_FLOUR")
- val GEMSTONE_POWDER = ItemId("SKYBLOCK_POWDER_GEMSTONE")
- val GOD_POTION = ItemId("GOD_POTION_2")
- val GOLD_ESSENCE = ItemId("ESSENCE_GOLD")
- val KISMET_FEATHER = ItemId("KISMET_FEATHER")
- val MITHRIL_POWDER = ItemId("SKYBLOCK_POWDER_MITHRIL")
- val NIL = ItemId("SKYBLOCK_NIL")
- val PELT = ItemId("SKYBLOCK_PELT")
- val SLEEPING_EYE = ItemId("SLEEPING_EYE")
- val SUMMONING_EYE = ItemId("SUMMONING_EYE")
- }
-} \ No newline at end of file
diff --git a/src/main/kotlin/moe/nea/ledger/database/DBSchema.kt b/src/main/kotlin/moe/nea/ledger/database/DBSchema.kt
deleted file mode 100644
index 492f261..0000000
--- a/src/main/kotlin/moe/nea/ledger/database/DBSchema.kt
+++ /dev/null
@@ -1,545 +0,0 @@
-package moe.nea.ledger.database
-
-import moe.nea.ledger.UUIDUtil
-import java.sql.Connection
-import java.sql.PreparedStatement
-import java.sql.ResultSet
-import java.time.Instant
-import java.util.UUID
-
-interface DBSchema {
- val tables: List<Table>
-}
-
-interface DBType<T> {
- val dbType: String
-
- fun get(result: ResultSet, index: Int): T
- fun set(stmt: PreparedStatement, index: Int, value: T)
- fun getName(): String = javaClass.simpleName
- fun <R> mapped(
- from: (R) -> T,
- to: (T) -> R,
- ): DBType<R> {
- return object : DBType<R> {
- override fun getName(): String {
- return "Mapped(${this@DBType.getName()})"
- }
-
- override val dbType: String
- get() = this@DBType.dbType
-
- override fun get(result: ResultSet, index: Int): R {
- return to(this@DBType.get(result, index))
- }
-
- override fun set(stmt: PreparedStatement, index: Int, value: R) {
- this@DBType.set(stmt, index, from(value))
- }
- }
- }
-}
-
-object DBUuid : DBType<UUID> {
- override val dbType: String
- get() = "TEXT"
-
- override fun get(result: ResultSet, index: Int): UUID {
- return UUIDUtil.parseDashlessUuid(result.getString(index))
- }
-
- override fun set(stmt: PreparedStatement, index: Int, value: UUID) {
- stmt.setString(index, value.toString())
- }
-}
-
-object DBUlid : DBType<UUIDUtil.ULIDWrapper> {
- override val dbType: String
- get() = "TEXT"
-
- override fun get(result: ResultSet, index: Int): UUIDUtil.ULIDWrapper {
- val text = result.getString(index)
- return UUIDUtil.ULIDWrapper(text)
- }
-
- override fun set(stmt: PreparedStatement, index: Int, value: UUIDUtil.ULIDWrapper) {
- stmt.setString(index, value.wrapped)
- }
-}
-
-object DBString : DBType<String> {
- override val dbType: String
- get() = "TEXT"
-
- override fun get(result: ResultSet, index: Int): String {
- return result.getString(index)
- }
-
- override fun set(stmt: PreparedStatement, index: Int, value: String) {
- stmt.setString(index, value)
- }
-}
-
-class DBEnum<T : Enum<T>>(
- val type: Class<T>,
-) : DBType<T> {
- companion object {
- inline operator fun <reified T : Enum<T>> invoke(): DBEnum<T> {
- return DBEnum(T::class.java)
- }
- }
-
- override val dbType: String
- get() = "TEXT"
-
- override fun getName(): String {
- return "DBEnum(${type.simpleName})"
- }
-
- override fun set(stmt: PreparedStatement, index: Int, value: T) {
- stmt.setString(index, value.name)
- }
-
- override fun get(result: ResultSet, index: Int): T {
- val name = result.getString(index)
- return java.lang.Enum.valueOf(type, name)
- }
-}
-
-object DBDouble : DBType<Double> {
- override val dbType: String
- get() = "DOUBLE"
-
- override fun get(result: ResultSet, index: Int): Double {
- return result.getDouble(index)
- }
-
- override fun set(stmt: PreparedStatement, index: Int, value: Double) {
- stmt.setDouble(index, value)
- }
-}
-
-object DBInt : DBType<Long> {
- override val dbType: String
- get() = "INTEGER"
-
- override fun get(result: ResultSet, index: Int): Long {
- return result.getLong(index)
- }
-
- override fun set(stmt: PreparedStatement, index: Int, value: Long) {
- stmt.setLong(index, value)
- }
-}
-
-object DBInstant : DBType<Instant> {
- override val dbType: String
- get() = "INTEGER"
-
- override fun set(stmt: PreparedStatement, index: Int, value: Instant) {
- stmt.setLong(index, value.toEpochMilli())
- }
-
- override fun get(result: ResultSet, index: Int): Instant {
- return Instant.ofEpochMilli(result.getLong(index))
- }
-}
-
-class Column<T> @Deprecated("Use Table.column instead") constructor(
- val table: Table,
- val name: String,
- val type: DBType<T>
-) {
- val sqlName get() = "`$name`"
- val qualifiedSqlName get() = table.sqlName + "." + sqlName
-}
-
-interface Constraint {
- val affectedColumns: Collection<Column<*>>
- fun asSQL(): String
-}
-
-class UniqueConstraint(val columns: List<Column<*>>) : Constraint {
- init {
- require(columns.isNotEmpty())
- }
-
- override val affectedColumns: Collection<Column<*>>
- get() = columns
-
- override fun asSQL(): String {
- return "UNIQUE (${columns.joinToString() { it.sqlName }})"
- }
-}
-
-abstract class Table(val name: String) {
- val sqlName get() = "`$name`"
- protected val _mutable_columns: MutableList<Column<*>> = mutableListOf()
- protected val _mutable_constraints: MutableList<Constraint> = mutableListOf()
- val columns: List<Column<*>> get() = _mutable_columns
- val constraints get() = _mutable_constraints
- protected fun unique(vararg columns: Column<*>) {
- _mutable_constraints.add(UniqueConstraint(columns.toList()))
- }
-
- protected fun <T> column(name: String, type: DBType<T>): Column<T> {
- @Suppress("DEPRECATION") val column = Column(this, name, type)
- _mutable_columns.add(column)
- return column
- }
-
- fun debugSchema() {
- val nameWidth = columns.maxOf { it.name.length }
- val typeWidth = columns.maxOf { it.type.getName().length }
- val totalWidth = maxOf(2 + nameWidth + 3 + typeWidth + 2, name.length + 4)
- val adjustedTypeWidth = totalWidth - nameWidth - 2 - 3 - 2
-
- var string = "\n"
- string += ("+" + "-".repeat(totalWidth - 2) + "+\n")
- string += ("| $name${" ".repeat(totalWidth - 4 - name.length)} |\n")
- string += ("+" + "-".repeat(totalWidth - 2) + "+\n")
- for (column in columns) {
- string += ("| ${column.name}${" ".repeat(nameWidth - column.name.length)} |")
- string += (" ${column.type.getName()}" +
- "${" ".repeat(adjustedTypeWidth - column.type.getName().length)} |\n")
- }
- string += ("+" + "-".repeat(totalWidth - 2) + "+")
- println(string)
- }
-
- fun createIfNotExists(
- connection: Connection,
- filteredColumns: List<Column<*>> = columns
- ) {
- val properties = mutableListOf<String>()
- for (column in filteredColumns) {
- properties.add("${column.sqlName} ${column.type.dbType}")
- }
- val columnSet = filteredColumns.toSet()
- for (constraint in constraints) {
- if (columnSet.containsAll(constraint.affectedColumns)) {
- properties.add(constraint.asSQL())
- }
- }
- connection.prepareAndLog("CREATE TABLE IF NOT EXISTS $sqlName (" + properties.joinToString() + ")")
- .execute()
- }
-
- fun alterTableAddColumns(
- connection: Connection,
- newColumns: List<Column<*>>
- ) {
- for (column in newColumns) {
- connection.prepareAndLog("ALTER TABLE $sqlName ADD ${column.sqlName} ${column.type.dbType}")
- .execute()
- }
- for (constraint in constraints) {
- // TODO: automatically add constraints, maybe (or maybe move constraints into the upgrade schema)
- }
- }
-
- enum class OnConflict {
- FAIL,
- IGNORE,
- REPLACE,
- ;
-
- fun asSql(): String {
- return name
- }
- }
-
- fun insert(connection: Connection, onConflict: OnConflict = OnConflict.FAIL, block: (InsertStatement) -> Unit) {
- val insert = InsertStatement(HashMap())
- block(insert)
- require(insert.properties.keys == columns.toSet())
- val columnNames = columns.joinToString { it.sqlName }
- val valueNames = columns.joinToString { "?" }
- val statement =
- connection.prepareAndLog("INSERT OR ${onConflict.asSql()} INTO $sqlName ($columnNames) VALUES ($valueNames)")
- for ((index, column) in columns.withIndex()) {
- (column as Column<Any>).type.set(statement, index + 1, insert.properties[column]!!)
- }
- statement.execute()
- }
-
- fun from(connection: Connection): Query {
- return Query(connection, mutableListOf(), this)
- }
-
- fun selectAll(connection: Connection): Query {
- return Query(connection, columns.toMutableList(), this)
- }
-}
-
-class InsertStatement(val properties: MutableMap<Column<*>, Any>) {
- operator fun <T : Any> set(key: Column<T>, value: T) {
- properties[key] = value
- }
-}
-
-fun Connection.prepareAndLog(statement: String): PreparedStatement {
- println("Preparing to execute $statement")
- return prepareStatement(statement)
-}
-
-interface SQLQueryComponent {
- fun asSql(): String
-
- /**
- * @return the next writable index (should equal to the amount of `?` in [asSql] + [startIndex])
- */
- fun appendToStatement(stmt: PreparedStatement, startIndex: Int): Int
-
- companion object {
- fun standalone(sql: String): SQLQueryComponent {
- return object : SQLQueryComponent {
- override fun asSql(): String {
- return sql
- }
-
- override fun appendToStatement(stmt: PreparedStatement, startIndex: Int): Int {
- return startIndex
- }
- }
- }
- }
-}
-
-interface BooleanExpression : SQLQueryComponent
-
-data class ORExpression(
- val elements: List<BooleanExpression>
-) : BooleanExpression {
- init {
- require(elements.isNotEmpty())
- }
-
- override fun asSql(): String {
- return (elements + SQLQueryComponent.standalone("FALSE")).joinToString(" OR ", "(", ")") { it.asSql() }
- }
-
- override fun appendToStatement(stmt: PreparedStatement, startIndex: Int): Int {
- var index = startIndex
- for (element in elements) {
- index = element.appendToStatement(stmt, index)
- }
- return index
- }
-}
-
-
-data class ANDExpression(
- val elements: List<BooleanExpression>
-) : BooleanExpression {
- init {
- require(elements.isNotEmpty())
- }
-
- override fun asSql(): String {
- return (elements + SQLQueryComponent.standalone("TRUE")).joinToString(" AND ", "(", ")") { it.asSql() }
- }
-
- override fun appendToStatement(stmt: PreparedStatement, startIndex: Int): Int {
- var index = startIndex
- for (element in elements) {
- index = element.appendToStatement(stmt, index)
- }
- return index
- }
-}
-
-class ClauseBuilder {
- fun <T> column(column: Column<T>): Operand<T> = Operand.ColumnOperand(column)
- fun string(string: String): Operand.StringOperand = Operand.StringOperand(string)
- infix fun Operand<*>.eq(operand: Operand<*>) = Clause.EqualsClause(this, operand)
- infix fun Operand<*>.like(op: Operand.StringOperand) = Clause.LikeClause(this, op)
- infix fun Operand<*>.like(op: String) = Clause.LikeClause(this, string(op))
-}
-
-interface Clause : BooleanExpression {
- companion object {
- operator fun invoke(builder: ClauseBuilder.() -> Clause): Clause {
- return builder(ClauseBuilder())
- }
- }
-
- data class EqualsClause(val left: Operand<*>, val right: Operand<*>) : Clause { // TODO: typecheck this somehow
- override fun asSql(): String {
- return left.asSql() + " = " + right.asSql()
- }
-
- override fun appendToStatement(stmt: PreparedStatement, startIndex: Int): Int {
- var index = startIndex
- index = left.appendToStatement(stmt, index)
- index = right.appendToStatement(stmt, index)
- return index
- }
- }
-
- data class LikeClause<T>(val left: Operand<T>, val right: Operand.StringOperand) : Clause {
- //TODO: check type safety with this one
- override fun asSql(): String {
- return "(" + left.asSql() + " LIKE " + right.asSql() + ")"
- }
-
- override fun appendToStatement(stmt: PreparedStatement, startIndex: Int): Int {
- var index = startIndex
- index = left.appendToStatement(stmt, index)
- index = right.appendToStatement(stmt, index)
- return index
- }
- }
-}
-
-interface Operand<T> : SQLQueryComponent {
- data class ColumnOperand<T>(val column: Column<T>) : Operand<T> {
- override fun asSql(): String {
- return column.qualifiedSqlName
- }
-
- override fun appendToStatement(stmt: PreparedStatement, startIndex: Int): Int {
- return startIndex
- }
- }
-
- data class StringOperand(val value: String) : Operand<String> {
- override fun asSql(): String {
- return "?"
- }
-
- override fun appendToStatement(stmt: PreparedStatement, startIndex: Int): Int {
- stmt.setString(startIndex, value)
- return 1 + startIndex
- }
- }
-}
-
-data class Join(
- val table: Table,
-//TODO: aliased columns val tableAlias: String,
- val filter: Clause,
-) : SQLQueryComponent {
- // JOIN ItemEntry on LogEntry.transactionId = ItemEntry.transactionId
- override fun asSql(): String {
- return "JOIN ${table.sqlName} ON ${filter.asSql()}"
- }
-
- override fun appendToStatement(stmt: PreparedStatement, startIndex: Int): Int {
- return filter.appendToStatement(stmt, startIndex)
- }
-}
-
-fun List<SQLQueryComponent>.concatToFilledPreparedStatement(connection: Connection): PreparedStatement {
- var query = ""
- for (element in this) {
- if (query.isNotEmpty()) {
- query += " "
- }
- query += element.asSql()
- }
- val statement = connection.prepareAndLog(query)
- var index = 1
- for (element in this) {
- val nextIndex = element.appendToStatement(statement, index)
- if (nextIndex < index) error("$element went back in time")
- index = nextIndex
- }
- return statement
-}
-
-class Query(
- val connection: Connection,
- val selectedColumns: MutableList<Column<*>>,
- var table: Table,
- var limit: UInt? = null,
- var skip: UInt? = null,
- val joins: MutableList<Join> = mutableListOf(),
- val conditions: MutableList<BooleanExpression> = mutableListOf(),
-// var order: OrderClause?= null,
-) : Iterable<ResultRow> {
- fun join(table: Table, on: Clause): Query {
- joins.add(Join(table, on))
- return this
- }
-
- fun where(binOp: BooleanExpression): Query {
- conditions.add(binOp)
- return this
- }
-
- fun select(vararg columns: Column<*>): Query {
- selectedColumns.addAll(columns)
- return this
- }
-
- fun skip(skip: UInt): Query {
- require(limit != null)
- this.skip = skip
- return this
- }
-
- fun limit(limit: UInt): Query {
- this.limit = limit
- return this
- }
-
- override fun iterator(): Iterator<ResultRow> {
- val columnSelections = selectedColumns.joinToString { it.qualifiedSqlName }
- val elements = mutableListOf(
- SQLQueryComponent.standalone("SELECT $columnSelections FROM ${table.sqlName}"),
- )
- elements.addAll(joins)
- if (conditions.any()) {
- elements.add(SQLQueryComponent.standalone("WHERE"))
- elements.add(ANDExpression(conditions))
- }
- if (limit != null) {
- elements.add(SQLQueryComponent.standalone("LIMIT $limit"))
- if (skip != null) {
- elements.add(SQLQueryComponent.standalone("OFFSET $skip"))
- }
- }
- val prepared = elements.concatToFilledPreparedStatement(connection)
- val results = prepared.executeQuery()
- return object : Iterator<ResultRow> {
- var hasAdvanced = false
- var hasEnded = false
- override fun hasNext(): Boolean {
- if (hasEnded) return false
- if (hasAdvanced) return true
- if (results.next()) {
- hasAdvanced = true
- return true
- } else {
- results.close() // TODO: somehow enforce closing this
- hasEnded = true
- return false
- }
- }
-
- override fun next(): ResultRow {
- if (!hasNext()) {
- throw NoSuchElementException()
- }
- hasAdvanced = false
- return ResultRow(selectedColumns.withIndex().associate {
- it.value to it.value.type.get(results, it.index + 1)
- })
- }
-
- }
- }
-}
-
-class ResultRow(val columnValues: Map<Column<*>, *>) {
- operator fun <T> get(column: Column<T>): T {
- val value = columnValues[column]
- ?: error("Invalid column ${column.name}. Only ${columnValues.keys.joinToString { it.name }} are available.")
- return value as T
- }
-}
-
-
-
-
diff --git a/src/main/kotlin/moe/nea/ledger/events/BeforeGuiAction.kt b/src/main/kotlin/moe/nea/ledger/events/BeforeGuiAction.kt
deleted file mode 100644
index 098912a..0000000
--- a/src/main/kotlin/moe/nea/ledger/events/BeforeGuiAction.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package moe.nea.ledger.events
-
-import net.minecraft.client.gui.GuiScreen
-import net.minecraft.client.gui.inventory.GuiChest
-import net.minecraft.inventory.ContainerChest
-import net.minecraftforge.fml.common.eventhandler.Event
-
-data class BeforeGuiAction(val gui: GuiScreen) : Event() {
- val chest = gui as? GuiChest
- val chestSlots = chest?.inventorySlots as ContainerChest?
-}
diff --git a/test.png b/test.png
deleted file mode 100644
index 6b5bba0..0000000
--- a/test.png
+++ /dev/null
Binary files differ