diff --git a/konata/pom.xml b/konata/pom.xml deleted file mode 100644 index 0b8dedc..0000000 --- a/konata/pom.xml +++ /dev/null @@ -1,108 +0,0 @@ - - - 4.0.0 - - org.nisemoe - konata - 0.0.1-SNAPSHOT - - - 21 - 1.9.22 - - - - src/main/kotlin - src/test/kotlin - - - org.jetbrains.kotlin - kotlin-maven-plugin - ${kotlin.version} - - - compile - compile - - compile - - - - test-compile - test-compile - - test-compile - - - - - - org.codehaus.mojo - exec-maven-plugin - 1.6.0 - - MainKt - - - - - - - - - org.apache.commons - commons-compress - 1.25.0 - - - org.tukaani - xz - 1.9 - - - - - org.jetbrains.bio - viktor - 1.2.0 - - - - - org.apache.commons - commons-math3 - 3.6.1 - - - - - - org.jetbrains.kotlinx - kotlinx-coroutines-core - 1.7.3 - - - - org.jetbrains.kotlin - kotlin-test-junit5 - ${kotlin.version} - test - - - - org.junit.jupiter - junit-jupiter - 5.10.0 - test - - - - org.jetbrains.kotlin - kotlin-stdlib - ${kotlin.version} - - - - \ No newline at end of file diff --git a/konata/readme.md b/konata/readme.md deleted file mode 100644 index 44215f4..0000000 --- a/konata/readme.md +++ /dev/null @@ -1,111 +0,0 @@ -# konata - ->osu! utility lib in kotlin for fast replay comparison with multithreading support - -This module has the specific purpose of **high-throughput** replay comparison, and only works with replay data as supplied by the osu!api; it does not work with .osr files. - -[circleguard](https://github.com/circleguard/circleguard) is a better tool if you are looking for a more complete solution, as it has a GUI and supports .osr files. - -this module was built with a narrow task in mind, and I do not have plans to implement more features (especially if circleguard already covers them) - -# Usage - -### Replay data class - -`Replay` is the main data class you'll be throwing around. The only required field is the replay data (verbatim as fetched by the osu!api) in string format. - -You can also pass additional parameters: - -| parameter | type | required? | notes | -|-----------|------|------------------------------|-------------------------------------------------------------------------------------------------------------| -| id | Long | not for pairs, yes for sets* | used to find the replay in the output, does NOT have to match osu!api, it can be any identifier you'd like. | -| mods | Int | no (defaults to NoMod) | exact value as fetched by the osu!api, it's used to flip the replay y-axis when HR is enabled. | - -*You are forced to set the id when using the replay in a set comparison, as it is the identifier that will allow you to match the input to the results. - -Example: - -```kotlin -// Simplest replay -val replay: Replay = Replay(replayString) - -// A NoMod replay with id 1 -val replay: Replay = Replay(replayString, id = 1, mods = 0) - -// A HDHR (24) replay with id 2 -val replay: Replay = Replay(replayString, id = 2, mods = 24) -``` - -### Replay pairs (2 replays) - -The replay strings must be exactly as provided by the osu!api replay endpoint. - -The following code calculates the similarity ratio and correlation ratio between two replays, without specifying any mods. - -```kotlin -// Compare using objects -val replay1: Replay = Replay(replay1String) -val replay2: Replay = Replay(replay2String) - -val result: ReplayPairComparison = compareReplayPair(replay1, replay2) -println(result.similarity) // 20.365197244184895 -println(result.correlation) // 0.9770151700235653 - -// You can also pass the replay data directly as strings -val similarity: ReplayPairComparison = compareReplayPair(replay1String, replay2String) -println(result.similarity) // 20.365197244184895 -println(result.correlation) // 0.9770151700235653 -``` - -### Replay sets (n replays) - -If we decide to pass a list of replays, there will be optimizations such as multi-threading involved, which can speed up the calculations. - -When comparing sets, you *must* set the replay id (it does not have to match the osu! replay id), as it is the identifier that will -allow you to match the input to the results. - -```kotlin -// Compare using objects -val replays: Array = arrayOf( - Replay("...", id = 1), - Replay("...", id = 2) -) - -val result: List = compareReplaySet(replays) -println(result[0].replay1Id) // 1 -println(result[0].replay2Id) // 2 -println(result[0].similarity) // 155.20954003316618 -println(result[0].correlation) // 0.9859198745055805 -``` - -By default, the `compareReplaySet` method will default to using as many threads as there are cores on your system. -You can change this behaviour by manually passing an amount of cores to use: - -```kotlin -compareReplaySet(replays, numThreads=4) -``` - -# Benchmarks - -### Performance - -On my development machine (5900X), the following benchmarks were obtained. - -I processed 10 batches of 100 replays each. The min/max/avg time refer to single batches. - -| | version | min | max | avg | total | pairs/second | -|-------------|-------------|------|------|------|-------|--------------| -| | v20240211 | 3.1s | 4.2s | 3.3s | 32.7s | 1501/s | -| | v20240211v2 | 2.5s | 3.7s | 2.7s | 26.7s | 1843/s | -| **current** | v20240211v3 | 1.1s | 2.1s | 1.3s | 13.0s | 3789/s | - -### Accuracy (compared to Circleguard) - ->as of the last version, konata and circleguard give the same results, with a neglibile margin of error. - -After selecting a random dataset of ~50,000 osu!std replays for different beatmaps, I compared the results from konata to circleguard, using the latter as the ground truth. - -| metric | avg. delta | std. dev. | median | min | max | -|---------------|------------|------------|-----------|-----------|-----------| -| `SIMILARITY` | 0 | 0.000033 | 0 | -0.005373 | 0.007381 | -| `CORRELATION` | -0.000643 | 0.001342 | -0.000433 | -0.041833 | 0.026300 | diff --git a/mari/.github/FUNDING.yml b/mari/.github/FUNDING.yml deleted file mode 100644 index 05ab66b..0000000 --- a/mari/.github/FUNDING.yml +++ /dev/null @@ -1 +0,0 @@ -patreon: nise_moe \ No newline at end of file diff --git a/mari/pom.xml b/mari/pom.xml deleted file mode 100644 index c19f75d..0000000 --- a/mari/pom.xml +++ /dev/null @@ -1,112 +0,0 @@ - - - 4.0.0 - - org.nisemoe - mari - 0.0.1-SNAPSHOT - - - 21 - 1.9.22 - - - - src/main/kotlin - src/test/kotlin - - - org.jetbrains.kotlin - kotlin-maven-plugin - ${kotlin.version} - - - compile - compile - - compile - - - - test-compile - test-compile - - test-compile - - - - - - kotlinx-serialization - - - - - org.jetbrains.kotlin - kotlin-maven-serialization - ${kotlin.version} - - - - - org.codehaus.mojo - exec-maven-plugin - 1.6.0 - - MainKt - - - - - - - - - com.aayushatharva.brotli4j - brotli4j - 1.16.0 - - - - - org.apache.commons - commons-compress - 1.25.0 - - - org.tukaani - xz - 1.9 - - - - - org.jetbrains.kotlinx - kotlinx-serialization-json - 1.6.3 - - - - org.jetbrains.kotlin - kotlin-test-junit5 - ${kotlin.version} - test - - - - org.junit.jupiter - junit-jupiter - 5.10.0 - test - - - - org.jetbrains.kotlin - kotlin-stdlib - ${kotlin.version} - - - - \ No newline at end of file diff --git a/mari/readme.md b/mari/readme.md deleted file mode 100644 index 374ef48..0000000 --- a/mari/readme.md +++ /dev/null @@ -1,29 +0,0 @@ -# mari - ->osu! utility lib in kotlin to manipulate replays and judgement data in a safe and performant way. - -This module allows [nise.moe](https://nise.moe) to juggle a ton of replays and data around. - -# Usage - -### Compress / decompress judgement data - -The `Judgement` data class ought to represent the way a player has played a specific beatmap. It contains the hit error, distance, etc for each hit object. The structure is based off the Circleguard `Investigations.judgements` return type. - -You can use `CompressJudgements.compress` and `CompressJudgements.decompress` to losslessly store and retrieve judgement data. According to my estimates, the compressed data is about 33% the size of the original data. - -### Decode a replay - ->This method is fundamentally written with the assumption that it'll be used on user-provided replays. It is thus designed to be safe and to not crash on invalid replays. - -`OsuReplay` allows you to safely decode an `.osr` file. Once you've parsed the file, you can instantiate a new class: - -```kotlin -val replay = OsuReplay(replayFile.bytes) -``` - -If everything goes well, you can then start reading the replay data. - -```kotlin -println(replay.playerName) // mrekk -``` diff --git a/nise-backend/Build.sh b/nise-backend/Build.sh index 78389e9..7a09b3b 100755 --- a/nise-backend/Build.sh +++ b/nise-backend/Build.sh @@ -17,11 +17,6 @@ IMAGE_VERSION="latest" # Clean up previous build artifacts rm -rf target/ -# Build subdependencies -echo "Building subdependencies..." -(cd ../mari && mvn clean install) || { echo "Building mari failed"; exit 1; } -(cd ../konata && mvn clean install) || { echo "Building konata failed"; exit 1; } - # Clean and build the Maven project echo "Building main project..." mvn clean package || { echo "Maven build failed"; exit 1; } diff --git a/nise-backend/pom.xml b/nise-backend/pom.xml index 31a2aa5..81e86b5 100644 --- a/nise-backend/pom.xml +++ b/nise-backend/pom.xml @@ -58,17 +58,6 @@ ${testcontainers.version} test - - - org.nisemoe - konata - 0.0.1-SNAPSHOT - - - org.nisemoe - mari - 0.0.1-SNAPSHOT - com.fasterxml.jackson.dataformat jackson-dataformat-xml @@ -140,6 +129,45 @@ kotlin-stdlib + + + org.jetbrains.bio + viktor + 1.2.0 + + + + + org.apache.commons + commons-math3 + 3.6.1 + + + + + org.apache.commons + commons-compress + 1.26.1 + + + org.tukaani + xz + 1.9 + + + + org.jetbrains.kotlin + kotlin-test-junit5 + ${kotlin.version} + test + + + + org.junit.jupiter + junit-jupiter + 5.10.0 + test + org.springframework.boot spring-boot-starter-test diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/controller/UploadReplayController.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/controller/UploadReplayController.kt index 0354382..f30eb97 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/nise/controller/UploadReplayController.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/controller/UploadReplayController.kt @@ -5,10 +5,10 @@ import com.nisemoe.generated.tables.references.BEATMAPS import com.nisemoe.generated.tables.references.SCORES import com.nisemoe.generated.tables.references.USER_SCORES import com.nisemoe.generated.tables.references.USER_SCORES_SIMILARITY -import com.nisemoe.konata.Replay -import com.nisemoe.konata.compareSingleReplayWithSet import com.nisemoe.nise.database.BeatmapService import com.nisemoe.nise.integrations.CircleguardService +import com.nisemoe.nise.konata.Replay +import com.nisemoe.nise.konata.compareSingleReplayWithSet import com.nisemoe.nise.osu.OsuApi import com.nisemoe.nise.scheduler.ImportScores import org.jooq.DSLContext diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/database/ScoreService.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/database/ScoreService.kt index f95ca1a..bbd2d3b 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/nise/database/ScoreService.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/database/ScoreService.kt @@ -5,11 +5,10 @@ import com.nisemoe.generated.tables.records.ScoresJudgementsRecord import com.nisemoe.generated.tables.records.ScoresRecord import com.nisemoe.generated.tables.references.* import com.nisemoe.nise.* -import com.nisemoe.nise.integrations.CircleguardService import com.nisemoe.nise.osu.Mod import com.nisemoe.nise.osu.OsuApi import com.nisemoe.nise.service.AuthService -import org.apache.commons.compress.compressors.lzma.LZMACompressorInputStream +import com.nisemoe.nise.service.CompressReplay import org.jooq.Condition import org.jooq.DSLContext import org.jooq.Record @@ -20,7 +19,6 @@ import org.nisemoe.mari.judgements.Judgement import org.slf4j.LoggerFactory import org.springframework.stereotype.Service import java.time.LocalDateTime -import java.util.* import kotlin.math.roundToInt @Service @@ -82,9 +80,9 @@ class ScoreService( .where(SCORES.REPLAY_ID.eq(replayId)) .fetchOne() ?: return null - val replayData = result.get(SCORES.REPLAY, String::class.java) ?: return null + val replayData = result.get(SCORES.REPLAY, ByteArray::class.java) ?: return null - val replay = decompressData(replayData) + val replay = CompressReplay.decompressReplay(replayData) var beatmapFile = result.get(BEATMAPS.BEATMAP_FILE, String::class.java) if(beatmapFile == null) { @@ -111,11 +109,6 @@ class ScoreService( ) } - private fun decompressData(replayString: String): ByteArray = - Base64.getDecoder().decode(replayString).inputStream().use { byteStream -> - LZMACompressorInputStream(byteStream).readBytes() - } - fun getReplayData(replayId: Long): ReplayData? { val result = dslContext.select( SCORES.ID, diff --git a/mari/src/main/kotlin/org/nisemoe/mari/judgements/CompressJudgements.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/judgements/CompressJudgements.kt similarity index 100% rename from mari/src/main/kotlin/org/nisemoe/mari/judgements/CompressJudgements.kt rename to nise-backend/src/main/kotlin/com/nisemoe/nise/judgements/CompressJudgements.kt diff --git a/mari/src/main/kotlin/org/nisemoe/mari/judgements/JudgementModel.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/judgements/JudgementModel.kt similarity index 100% rename from mari/src/main/kotlin/org/nisemoe/mari/judgements/JudgementModel.kt rename to nise-backend/src/main/kotlin/com/nisemoe/nise/judgements/JudgementModel.kt diff --git a/konata/src/main/kotlin/com/nisemoe/konata/CompareReplayPair.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/konata/CompareReplayPair.kt similarity index 96% rename from konata/src/main/kotlin/com/nisemoe/konata/CompareReplayPair.kt rename to nise-backend/src/main/kotlin/com/nisemoe/nise/konata/CompareReplayPair.kt index 791c412..8099e2a 100644 --- a/konata/src/main/kotlin/com/nisemoe/konata/CompareReplayPair.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/konata/CompareReplayPair.kt @@ -1,7 +1,7 @@ -package com.nisemoe.konata +package com.nisemoe.nise.konata -import com.nisemoe.konata.algorithms.calculateCorrelation -import com.nisemoe.konata.algorithms.calculateDistance +import com.nisemoe.nise.konata.algorithms.calculateCorrelation +import com.nisemoe.nise.konata.algorithms.calculateDistance import org.jetbrains.bio.viktor.F64Array diff --git a/konata/src/main/kotlin/com/nisemoe/konata/CompareReplaySet.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/konata/CompareReplaySet.kt similarity index 98% rename from konata/src/main/kotlin/com/nisemoe/konata/CompareReplaySet.kt rename to nise-backend/src/main/kotlin/com/nisemoe/nise/konata/CompareReplaySet.kt index 29ce1d6..d1e0acb 100644 --- a/konata/src/main/kotlin/com/nisemoe/konata/CompareReplaySet.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/konata/CompareReplaySet.kt @@ -1,4 +1,4 @@ -package com.nisemoe.konata +package com.nisemoe.nise.konata import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.coroutineScope diff --git a/konata/src/main/kotlin/com/nisemoe/konata/Replay.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/konata/Replay.kt similarity index 80% rename from konata/src/main/kotlin/com/nisemoe/konata/Replay.kt rename to nise-backend/src/main/kotlin/com/nisemoe/nise/konata/Replay.kt index dc0ade8..ed0b574 100644 --- a/konata/src/main/kotlin/com/nisemoe/konata/Replay.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/konata/Replay.kt @@ -1,7 +1,7 @@ -package com.nisemoe.konata +package com.nisemoe.nise.konata -import com.nisemoe.konata.tools.getEvents -import com.nisemoe.konata.tools.processReplayData +import com.nisemoe.nise.konata.tools.getEvents +import com.nisemoe.nise.konata.tools.processReplayData import org.jetbrains.bio.viktor.F64Array class Replay(string: String, id: Long? = null, mods: Int = 0) { diff --git a/konata/src/main/kotlin/com/nisemoe/konata/ReplayDto.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/konata/ReplayDto.kt similarity index 92% rename from konata/src/main/kotlin/com/nisemoe/konata/ReplayDto.kt rename to nise-backend/src/main/kotlin/com/nisemoe/nise/konata/ReplayDto.kt index 99d4b41..1005af6 100644 --- a/konata/src/main/kotlin/com/nisemoe/konata/ReplayDto.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/konata/ReplayDto.kt @@ -1,4 +1,4 @@ -package com.nisemoe.konata +package com.nisemoe.nise.konata data class ReplayPairComparison( val similarity: Double, diff --git a/konata/src/main/kotlin/com/nisemoe/konata/algorithms/Correlation.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/konata/algorithms/Correlation.kt similarity index 98% rename from konata/src/main/kotlin/com/nisemoe/konata/algorithms/Correlation.kt rename to nise-backend/src/main/kotlin/com/nisemoe/nise/konata/algorithms/Correlation.kt index 2c11de5..8b8fd83 100644 --- a/konata/src/main/kotlin/com/nisemoe/konata/algorithms/Correlation.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/konata/algorithms/Correlation.kt @@ -1,4 +1,4 @@ -package com.nisemoe.konata.algorithms +package com.nisemoe.nise.konata.algorithms import kotlinx.coroutines.* import org.apache.commons.math3.stat.descriptive.rank.Median diff --git a/konata/src/main/kotlin/com/nisemoe/konata/algorithms/Distance.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/konata/algorithms/Distance.kt similarity index 95% rename from konata/src/main/kotlin/com/nisemoe/konata/algorithms/Distance.kt rename to nise-backend/src/main/kotlin/com/nisemoe/nise/konata/algorithms/Distance.kt index 54db89c..eb1058a 100644 --- a/konata/src/main/kotlin/com/nisemoe/konata/algorithms/Distance.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/konata/algorithms/Distance.kt @@ -1,4 +1,4 @@ -package com.nisemoe.konata.algorithms +package com.nisemoe.nise.konata.algorithms import org.jetbrains.bio.viktor.F64Array import org.jetbrains.bio.viktor._I diff --git a/konata/src/main/kotlin/com/nisemoe/konata/tools/DecodeReplay.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/konata/tools/DecodeReplay.kt similarity index 89% rename from konata/src/main/kotlin/com/nisemoe/konata/tools/DecodeReplay.kt rename to nise-backend/src/main/kotlin/com/nisemoe/nise/konata/tools/DecodeReplay.kt index 1d4177d..5f3a644 100644 --- a/konata/src/main/kotlin/com/nisemoe/konata/tools/DecodeReplay.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/konata/tools/DecodeReplay.kt @@ -1,6 +1,6 @@ -package com.nisemoe.konata.tools +package com.nisemoe.nise.konata.tools -import com.nisemoe.konata.ReplayEvent +import com.nisemoe.nise.konata.ReplayEvent import org.apache.commons.compress.compressors.lzma.LZMACompressorInputStream import java.util.* import kotlin.collections.ArrayList @@ -16,7 +16,7 @@ private fun decompressData(replayString: String): ByteArray = LZMACompressorInputStream(byteStream).readBytes() } -internal fun processEvents(replayDataStr: String): ArrayList { +fun processEvents(replayDataStr: String): ArrayList { val eventStrings = replayDataStr.split(",") val playData = ArrayList(eventStrings.size) eventStrings.forEachIndexed { index, eventStr -> diff --git a/konata/src/main/kotlin/com/nisemoe/konata/tools/ProcessEvents.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/konata/tools/ProcessEvents.kt similarity index 96% rename from konata/src/main/kotlin/com/nisemoe/konata/tools/ProcessEvents.kt rename to nise-backend/src/main/kotlin/com/nisemoe/nise/konata/tools/ProcessEvents.kt index 85f9ac8..1f6a91f 100644 --- a/konata/src/main/kotlin/com/nisemoe/konata/tools/ProcessEvents.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/konata/tools/ProcessEvents.kt @@ -1,6 +1,6 @@ -package com.nisemoe.konata.tools +package com.nisemoe.nise.konata.tools -import com.nisemoe.konata.ReplayEvent +import com.nisemoe.nise.konata.ReplayEvent import org.jetbrains.bio.viktor.F64Array fun processReplayData(events: ArrayList): F64Array { diff --git a/mari/src/main/kotlin/org/nisemoe/mari/replays/OsuReplay.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/replays/OsuReplay.kt similarity index 100% rename from mari/src/main/kotlin/org/nisemoe/mari/replays/OsuReplay.kt rename to nise-backend/src/main/kotlin/com/nisemoe/nise/replays/OsuReplay.kt diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/scheduler/FixOldScores.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/scheduler/FixOldScores.kt index f63e382..dc7f79f 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/nise/scheduler/FixOldScores.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/scheduler/FixOldScores.kt @@ -5,6 +5,7 @@ import com.nisemoe.generated.tables.references.BEATMAPS import com.nisemoe.generated.tables.references.SCORES import com.nisemoe.nise.integrations.CircleguardService import com.nisemoe.nise.osu.OsuApi +import com.nisemoe.nise.service.CompressReplay import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.joinAll @@ -29,7 +30,7 @@ class FixOldScores( companion object { - const val CURRENT_VERSION = 7 + const val CURRENT_VERSION = 8 } @@ -112,86 +113,110 @@ class FixOldScores( } } - fun processScore(score: ScoresRecord) { - - // Fetch the beatmap file from database - var beatmapFile = dslContext.select(BEATMAPS.BEATMAP_FILE) - .from(BEATMAPS) - .where(BEATMAPS.BEATMAP_ID.eq(score.beatmapId)) - .fetchOneInto(String::class.java) - - if(beatmapFile == null) { - this.logger.warn("Failed to fetch beatmap file for beatmap_id = ${score.beatmapId} from database") - - beatmapFile = this.osuApi.getBeatmapFile(beatmapId = score.beatmapId!!) - - if(beatmapFile == null) { - this.logger.error("Failed to fetch beatmap file for beatmap_id = ${score.beatmapId} from osu!api") - return - } else { - dslContext.update(BEATMAPS) - .set(BEATMAPS.BEATMAP_FILE, beatmapFile) - .where(BEATMAPS.BEATMAP_ID.eq(score.beatmapId)) - .execute() - } - } - - val processedReplay: CircleguardService.ReplayResponse? = try { - this.circleguardService.processReplay( - replayData = score.replay!!.decodeToString(), beatmapData = beatmapFile, mods = score.mods ?: 0 - ).get() - } catch (e: Exception) { - this.logger.error("Circleguard failed to process replay with score_id: ${score.id}") - this.logger.error(e.stackTraceToString()) + if(score.replay == null) { + dslContext.update(SCORES) + .set(SCORES.VERSION, CURRENT_VERSION) + .where(SCORES.REPLAY_ID.eq(score.replayId)) + .execute() return } - if (processedReplay == null || processedReplay.judgements.isEmpty()) { - this.logger.error("Circleguard returned null and failed to process replay with score_id: ${score.id}") - return - } + val compressReplay = CompressReplay.compressReplay(score.replay!!) - val scoreId = dslContext.update(SCORES) - .set(SCORES.UR, processedReplay.ur) - .set(SCORES.ADJUSTED_UR, processedReplay.adjusted_ur) - .set(SCORES.FRAMETIME, processedReplay.frametime) - .set(SCORES.SNAPS, processedReplay.snaps) - .set(SCORES.MEAN_ERROR, processedReplay.mean_error) - .set(SCORES.ERROR_VARIANCE, processedReplay.error_variance) - .set(SCORES.ERROR_STANDARD_DEVIATION, processedReplay.error_standard_deviation) - .set(SCORES.MINIMUM_ERROR, processedReplay.minimum_error) - .set(SCORES.MAXIMUM_ERROR, processedReplay.maximum_error) - .set(SCORES.ERROR_RANGE, processedReplay.error_range) - .set(SCORES.ERROR_COEFFICIENT_OF_VARIATION, processedReplay.error_coefficient_of_variation) - .set(SCORES.ERROR_KURTOSIS, processedReplay.error_kurtosis) - .set(SCORES.ERROR_SKEWNESS, processedReplay.error_skewness) - .set(SCORES.SNAPS, processedReplay.snaps) - .set(SCORES.EDGE_HITS, processedReplay.edge_hits) - .set(SCORES.KEYPRESSES_TIMES, processedReplay.keypresses_times?.toTypedArray()) - .set(SCORES.KEYPRESSES_MEDIAN, processedReplay.keypresses_median) - .set(SCORES.KEYPRESSES_MEDIAN_ADJUSTED, processedReplay.keypresses_median_adjusted) - .set(SCORES.KEYPRESSES_STANDARD_DEVIATION, processedReplay.keypresses_standard_deviation) - .set(SCORES.KEYPRESSES_STANDARD_DEVIATION_ADJUSTED, processedReplay.keypresses_standard_deviation_adjusted) - .set(SCORES.SLIDEREND_RELEASE_TIMES, processedReplay.sliderend_release_times?.toTypedArray()) - .set(SCORES.SLIDEREND_RELEASE_MEDIAN, processedReplay.sliderend_release_median) - .set(SCORES.SLIDEREND_RELEASE_MEDIAN_ADJUSTED, processedReplay.sliderend_release_median_adjusted) - .set(SCORES.SLIDEREND_RELEASE_STANDARD_DEVIATION, processedReplay.sliderend_release_standard_deviation) - .set(SCORES.SLIDEREND_RELEASE_STANDARD_DEVIATION_ADJUSTED, processedReplay.sliderend_release_standard_deviation_adjusted) - .set(SCORES.JUDGEMENTS, CompressJudgements.compress(processedReplay.judgements)) - .where(SCORES.REPLAY_ID.eq(score.replayId)) - .returningResult(SCORES.ID) - .fetchOne()?.getValue(SCORES.ID) - - if (scoreId == null) { - this.logger.debug("Weird, failed to insert score into scores table. At least, it did not return an ID.") + // sanity check + val decompressedReplay = CompressReplay.decompressReplay(compressReplay) + if(!decompressedReplay.contentEquals(score.replay!!)) { + logger.error("Decompressed replay does not match original replay for score_id: ${score.id}") return } dslContext.update(SCORES) + .set(SCORES.REPLAY, compressReplay) .set(SCORES.VERSION, CURRENT_VERSION) - .where(SCORES.ID.eq(scoreId)) + .where(SCORES.REPLAY_ID.eq(score.replayId)) .execute() } +// fun processScore(score: ScoresRecord) { +// +// // Fetch the beatmap file from database +// var beatmapFile = dslContext.select(BEATMAPS.BEATMAP_FILE) +// .from(BEATMAPS) +// .where(BEATMAPS.BEATMAP_ID.eq(score.beatmapId)) +// .fetchOneInto(String::class.java) +// +// if(beatmapFile == null) { +// this.logger.warn("Failed to fetch beatmap file for beatmap_id = ${score.beatmapId} from database") +// +// beatmapFile = this.osuApi.getBeatmapFile(beatmapId = score.beatmapId!!) +// +// if(beatmapFile == null) { +// this.logger.error("Failed to fetch beatmap file for beatmap_id = ${score.beatmapId} from osu!api") +// return +// } else { +// dslContext.update(BEATMAPS) +// .set(BEATMAPS.BEATMAP_FILE, beatmapFile) +// .where(BEATMAPS.BEATMAP_ID.eq(score.beatmapId)) +// .execute() +// } +// } +// +// val processedReplay: CircleguardService.ReplayResponse? = try { +// this.circleguardService.processReplay( +// replayData = score.replay!!.decodeToString(), beatmapData = beatmapFile, mods = score.mods ?: 0 +// ).get() +// } catch (e: Exception) { +// this.logger.error("Circleguard failed to process replay with score_id: ${score.id}") +// this.logger.error(e.stackTraceToString()) +// return +// } +// +// if (processedReplay == null || processedReplay.judgements.isEmpty()) { +// this.logger.error("Circleguard returned null and failed to process replay with score_id: ${score.id}") +// return +// } +// +// val scoreId = dslContext.update(SCORES) +// .set(SCORES.UR, processedReplay.ur) +// .set(SCORES.ADJUSTED_UR, processedReplay.adjusted_ur) +// .set(SCORES.FRAMETIME, processedReplay.frametime) +// .set(SCORES.SNAPS, processedReplay.snaps) +// .set(SCORES.MEAN_ERROR, processedReplay.mean_error) +// .set(SCORES.ERROR_VARIANCE, processedReplay.error_variance) +// .set(SCORES.ERROR_STANDARD_DEVIATION, processedReplay.error_standard_deviation) +// .set(SCORES.MINIMUM_ERROR, processedReplay.minimum_error) +// .set(SCORES.MAXIMUM_ERROR, processedReplay.maximum_error) +// .set(SCORES.ERROR_RANGE, processedReplay.error_range) +// .set(SCORES.ERROR_COEFFICIENT_OF_VARIATION, processedReplay.error_coefficient_of_variation) +// .set(SCORES.ERROR_KURTOSIS, processedReplay.error_kurtosis) +// .set(SCORES.ERROR_SKEWNESS, processedReplay.error_skewness) +// .set(SCORES.SNAPS, processedReplay.snaps) +// .set(SCORES.EDGE_HITS, processedReplay.edge_hits) +// .set(SCORES.KEYPRESSES_TIMES, processedReplay.keypresses_times?.toTypedArray()) +// .set(SCORES.KEYPRESSES_MEDIAN, processedReplay.keypresses_median) +// .set(SCORES.KEYPRESSES_MEDIAN_ADJUSTED, processedReplay.keypresses_median_adjusted) +// .set(SCORES.KEYPRESSES_STANDARD_DEVIATION, processedReplay.keypresses_standard_deviation) +// .set(SCORES.KEYPRESSES_STANDARD_DEVIATION_ADJUSTED, processedReplay.keypresses_standard_deviation_adjusted) +// .set(SCORES.SLIDEREND_RELEASE_TIMES, processedReplay.sliderend_release_times?.toTypedArray()) +// .set(SCORES.SLIDEREND_RELEASE_MEDIAN, processedReplay.sliderend_release_median) +// .set(SCORES.SLIDEREND_RELEASE_MEDIAN_ADJUSTED, processedReplay.sliderend_release_median_adjusted) +// .set(SCORES.SLIDEREND_RELEASE_STANDARD_DEVIATION, processedReplay.sliderend_release_standard_deviation) +// .set(SCORES.SLIDEREND_RELEASE_STANDARD_DEVIATION_ADJUSTED, processedReplay.sliderend_release_standard_deviation_adjusted) +// .set(SCORES.JUDGEMENTS, CompressJudgements.compress(processedReplay.judgements)) +// .where(SCORES.REPLAY_ID.eq(score.replayId)) +// .returningResult(SCORES.ID) +// .fetchOne()?.getValue(SCORES.ID) +// +// if (scoreId == null) { +// this.logger.debug("Weird, failed to insert score into scores table. At least, it did not return an ID.") +// return +// } +// +// dslContext.update(SCORES) +// .set(SCORES.VERSION, CURRENT_VERSION) +// .where(SCORES.ID.eq(scoreId)) +// .execute() +// } + } \ No newline at end of file diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/scheduler/ImportScores.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/scheduler/ImportScores.kt index 7f9e907..06af7f5 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/nise/scheduler/ImportScores.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/scheduler/ImportScores.kt @@ -2,19 +2,20 @@ package com.nisemoe.nise.scheduler import com.nisemoe.generated.tables.records.ScoresRecord import com.nisemoe.generated.tables.references.* -import com.nisemoe.konata.Replay -import com.nisemoe.konata.ReplaySetComparison -import com.nisemoe.konata.compareReplaySet import com.nisemoe.nise.UserQueueDetails import com.nisemoe.nise.database.ScoreService import com.nisemoe.nise.database.UserService import com.nisemoe.nise.integrations.CircleguardService import com.nisemoe.nise.integrations.DiscordEmbed import com.nisemoe.nise.integrations.DiscordService +import com.nisemoe.nise.konata.Replay +import com.nisemoe.nise.konata.ReplaySetComparison +import com.nisemoe.nise.konata.compareReplaySet import com.nisemoe.nise.osu.Mod import com.nisemoe.nise.osu.OsuApi import com.nisemoe.nise.osu.OsuApiModels import com.nisemoe.nise.service.CacheService +import com.nisemoe.nise.service.CompressReplay import com.nisemoe.nise.service.UpdateUserQueueService import kotlinx.serialization.Serializable import org.jooq.DSLContext @@ -739,8 +740,10 @@ class ImportScores( return } + val compressedReplay = CompressReplay.compressReplay(scoreReplay.content.toByteArray()) + val scoreId = dslContext.update(SCORES) - .set(SCORES.REPLAY, scoreReplay.content.toByteArray()) + .set(SCORES.REPLAY, compressedReplay) .set(SCORES.UR, processedReplay.ur) .set(SCORES.ADJUSTED_UR, processedReplay.adjusted_ur) .set(SCORES.FRAMETIME, processedReplay.frametime) diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/service/CompressReplay.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/service/CompressReplay.kt new file mode 100644 index 0000000..7382c3c --- /dev/null +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/service/CompressReplay.kt @@ -0,0 +1,38 @@ +package com.nisemoe.nise.service + +import com.aayushatharva.brotli4j.Brotli4jLoader +import com.aayushatharva.brotli4j.decoder.Decoder +import com.aayushatharva.brotli4j.encoder.Encoder +import org.apache.commons.compress.compressors.lzma.LZMACompressorInputStream +import java.util.* + +class CompressReplay { + + companion object { + + init { + Brotli4jLoader.ensureAvailability() + } + + private val brotliParameters: Encoder.Parameters = Encoder.Parameters() + .setQuality(11) + + fun compressReplay(replay: String): ByteArray { + return compressReplay(replay.toByteArray()) + } + + fun compressReplay(replay: ByteArray): ByteArray { +// val replayData = Base64.getDecoder().decode(replay).inputStream().use { byteStream -> +// LZMACompressorInputStream(byteStream).readBytes() +// } + + return Encoder.compress(replay, brotliParameters) + } + + fun decompressReplay(replay: ByteArray): ByteArray { + return Decoder.decompress(replay).decompressedData + } + + } + +} \ No newline at end of file diff --git a/mari/src/test/kotlin/org/nisemoe/mari/judgements/CompressJudgementsTest.kt b/nise-backend/src/test/kotlin/com/nisemoe/nise/CompressJudgementsTest.kt similarity index 91% rename from mari/src/test/kotlin/org/nisemoe/mari/judgements/CompressJudgementsTest.kt rename to nise-backend/src/test/kotlin/com/nisemoe/nise/CompressJudgementsTest.kt index 06b7063..44f9b2e 100644 --- a/mari/src/test/kotlin/org/nisemoe/mari/judgements/CompressJudgementsTest.kt +++ b/nise-backend/src/test/kotlin/com/nisemoe/nise/CompressJudgementsTest.kt @@ -1,8 +1,10 @@ -package org.nisemoe.mari.judgements +package com.nisemoe.nise import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test +import org.nisemoe.mari.judgements.CompressJudgements +import org.nisemoe.mari.judgements.Judgement class CompressJudgementsTest { diff --git a/konata/src/test/kotlin/com/nisemoe/konata/CorrelationTest.kt b/nise-backend/src/test/kotlin/com/nisemoe/nise/CorrelationTest.kt similarity index 99% rename from konata/src/test/kotlin/com/nisemoe/konata/CorrelationTest.kt rename to nise-backend/src/test/kotlin/com/nisemoe/nise/CorrelationTest.kt index 2dfb668..f33d4d0 100644 --- a/konata/src/test/kotlin/com/nisemoe/konata/CorrelationTest.kt +++ b/nise-backend/src/test/kotlin/com/nisemoe/nise/CorrelationTest.kt @@ -1,5 +1,7 @@ -package com.nisemoe.konata +package com.nisemoe.nise +import com.nisemoe.nise.konata.Replay +import com.nisemoe.nise.konata.compareReplayPair import org.junit.jupiter.api.Test import kotlin.test.assertEquals diff --git a/konata/src/test/kotlin/com/nisemoe/konata/HardRockTest.kt b/nise-backend/src/test/kotlin/com/nisemoe/nise/HardRockTest.kt similarity index 99% rename from konata/src/test/kotlin/com/nisemoe/konata/HardRockTest.kt rename to nise-backend/src/test/kotlin/com/nisemoe/nise/HardRockTest.kt index 1d878d7..c1da753 100644 --- a/konata/src/test/kotlin/com/nisemoe/konata/HardRockTest.kt +++ b/nise-backend/src/test/kotlin/com/nisemoe/nise/HardRockTest.kt @@ -1,5 +1,7 @@ -package com.nisemoe.konata +package com.nisemoe.nise +import com.nisemoe.nise.konata.Replay +import com.nisemoe.nise.konata.compareReplayPair import org.junit.jupiter.api.Test import kotlin.test.assertEquals diff --git a/konata/src/test/kotlin/com/nisemoe/konata/ReplayTest.kt b/nise-backend/src/test/kotlin/com/nisemoe/nise/ReplayTest.kt similarity index 99% rename from konata/src/test/kotlin/com/nisemoe/konata/ReplayTest.kt rename to nise-backend/src/test/kotlin/com/nisemoe/nise/ReplayTest.kt index 9b19860..c27db44 100644 --- a/konata/src/test/kotlin/com/nisemoe/konata/ReplayTest.kt +++ b/nise-backend/src/test/kotlin/com/nisemoe/nise/ReplayTest.kt @@ -1,5 +1,8 @@ -package com.nisemoe.konata +package com.nisemoe.nise +import com.nisemoe.nise.konata.Replay +import com.nisemoe.nise.konata.compareReplayPair +import com.nisemoe.nise.konata.compareReplaySet import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import java.nio.file.Files diff --git a/nise-backend/src/test/kotlin/com/nisemoe/nise/database/Compression.kt b/nise-backend/src/test/kotlin/com/nisemoe/nise/database/Compression.kt new file mode 100644 index 0000000..73ee595 --- /dev/null +++ b/nise-backend/src/test/kotlin/com/nisemoe/nise/database/Compression.kt @@ -0,0 +1,24 @@ +package com.nisemoe.nise.database + +import com.nisemoe.nise.service.CompressReplay +import org.junit.jupiter.api.Test +import org.slf4j.LoggerFactory + +class Compression { + + private val logger = LoggerFactory.getLogger(javaClass) + + val replayFile = "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" + val BASE_SIZE = 18907 + + @Test + fun compressionShit() { + val compressed = CompressReplay.compressReplay(replayFile) + + + logger.info("Compressed replay collection from ${replayFile.length} bytes to ${compressed.size} bytes [reference: $BASE_SIZE]") + + assert(compressed.size <= BASE_SIZE) + } + +} \ No newline at end of file diff --git a/nise-backend/src/test/kotlin/com/nisemoe/nise/osu/Whatever.kt b/nise-backend/src/test/kotlin/com/nisemoe/nise/osu/Whatever.kt new file mode 100644 index 0000000..232e176 --- /dev/null +++ b/nise-backend/src/test/kotlin/com/nisemoe/nise/osu/Whatever.kt @@ -0,0 +1,69 @@ +package com.nisemoe.nise.osu + +import com.nisemoe.nise.service.CompressReplay +import com.nisemoe.generated.tables.Scores.Companion.SCORES +import com.nisemoe.generated.tables.records.ScoresRecord +import com.nisemoe.nise.database.UserService +import com.nisemoe.nise.konata.tools.getEvents +import com.nisemoe.nise.konata.tools.processEvents +import com.nisemoe.nise.scheduler.GlobalCache +import org.apache.commons.compress.compressors.lzma.LZMACompressorInputStream +import org.jooq.DSLContext +import org.jooq.impl.DSL +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.mock.mockito.MockBean +import org.springframework.test.context.ActiveProfiles +import java.util.* + + +@SpringBootTest +@ActiveProfiles("postgres") +@MockBean(GlobalCache::class, UserService::class) +@Disabled +class Whatever { + + private val logger = LoggerFactory.getLogger(javaClass) + + @Autowired + private lateinit var dslContext: DSLContext + + @Test + fun compressAndVerifyBulkWithB64Decode() { + val scores = dslContext.select() + .from(SCORES) + .where(SCORES.REPLAY.isNotNull) + .orderBy(DSL.rand()) + .fetchInto(ScoresRecord::class.java) + + for(score in scores) { + val replayString = score.replay!! + val replay = Base64.getDecoder().decode(replayString).inputStream().use { byteStream -> + LZMACompressorInputStream(byteStream).readBytes() + } + + val compressed = CompressReplay.compressReplay(score.replay!!) + + logger.info("Compressed replay collection from ${score.replay!!.size} bytes to ${compressed.size} bytes") + val spaceSaved = (1 - compressed.size.toDouble() / score.replay!!.size) * 100 + logger.info("Space saved: %.2f%%".format(spaceSaved)) + + val decompressed = CompressReplay.decompressReplay(compressed) + + assert(replay.size > compressed.size) + assert(replay.contentEquals(decompressed)) + + val replayString1 = String(replayString, Charsets.UTF_8).trimEnd(',') + val replayString2 = String(decompressed, Charsets.UTF_8).trimEnd(',') + + val replayEvents = getEvents(replayString1) + val decompressedEvents = processEvents(replayString2) + + assert(replayEvents.size == decompressedEvents.size) + } + } + +} \ No newline at end of file diff --git a/konata/src/test/resources/replays.txt b/nise-backend/src/test/resources/replays.txt similarity index 100% rename from konata/src/test/resources/replays.txt rename to nise-backend/src/test/resources/replays.txt diff --git a/konata/src/test/resources/replays_1.txt b/nise-backend/src/test/resources/replays_1.txt similarity index 100% rename from konata/src/test/resources/replays_1.txt rename to nise-backend/src/test/resources/replays_1.txt