diff options
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 @@ -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 Binary files differindex b71185a..b71185a 100644 --- a/src/main/resources/ledgerkeystore.jks +++ b/mod/src/main/resources/ledgerkeystore.jks 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 Binary files differdeleted file mode 100644 index 6b5bba0..0000000 --- a/test.png +++ /dev/null |