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