From 402c89b20d648c132d85edc1974837696d7129ce Mon Sep 17 00:00:00 2001 From: "nise.moe" Date: Wed, 6 Mar 2024 23:46:21 +0100 Subject: [PATCH] Open sourced mari module --- mari/pom.xml | 112 ++++++++++++++++++ mari/readme.md | 29 +++++ .../mari/judgements/CompressJudgements.kt | 111 +++++++++++++++++ .../nisemoe/mari/judgements/JudgementModel.kt | 45 +++++++ .../org/nisemoe/mari/replays}/OsuReplay.kt | 36 +++--- .../mari/judgements/CompressJudgementsTest.kt | 30 +++++ nise-backend/pom.xml | 5 + .../main/kotlin/com/nisemoe/nise/Format.kt | 12 +- .../main/kotlin/com/nisemoe/nise/Models.kt | 9 +- .../nise/controller/UploadReplayController.kt | 7 +- .../com/nisemoe/nise/database/ScoreService.kt | 29 +++-- .../nisemoe/nise/database/UserScoreService.kt | 7 +- .../nise/integrations/CircleguardService.kt | 30 +---- .../nisemoe/nise/scheduler/FixOldScores.kt | 7 +- .../nisemoe/nise/scheduler/ImportScores.kt | 7 +- .../nise/service/CompressJudgements.kt | 99 ---------------- .../scheduler/JudgementCompressionTest.kt | 8 +- 17 files changed, 389 insertions(+), 194 deletions(-) create mode 100644 mari/pom.xml create mode 100644 mari/readme.md create mode 100644 mari/src/main/kotlin/org/nisemoe/mari/judgements/CompressJudgements.kt create mode 100644 mari/src/main/kotlin/org/nisemoe/mari/judgements/JudgementModel.kt rename {nise-backend/src/main/kotlin/com/nisemoe/nise/osu => mari/src/main/kotlin/org/nisemoe/mari/replays}/OsuReplay.kt (89%) create mode 100644 mari/src/test/kotlin/org/nisemoe/mari/judgements/CompressJudgementsTest.kt delete mode 100644 nise-backend/src/main/kotlin/com/nisemoe/nise/service/CompressJudgements.kt diff --git a/mari/pom.xml b/mari/pom.xml new file mode 100644 index 0000000..c19f75d --- /dev/null +++ b/mari/pom.xml @@ -0,0 +1,112 @@ + + + 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 new file mode 100644 index 0000000..374ef48 --- /dev/null +++ b/mari/readme.md @@ -0,0 +1,29 @@ +# 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/mari/src/main/kotlin/org/nisemoe/mari/judgements/CompressJudgements.kt b/mari/src/main/kotlin/org/nisemoe/mari/judgements/CompressJudgements.kt new file mode 100644 index 0000000..19dd7b0 --- /dev/null +++ b/mari/src/main/kotlin/org/nisemoe/mari/judgements/CompressJudgements.kt @@ -0,0 +1,111 @@ +package org.nisemoe.mari.judgements + +import com.aayushatharva.brotli4j.Brotli4jLoader +import com.aayushatharva.brotli4j.decoder.Decoder +import com.aayushatharva.brotli4j.encoder.Encoder +import java.io.ByteArrayOutputStream +import java.nio.ByteBuffer +import kotlin.math.round + +/** + * Compresses and decompresses score judgements with lossless accuracy. + */ +class CompressJudgements { + + companion object { + + init { + Brotli4jLoader.ensureAvailability() + } + + private val brotliParameters: Encoder.Parameters = Encoder.Parameters() + .setQuality(11) + + /** + * Writes a variable-length quantity to the buffer. + * See: https://en.wikipedia.org/wiki/Variable-length_quantity + */ + private fun ByteBuffer.putVLQ(value: Int) { + var currentValue = value + do { + var temp = (currentValue and 0x7F) + currentValue = currentValue ushr 7 + if (currentValue != 0) { + temp = temp or 0x80 + } + this.put(temp.toByte()) + } while (currentValue != 0) + } + + /** + * Reads a variable-length quantity from the buffer. + * See: https://en.wikipedia.org/wiki/Variable-length_quantity + */ + private fun ByteBuffer.getVLQ(): Int { + var result = 0 + var shift = 0 + var b: Byte + do { + b = this.get() + result = result or ((b.toInt() and 0x7F) shl shift) + shift += 7 + } while (b.toInt() and 0x80 != 0) + return result + } + + fun compress(judgements: List): ByteArray { + val byteStream = ByteArrayOutputStream() + var lastTimestamp = 0.0 + + judgements.forEach { judgement -> + byteStream.use { stream -> + /** + * We allocate an arbitrary amount of buffer which *hopefully* is enough. + */ + ByteBuffer.allocate(4096).let { buffer -> + buffer.putVLQ((judgement.time - lastTimestamp).toInt()) + buffer.putVLQ(round(judgement.x * 100).toInt()) + buffer.putVLQ(round(judgement.y * 100).toInt()) + buffer.put(judgement.type.ordinal.toByte()) + buffer.putVLQ((judgement.distanceToCenter * 100).toInt()) + buffer.putVLQ((judgement.distanceToEdge * 100).toInt()) + buffer.putVLQ(judgement.error.toInt()) + + lastTimestamp = judgement.time + stream.write(buffer.array(), 0, buffer.position()) + } + } + } + + return Encoder.compress(byteStream.toByteArray(), brotliParameters) + } + + fun decompress(compressedData: ByteArray): List { + val data = Decoder.decompress(compressedData).decompressedData ?: return emptyList() + + val buffer = ByteBuffer.wrap(data) + val judgements = mutableListOf() + var lastTime = 0.0 + + while (buffer.hasRemaining()) { + val deltaTime = buffer.getVLQ() + lastTime += deltaTime + + judgements.add( + Judgement( + time = lastTime, + x = buffer.getVLQ() / 100.0, + y = buffer.getVLQ() / 100.0, + type = Judgement.Type.entries[buffer.get().toInt()], + distanceToCenter = buffer.getVLQ() / 100.0, + distanceToEdge = buffer.getVLQ() / 100.0, + error = buffer.getVLQ().toDouble() + )) + } + + return judgements + } + + } + +} \ No newline at end of file diff --git a/mari/src/main/kotlin/org/nisemoe/mari/judgements/JudgementModel.kt b/mari/src/main/kotlin/org/nisemoe/mari/judgements/JudgementModel.kt new file mode 100644 index 0000000..2b5a111 --- /dev/null +++ b/mari/src/main/kotlin/org/nisemoe/mari/judgements/JudgementModel.kt @@ -0,0 +1,45 @@ +package org.nisemoe.mari.judgements + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Represents a judgement on a hit object. + * The structure *might* seem arbitrary but it's more or less what Circleguard provides. + */ +@Serializable +data class Judgement( + val time: Double, + val x: Double, + val y: Double, + val type: Type, + + @SerialName("distance_center") + val distanceToCenter: Double, + + @SerialName("distance_edge") + val distanceToEdge: Double, + + + val error: Double +) { + + @Serializable + enum class Type { + + @SerialName("Hit300") + THREE_HUNDRED, + + @SerialName("Hit100") + ONE_HUNDRED, + + @SerialName("Hit50") + FIFTY, + + @SerialName("Miss") + MISS + + } + +} + diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/osu/OsuReplay.kt b/mari/src/main/kotlin/org/nisemoe/mari/replays/OsuReplay.kt similarity index 89% rename from nise-backend/src/main/kotlin/com/nisemoe/nise/osu/OsuReplay.kt rename to mari/src/main/kotlin/org/nisemoe/mari/replays/OsuReplay.kt index 33c1830..cbf0fb9 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/nise/osu/OsuReplay.kt +++ b/mari/src/main/kotlin/org/nisemoe/mari/replays/OsuReplay.kt @@ -1,4 +1,4 @@ -package com.nisemoe.nise.osu +package org.nisemoe.mari.replays import org.apache.commons.compress.compressors.lzma.LZMACompressorInputStream import org.apache.commons.compress.compressors.lzma.LZMACompressorOutputStream @@ -9,14 +9,21 @@ import java.nio.ByteOrder import java.nio.charset.StandardCharsets import java.util.* +/** + * Decodes an osu! replay file from a byte array. + */ class OsuReplay(fileContent: ByteArray) { companion object { - // ~4mb + /** + * We restrict the maximum replay file size to roughly 4MB. + */ private val EXPECTED_FILE_SIZE = 0 .. 4194304 - // ~512kb + /** + * We restrict the maximum string length to roughly 500KB. + */ private const val MAX_STRING_LENGTH = 512000 private val EXPECTED_STRING_LENGTH = 0 .. MAX_STRING_LENGTH @@ -64,9 +71,6 @@ class OsuReplay(fileContent: ByteArray) { private fun decode() { try { gameMode = dis.readByte().toInt() - if(gameMode != 0) { - throw SecurityException("Invalid game mode") - } gameVersion = readIntLittleEndian() beatmapHash = dis.readCompressedReplayData() @@ -109,22 +113,18 @@ class OsuReplay(fileContent: ByteArray) { } private fun DataInputStream.readCompressedReplayData(length: Int): String { - // Read the compressed data val compressedData = ByteArray(length) readFully(compressedData) - // Decompress the data - val decompressedStream = LZMACompressorInputStream(compressedData.inputStream()) - val decompressedData = decompressedStream.readBytes() - decompressedStream.close() + val compressedOutputStream = ByteArrayOutputStream().use { outputStream -> + LZMACompressorInputStream(compressedData.inputStream()).use { decompressedStream -> + LZMACompressorOutputStream(outputStream).use { lzmaCompressorOutputStream -> + decompressedStream.copyTo(lzmaCompressorOutputStream) + } + } + outputStream + } - // Compress the decompressed data - val compressedOutputStream = ByteArrayOutputStream() - val lzmaCompressorOutputStream = LZMACompressorOutputStream(compressedOutputStream) - lzmaCompressorOutputStream.write(decompressedData) - lzmaCompressorOutputStream.close() - - // Now encode the re-compressed data to Base64 return Base64.getEncoder().encodeToString(compressedOutputStream.toByteArray()) } diff --git a/mari/src/test/kotlin/org/nisemoe/mari/judgements/CompressJudgementsTest.kt b/mari/src/test/kotlin/org/nisemoe/mari/judgements/CompressJudgementsTest.kt new file mode 100644 index 0000000..9d5f761 --- /dev/null +++ b/mari/src/test/kotlin/org/nisemoe/mari/judgements/CompressJudgementsTest.kt @@ -0,0 +1,30 @@ +package org.nisemoe.mari.judgements + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class CompressJudgementsTest { + + @Test + fun testCompress() { + val judgements = listOf( + Judgement(time = 1.0, x = 1.0, y = 1.0, type = Judgement.Type.THREE_HUNDRED, distanceToCenter = 1.0, distanceToEdge = 1.0, error = 1.0), + Judgement(time = 2.0, x = 2.0, y = 2.0, type = Judgement.Type.THREE_HUNDRED, distanceToCenter = 2.0, distanceToEdge = 2.0, error = 2.0) + ) + val compressedData = CompressJudgements.compress(judgements) + assertTrue(compressedData.isNotEmpty()) + } + + @Test + fun testCompressAndDecompress() { + val originalJudgements = listOf( + Judgement(time = 1.123456789123456, x = 1.0, y = 1.0, type = Judgement.Type.THREE_HUNDRED, distanceToCenter = 1.0, distanceToEdge = 1.0, error = 1.0), + Judgement(time = 2.123456789123456, x = 2.0, y = 2.0, type = Judgement.Type.THREE_HUNDRED, distanceToCenter = 2.0, distanceToEdge = 2.0, error = 2.0) + ) + val compressedData = CompressJudgements.compress(originalJudgements) + val decompressedJudgements = CompressJudgements.decompress(compressedData) + assertEquals(originalJudgements, decompressedJudgements) + } + +} \ No newline at end of file diff --git a/nise-backend/pom.xml b/nise-backend/pom.xml index 8d165bd..072aeb5 100644 --- a/nise-backend/pom.xml +++ b/nise-backend/pom.xml @@ -64,6 +64,11 @@ konata 0.0.1-SNAPSHOT + + org.nisemoe + mari + 0.0.1-SNAPSHOT + com.fasterxml.jackson.dataformat jackson-dataformat-xml diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/Format.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/Format.kt index 64ca405..6761b7f 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/nise/Format.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/Format.kt @@ -1,7 +1,7 @@ package com.nisemoe.nise import com.nisemoe.generated.enums.JudgementType -import com.nisemoe.nise.integrations.CircleguardService +import org.nisemoe.mari.judgements.Judgement import java.time.LocalDateTime import java.time.ZoneOffset import java.time.format.DateTimeFormatter @@ -25,12 +25,12 @@ class Format { return Date.from(localDateTime.atZone(ZoneOffset.UTC).toInstant()) } - fun fromJudgementType(circleGuardJudgementType: CircleguardService.JudgementType): JudgementType { + fun fromJudgementType(circleGuardJudgementType: Judgement.Type): JudgementType { return when (circleGuardJudgementType) { - CircleguardService.JudgementType.THREE_HUNDRED -> JudgementType.`300` - CircleguardService.JudgementType.ONE_HUNDRED -> JudgementType.`100` - CircleguardService.JudgementType.FIFTY -> JudgementType.`50` - CircleguardService.JudgementType.MISS -> JudgementType.Miss + Judgement.Type.THREE_HUNDRED -> JudgementType.`300` + Judgement.Type.ONE_HUNDRED -> JudgementType.`100` + Judgement.Type.FIFTY -> JudgementType.`50` + Judgement.Type.MISS -> JudgementType.Miss } } diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/Models.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/Models.kt index a3a4f71..c2bd258 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/nise/Models.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/Models.kt @@ -1,9 +1,8 @@ package com.nisemoe.nise -import com.nisemoe.nise.integrations.CircleguardService import kotlinx.serialization.Serializable +import org.nisemoe.mari.judgements.Judgement import java.time.OffsetDateTime -import java.util.UUID data class UserQueueDetails( val isProcessing: Boolean, @@ -100,7 +99,7 @@ data class ReplayViewerData( val beatmap: String, val replay: String, val mods: Int, - val judgements: List + val judgements: List ) data class ReplayPairViewerData( @@ -108,8 +107,8 @@ data class ReplayPairViewerData( val replay1: String, val replay2: String, val mods: Int, - val judgements1: List, - val judgements2: List + val judgements1: List, + val judgements2: List ) data class ReplayData( 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 eb62822..0354382 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 @@ -10,10 +10,10 @@ import com.nisemoe.konata.compareSingleReplayWithSet import com.nisemoe.nise.database.BeatmapService import com.nisemoe.nise.integrations.CircleguardService import com.nisemoe.nise.osu.OsuApi -import com.nisemoe.nise.osu.OsuReplay import com.nisemoe.nise.scheduler.ImportScores -import com.nisemoe.nise.service.CompressJudgements import org.jooq.DSLContext +import org.nisemoe.mari.judgements.CompressJudgements +import org.nisemoe.mari.replays.OsuReplay import org.slf4j.LoggerFactory import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.PostMapping @@ -26,7 +26,6 @@ import java.time.OffsetDateTime class UploadReplayController( private val dslContext: DSLContext, private val beatmapService: BeatmapService, - private val compressJudgements: CompressJudgements, private val circleguardService: CircleguardService, private val osuApi: OsuApi ) { @@ -175,7 +174,7 @@ class UploadReplayController( .set(USER_SCORES.KEYPRESSES_STANDARD_DEVIATION_ADJUSTED, analysis.keypresses_standard_deviation_adjusted) .set(USER_SCORES.SLIDEREND_RELEASE_MEDIAN_ADJUSTED, analysis.sliderend_release_median_adjusted) .set(USER_SCORES.SLIDEREND_RELEASE_STANDARD_DEVIATION_ADJUSTED, analysis.sliderend_release_standard_deviation_adjusted) - .set(SCORES.JUDGEMENTS, compressJudgements.serialize(analysis.judgements)) + .set(SCORES.JUDGEMENTS, CompressJudgements.compress(analysis.judgements)) .returning(USER_SCORES.ID) .fetchOne() 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 09b22d8..3b85cf8 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 @@ -9,13 +9,14 @@ 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 com.nisemoe.nise.service.CompressJudgements import org.apache.commons.compress.compressors.lzma.LZMACompressorInputStream import org.jooq.Condition import org.jooq.DSLContext import org.jooq.Record import org.jooq.impl.DSL import org.jooq.impl.DSL.avg +import org.nisemoe.mari.judgements.CompressJudgements +import org.nisemoe.mari.judgements.Judgement import org.slf4j.LoggerFactory import org.springframework.stereotype.Service import java.time.LocalDateTime @@ -25,10 +26,8 @@ import kotlin.math.roundToInt @Service class ScoreService( private val dslContext: DSLContext, - private val beatmapService: BeatmapService, private val authService: AuthService, - private val osuApi: OsuApi, - private val compressJudgements: CompressJudgements + private val osuApi: OsuApi ) { private val logger = LoggerFactory.getLogger(javaClass) @@ -502,16 +501,16 @@ class ScoreService( replayData.comparable_adjusted_ur = otherScores.get("avg_adjusted_ur", Double::class.java) } - fun mapLegacyJudgement(judgementType: JudgementType): CircleguardService.JudgementType { + fun mapLegacyJudgement(judgementType: JudgementType): Judgement.Type { return when(judgementType) { - JudgementType.Miss -> CircleguardService.JudgementType.MISS - JudgementType.`300` -> CircleguardService.JudgementType.THREE_HUNDRED - JudgementType.`100` -> CircleguardService.JudgementType.ONE_HUNDRED - JudgementType.`50` -> CircleguardService.JudgementType.FIFTY + JudgementType.Miss -> Judgement.Type.MISS + JudgementType.`300` -> Judgement.Type.THREE_HUNDRED + JudgementType.`100` -> Judgement.Type.ONE_HUNDRED + JudgementType.`50` -> Judgement.Type.FIFTY } } - fun getJudgements(replayId: Long): List { + fun getJudgements(replayId: Long): List { val judgementsRecord = dslContext.select(SCORES.JUDGEMENTS) .from(SCORES) .where(SCORES.REPLAY_ID.eq(replayId)) @@ -527,19 +526,19 @@ class ScoreService( .where(SCORES_JUDGEMENTS.SCORE_ID.eq(scoreId)) .fetchInto(ScoresJudgementsRecord::class.java) return judgementRecords.map { - CircleguardService.ScoreJudgement( + Judgement( x = it.x!!, y = it.y!!, error = it.error!!, - distance_center = it.distanceCenter!!, - distance_edge = it.distanceEdge!!, + distanceToCenter = it.distanceCenter!!, + distanceToEdge = it.distanceEdge!!, time = it.time!!, type = mapLegacyJudgement(it.type!!) ) } } - return compressJudgements.deserialize(judgementsRecord.judgements!!) + return CompressJudgements.decompress(judgementsRecord.judgements!!) } fun getHitDistribution(scoreId: Int): Map { @@ -552,7 +551,7 @@ class ScoreService( return this.getHitDistributionLegacy(scoreId) } - val judgements = compressJudgements.deserialize(judgementsRecord.judgements!!) + val judgements = CompressJudgements.decompress(judgementsRecord.judgements!!) val errorDistribution = mutableMapOf>() var totalHits = 0 diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/database/UserScoreService.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/database/UserScoreService.kt index 42d9b87..23ec651 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/nise/database/UserScoreService.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/database/UserScoreService.kt @@ -6,8 +6,8 @@ import com.nisemoe.nise.Format import com.nisemoe.nise.ReplayData import com.nisemoe.nise.ReplayDataSimilarScore import com.nisemoe.nise.osu.Mod -import com.nisemoe.nise.service.CompressJudgements import org.jooq.DSLContext +import org.nisemoe.mari.judgements.CompressJudgements import org.springframework.stereotype.Service import java.time.LocalDateTime import java.util.* @@ -16,8 +16,7 @@ import kotlin.math.roundToInt @Service class UserScoreService( private val dslContext: DSLContext, - private val scoreService: ScoreService, - private val compressJudgements: CompressJudgements + private val scoreService: ScoreService ) { fun getReplayData(replayId: UUID): ReplayData? { @@ -165,7 +164,7 @@ class UserScoreService( } fun getHitDistribution(compressedJudgements: ByteArray): Map { - val judgements = compressJudgements.deserialize(compressedJudgements) + val judgements = CompressJudgements.decompress(compressedJudgements) val errorDistribution = mutableMapOf>() var totalHits = 0 diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/integrations/CircleguardService.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/integrations/CircleguardService.kt index 4360ed5..d756096 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/nise/integrations/CircleguardService.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/integrations/CircleguardService.kt @@ -2,9 +2,9 @@ package com.nisemoe.nise.integrations import com.nisemoe.nise.osu.Mod import com.nisemoe.nise.scheduler.ImportScores -import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json +import org.nisemoe.mari.judgements.Judgement import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Service import java.net.URI @@ -32,32 +32,6 @@ class CircleguardService { val mods: Int ) - @Serializable - data class ScoreJudgement( - val time: Double, - val x: Double, - val y: Double, - val type: JudgementType, - val distance_center: Double, - val distance_edge: Double, - val error: Double - ) - - @Serializable - enum class JudgementType { - @SerialName("Hit300") - THREE_HUNDRED, - - @SerialName("Hit100") - ONE_HUNDRED, - - @SerialName("Hit50") - FIFTY, - - @SerialName("Miss") - MISS - } - @Serializable data class ReplayResponse( val ur: Double?, @@ -87,7 +61,7 @@ class CircleguardService { val sliderend_release_standard_deviation: Double?, val sliderend_release_standard_deviation_adjusted: Double?, - val judgements: List + val judgements: List ) fun postProcessReplay(replayResponse: ReplayResponse, mods: Int = 0) { 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 00ab659..f63e382 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,13 +5,13 @@ 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.CompressJudgements import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import org.jooq.DSLContext +import org.nisemoe.mari.judgements.CompressJudgements import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Value import org.springframework.context.annotation.Profile @@ -24,8 +24,7 @@ import java.time.OffsetDateTime class FixOldScores( private val dslContext: DSLContext, private val osuApi: OsuApi, - private val circleguardService: CircleguardService, - private val compressJudgements: CompressJudgements + private val circleguardService: CircleguardService ){ companion object { @@ -179,7 +178,7 @@ class FixOldScores( .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.serialize(processedReplay.judgements)) + .set(SCORES.JUDGEMENTS, CompressJudgements.compress(processedReplay.judgements)) .where(SCORES.REPLAY_ID.eq(score.replayId)) .returningResult(SCORES.ID) .fetchOne()?.getValue(SCORES.ID) 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 c3b8908..d7a5b16 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 @@ -15,11 +15,11 @@ 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.CompressJudgements import com.nisemoe.nise.service.UpdateUserQueueService import kotlinx.serialization.Serializable import org.jooq.DSLContext import org.jooq.Query +import org.nisemoe.mari.judgements.CompressJudgements import org.slf4j.LoggerFactory import org.springframework.beans.factory.InitializingBean import org.springframework.beans.factory.annotation.Value @@ -48,8 +48,7 @@ class ImportScores( private val scoreService: ScoreService, private val updateUserQueueService: UpdateUserQueueService, private val circleguardService: CircleguardService, - private val messagingTemplate: SimpMessagingTemplate, - private val compressJudgements: CompressJudgements + private val messagingTemplate: SimpMessagingTemplate ) : InitializingBean { private val userToUpdateBucket = mutableListOf() @@ -760,7 +759,7 @@ class ImportScores( .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.serialize(processedReplay.judgements)) + .set(SCORES.JUDGEMENTS, CompressJudgements.compress(processedReplay.judgements)) .where(SCORES.REPLAY_ID.eq(score.best_id)) .returningResult(SCORES.ID) .fetchOne()?.getValue(SCORES.ID) diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/service/CompressJudgements.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/service/CompressJudgements.kt deleted file mode 100644 index 24a1597..0000000 --- a/nise-backend/src/main/kotlin/com/nisemoe/nise/service/CompressJudgements.kt +++ /dev/null @@ -1,99 +0,0 @@ -package com.nisemoe.nise.service - -import com.aayushatharva.brotli4j.Brotli4jLoader -import com.aayushatharva.brotli4j.decoder.Decoder -import com.aayushatharva.brotli4j.encoder.Encoder -import com.nisemoe.nise.integrations.CircleguardService -import org.springframework.stereotype.Service -import java.io.ByteArrayOutputStream -import java.nio.ByteBuffer -import kotlin.math.round - -@Service -class CompressJudgements { - - val brotliParameters: Encoder.Parameters = Encoder.Parameters() - .setQuality(11) - - init { - Brotli4jLoader.ensureAvailability() - } - - fun ByteBuffer.putVLQ(value: Int) { - var currentValue = value - do { - var temp = (currentValue and 0x7F) - currentValue = currentValue ushr 7 - if (currentValue != 0) { - temp = temp or 0x80 - } - this.put(temp.toByte()) - } while (currentValue != 0) - } - - fun ByteBuffer.getVLQ(): Int { - var result = 0 - var shift = 0 - var b: Byte - do { - b = this.get() - result = result or ((b.toInt() and 0x7F) shl shift) - shift += 7 - } while (b.toInt() and 0x80 != 0) - return result - } - - fun serialize(judgements: List): ByteArray { - val byteStream = ByteArrayOutputStream() - var lastTimestamp = 0.0 - - judgements.forEach { judgement -> - byteStream.use { stream -> - /** - * We allocate an arbitrary amount of buffer which *hopefully* is enough. - */ - ByteBuffer.allocate(4096).let { buffer -> - buffer.putVLQ((judgement.time - lastTimestamp).toInt()) - buffer.putVLQ(round(judgement.x * 100).toInt()) - buffer.putVLQ(round(judgement.y * 100).toInt()) - buffer.put(judgement.type.ordinal.toByte()) - buffer.putVLQ((judgement.distance_center * 100).toInt()) - buffer.putVLQ((judgement.distance_edge * 100).toInt()) - buffer.putVLQ(judgement.error.toInt()) - - lastTimestamp = judgement.time - stream.write(buffer.array(), 0, buffer.position()) - } - } - } - - return Encoder.compress(byteStream.toByteArray(), brotliParameters) - } - - fun deserialize(compressedData: ByteArray): List { - val data = Decoder.decompress(compressedData).decompressedData ?: return emptyList() - - val buffer = ByteBuffer.wrap(data) - val judgements = mutableListOf() - var lastTime = 0.0 - - while (buffer.hasRemaining()) { - val deltaTime = buffer.getVLQ() - lastTime += deltaTime - - judgements.add( - CircleguardService.ScoreJudgement( - time = lastTime, - x = buffer.getVLQ() / 100.0, - y = buffer.getVLQ() / 100.0, - type = CircleguardService.JudgementType.entries[buffer.get().toInt()], - distance_center = buffer.getVLQ() / 100.0, - distance_edge = buffer.getVLQ() / 100.0, - error = buffer.getVLQ().toDouble() - )) - } - - return judgements - } - -} \ No newline at end of file diff --git a/nise-backend/src/test/kotlin/com/nisemoe/nise/scheduler/JudgementCompressionTest.kt b/nise-backend/src/test/kotlin/com/nisemoe/nise/scheduler/JudgementCompressionTest.kt index 81293a1..83b0f0a 100644 --- a/nise-backend/src/test/kotlin/com/nisemoe/nise/scheduler/JudgementCompressionTest.kt +++ b/nise-backend/src/test/kotlin/com/nisemoe/nise/scheduler/JudgementCompressionTest.kt @@ -5,11 +5,7 @@ import com.nisemoe.generated.tables.references.BEATMAPS import com.nisemoe.generated.tables.references.SCORES import com.nisemoe.nise.database.UserService import com.nisemoe.nise.integrations.CircleguardService -import com.nisemoe.nise.osu.OsuApi -import com.nisemoe.nise.osu.TokenService -import com.nisemoe.nise.service.AuthService -import com.nisemoe.nise.service.CacheService -import com.nisemoe.nise.service.CompressJudgements +import com.nisemoe.nise.osu.CompressJudgements import kotlinx.serialization.json.Json import org.jooq.DSLContext import org.junit.jupiter.api.Assertions.* @@ -17,10 +13,8 @@ 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.autoconfigure.flyway.FlywayAutoConfiguration import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.mock.mockito.MockBean -import org.springframework.context.annotation.Import import org.springframework.test.context.ActiveProfiles @SpringBootTest