From 6c477b73438b131514569cb7105bcc3784865855 Mon Sep 17 00:00:00 2001 From: "nise.moe" Date: Tue, 5 Mar 2024 00:32:57 +0100 Subject: [PATCH] Uploading user scores --- .../com/nisemoe/konata/CompareReplaySet.kt | 38 +++ .../kotlin/com/nisemoe/generated/Public.kt | 7 + .../kotlin/com/nisemoe/generated/keys/Keys.kt | 4 + .../com/nisemoe/generated/tables/Beatmaps.kt | 15 +- .../generated/tables/UserScoresSimilarity.kt | 174 ++++++++++++++ .../tables/records/BeatmapsRecord.kt | 30 ++- .../records/UserScoresSimilarityRecord.kt | 174 ++++++++++++++ .../generated/tables/references/Tables.kt | 6 + .../main/kotlin/com/nisemoe/nise/Models.kt | 54 +++++ .../nise/controller/UploadReplayController.kt | 144 +++++++++++- .../nise/controller/UserScoresController.kt | 24 ++ .../nisemoe/nise/database/UserScoreService.kt | 218 ++++++++++++++++++ .../kotlin/com/nisemoe/nise/osu/OsuApi.kt | 5 +- .../com/nisemoe/nise/osu/OsuApiModels.kt | 18 ++ .../kotlin/com/nisemoe/nise/osu/OsuReplay.kt | 2 +- .../nisemoe/nise/scheduler/FixOldScores.kt | 2 +- .../migration/V0.0.1.028__alter_beatmaps.sql | 2 + ...0.1.029__create_user_scores_similarity.sql | 14 ++ nise-frontend/src/app/app-routing.module.ts | 3 + nise-frontend/src/app/format.ts | 4 +- nise-frontend/src/app/home/home.component.css | 21 +- .../src/app/home/home.component.html | 10 +- nise-frontend/src/app/home/home.component.ts | 4 +- nise-frontend/src/app/replays.ts | 53 +++++ .../view-user-score.component.css | 21 ++ .../view-user-score.component.html | 203 ++++++++++++++++ .../view-user-score.component.ts | 172 ++++++++++++++ 27 files changed, 1387 insertions(+), 35 deletions(-) create mode 100644 nise-backend/src/main/kotlin/com/nisemoe/generated/tables/UserScoresSimilarity.kt create mode 100644 nise-backend/src/main/kotlin/com/nisemoe/generated/tables/records/UserScoresSimilarityRecord.kt create mode 100644 nise-backend/src/main/kotlin/com/nisemoe/nise/controller/UserScoresController.kt create mode 100644 nise-backend/src/main/kotlin/com/nisemoe/nise/database/UserScoreService.kt create mode 100644 nise-backend/src/main/resources/db/migration/V0.0.1.028__alter_beatmaps.sql create mode 100644 nise-backend/src/main/resources/db/migration/V0.0.1.029__create_user_scores_similarity.sql create mode 100644 nise-frontend/src/app/view-user-score/view-user-score.component.css create mode 100644 nise-frontend/src/app/view-user-score/view-user-score.component.html create mode 100644 nise-frontend/src/app/view-user-score/view-user-score.component.ts diff --git a/konata/src/main/kotlin/com/nisemoe/konata/CompareReplaySet.kt b/konata/src/main/kotlin/com/nisemoe/konata/CompareReplaySet.kt index 0eb1dd8..29ce1d6 100644 --- a/konata/src/main/kotlin/com/nisemoe/konata/CompareReplaySet.kt +++ b/konata/src/main/kotlin/com/nisemoe/konata/CompareReplaySet.kt @@ -53,6 +53,44 @@ fun compareReplaySet(replaySet: Array, } } + dispatcher.close() + return@runBlocking result +} + +fun compareSingleReplayWithSet( + singleReplay: Replay, + replaySet: Array, + numThreads: Int = Runtime.getRuntime().availableProcessors() +): List = runBlocking { + if(replaySet.any { it.id == null }) + throw IllegalArgumentException("All replays must have an ID when calling compareSingleReplayWithSet!") + + val dispatcher = Executors + .newFixedThreadPool(numThreads) + .asCoroutineDispatcher() + + val result = mutableListOf() + + coroutineScope { + replaySet.forEach { replay -> + launch(dispatcher) { + val comparisonResult = compareReplayPair(singleReplay, replay) + result.add( + ReplaySetComparison( + replay1Id = singleReplay.id!!, + replay1Mods = singleReplay.mods, + + replay2Id = replay.id!!, + replay2Mods = replay.mods, + + similarity = comparisonResult.similarity, + correlation = comparisonResult.correlation + ) + ) + } + } + } + dispatcher.close() return@runBlocking result } \ No newline at end of file diff --git a/nise-backend/src/main/kotlin/com/nisemoe/generated/Public.kt b/nise-backend/src/main/kotlin/com/nisemoe/generated/Public.kt index c47cbeb..65669ad 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/generated/Public.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/generated/Public.kt @@ -23,6 +23,7 @@ import com.nisemoe.generated.tables.ScoresJudgements import com.nisemoe.generated.tables.ScoresSimilarity import com.nisemoe.generated.tables.UpdateUserQueue import com.nisemoe.generated.tables.UserScores +import com.nisemoe.generated.tables.UserScoresSimilarity import com.nisemoe.generated.tables.Users import kotlin.collections.List @@ -91,6 +92,11 @@ open class Public : SchemaImpl("public", DefaultCatalog.DEFAULT_CATALOG) { */ val USER_SCORES: UserScores get() = UserScores.USER_SCORES + /** + * The table public.user_scores_similarity. + */ + val USER_SCORES_SIMILARITY: UserScoresSimilarity get() = UserScoresSimilarity.USER_SCORES_SIMILARITY + /** * The table public.users. */ @@ -121,6 +127,7 @@ open class Public : SchemaImpl("public", DefaultCatalog.DEFAULT_CATALOG) { ScoresSimilarity.SCORES_SIMILARITY, UpdateUserQueue.UPDATE_USER_QUEUE, UserScores.USER_SCORES, + UserScoresSimilarity.USER_SCORES_SIMILARITY, Users.USERS ) } diff --git a/nise-backend/src/main/kotlin/com/nisemoe/generated/keys/Keys.kt b/nise-backend/src/main/kotlin/com/nisemoe/generated/keys/Keys.kt index 4dd9612..197ac91 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/generated/keys/Keys.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/generated/keys/Keys.kt @@ -12,6 +12,7 @@ import com.nisemoe.generated.tables.Scores import com.nisemoe.generated.tables.ScoresJudgements import com.nisemoe.generated.tables.ScoresSimilarity import com.nisemoe.generated.tables.UpdateUserQueue +import com.nisemoe.generated.tables.UserScoresSimilarity import com.nisemoe.generated.tables.Users import com.nisemoe.generated.tables.records.BeatmapsRecord import com.nisemoe.generated.tables.records.FlywaySchemaHistoryRecord @@ -21,6 +22,7 @@ import com.nisemoe.generated.tables.records.ScoresJudgementsRecord import com.nisemoe.generated.tables.records.ScoresRecord import com.nisemoe.generated.tables.records.ScoresSimilarityRecord import com.nisemoe.generated.tables.records.UpdateUserQueueRecord +import com.nisemoe.generated.tables.records.UserScoresSimilarityRecord import com.nisemoe.generated.tables.records.UsersRecord import org.jooq.ForeignKey @@ -44,6 +46,8 @@ val SCORES_JUDGEMENTS_PKEY: UniqueKey = Internal.createU val SCORES_SIMILARITY_PKEY: UniqueKey = Internal.createUniqueKey(ScoresSimilarity.SCORES_SIMILARITY, DSL.name("scores_similarity_pkey"), arrayOf(ScoresSimilarity.SCORES_SIMILARITY.ID), true) val UNIQUE_BEATMAP_REPLAY_IDS: UniqueKey = Internal.createUniqueKey(ScoresSimilarity.SCORES_SIMILARITY, DSL.name("unique_beatmap_replay_ids"), arrayOf(ScoresSimilarity.SCORES_SIMILARITY.BEATMAP_ID, ScoresSimilarity.SCORES_SIMILARITY.REPLAY_ID_1, ScoresSimilarity.SCORES_SIMILARITY.REPLAY_ID_2), true) val UPDATE_USER_QUEUE_PKEY: UniqueKey = Internal.createUniqueKey(UpdateUserQueue.UPDATE_USER_QUEUE, DSL.name("update_user_queue_pkey"), arrayOf(UpdateUserQueue.UPDATE_USER_QUEUE.ID), true) +val USER_SCORES_SIMILARITY_PKEY: UniqueKey = Internal.createUniqueKey(UserScoresSimilarity.USER_SCORES_SIMILARITY, DSL.name("user_scores_similarity_pkey"), arrayOf(UserScoresSimilarity.USER_SCORES_SIMILARITY.ID), true) +val USER_SCORES_UNIQUE_BEATMAP_REPLAY_IDS: UniqueKey = Internal.createUniqueKey(UserScoresSimilarity.USER_SCORES_SIMILARITY, DSL.name("user_scores_unique_beatmap_replay_ids"), arrayOf(UserScoresSimilarity.USER_SCORES_SIMILARITY.BEATMAP_ID, UserScoresSimilarity.USER_SCORES_SIMILARITY.REPLAY_ID_USER, UserScoresSimilarity.USER_SCORES_SIMILARITY.REPLAY_ID_OSU), true) val USERS_PKEY: UniqueKey = Internal.createUniqueKey(Users.USERS, DSL.name("users_pkey"), arrayOf(Users.USERS.USER_ID), true) // ------------------------------------------------------------------------- diff --git a/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/Beatmaps.kt b/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/Beatmaps.kt index f9c8062..41cbe96 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/Beatmaps.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/Beatmaps.kt @@ -16,7 +16,7 @@ import org.jooq.ForeignKey import org.jooq.Name import org.jooq.Record import org.jooq.Records -import org.jooq.Row11 +import org.jooq.Row12 import org.jooq.Schema import org.jooq.SelectField import org.jooq.Table @@ -117,6 +117,11 @@ open class Beatmaps( */ val BEATMAP_FILE: TableField = createField(DSL.name("beatmap_file"), SQLDataType.CLOB, this, "") + /** + * The column public.beatmaps.beatmap_hash. + */ + val BEATMAP_HASH: TableField = createField(DSL.name("beatmap_hash"), SQLDataType.VARCHAR(32), this, "") + private constructor(alias: Name, aliased: Table?): this(alias, null, null, aliased, null) private constructor(alias: Name, aliased: Table?, parameters: Array?>?): this(alias, null, null, aliased, parameters) @@ -158,18 +163,18 @@ open class Beatmaps( override fun rename(name: Table<*>): Beatmaps = Beatmaps(name.getQualifiedName(), null) // ------------------------------------------------------------------------- - // Row11 type methods + // Row12 type methods // ------------------------------------------------------------------------- - override fun fieldsRow(): Row11 = super.fieldsRow() as Row11 + override fun fieldsRow(): Row12 = super.fieldsRow() as Row12 /** * Convenience mapping calling {@link SelectField#convertFrom(Function)}. */ - fun mapping(from: (Int?, String?, Int?, String?, String?, Double?, String?, String?, OffsetDateTime?, String?, String?) -> U): SelectField = convertFrom(Records.mapping(from)) + fun mapping(from: (Int?, String?, Int?, String?, String?, Double?, String?, String?, OffsetDateTime?, String?, String?, String?) -> U): SelectField = convertFrom(Records.mapping(from)) /** * Convenience mapping calling {@link SelectField#convertFrom(Class, * Function)}. */ - fun mapping(toType: Class, from: (Int?, String?, Int?, String?, String?, Double?, String?, String?, OffsetDateTime?, String?, String?) -> U): SelectField = convertFrom(toType, Records.mapping(from)) + fun mapping(toType: Class, from: (Int?, String?, Int?, String?, String?, Double?, String?, String?, OffsetDateTime?, String?, String?, String?) -> U): SelectField = convertFrom(toType, Records.mapping(from)) } diff --git a/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/UserScoresSimilarity.kt b/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/UserScoresSimilarity.kt new file mode 100644 index 0000000..046c060 --- /dev/null +++ b/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/UserScoresSimilarity.kt @@ -0,0 +1,174 @@ +/* + * This file is generated by jOOQ. + */ +package com.nisemoe.generated.tables + + +import com.nisemoe.generated.Public +import com.nisemoe.generated.keys.USER_SCORES_SIMILARITY_PKEY +import com.nisemoe.generated.keys.USER_SCORES_UNIQUE_BEATMAP_REPLAY_IDS +import com.nisemoe.generated.tables.records.UserScoresSimilarityRecord + +import java.time.OffsetDateTime +import java.util.UUID +import java.util.function.Function + +import kotlin.collections.List + +import org.jooq.Field +import org.jooq.ForeignKey +import org.jooq.Identity +import org.jooq.Name +import org.jooq.Record +import org.jooq.Records +import org.jooq.Row9 +import org.jooq.Schema +import org.jooq.SelectField +import org.jooq.Table +import org.jooq.TableField +import org.jooq.TableOptions +import org.jooq.UniqueKey +import org.jooq.impl.DSL +import org.jooq.impl.Internal +import org.jooq.impl.SQLDataType +import org.jooq.impl.TableImpl + + +/** + * This class is generated by jOOQ. + */ +@Suppress("UNCHECKED_CAST") +open class UserScoresSimilarity( + alias: Name, + child: Table?, + path: ForeignKey?, + aliased: Table?, + parameters: Array?>? +): TableImpl( + alias, + Public.PUBLIC, + child, + path, + aliased, + parameters, + DSL.comment(""), + TableOptions.table() +) { + companion object { + + /** + * The reference instance of public.user_scores_similarity + */ + val USER_SCORES_SIMILARITY: UserScoresSimilarity = UserScoresSimilarity() + } + + /** + * The class holding records for this type + */ + override fun getRecordType(): Class = UserScoresSimilarityRecord::class.java + + /** + * The column public.user_scores_similarity.id. + */ + val ID: TableField = createField(DSL.name("id"), SQLDataType.INTEGER.nullable(false).identity(true), this, "") + + /** + * The column public.user_scores_similarity.beatmap_id. + */ + val BEATMAP_ID: TableField = createField(DSL.name("beatmap_id"), SQLDataType.INTEGER, this, "") + + /** + * The column public.user_scores_similarity.replay_id_user. + */ + val REPLAY_ID_USER: TableField = createField(DSL.name("replay_id_user"), SQLDataType.UUID, this, "") + + /** + * The column public.user_scores_similarity.replay_id_osu. + */ + val REPLAY_ID_OSU: TableField = createField(DSL.name("replay_id_osu"), SQLDataType.BIGINT, this, "") + + /** + * The column public.user_scores_similarity.similarity. + */ + val SIMILARITY: TableField = createField(DSL.name("similarity"), SQLDataType.DOUBLE, this, "") + + /** + * The column public.user_scores_similarity.correlation. + */ + val CORRELATION: TableField = createField(DSL.name("correlation"), SQLDataType.DOUBLE, this, "") + + /** + * The column public.user_scores_similarity.created_at. + */ + val CREATED_AT: TableField = createField(DSL.name("created_at"), SQLDataType.TIMESTAMPWITHTIMEZONE(6).defaultValue(DSL.field(DSL.raw("CURRENT_TIMESTAMP"), SQLDataType.TIMESTAMPWITHTIMEZONE)), this, "") + + /** + * The column public.user_scores_similarity.cg_similarity. + */ + val CG_SIMILARITY: TableField = createField(DSL.name("cg_similarity"), SQLDataType.DOUBLE, this, "") + + /** + * The column public.user_scores_similarity.cg_correlation. + */ + val CG_CORRELATION: TableField = createField(DSL.name("cg_correlation"), SQLDataType.DOUBLE, this, "") + + private constructor(alias: Name, aliased: Table?): this(alias, null, null, aliased, null) + private constructor(alias: Name, aliased: Table?, parameters: Array?>?): this(alias, null, null, aliased, parameters) + + /** + * Create an aliased public.user_scores_similarity table + * reference + */ + constructor(alias: String): this(DSL.name(alias)) + + /** + * Create an aliased public.user_scores_similarity table + * reference + */ + constructor(alias: Name): this(alias, null) + + /** + * Create a public.user_scores_similarity table reference + */ + constructor(): this(DSL.name("user_scores_similarity"), null) + + constructor(child: Table, key: ForeignKey): this(Internal.createPathAlias(child, key), child, key, USER_SCORES_SIMILARITY, null) + override fun getSchema(): Schema? = if (aliased()) null else Public.PUBLIC + override fun getIdentity(): Identity = super.getIdentity() as Identity + override fun getPrimaryKey(): UniqueKey = USER_SCORES_SIMILARITY_PKEY + override fun getUniqueKeys(): List> = listOf(USER_SCORES_UNIQUE_BEATMAP_REPLAY_IDS) + override fun `as`(alias: String): UserScoresSimilarity = UserScoresSimilarity(DSL.name(alias), this) + override fun `as`(alias: Name): UserScoresSimilarity = UserScoresSimilarity(alias, this) + override fun `as`(alias: Table<*>): UserScoresSimilarity = UserScoresSimilarity(alias.getQualifiedName(), this) + + /** + * Rename this table + */ + override fun rename(name: String): UserScoresSimilarity = UserScoresSimilarity(DSL.name(name), null) + + /** + * Rename this table + */ + override fun rename(name: Name): UserScoresSimilarity = UserScoresSimilarity(name, null) + + /** + * Rename this table + */ + override fun rename(name: Table<*>): UserScoresSimilarity = UserScoresSimilarity(name.getQualifiedName(), null) + + // ------------------------------------------------------------------------- + // Row9 type methods + // ------------------------------------------------------------------------- + override fun fieldsRow(): Row9 = super.fieldsRow() as Row9 + + /** + * Convenience mapping calling {@link SelectField#convertFrom(Function)}. + */ + fun mapping(from: (Int?, Int?, UUID?, Long?, Double?, Double?, OffsetDateTime?, Double?, Double?) -> U): SelectField = convertFrom(Records.mapping(from)) + + /** + * Convenience mapping calling {@link SelectField#convertFrom(Class, + * Function)}. + */ + fun mapping(toType: Class, from: (Int?, Int?, UUID?, Long?, Double?, Double?, OffsetDateTime?, Double?, Double?) -> U): SelectField = convertFrom(toType, Records.mapping(from)) +} diff --git a/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/records/BeatmapsRecord.kt b/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/records/BeatmapsRecord.kt index b482169..a630257 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/records/BeatmapsRecord.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/records/BeatmapsRecord.kt @@ -10,8 +10,8 @@ import java.time.OffsetDateTime import org.jooq.Field import org.jooq.Record1 -import org.jooq.Record11 -import org.jooq.Row11 +import org.jooq.Record12 +import org.jooq.Row12 import org.jooq.impl.UpdatableRecordImpl @@ -19,7 +19,7 @@ import org.jooq.impl.UpdatableRecordImpl * This class is generated by jOOQ. */ @Suppress("UNCHECKED_CAST") -open class BeatmapsRecord private constructor() : UpdatableRecordImpl(Beatmaps.BEATMAPS), Record11 { +open class BeatmapsRecord private constructor() : UpdatableRecordImpl(Beatmaps.BEATMAPS), Record12 { open var beatmapId: Int? set(value): Unit = set(0, value) @@ -65,6 +65,10 @@ open class BeatmapsRecord private constructor() : UpdatableRecordImpl = super.key() as Record1 // ------------------------------------------------------------------------- - // Record11 type implementation + // Record12 type implementation // ------------------------------------------------------------------------- - override fun fieldsRow(): Row11 = super.fieldsRow() as Row11 - override fun valuesRow(): Row11 = super.valuesRow() as Row11 + override fun fieldsRow(): Row12 = super.fieldsRow() as Row12 + override fun valuesRow(): Row12 = super.valuesRow() as Row12 override fun field1(): Field = Beatmaps.BEATMAPS.BEATMAP_ID override fun field2(): Field = Beatmaps.BEATMAPS.ARTIST override fun field3(): Field = Beatmaps.BEATMAPS.BEATMAPSET_ID @@ -88,6 +92,7 @@ open class BeatmapsRecord private constructor() : UpdatableRecordImpl = Beatmaps.BEATMAPS.SYS_LAST_UPDATE override fun field10(): Field = Beatmaps.BEATMAPS.LAST_REPLAY_CHECK override fun field11(): Field = Beatmaps.BEATMAPS.BEATMAP_FILE + override fun field12(): Field = Beatmaps.BEATMAPS.BEATMAP_HASH override fun component1(): Int? = beatmapId override fun component2(): String? = artist override fun component3(): Int? = beatmapsetId @@ -99,6 +104,7 @@ open class BeatmapsRecord private constructor() : UpdatableRecordImpl(UserScoresSimilarity.USER_SCORES_SIMILARITY), Record9 { + + open var id: Int? + set(value): Unit = set(0, value) + get(): Int? = get(0) as Int? + + open var beatmapId: Int? + set(value): Unit = set(1, value) + get(): Int? = get(1) as Int? + + open var replayIdUser: UUID? + set(value): Unit = set(2, value) + get(): UUID? = get(2) as UUID? + + open var replayIdOsu: Long? + set(value): Unit = set(3, value) + get(): Long? = get(3) as Long? + + open var similarity: Double? + set(value): Unit = set(4, value) + get(): Double? = get(4) as Double? + + open var correlation: Double? + set(value): Unit = set(5, value) + get(): Double? = get(5) as Double? + + open var createdAt: OffsetDateTime? + set(value): Unit = set(6, value) + get(): OffsetDateTime? = get(6) as OffsetDateTime? + + open var cgSimilarity: Double? + set(value): Unit = set(7, value) + get(): Double? = get(7) as Double? + + open var cgCorrelation: Double? + set(value): Unit = set(8, value) + get(): Double? = get(8) as Double? + + // ------------------------------------------------------------------------- + // Primary key information + // ------------------------------------------------------------------------- + + override fun key(): Record1 = super.key() as Record1 + + // ------------------------------------------------------------------------- + // Record9 type implementation + // ------------------------------------------------------------------------- + + override fun fieldsRow(): Row9 = super.fieldsRow() as Row9 + override fun valuesRow(): Row9 = super.valuesRow() as Row9 + override fun field1(): Field = UserScoresSimilarity.USER_SCORES_SIMILARITY.ID + override fun field2(): Field = UserScoresSimilarity.USER_SCORES_SIMILARITY.BEATMAP_ID + override fun field3(): Field = UserScoresSimilarity.USER_SCORES_SIMILARITY.REPLAY_ID_USER + override fun field4(): Field = UserScoresSimilarity.USER_SCORES_SIMILARITY.REPLAY_ID_OSU + override fun field5(): Field = UserScoresSimilarity.USER_SCORES_SIMILARITY.SIMILARITY + override fun field6(): Field = UserScoresSimilarity.USER_SCORES_SIMILARITY.CORRELATION + override fun field7(): Field = UserScoresSimilarity.USER_SCORES_SIMILARITY.CREATED_AT + override fun field8(): Field = UserScoresSimilarity.USER_SCORES_SIMILARITY.CG_SIMILARITY + override fun field9(): Field = UserScoresSimilarity.USER_SCORES_SIMILARITY.CG_CORRELATION + override fun component1(): Int? = id + override fun component2(): Int? = beatmapId + override fun component3(): UUID? = replayIdUser + override fun component4(): Long? = replayIdOsu + override fun component5(): Double? = similarity + override fun component6(): Double? = correlation + override fun component7(): OffsetDateTime? = createdAt + override fun component8(): Double? = cgSimilarity + override fun component9(): Double? = cgCorrelation + override fun value1(): Int? = id + override fun value2(): Int? = beatmapId + override fun value3(): UUID? = replayIdUser + override fun value4(): Long? = replayIdOsu + override fun value5(): Double? = similarity + override fun value6(): Double? = correlation + override fun value7(): OffsetDateTime? = createdAt + override fun value8(): Double? = cgSimilarity + override fun value9(): Double? = cgCorrelation + + override fun value1(value: Int?): UserScoresSimilarityRecord { + set(0, value) + return this + } + + override fun value2(value: Int?): UserScoresSimilarityRecord { + set(1, value) + return this + } + + override fun value3(value: UUID?): UserScoresSimilarityRecord { + set(2, value) + return this + } + + override fun value4(value: Long?): UserScoresSimilarityRecord { + set(3, value) + return this + } + + override fun value5(value: Double?): UserScoresSimilarityRecord { + set(4, value) + return this + } + + override fun value6(value: Double?): UserScoresSimilarityRecord { + set(5, value) + return this + } + + override fun value7(value: OffsetDateTime?): UserScoresSimilarityRecord { + set(6, value) + return this + } + + override fun value8(value: Double?): UserScoresSimilarityRecord { + set(7, value) + return this + } + + override fun value9(value: Double?): UserScoresSimilarityRecord { + set(8, value) + return this + } + + override fun values(value1: Int?, value2: Int?, value3: UUID?, value4: Long?, value5: Double?, value6: Double?, value7: OffsetDateTime?, value8: Double?, value9: Double?): UserScoresSimilarityRecord { + this.value1(value1) + this.value2(value2) + this.value3(value3) + this.value4(value4) + this.value5(value5) + this.value6(value6) + this.value7(value7) + this.value8(value8) + this.value9(value9) + return this + } + + /** + * Create a detached, initialised UserScoresSimilarityRecord + */ + constructor(id: Int? = null, beatmapId: Int? = null, replayIdUser: UUID? = null, replayIdOsu: Long? = null, similarity: Double? = null, correlation: Double? = null, createdAt: OffsetDateTime? = null, cgSimilarity: Double? = null, cgCorrelation: Double? = null): this() { + this.id = id + this.beatmapId = beatmapId + this.replayIdUser = replayIdUser + this.replayIdOsu = replayIdOsu + this.similarity = similarity + this.correlation = correlation + this.createdAt = createdAt + this.cgSimilarity = cgSimilarity + this.cgCorrelation = cgCorrelation + resetChangedOnNotNull() + } +} diff --git a/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/references/Tables.kt b/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/references/Tables.kt index f903481..484dc09 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/references/Tables.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/references/Tables.kt @@ -13,6 +13,7 @@ import com.nisemoe.generated.tables.ScoresJudgements import com.nisemoe.generated.tables.ScoresSimilarity import com.nisemoe.generated.tables.UpdateUserQueue import com.nisemoe.generated.tables.UserScores +import com.nisemoe.generated.tables.UserScoresSimilarity import com.nisemoe.generated.tables.Users @@ -62,6 +63,11 @@ val UPDATE_USER_QUEUE: UpdateUserQueue = UpdateUserQueue.UPDATE_USER_QUEUE */ val USER_SCORES: UserScores = UserScores.USER_SCORES +/** + * The table public.user_scores_similarity. + */ +val USER_SCORES_SIMILARITY: UserScoresSimilarity = UserScoresSimilarity.USER_SCORES_SIMILARITY + /** * The table public.users. */ 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 f7fff2a..312a036 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/nise/Models.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/Models.kt @@ -3,6 +3,7 @@ package com.nisemoe.nise import com.nisemoe.nise.integrations.CircleguardService import kotlinx.serialization.Serializable import java.time.OffsetDateTime +import java.util.UUID data class UserQueueDetails( val isProcessing: Boolean, @@ -111,6 +112,59 @@ data class ReplayPairViewerData( val judgements2: List ) +data class UserReplayData( + val replay_id: UUID, + val osu_replay_id: Long?, + val username: String, + val beatmap_id: Int, + val beatmap_beatmapset_id: Int, + val beatmap_artist: String, + val beatmap_title: String, + val beatmap_star_rating: Double, + val beatmap_creator: String, + val beatmap_version: String, + val score: Int, + val mods: List, + val ur: Double?, + val adjusted_ur: Double?, + val frametime: Int, + val snaps: Int, + val hits: Int, + + var mean_error: Double?, + var error_variance: Double?, + var error_standard_deviation: Double?, + var minimum_error: Double?, + var maximum_error: Double?, + var error_range: Double?, + var error_coefficient_of_variation: Double?, + var error_kurtosis: Double?, + var error_skewness: Double?, + + var comparable_samples: Int? = null, + var comparable_mean_error: Double? = null, + var comparable_error_variance: Double? = null, + var comparable_error_standard_deviation: Double? = null, + var comparable_minimum_error: Double? = null, + var comparable_maximum_error: Double? = null, + var comparable_error_range: Double? = null, + var comparable_error_coefficient_of_variation: Double? = null, + var comparable_error_kurtosis: Double? = null, + var comparable_error_skewness: Double? = null, + + val perfect: Boolean, + val max_combo: Int, + + val count_300: Int, + val count_100: Int, + val count_50: Int, + val count_miss: Int, + + val similar_scores: List, + val error_distribution: Map, + val charts: List +) + data class ReplayData( val replay_id: Long, val user_id: Int, 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 fc85221..03aac9b 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 @@ -1,19 +1,26 @@ package com.nisemoe.nise.controller +import com.nisemoe.generated.tables.records.UserScoresRecord +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.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.slf4j.LoggerFactory import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController import org.springframework.web.multipart.MultipartFile -import java.util.UUID +import java.time.OffsetDateTime @RestController class UploadReplayController( @@ -24,6 +31,8 @@ class UploadReplayController( private val osuApi: OsuApi ) { + private val logger = LoggerFactory.getLogger(javaClass) + private val maxFileSize: Long = 4 * 1024 * 1024 // ~4MB data class AnalyzeReplayResult( @@ -44,24 +53,55 @@ class UploadReplayController( return ResponseEntity.badRequest().build() } - // Fetch the beatmap id - val beatmapId = this.osuApi.getBeatmapIdFromHash(replay.beatmapHash!!) - ?: return ResponseEntity.badRequest().build() + val beatmapId: Int? + val beatmapFile: String? - // TODO: Add beatmap to database if it doesn't exist + val beatmapFromDatabase = dslContext.selectFrom(BEATMAPS) + .where(BEATMAPS.BEATMAP_HASH.eq(replay.beatmapHash)) + .fetchOne() - val beatmapFile = this.beatmapService.getBeatmapFile(beatmapId) - ?: return ResponseEntity.badRequest().build() + if(beatmapFromDatabase != null) { + beatmapId = beatmapFromDatabase.get(BEATMAPS.BEATMAP_ID) + beatmapFile = beatmapFromDatabase.get(BEATMAPS.BEATMAP_FILE) + } else { + val beatmap = this.osuApi.getBeatmapFromHash(replay.beatmapHash!!) + ?: return ResponseEntity.badRequest().build() + + beatmapId = beatmap.id + + beatmapFile = this.beatmapService.getBeatmapFile(beatmap.id) + ?: return ResponseEntity.badRequest().build() + + if(!dslContext.fetchExists(BEATMAPS, BEATMAPS.BEATMAP_ID.eq(beatmap.id))) { + dslContext.insertInto(BEATMAPS) + .set(BEATMAPS.BEATMAP_ID, beatmap.id) + .set(BEATMAPS.BEATMAPSET_ID, beatmap.beatmapset_id) + .set(BEATMAPS.ARTIST, beatmap.beatmapset.artist) + .set(BEATMAPS.TITLE, beatmap.beatmapset.title) + .set(BEATMAPS.VERSION, beatmap.version) + .set(BEATMAPS.CREATOR, beatmap.beatmapset.creator) + .set(BEATMAPS.STAR_RATING, beatmap.difficulty_rating) + .set(BEATMAPS.SOURCE, beatmap.beatmapset.source) + .set(BEATMAPS.BEATMAP_HASH, replay.beatmapHash) + .set(BEATMAPS.BEATMAP_FILE, beatmapFile) + .execute() + } + else { + dslContext.update(BEATMAPS) + .set(BEATMAPS.BEATMAP_HASH, replay.beatmapHash) + .set(BEATMAPS.BEATMAP_FILE, beatmapFile) + .where(BEATMAPS.BEATMAP_ID.eq(beatmap.id)) + .execute() + } + } // Analyze the replay val analysis = this.circleguardService.processReplay( replayData = replay.replayData!!, - beatmapData = beatmapFile, + beatmapData = beatmapFile!!, mods = replay.modsUsed ).get() - // TODO: Compare with all other replays in the same beatmap - val newUserReplayId = dslContext.insertInto(USER_SCORES) .set(USER_SCORES.BEATMAP_ID, beatmapId) .set(USER_SCORES.BEATMAP_HASH, replay.beatmapHash) @@ -117,6 +157,90 @@ class UploadReplayController( return ResponseEntity.internalServerError().build() } + if(replay.onlineScoreID != null) { + try { + checkSimilarity(beatmapId, replay, newUserReplayId) + } catch (exception: Exception) { + this.logger.error("Failed to process similarity.") + this.logger.error(exception.stackTraceToString()) + } + + } + return ResponseEntity.ok(AnalyzeReplayResult(newUserReplayId.get(USER_SCORES.ID).toString())) } + + private fun checkSimilarity( + beatmapId: Int?, + replay: OsuReplay, + newUserReplayId: UserScoresRecord + ) { + val allReplays = dslContext.select( + SCORES.REPLAY_ID.`as`("replayId"), + SCORES.MODS.`as`("replayMods"), + SCORES.REPLAY.`as`("replayData") + ) + .from(SCORES) + .where(SCORES.BEATMAP_ID.eq(beatmapId)) + .and(SCORES.REPLAY.isNotNull) + .and(SCORES.IS_BANNED.isFalse) + .fetchInto(ImportScores.ReplayDto::class.java) + + if (allReplays.size > 2) { + + val referenceReplay = + Replay(string = replay.replayData!!, id = replay.onlineScoreID ?: -1, mods = replay.modsUsed) + + val replaysForKonata = allReplays + .filter { it.replayId != referenceReplay.id } + .map { Replay(string = it.replayData, id = it.replayId, mods = it.replayMods) } + .toTypedArray() + + val comparisonResult = compareSingleReplayWithSet(referenceReplay, replaysForKonata) + for (similarityEntry in comparisonResult) { + if (similarityEntry.similarity < 10 || similarityEntry.correlation > 0.997) { + + var cgSimilarity: Double? = null + var cgCorrelation: Double? = null + + try { + + val replayDto1 = ImportScores.ReplayDto( + replayId = similarityEntry.replay1Id, + replayMods = similarityEntry.replay1Mods, + replayData = allReplays.find { it.replayId == similarityEntry.replay1Id }!!.replayData + ) + + val replayDto2 = ImportScores.ReplayDto( + replayId = similarityEntry.replay2Id, + replayMods = similarityEntry.replay2Mods, + replayData = allReplays.find { it.replayId == similarityEntry.replay2Id }!!.replayData + ) + + val cgResult = circleguardService.processSimilarity(listOf(replayDto1, replayDto2)) + .get() + + cgSimilarity = cgResult.result.first().similarity + cgCorrelation = cgResult.result.first().correlation + } catch (exception: Exception) { + this.logger.error("Failed to process similarity with circleguard.") + this.logger.error(exception.stackTraceToString()) + } + + dslContext + .insertInto(USER_SCORES_SIMILARITY) + .set(USER_SCORES_SIMILARITY.BEATMAP_ID, beatmapId) + .set(USER_SCORES_SIMILARITY.REPLAY_ID_USER, newUserReplayId.id) + .set(USER_SCORES_SIMILARITY.REPLAY_ID_OSU, similarityEntry.replay2Id) + .set(USER_SCORES_SIMILARITY.SIMILARITY, similarityEntry.similarity) + .set(USER_SCORES_SIMILARITY.CORRELATION, similarityEntry.correlation) + .set(USER_SCORES_SIMILARITY.CREATED_AT, OffsetDateTime.now()) + .set(USER_SCORES_SIMILARITY.CG_SIMILARITY, cgSimilarity) + .set(USER_SCORES_SIMILARITY.CG_CORRELATION, cgCorrelation) + .onDuplicateKeyIgnore() + .execute() + } + } + } + } } \ No newline at end of file diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/controller/UserScoresController.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/controller/UserScoresController.kt new file mode 100644 index 0000000..bec508d --- /dev/null +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/controller/UserScoresController.kt @@ -0,0 +1,24 @@ +package com.nisemoe.nise.controller + +import com.nisemoe.nise.UserReplayData +import com.nisemoe.nise.database.UserScoreService +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RestController +import java.util.UUID + +@RestController +class UserScoresController( + private val userScoreService: UserScoreService +) { + + @GetMapping("user-score/{replayId}") + fun getScoreDetails(@PathVariable replayId: UUID): ResponseEntity { + val replayData = this.userScoreService.getReplayData(replayId) + ?: return ResponseEntity.notFound().build() + + return ResponseEntity.ok(replayData) + } + +} \ No newline at end of file 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 new file mode 100644 index 0000000..6ad5837 --- /dev/null +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/database/UserScoreService.kt @@ -0,0 +1,218 @@ +package com.nisemoe.nise.database + +import com.nisemoe.generated.tables.references.* +import com.nisemoe.nise.* +import com.nisemoe.nise.osu.Mod +import com.nisemoe.nise.service.AuthService +import com.nisemoe.nise.service.CompressJudgements +import org.jooq.DSLContext +import org.jooq.Record +import org.jooq.impl.DSL +import org.springframework.stereotype.Service +import java.time.LocalDateTime +import java.util.* +import kotlin.math.roundToInt + +@Service +class UserScoreService( + private val dslContext: DSLContext, + private val authService: AuthService, + private val compressJudgements: CompressJudgements +) { + + fun getCharts(db: Record): List { + if (!authService.isAdmin()) return emptyList() + + return listOf(USER_SCORES.SLIDEREND_RELEASE_TIMES to "slider end release times", USER_SCORES.KEYPRESSES_TIMES to "keypress times") + .mapNotNull { (scoreType, title) -> db.get(scoreType)?.let { ReplayDataChart(title, it.filterNotNull()) } } + } + + fun getReplayData(replayId: UUID): UserReplayData? { + val result = dslContext.select( + USER_SCORES.ID, + USER_SCORES.PLAYER_NAME, + BEATMAPS.BEATMAP_ID, + BEATMAPS.BEATMAPSET_ID, + BEATMAPS.ARTIST, + BEATMAPS.TITLE, + BEATMAPS.STAR_RATING, + BEATMAPS.CREATOR, + BEATMAPS.VERSION, + USER_SCORES.ONLINE_SCORE_ID, + USER_SCORES.TOTAL_SCORE, + USER_SCORES.FRAMETIME, + USER_SCORES.UR, + USER_SCORES.ADJUSTED_UR, + USER_SCORES.MODS, + USER_SCORES.SNAPS, + USER_SCORES.EDGE_HITS, + USER_SCORES.PERFECT, + USER_SCORES.MAX_COMBO, + USER_SCORES.COUNT_300, + USER_SCORES.COUNT_100, + USER_SCORES.COUNT_50, + USER_SCORES.COUNT_MISS, + USER_SCORES.MEAN_ERROR, + USER_SCORES.ERROR_VARIANCE, + USER_SCORES.ERROR_STANDARD_DEVIATION, + USER_SCORES.MINIMUM_ERROR, + USER_SCORES.MAXIMUM_ERROR, + USER_SCORES.ERROR_RANGE, + USER_SCORES.ERROR_COEFFICIENT_OF_VARIATION, + USER_SCORES.ERROR_KURTOSIS, + USER_SCORES.ERROR_SKEWNESS, + USER_SCORES.SLIDEREND_RELEASE_TIMES, + USER_SCORES.KEYPRESSES_TIMES, + USER_SCORES.JUDGEMENTS + ) + .from(USER_SCORES) + .join(BEATMAPS).on(USER_SCORES.BEATMAP_ID.eq(BEATMAPS.BEATMAP_ID)) + .where(USER_SCORES.ID.eq(replayId)) + .fetchOne() ?: return null + + val beatmapId = result.get(BEATMAPS.BEATMAP_ID, Int::class.java) + val hitDistribution = this.getHitDistribution(result.get(USER_SCORES.JUDGEMENTS, ByteArray::class.java)) + val charts = this.getCharts(result) + + val replayData = UserReplayData( + replay_id = replayId, + osu_replay_id = result.get(USER_SCORES.ONLINE_SCORE_ID, Long::class.java), + username = result.get(USER_SCORES.PLAYER_NAME, String::class.java), + beatmap_id = beatmapId, + beatmap_beatmapset_id = result.get(BEATMAPS.BEATMAPSET_ID, Int::class.java), + beatmap_artist = result.get(BEATMAPS.ARTIST, String::class.java), + beatmap_title = result.get(BEATMAPS.TITLE, String::class.java), + beatmap_star_rating = result.get(BEATMAPS.STAR_RATING, Double::class.java), + beatmap_creator = result.get(BEATMAPS.CREATOR, String::class.java), + beatmap_version = result.get(BEATMAPS.VERSION, String::class.java), + frametime = result.get(USER_SCORES.FRAMETIME, Double::class.java).toInt(), + ur = result.get(USER_SCORES.UR, Double::class.java), + adjusted_ur = result.get(USER_SCORES.ADJUSTED_UR, Double::class.java), + score = result.get(USER_SCORES.TOTAL_SCORE, Int::class.java), + mods = Mod.parseModCombination(result.get(USER_SCORES.MODS, Int::class.java)), + snaps = result.get(USER_SCORES.SNAPS, Int::class.java), + hits = result.get(USER_SCORES.EDGE_HITS, Int::class.java), + perfect = result.get(USER_SCORES.PERFECT, Boolean::class.java), + max_combo = result.get(USER_SCORES.MAX_COMBO, Int::class.java), + count_300 = result.get(USER_SCORES.COUNT_300, Int::class.java), + count_100 = result.get(USER_SCORES.COUNT_100, Int::class.java), + count_50 = result.get(USER_SCORES.COUNT_50, Int::class.java), + count_miss = result.get(USER_SCORES.COUNT_MISS, Int::class.java), + error_distribution = hitDistribution, + mean_error = result.get(USER_SCORES.MEAN_ERROR, Double::class.java), + error_variance = result.get(USER_SCORES.ERROR_VARIANCE, Double::class.java), + error_standard_deviation = result.get(USER_SCORES.ERROR_STANDARD_DEVIATION, Double::class.java), + minimum_error = result.get(USER_SCORES.MINIMUM_ERROR, Double::class.java), + maximum_error = result.get(USER_SCORES.MAXIMUM_ERROR, Double::class.java), + error_range = result.get(USER_SCORES.ERROR_RANGE, Double::class.java), + error_coefficient_of_variation = result.get(USER_SCORES.ERROR_COEFFICIENT_OF_VARIATION, Double::class.java), + error_kurtosis = result.get(USER_SCORES.ERROR_KURTOSIS, Double::class.java), + error_skewness = result.get(USER_SCORES.ERROR_SKEWNESS, Double::class.java), + charts = charts, + similar_scores = this.getSimilarScores(replayId) + ) + this.loadComparableReplayData(replayData) + return replayData + } + + fun getSimilarScores(scoreId: UUID): List { + val similarScoresWithReplayData = dslContext + .select( + USER_SCORES_SIMILARITY.SIMILARITY, + USER_SCORES_SIMILARITY.CORRELATION, + SCORES.REPLAY_ID, + SCORES.USER_ID, + USERS.USERNAME, + SCORES.DATE, + SCORES.PP, + SCORES.BEATMAP_ID + ) + .from(USER_SCORES_SIMILARITY) + .join(SCORES) + .on(SCORES.REPLAY_ID.eq(USER_SCORES_SIMILARITY.REPLAY_ID_OSU)) + .join(USERS) + .on(SCORES.USER_ID.eq(USERS.USER_ID)) + .where(USER_SCORES_SIMILARITY.REPLAY_ID_USER.eq(scoreId)) + .fetch() + + return similarScoresWithReplayData.map { record -> + ReplayDataSimilarScore( + replay_id = record.get(SCORES.REPLAY_ID, Long::class.java), + user_id = record.get(SCORES.USER_ID, Int::class.java), + username = record.get(USERS.USERNAME, String::class.java), + date = Format.formatLocalDateTime(record.get(SCORES.DATE, LocalDateTime::class.java)), + pp = record.get(SCORES.PP, Double::class.java), + similarity = record.get(SCORES_SIMILARITY.SIMILARITY, Double::class.java), + correlation = record.get(SCORES_SIMILARITY.CORRELATION, Double::class.java) + ) + } + } + + fun loadComparableReplayData(replayData: UserReplayData) { + // Total samples + val totalSamples = dslContext.fetchCount( + SCORES, SCORES.BEATMAP_ID.eq(replayData.beatmap_id) + ) + + if(totalSamples <= 0) { + return + } + + // We will select same beatmap_id and same mods + val otherScores = dslContext.select( + DSL.avg(SCORES.MEAN_ERROR).`as`("avg_mean_error"), + DSL.avg(SCORES.ERROR_VARIANCE).`as`("avg_error_variance"), + DSL.avg(SCORES.ERROR_STANDARD_DEVIATION).`as`("avg_error_standard_deviation"), + DSL.avg(SCORES.MINIMUM_ERROR).`as`("avg_minimum_error"), + DSL.avg(SCORES.MAXIMUM_ERROR).`as`("avg_maximum_error"), + DSL.avg(SCORES.ERROR_RANGE).`as`("avg_error_range"), + DSL.avg(SCORES.ERROR_COEFFICIENT_OF_VARIATION).`as`("avg_error_coefficient_of_variation"), + DSL.avg(SCORES.ERROR_KURTOSIS).`as`("avg_error_kurtosis"), + DSL.avg(SCORES.ERROR_SKEWNESS).`as`("avg_error_skewness") + ) + .from(SCORES) + .where(SCORES.BEATMAP_ID.eq(replayData.beatmap_id)) + .fetchOne() ?: return + + replayData.comparable_samples = totalSamples + + replayData.comparable_mean_error = otherScores.get("avg_mean_error", Double::class.java) + replayData.comparable_error_variance = otherScores.get("avg_error_variance", Double::class.java) + replayData.comparable_error_standard_deviation = otherScores.get("avg_error_standard_deviation", Double::class.java) + replayData.comparable_minimum_error = otherScores.get("avg_minimum_error", Double::class.java) + replayData.comparable_maximum_error = otherScores.get("avg_maximum_error", Double::class.java) + replayData.comparable_error_range = otherScores.get("avg_error_range", Double::class.java) + replayData.comparable_error_coefficient_of_variation = otherScores.get("avg_error_coefficient_of_variation", Double::class.java) + replayData.comparable_error_kurtosis = otherScores.get("avg_error_kurtosis", Double::class.java) + replayData.comparable_error_skewness = otherScores.get("avg_error_skewness", Double::class.java) + } + + + fun getHitDistribution(compressedJudgements: ByteArray): Map { + val judgements = compressJudgements.deserialize(compressedJudgements) + + val errorDistribution = mutableMapOf>() + var totalHits = 0 + + judgements.forEach { hit -> + val error = (hit.error.roundToInt() / 2) * 2 + val judgementType = hit.type // Assuming this is how you get the judgement type + errorDistribution.getOrPut(error) { mutableMapOf("MISS" to 0, "THREE_HUNDRED" to 0, "ONE_HUNDRED" to 0, "FIFTY" to 0) } + .apply { + this[judgementType.toString()] = this.getOrDefault(judgementType.toString(), 0) + 1 + } + totalHits += 1 + } + + return errorDistribution.mapValues { (_, judgementCounts) -> + judgementCounts.values.sum() + DistributionEntry( + percentageMiss = (judgementCounts.getOrDefault("MISS", 0).toDouble() / totalHits) * 100, + percentage300 = (judgementCounts.getOrDefault("THREE_HUNDRED", 0).toDouble() / totalHits) * 100, + percentage100 = (judgementCounts.getOrDefault("ONE_HUNDRED", 0).toDouble() / totalHits) * 100, + percentage50 = (judgementCounts.getOrDefault("FIFTY", 0).toDouble() / totalHits) * 100 + ) + } + } + +} \ No newline at end of file diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/osu/OsuApi.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/osu/OsuApi.kt index b845d1c..af95d33 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/nise/osu/OsuApi.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/osu/OsuApi.kt @@ -118,7 +118,7 @@ class OsuApi( } } - fun getBeatmapIdFromHash(beatmapHash: String): Int? { + fun getBeatmapFromHash(beatmapHash: String): OsuApiModels.Beatmap? { val queryParams = mapOf( "checksum" to beatmapHash ) @@ -130,8 +130,7 @@ class OsuApi( } return if (response.statusCode() == 200) { - val beatmap = serializer.decodeFromString(OsuApiModels.BeatmapCompact.serializer(), response.body()) - beatmap.id ?: null + return serializer.decodeFromString(OsuApiModels.Beatmap.serializer(), response.body()) } else { null } diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/osu/OsuApiModels.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/osu/OsuApiModels.kt index 0ed5881..d2f6c5a 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/nise/osu/OsuApiModels.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/osu/OsuApiModels.kt @@ -177,6 +177,24 @@ class OsuApiModels { val beatmaps: List? ) + @Serializable + data class Beatmap( + val beatmapset_id: Int, + val difficulty_rating: Double?, + val id: Int, + val version: String?, + val beatmapset: BeatmapSet, + val max_combo: Int? + ) + + @Serializable + data class BeatmapSet( + val artist: String?, + val creator: String?, + val source: String?, + val title: String? + ) + @Serializable data class ReplayResponse( val content: String diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/osu/OsuReplay.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/osu/OsuReplay.kt index bcfeac6..33c1830 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/nise/osu/OsuReplay.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/osu/OsuReplay.kt @@ -50,7 +50,7 @@ class OsuReplay(fileContent: ByteArray) { var timestamp: Long = 0 var replayLength: Int = 0 var replayData: String? = null - var onlineScoreID: Long = 0 + var onlineScoreID: Long? = null var additionalModInfo: Double = 0.0 init { 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 fa5bdc5..95f0879 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 @@ -49,7 +49,7 @@ class FixOldScores( * You can set a really old date (e.g. 2023-01-01) here to process all scores. */ private val minCutoff = OffsetDateTime.of( - 2024, 3, 1, 0, 0, 0, 0, OffsetDateTime.now().offset + 2024, 1, 1, 0, 0, 0, 0, OffsetDateTime.now().offset ) @Scheduled(fixedDelay = 120000, initialDelay = 0) diff --git a/nise-backend/src/main/resources/db/migration/V0.0.1.028__alter_beatmaps.sql b/nise-backend/src/main/resources/db/migration/V0.0.1.028__alter_beatmaps.sql new file mode 100644 index 0000000..324c2b7 --- /dev/null +++ b/nise-backend/src/main/resources/db/migration/V0.0.1.028__alter_beatmaps.sql @@ -0,0 +1,2 @@ +ALTER TABLE public.beatmaps + ADD COLUMN beatmap_hash varchar(32); \ No newline at end of file diff --git a/nise-backend/src/main/resources/db/migration/V0.0.1.029__create_user_scores_similarity.sql b/nise-backend/src/main/resources/db/migration/V0.0.1.029__create_user_scores_similarity.sql new file mode 100644 index 0000000..19852ec --- /dev/null +++ b/nise-backend/src/main/resources/db/migration/V0.0.1.029__create_user_scores_similarity.sql @@ -0,0 +1,14 @@ +CREATE TABLE public.user_scores_similarity +( + id serial4 NOT NULL, + beatmap_id int4 NULL, + replay_id_user uuid NULL, + replay_id_osu int8 NULL, + similarity float8 NULL, + correlation float8 NULL, + created_at timestamptz DEFAULT CURRENT_TIMESTAMP NULL, + cg_similarity float8 NULL, + cg_correlation float8 NULL, + CONSTRAINT user_scores_similarity_pkey PRIMARY KEY (id), + CONSTRAINT user_scores_unique_beatmap_replay_ids UNIQUE (beatmap_id, replay_id_user, replay_id_osu) +); \ No newline at end of file diff --git a/nise-frontend/src/app/app-routing.module.ts b/nise-frontend/src/app/app-routing.module.ts index b67c0c9..97e9521 100644 --- a/nise-frontend/src/app/app-routing.module.ts +++ b/nise-frontend/src/app/app-routing.module.ts @@ -7,6 +7,7 @@ import {ViewScoreComponent} from "./view-score/view-score.component"; import {ViewUserComponent} from "./view-user/view-user.component"; import {ViewReplayPairComponent} from "./view-replay-pair/view-replay-pair.component"; import {SearchComponent} from "./search/search.component"; +import {ViewUserScoreComponent} from "./view-user-score/view-user-score.component"; const routes: Routes = [ {path: 'sus/:f', component: ViewSuspiciousScoresComponent, title: '/sus/'}, @@ -20,6 +21,8 @@ const routes: Routes = [ {path: 'search', component: SearchComponent}, {path: 'p/:replay1Id/:replay2Id', component: ViewReplayPairComponent}, + {path: 'c/:replayId', component: ViewUserScoreComponent}, + {path: '**', component: HomeComponent, title: '/nise.moe/'}, ]; diff --git a/nise-frontend/src/app/format.ts b/nise-frontend/src/app/format.ts index 1c13b88..43dda4c 100644 --- a/nise-frontend/src/app/format.ts +++ b/nise-frontend/src/app/format.ts @@ -1,4 +1,4 @@ -import {ReplayData} from "./replays"; +import {ReplayData, UserReplayData} from "./replays"; export function formatDuration(seconds: number): string | null { if(!seconds) { @@ -31,7 +31,7 @@ export function countryCodeToFlag(countryCode: string): string { return String.fromCodePoint(...codePoints); } -export function calculateAccuracy(replayData: ReplayData): number { +export function calculateAccuracy(replayData: ReplayData | UserReplayData): number { if(!replayData) { return 0; } diff --git a/nise-frontend/src/app/home/home.component.css b/nise-frontend/src/app/home/home.component.css index 13c849e..e3ca8c0 100644 --- a/nise-frontend/src/app/home/home.component.css +++ b/nise-frontend/src/app/home/home.component.css @@ -1,7 +1,6 @@ .main.term { - /* Add some padding or margins as needed for aesthetics */ padding: 20px; - box-sizing: border-box; /* Includes padding and border in the element's total width and height */ + box-sizing: border-box; } .main .subcontainer:nth-child(1) { @@ -16,3 +15,21 @@ .subcontainer .term { width: fit-content; } + +.upload-button { + background-color: #0c090a; + font-family: monospace, monospace; + border: 2px dotted #CCCCCC; + display: block; + margin-bottom: 12px; + padding: 10px; + color: white +} + +.upload-button:hover { + background-color: #211f1f; +} + +.upload-button.disabled { + pointer-events: none; +} diff --git a/nise-frontend/src/app/home/home.component.html b/nise-frontend/src/app/home/home.component.html index c6f1e47..5aaeb39 100644 --- a/nise-frontend/src/app/home/home.component.html +++ b/nise-frontend/src/app/home/home.component.html @@ -34,8 +34,14 @@ -