Uploading user scores

This commit is contained in:
nise.moe 2024-03-05 00:32:57 +01:00
parent 48cf50d448
commit 6c477b7343
27 changed files with 1387 additions and 35 deletions

View File

@ -53,6 +53,44 @@ fun compareReplaySet(replaySet: Array<Replay>,
} }
} }
dispatcher.close()
return@runBlocking result
}
fun compareSingleReplayWithSet(
singleReplay: Replay,
replaySet: Array<Replay>,
numThreads: Int = Runtime.getRuntime().availableProcessors()
): List<ReplaySetComparison> = 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<ReplaySetComparison>()
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() dispatcher.close()
return@runBlocking result return@runBlocking result
} }

View File

@ -23,6 +23,7 @@ import com.nisemoe.generated.tables.ScoresJudgements
import com.nisemoe.generated.tables.ScoresSimilarity import com.nisemoe.generated.tables.ScoresSimilarity
import com.nisemoe.generated.tables.UpdateUserQueue import com.nisemoe.generated.tables.UpdateUserQueue
import com.nisemoe.generated.tables.UserScores import com.nisemoe.generated.tables.UserScores
import com.nisemoe.generated.tables.UserScoresSimilarity
import com.nisemoe.generated.tables.Users import com.nisemoe.generated.tables.Users
import kotlin.collections.List import kotlin.collections.List
@ -91,6 +92,11 @@ open class Public : SchemaImpl("public", DefaultCatalog.DEFAULT_CATALOG) {
*/ */
val USER_SCORES: UserScores get() = UserScores.USER_SCORES val USER_SCORES: UserScores get() = UserScores.USER_SCORES
/**
* The table <code>public.user_scores_similarity</code>.
*/
val USER_SCORES_SIMILARITY: UserScoresSimilarity get() = UserScoresSimilarity.USER_SCORES_SIMILARITY
/** /**
* The table <code>public.users</code>. * The table <code>public.users</code>.
*/ */
@ -121,6 +127,7 @@ open class Public : SchemaImpl("public", DefaultCatalog.DEFAULT_CATALOG) {
ScoresSimilarity.SCORES_SIMILARITY, ScoresSimilarity.SCORES_SIMILARITY,
UpdateUserQueue.UPDATE_USER_QUEUE, UpdateUserQueue.UPDATE_USER_QUEUE,
UserScores.USER_SCORES, UserScores.USER_SCORES,
UserScoresSimilarity.USER_SCORES_SIMILARITY,
Users.USERS Users.USERS
) )
} }

View File

@ -12,6 +12,7 @@ import com.nisemoe.generated.tables.Scores
import com.nisemoe.generated.tables.ScoresJudgements import com.nisemoe.generated.tables.ScoresJudgements
import com.nisemoe.generated.tables.ScoresSimilarity import com.nisemoe.generated.tables.ScoresSimilarity
import com.nisemoe.generated.tables.UpdateUserQueue import com.nisemoe.generated.tables.UpdateUserQueue
import com.nisemoe.generated.tables.UserScoresSimilarity
import com.nisemoe.generated.tables.Users import com.nisemoe.generated.tables.Users
import com.nisemoe.generated.tables.records.BeatmapsRecord import com.nisemoe.generated.tables.records.BeatmapsRecord
import com.nisemoe.generated.tables.records.FlywaySchemaHistoryRecord 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.ScoresRecord
import com.nisemoe.generated.tables.records.ScoresSimilarityRecord import com.nisemoe.generated.tables.records.ScoresSimilarityRecord
import com.nisemoe.generated.tables.records.UpdateUserQueueRecord import com.nisemoe.generated.tables.records.UpdateUserQueueRecord
import com.nisemoe.generated.tables.records.UserScoresSimilarityRecord
import com.nisemoe.generated.tables.records.UsersRecord import com.nisemoe.generated.tables.records.UsersRecord
import org.jooq.ForeignKey import org.jooq.ForeignKey
@ -44,6 +46,8 @@ val SCORES_JUDGEMENTS_PKEY: UniqueKey<ScoresJudgementsRecord> = Internal.createU
val SCORES_SIMILARITY_PKEY: UniqueKey<ScoresSimilarityRecord> = Internal.createUniqueKey(ScoresSimilarity.SCORES_SIMILARITY, DSL.name("scores_similarity_pkey"), arrayOf(ScoresSimilarity.SCORES_SIMILARITY.ID), true) val SCORES_SIMILARITY_PKEY: UniqueKey<ScoresSimilarityRecord> = Internal.createUniqueKey(ScoresSimilarity.SCORES_SIMILARITY, DSL.name("scores_similarity_pkey"), arrayOf(ScoresSimilarity.SCORES_SIMILARITY.ID), true)
val UNIQUE_BEATMAP_REPLAY_IDS: UniqueKey<ScoresSimilarityRecord> = 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 UNIQUE_BEATMAP_REPLAY_IDS: UniqueKey<ScoresSimilarityRecord> = 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<UpdateUserQueueRecord> = Internal.createUniqueKey(UpdateUserQueue.UPDATE_USER_QUEUE, DSL.name("update_user_queue_pkey"), arrayOf(UpdateUserQueue.UPDATE_USER_QUEUE.ID), true) val UPDATE_USER_QUEUE_PKEY: UniqueKey<UpdateUserQueueRecord> = 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<UserScoresSimilarityRecord> = 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<UserScoresSimilarityRecord> = 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<UsersRecord> = Internal.createUniqueKey(Users.USERS, DSL.name("users_pkey"), arrayOf(Users.USERS.USER_ID), true) val USERS_PKEY: UniqueKey<UsersRecord> = Internal.createUniqueKey(Users.USERS, DSL.name("users_pkey"), arrayOf(Users.USERS.USER_ID), true)
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------

View File

@ -16,7 +16,7 @@ import org.jooq.ForeignKey
import org.jooq.Name import org.jooq.Name
import org.jooq.Record import org.jooq.Record
import org.jooq.Records import org.jooq.Records
import org.jooq.Row11 import org.jooq.Row12
import org.jooq.Schema import org.jooq.Schema
import org.jooq.SelectField import org.jooq.SelectField
import org.jooq.Table import org.jooq.Table
@ -117,6 +117,11 @@ open class Beatmaps(
*/ */
val BEATMAP_FILE: TableField<BeatmapsRecord, String?> = createField(DSL.name("beatmap_file"), SQLDataType.CLOB, this, "") val BEATMAP_FILE: TableField<BeatmapsRecord, String?> = createField(DSL.name("beatmap_file"), SQLDataType.CLOB, this, "")
/**
* The column <code>public.beatmaps.beatmap_hash</code>.
*/
val BEATMAP_HASH: TableField<BeatmapsRecord, String?> = createField(DSL.name("beatmap_hash"), SQLDataType.VARCHAR(32), this, "")
private constructor(alias: Name, aliased: Table<BeatmapsRecord>?): this(alias, null, null, aliased, null) private constructor(alias: Name, aliased: Table<BeatmapsRecord>?): this(alias, null, null, aliased, null)
private constructor(alias: Name, aliased: Table<BeatmapsRecord>?, parameters: Array<Field<*>?>?): this(alias, null, null, aliased, parameters) private constructor(alias: Name, aliased: Table<BeatmapsRecord>?, parameters: Array<Field<*>?>?): this(alias, null, null, aliased, parameters)
@ -158,18 +163,18 @@ open class Beatmaps(
override fun rename(name: Table<*>): Beatmaps = Beatmaps(name.getQualifiedName(), null) override fun rename(name: Table<*>): Beatmaps = Beatmaps(name.getQualifiedName(), null)
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Row11 type methods // Row12 type methods
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
override fun fieldsRow(): Row11<Int?, String?, Int?, String?, String?, Double?, String?, String?, OffsetDateTime?, String?, String?> = super.fieldsRow() as Row11<Int?, String?, Int?, String?, String?, Double?, String?, String?, OffsetDateTime?, String?, String?> override fun fieldsRow(): Row12<Int?, String?, Int?, String?, String?, Double?, String?, String?, OffsetDateTime?, String?, String?, String?> = super.fieldsRow() as Row12<Int?, String?, Int?, String?, String?, Double?, String?, String?, OffsetDateTime?, String?, String?, String?>
/** /**
* Convenience mapping calling {@link SelectField#convertFrom(Function)}. * Convenience mapping calling {@link SelectField#convertFrom(Function)}.
*/ */
fun <U> mapping(from: (Int?, String?, Int?, String?, String?, Double?, String?, String?, OffsetDateTime?, String?, String?) -> U): SelectField<U> = convertFrom(Records.mapping(from)) fun <U> mapping(from: (Int?, String?, Int?, String?, String?, Double?, String?, String?, OffsetDateTime?, String?, String?, String?) -> U): SelectField<U> = convertFrom(Records.mapping(from))
/** /**
* Convenience mapping calling {@link SelectField#convertFrom(Class, * Convenience mapping calling {@link SelectField#convertFrom(Class,
* Function)}. * Function)}.
*/ */
fun <U> mapping(toType: Class<U>, from: (Int?, String?, Int?, String?, String?, Double?, String?, String?, OffsetDateTime?, String?, String?) -> U): SelectField<U> = convertFrom(toType, Records.mapping(from)) fun <U> mapping(toType: Class<U>, from: (Int?, String?, Int?, String?, String?, Double?, String?, String?, OffsetDateTime?, String?, String?, String?) -> U): SelectField<U> = convertFrom(toType, Records.mapping(from))
} }

View File

@ -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<out Record>?,
path: ForeignKey<out Record, UserScoresSimilarityRecord>?,
aliased: Table<UserScoresSimilarityRecord>?,
parameters: Array<Field<*>?>?
): TableImpl<UserScoresSimilarityRecord>(
alias,
Public.PUBLIC,
child,
path,
aliased,
parameters,
DSL.comment(""),
TableOptions.table()
) {
companion object {
/**
* The reference instance of <code>public.user_scores_similarity</code>
*/
val USER_SCORES_SIMILARITY: UserScoresSimilarity = UserScoresSimilarity()
}
/**
* The class holding records for this type
*/
override fun getRecordType(): Class<UserScoresSimilarityRecord> = UserScoresSimilarityRecord::class.java
/**
* The column <code>public.user_scores_similarity.id</code>.
*/
val ID: TableField<UserScoresSimilarityRecord, Int?> = createField(DSL.name("id"), SQLDataType.INTEGER.nullable(false).identity(true), this, "")
/**
* The column <code>public.user_scores_similarity.beatmap_id</code>.
*/
val BEATMAP_ID: TableField<UserScoresSimilarityRecord, Int?> = createField(DSL.name("beatmap_id"), SQLDataType.INTEGER, this, "")
/**
* The column <code>public.user_scores_similarity.replay_id_user</code>.
*/
val REPLAY_ID_USER: TableField<UserScoresSimilarityRecord, UUID?> = createField(DSL.name("replay_id_user"), SQLDataType.UUID, this, "")
/**
* The column <code>public.user_scores_similarity.replay_id_osu</code>.
*/
val REPLAY_ID_OSU: TableField<UserScoresSimilarityRecord, Long?> = createField(DSL.name("replay_id_osu"), SQLDataType.BIGINT, this, "")
/**
* The column <code>public.user_scores_similarity.similarity</code>.
*/
val SIMILARITY: TableField<UserScoresSimilarityRecord, Double?> = createField(DSL.name("similarity"), SQLDataType.DOUBLE, this, "")
/**
* The column <code>public.user_scores_similarity.correlation</code>.
*/
val CORRELATION: TableField<UserScoresSimilarityRecord, Double?> = createField(DSL.name("correlation"), SQLDataType.DOUBLE, this, "")
/**
* The column <code>public.user_scores_similarity.created_at</code>.
*/
val CREATED_AT: TableField<UserScoresSimilarityRecord, OffsetDateTime?> = createField(DSL.name("created_at"), SQLDataType.TIMESTAMPWITHTIMEZONE(6).defaultValue(DSL.field(DSL.raw("CURRENT_TIMESTAMP"), SQLDataType.TIMESTAMPWITHTIMEZONE)), this, "")
/**
* The column <code>public.user_scores_similarity.cg_similarity</code>.
*/
val CG_SIMILARITY: TableField<UserScoresSimilarityRecord, Double?> = createField(DSL.name("cg_similarity"), SQLDataType.DOUBLE, this, "")
/**
* The column <code>public.user_scores_similarity.cg_correlation</code>.
*/
val CG_CORRELATION: TableField<UserScoresSimilarityRecord, Double?> = createField(DSL.name("cg_correlation"), SQLDataType.DOUBLE, this, "")
private constructor(alias: Name, aliased: Table<UserScoresSimilarityRecord>?): this(alias, null, null, aliased, null)
private constructor(alias: Name, aliased: Table<UserScoresSimilarityRecord>?, parameters: Array<Field<*>?>?): this(alias, null, null, aliased, parameters)
/**
* Create an aliased <code>public.user_scores_similarity</code> table
* reference
*/
constructor(alias: String): this(DSL.name(alias))
/**
* Create an aliased <code>public.user_scores_similarity</code> table
* reference
*/
constructor(alias: Name): this(alias, null)
/**
* Create a <code>public.user_scores_similarity</code> table reference
*/
constructor(): this(DSL.name("user_scores_similarity"), null)
constructor(child: Table<out Record>, key: ForeignKey<out Record, UserScoresSimilarityRecord>): 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<UserScoresSimilarityRecord, Int?> = super.getIdentity() as Identity<UserScoresSimilarityRecord, Int?>
override fun getPrimaryKey(): UniqueKey<UserScoresSimilarityRecord> = USER_SCORES_SIMILARITY_PKEY
override fun getUniqueKeys(): List<UniqueKey<UserScoresSimilarityRecord>> = 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<Int?, Int?, UUID?, Long?, Double?, Double?, OffsetDateTime?, Double?, Double?> = super.fieldsRow() as Row9<Int?, Int?, UUID?, Long?, Double?, Double?, OffsetDateTime?, Double?, Double?>
/**
* Convenience mapping calling {@link SelectField#convertFrom(Function)}.
*/
fun <U> mapping(from: (Int?, Int?, UUID?, Long?, Double?, Double?, OffsetDateTime?, Double?, Double?) -> U): SelectField<U> = convertFrom(Records.mapping(from))
/**
* Convenience mapping calling {@link SelectField#convertFrom(Class,
* Function)}.
*/
fun <U> mapping(toType: Class<U>, from: (Int?, Int?, UUID?, Long?, Double?, Double?, OffsetDateTime?, Double?, Double?) -> U): SelectField<U> = convertFrom(toType, Records.mapping(from))
}

View File

@ -10,8 +10,8 @@ import java.time.OffsetDateTime
import org.jooq.Field import org.jooq.Field
import org.jooq.Record1 import org.jooq.Record1
import org.jooq.Record11 import org.jooq.Record12
import org.jooq.Row11 import org.jooq.Row12
import org.jooq.impl.UpdatableRecordImpl import org.jooq.impl.UpdatableRecordImpl
@ -19,7 +19,7 @@ import org.jooq.impl.UpdatableRecordImpl
* This class is generated by jOOQ. * This class is generated by jOOQ.
*/ */
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
open class BeatmapsRecord private constructor() : UpdatableRecordImpl<BeatmapsRecord>(Beatmaps.BEATMAPS), Record11<Int?, String?, Int?, String?, String?, Double?, String?, String?, OffsetDateTime?, String?, String?> { open class BeatmapsRecord private constructor() : UpdatableRecordImpl<BeatmapsRecord>(Beatmaps.BEATMAPS), Record12<Int?, String?, Int?, String?, String?, Double?, String?, String?, OffsetDateTime?, String?, String?, String?> {
open var beatmapId: Int? open var beatmapId: Int?
set(value): Unit = set(0, value) set(value): Unit = set(0, value)
@ -65,6 +65,10 @@ open class BeatmapsRecord private constructor() : UpdatableRecordImpl<BeatmapsRe
set(value): Unit = set(10, value) set(value): Unit = set(10, value)
get(): String? = get(10) as String? get(): String? = get(10) as String?
open var beatmapHash: String?
set(value): Unit = set(11, value)
get(): String? = get(11) as String?
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Primary key information // Primary key information
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
@ -72,11 +76,11 @@ open class BeatmapsRecord private constructor() : UpdatableRecordImpl<BeatmapsRe
override fun key(): Record1<Int?> = super.key() as Record1<Int?> override fun key(): Record1<Int?> = super.key() as Record1<Int?>
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Record11 type implementation // Record12 type implementation
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
override fun fieldsRow(): Row11<Int?, String?, Int?, String?, String?, Double?, String?, String?, OffsetDateTime?, String?, String?> = super.fieldsRow() as Row11<Int?, String?, Int?, String?, String?, Double?, String?, String?, OffsetDateTime?, String?, String?> override fun fieldsRow(): Row12<Int?, String?, Int?, String?, String?, Double?, String?, String?, OffsetDateTime?, String?, String?, String?> = super.fieldsRow() as Row12<Int?, String?, Int?, String?, String?, Double?, String?, String?, OffsetDateTime?, String?, String?, String?>
override fun valuesRow(): Row11<Int?, String?, Int?, String?, String?, Double?, String?, String?, OffsetDateTime?, String?, String?> = super.valuesRow() as Row11<Int?, String?, Int?, String?, String?, Double?, String?, String?, OffsetDateTime?, String?, String?> override fun valuesRow(): Row12<Int?, String?, Int?, String?, String?, Double?, String?, String?, OffsetDateTime?, String?, String?, String?> = super.valuesRow() as Row12<Int?, String?, Int?, String?, String?, Double?, String?, String?, OffsetDateTime?, String?, String?, String?>
override fun field1(): Field<Int?> = Beatmaps.BEATMAPS.BEATMAP_ID override fun field1(): Field<Int?> = Beatmaps.BEATMAPS.BEATMAP_ID
override fun field2(): Field<String?> = Beatmaps.BEATMAPS.ARTIST override fun field2(): Field<String?> = Beatmaps.BEATMAPS.ARTIST
override fun field3(): Field<Int?> = Beatmaps.BEATMAPS.BEATMAPSET_ID override fun field3(): Field<Int?> = Beatmaps.BEATMAPS.BEATMAPSET_ID
@ -88,6 +92,7 @@ open class BeatmapsRecord private constructor() : UpdatableRecordImpl<BeatmapsRe
override fun field9(): Field<OffsetDateTime?> = Beatmaps.BEATMAPS.SYS_LAST_UPDATE override fun field9(): Field<OffsetDateTime?> = Beatmaps.BEATMAPS.SYS_LAST_UPDATE
override fun field10(): Field<String?> = Beatmaps.BEATMAPS.LAST_REPLAY_CHECK override fun field10(): Field<String?> = Beatmaps.BEATMAPS.LAST_REPLAY_CHECK
override fun field11(): Field<String?> = Beatmaps.BEATMAPS.BEATMAP_FILE override fun field11(): Field<String?> = Beatmaps.BEATMAPS.BEATMAP_FILE
override fun field12(): Field<String?> = Beatmaps.BEATMAPS.BEATMAP_HASH
override fun component1(): Int? = beatmapId override fun component1(): Int? = beatmapId
override fun component2(): String? = artist override fun component2(): String? = artist
override fun component3(): Int? = beatmapsetId override fun component3(): Int? = beatmapsetId
@ -99,6 +104,7 @@ open class BeatmapsRecord private constructor() : UpdatableRecordImpl<BeatmapsRe
override fun component9(): OffsetDateTime? = sysLastUpdate override fun component9(): OffsetDateTime? = sysLastUpdate
override fun component10(): String? = lastReplayCheck override fun component10(): String? = lastReplayCheck
override fun component11(): String? = beatmapFile override fun component11(): String? = beatmapFile
override fun component12(): String? = beatmapHash
override fun value1(): Int? = beatmapId override fun value1(): Int? = beatmapId
override fun value2(): String? = artist override fun value2(): String? = artist
override fun value3(): Int? = beatmapsetId override fun value3(): Int? = beatmapsetId
@ -110,6 +116,7 @@ open class BeatmapsRecord private constructor() : UpdatableRecordImpl<BeatmapsRe
override fun value9(): OffsetDateTime? = sysLastUpdate override fun value9(): OffsetDateTime? = sysLastUpdate
override fun value10(): String? = lastReplayCheck override fun value10(): String? = lastReplayCheck
override fun value11(): String? = beatmapFile override fun value11(): String? = beatmapFile
override fun value12(): String? = beatmapHash
override fun value1(value: Int?): BeatmapsRecord { override fun value1(value: Int?): BeatmapsRecord {
set(0, value) set(0, value)
@ -166,7 +173,12 @@ open class BeatmapsRecord private constructor() : UpdatableRecordImpl<BeatmapsRe
return this return this
} }
override fun values(value1: Int?, value2: String?, value3: Int?, value4: String?, value5: String?, value6: Double?, value7: String?, value8: String?, value9: OffsetDateTime?, value10: String?, value11: String?): BeatmapsRecord { override fun value12(value: String?): BeatmapsRecord {
set(11, value)
return this
}
override fun values(value1: Int?, value2: String?, value3: Int?, value4: String?, value5: String?, value6: Double?, value7: String?, value8: String?, value9: OffsetDateTime?, value10: String?, value11: String?, value12: String?): BeatmapsRecord {
this.value1(value1) this.value1(value1)
this.value2(value2) this.value2(value2)
this.value3(value3) this.value3(value3)
@ -178,13 +190,14 @@ open class BeatmapsRecord private constructor() : UpdatableRecordImpl<BeatmapsRe
this.value9(value9) this.value9(value9)
this.value10(value10) this.value10(value10)
this.value11(value11) this.value11(value11)
this.value12(value12)
return this return this
} }
/** /**
* Create a detached, initialised BeatmapsRecord * Create a detached, initialised BeatmapsRecord
*/ */
constructor(beatmapId: Int? = null, artist: String? = null, beatmapsetId: Int? = null, creator: String? = null, source: String? = null, starRating: Double? = null, title: String? = null, version: String? = null, sysLastUpdate: OffsetDateTime? = null, lastReplayCheck: String? = null, beatmapFile: String? = null): this() { constructor(beatmapId: Int? = null, artist: String? = null, beatmapsetId: Int? = null, creator: String? = null, source: String? = null, starRating: Double? = null, title: String? = null, version: String? = null, sysLastUpdate: OffsetDateTime? = null, lastReplayCheck: String? = null, beatmapFile: String? = null, beatmapHash: String? = null): this() {
this.beatmapId = beatmapId this.beatmapId = beatmapId
this.artist = artist this.artist = artist
this.beatmapsetId = beatmapsetId this.beatmapsetId = beatmapsetId
@ -196,6 +209,7 @@ open class BeatmapsRecord private constructor() : UpdatableRecordImpl<BeatmapsRe
this.sysLastUpdate = sysLastUpdate this.sysLastUpdate = sysLastUpdate
this.lastReplayCheck = lastReplayCheck this.lastReplayCheck = lastReplayCheck
this.beatmapFile = beatmapFile this.beatmapFile = beatmapFile
this.beatmapHash = beatmapHash
resetChangedOnNotNull() resetChangedOnNotNull()
} }
} }

View File

@ -0,0 +1,174 @@
/*
* This file is generated by jOOQ.
*/
package com.nisemoe.generated.tables.records
import com.nisemoe.generated.tables.UserScoresSimilarity
import java.time.OffsetDateTime
import java.util.UUID
import org.jooq.Field
import org.jooq.Record1
import org.jooq.Record9
import org.jooq.Row9
import org.jooq.impl.UpdatableRecordImpl
/**
* This class is generated by jOOQ.
*/
@Suppress("UNCHECKED_CAST")
open class UserScoresSimilarityRecord private constructor() : UpdatableRecordImpl<UserScoresSimilarityRecord>(UserScoresSimilarity.USER_SCORES_SIMILARITY), Record9<Int?, Int?, UUID?, Long?, Double?, Double?, OffsetDateTime?, Double?, Double?> {
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<Int?> = super.key() as Record1<Int?>
// -------------------------------------------------------------------------
// Record9 type implementation
// -------------------------------------------------------------------------
override fun fieldsRow(): Row9<Int?, Int?, UUID?, Long?, Double?, Double?, OffsetDateTime?, Double?, Double?> = super.fieldsRow() as Row9<Int?, Int?, UUID?, Long?, Double?, Double?, OffsetDateTime?, Double?, Double?>
override fun valuesRow(): Row9<Int?, Int?, UUID?, Long?, Double?, Double?, OffsetDateTime?, Double?, Double?> = super.valuesRow() as Row9<Int?, Int?, UUID?, Long?, Double?, Double?, OffsetDateTime?, Double?, Double?>
override fun field1(): Field<Int?> = UserScoresSimilarity.USER_SCORES_SIMILARITY.ID
override fun field2(): Field<Int?> = UserScoresSimilarity.USER_SCORES_SIMILARITY.BEATMAP_ID
override fun field3(): Field<UUID?> = UserScoresSimilarity.USER_SCORES_SIMILARITY.REPLAY_ID_USER
override fun field4(): Field<Long?> = UserScoresSimilarity.USER_SCORES_SIMILARITY.REPLAY_ID_OSU
override fun field5(): Field<Double?> = UserScoresSimilarity.USER_SCORES_SIMILARITY.SIMILARITY
override fun field6(): Field<Double?> = UserScoresSimilarity.USER_SCORES_SIMILARITY.CORRELATION
override fun field7(): Field<OffsetDateTime?> = UserScoresSimilarity.USER_SCORES_SIMILARITY.CREATED_AT
override fun field8(): Field<Double?> = UserScoresSimilarity.USER_SCORES_SIMILARITY.CG_SIMILARITY
override fun field9(): Field<Double?> = 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()
}
}

View File

@ -13,6 +13,7 @@ import com.nisemoe.generated.tables.ScoresJudgements
import com.nisemoe.generated.tables.ScoresSimilarity import com.nisemoe.generated.tables.ScoresSimilarity
import com.nisemoe.generated.tables.UpdateUserQueue import com.nisemoe.generated.tables.UpdateUserQueue
import com.nisemoe.generated.tables.UserScores import com.nisemoe.generated.tables.UserScores
import com.nisemoe.generated.tables.UserScoresSimilarity
import com.nisemoe.generated.tables.Users 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 val USER_SCORES: UserScores = UserScores.USER_SCORES
/**
* The table <code>public.user_scores_similarity</code>.
*/
val USER_SCORES_SIMILARITY: UserScoresSimilarity = UserScoresSimilarity.USER_SCORES_SIMILARITY
/** /**
* The table <code>public.users</code>. * The table <code>public.users</code>.
*/ */

View File

@ -3,6 +3,7 @@ package com.nisemoe.nise
import com.nisemoe.nise.integrations.CircleguardService import com.nisemoe.nise.integrations.CircleguardService
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.time.OffsetDateTime import java.time.OffsetDateTime
import java.util.UUID
data class UserQueueDetails( data class UserQueueDetails(
val isProcessing: Boolean, val isProcessing: Boolean,
@ -111,6 +112,59 @@ data class ReplayPairViewerData(
val judgements2: List<CircleguardService.ScoreJudgement> val judgements2: List<CircleguardService.ScoreJudgement>
) )
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<String>,
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<ReplayDataSimilarScore>,
val error_distribution: Map<Int, DistributionEntry>,
val charts: List<ReplayDataChart>
)
data class ReplayData( data class ReplayData(
val replay_id: Long, val replay_id: Long,
val user_id: Int, val user_id: Int,

View File

@ -1,19 +1,26 @@
package com.nisemoe.nise.controller 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.SCORES
import com.nisemoe.generated.tables.references.USER_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.database.BeatmapService
import com.nisemoe.nise.integrations.CircleguardService import com.nisemoe.nise.integrations.CircleguardService
import com.nisemoe.nise.osu.OsuApi import com.nisemoe.nise.osu.OsuApi
import com.nisemoe.nise.osu.OsuReplay import com.nisemoe.nise.osu.OsuReplay
import com.nisemoe.nise.scheduler.ImportScores
import com.nisemoe.nise.service.CompressJudgements import com.nisemoe.nise.service.CompressJudgements
import org.jooq.DSLContext import org.jooq.DSLContext
import org.slf4j.LoggerFactory
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
import org.springframework.web.multipart.MultipartFile import org.springframework.web.multipart.MultipartFile
import java.util.UUID import java.time.OffsetDateTime
@RestController @RestController
class UploadReplayController( class UploadReplayController(
@ -24,6 +31,8 @@ class UploadReplayController(
private val osuApi: OsuApi private val osuApi: OsuApi
) { ) {
private val logger = LoggerFactory.getLogger(javaClass)
private val maxFileSize: Long = 4 * 1024 * 1024 // ~4MB private val maxFileSize: Long = 4 * 1024 * 1024 // ~4MB
data class AnalyzeReplayResult( data class AnalyzeReplayResult(
@ -44,24 +53,55 @@ class UploadReplayController(
return ResponseEntity.badRequest().build() return ResponseEntity.badRequest().build()
} }
// Fetch the beatmap id val beatmapId: Int?
val beatmapId = this.osuApi.getBeatmapIdFromHash(replay.beatmapHash!!) val beatmapFile: String?
?: return ResponseEntity.badRequest().build()
// 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) if(beatmapFromDatabase != null) {
?: return ResponseEntity.badRequest().build() 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 // Analyze the replay
val analysis = this.circleguardService.processReplay( val analysis = this.circleguardService.processReplay(
replayData = replay.replayData!!, replayData = replay.replayData!!,
beatmapData = beatmapFile, beatmapData = beatmapFile!!,
mods = replay.modsUsed mods = replay.modsUsed
).get() ).get()
// TODO: Compare with all other replays in the same beatmap
val newUserReplayId = dslContext.insertInto(USER_SCORES) val newUserReplayId = dslContext.insertInto(USER_SCORES)
.set(USER_SCORES.BEATMAP_ID, beatmapId) .set(USER_SCORES.BEATMAP_ID, beatmapId)
.set(USER_SCORES.BEATMAP_HASH, replay.beatmapHash) .set(USER_SCORES.BEATMAP_HASH, replay.beatmapHash)
@ -117,6 +157,90 @@ class UploadReplayController(
return ResponseEntity.internalServerError().build() 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())) 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()
}
}
}
}
} }

View File

@ -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<UserReplayData> {
val replayData = this.userScoreService.getReplayData(replayId)
?: return ResponseEntity.notFound().build()
return ResponseEntity.ok(replayData)
}
}

View File

@ -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<ReplayDataChart> {
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<ReplayDataSimilarScore> {
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<Int, DistributionEntry> {
val judgements = compressJudgements.deserialize(compressedJudgements)
val errorDistribution = mutableMapOf<Int, MutableMap<String, Int>>()
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
)
}
}
}

View File

@ -118,7 +118,7 @@ class OsuApi(
} }
} }
fun getBeatmapIdFromHash(beatmapHash: String): Int? { fun getBeatmapFromHash(beatmapHash: String): OsuApiModels.Beatmap? {
val queryParams = mapOf( val queryParams = mapOf(
"checksum" to beatmapHash "checksum" to beatmapHash
) )
@ -130,8 +130,7 @@ class OsuApi(
} }
return if (response.statusCode() == 200) { return if (response.statusCode() == 200) {
val beatmap = serializer.decodeFromString(OsuApiModels.BeatmapCompact.serializer(), response.body()) return serializer.decodeFromString(OsuApiModels.Beatmap.serializer(), response.body())
beatmap.id ?: null
} else { } else {
null null
} }

View File

@ -177,6 +177,24 @@ class OsuApiModels {
val beatmaps: List<BeatmapCompact>? val beatmaps: List<BeatmapCompact>?
) )
@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 @Serializable
data class ReplayResponse( data class ReplayResponse(
val content: String val content: String

View File

@ -50,7 +50,7 @@ class OsuReplay(fileContent: ByteArray) {
var timestamp: Long = 0 var timestamp: Long = 0
var replayLength: Int = 0 var replayLength: Int = 0
var replayData: String? = null var replayData: String? = null
var onlineScoreID: Long = 0 var onlineScoreID: Long? = null
var additionalModInfo: Double = 0.0 var additionalModInfo: Double = 0.0
init { init {

View File

@ -49,7 +49,7 @@ class FixOldScores(
* You can set a really old date (e.g. 2023-01-01) here to process all scores. * You can set a really old date (e.g. 2023-01-01) here to process all scores.
*/ */
private val minCutoff = OffsetDateTime.of( 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) @Scheduled(fixedDelay = 120000, initialDelay = 0)

View File

@ -0,0 +1,2 @@
ALTER TABLE public.beatmaps
ADD COLUMN beatmap_hash varchar(32);

View File

@ -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)
);

View File

@ -7,6 +7,7 @@ import {ViewScoreComponent} from "./view-score/view-score.component";
import {ViewUserComponent} from "./view-user/view-user.component"; import {ViewUserComponent} from "./view-user/view-user.component";
import {ViewReplayPairComponent} from "./view-replay-pair/view-replay-pair.component"; import {ViewReplayPairComponent} from "./view-replay-pair/view-replay-pair.component";
import {SearchComponent} from "./search/search.component"; import {SearchComponent} from "./search/search.component";
import {ViewUserScoreComponent} from "./view-user-score/view-user-score.component";
const routes: Routes = [ const routes: Routes = [
{path: 'sus/:f', component: ViewSuspiciousScoresComponent, title: '/sus/'}, {path: 'sus/:f', component: ViewSuspiciousScoresComponent, title: '/sus/'},
@ -20,6 +21,8 @@ const routes: Routes = [
{path: 'search', component: SearchComponent}, {path: 'search', component: SearchComponent},
{path: 'p/:replay1Id/:replay2Id', component: ViewReplayPairComponent}, {path: 'p/:replay1Id/:replay2Id', component: ViewReplayPairComponent},
{path: 'c/:replayId', component: ViewUserScoreComponent},
{path: '**', component: HomeComponent, title: '/nise.moe/'}, {path: '**', component: HomeComponent, title: '/nise.moe/'},
]; ];

View File

@ -1,4 +1,4 @@
import {ReplayData} from "./replays"; import {ReplayData, UserReplayData} from "./replays";
export function formatDuration(seconds: number): string | null { export function formatDuration(seconds: number): string | null {
if(!seconds) { if(!seconds) {
@ -31,7 +31,7 @@ export function countryCodeToFlag(countryCode: string): string {
return String.fromCodePoint(...codePoints); return String.fromCodePoint(...codePoints);
} }
export function calculateAccuracy(replayData: ReplayData): number { export function calculateAccuracy(replayData: ReplayData | UserReplayData): number {
if(!replayData) { if(!replayData) {
return 0; return 0;
} }

View File

@ -1,7 +1,6 @@
.main.term { .main.term {
/* Add some padding or margins as needed for aesthetics */
padding: 20px; 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) { .main .subcontainer:nth-child(1) {
@ -16,3 +15,21 @@
.subcontainer .term { .subcontainer .term {
width: fit-content; 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;
}

View File

@ -34,8 +34,14 @@
<input type="file" id="fileUpload" style="display: none" (change)="this.uploadReplay($event)" <input type="file" id="fileUpload" style="display: none" (change)="this.uploadReplay($event)"
accept=".osr"> accept=".osr">
<label for="fileUpload" class="btn pointer text-center" style="border: 2px solid #CCCCCC; display: block; margin-bottom: 20px; padding: 10px; color: white"> <label for="fileUpload" class="btn pointer text-center upload-button" [class.disabled]="this.loading">
Upload Replay! <ng-container *ngIf="this.loading; else buttonBody">
Processing
<app-cute-loading></app-cute-loading>
</ng-container>
<ng-template #buttonBody>
Upload Replay!
</ng-template>
</label> </label>
<div class="term"> <div class="term">

View File

@ -8,6 +8,7 @@ import {ReplayData} from "../replays";
import {DecimalPipe, NgForOf, NgIf} from "@angular/common"; import {DecimalPipe, NgForOf, NgIf} from "@angular/common";
import {Router, RouterLink} from "@angular/router"; import {Router, RouterLink} from "@angular/router";
import {HttpClient} from "@angular/common/http"; import {HttpClient} from "@angular/common/http";
import {CuteLoadingComponent} from "../../corelib/components/cute-loading/cute-loading.component";
interface Statistics { interface Statistics {
total_beatmaps: number; total_beatmaps: number;
@ -29,7 +30,8 @@ interface AnalyzeReplayResponse {
DecimalPipe, DecimalPipe,
RouterLink, RouterLink,
NgIf, NgIf,
NgForOf NgForOf,
CuteLoadingComponent
], ],
styleUrls: ['./home.component.css'] styleUrls: ['./home.component.css']
}) })

View File

@ -13,6 +13,59 @@ export interface ReplayDataSimilarScore {
correlation: number; correlation: number;
} }
export interface UserReplayData {
replay_id: string;
osu_replay_id?: string;
username: string;
beatmap_id: number;
beatmap_beatmapset_id: number;
beatmap_artist: string;
beatmap_title: string;
beatmap_star_rating: number;
beatmap_creator: string;
beatmap_version: string;
score: number;
mods: string[];
ur?: number;
adjusted_ur?: number;
frametime: number;
snaps: number;
hits: number;
mean_error?: number;
error_variance?: number;
error_standard_deviation?: number;
minimum_error?: number;
maximum_error?: number;
error_range?: number;
error_coefficient_of_variation?: number;
error_kurtosis?: number;
error_skewness?: number;
perfect: boolean;
max_combo: number;
count_300: number;
count_100: number;
count_50: number;
count_miss: number;
comparable_samples?: number;
comparable_mean_error?: number,
comparable_error_variance?: number,
comparable_error_standard_deviation?: number,
comparable_minimum_error?: number,
comparable_maximum_error?: number,
comparable_error_range?: number,
comparable_error_coefficient_of_variation?: number,
comparable_error_kurtosis?: number,
comparable_error_skewness?: number,
similar_scores: ReplayDataSimilarScore[];
error_distribution: ErrorDistribution;
charts: ReplayDataChart[];
}
export interface ReplayData { export interface ReplayData {
replay_id: number | null; replay_id: number | null;
user_id: number; user_id: number;

View File

@ -0,0 +1,21 @@
/* Flex container */
.flex-container {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 20px; /* Adjust the gap between items as needed */
}
/* Flex items - default to full width to stack on smaller screens */
.flex-container > div {
flex: 0 0 100%;
box-sizing: border-box; /* To include padding and border in the element's total width and height */
}
/* Responsive columns */
@media (min-width: 768px) { /* Adjust the breakpoint as needed */
.flex-container > div {
flex: 0 0 15%;
max-width: 20%;
}
}

View File

@ -0,0 +1,203 @@
<ng-container *ngIf="this.isLoading">
<div class="main term">
<div class="text-center">
Loading, please wait...
</div>
</div>
</ng-container>
<ng-container *ngIf="this.error">
<div class="main term">
<div class="text-center">
An error occured. Maybe try again in a bit?
</div>
</div>
</ng-container>
<ng-container *ngIf="this.replayData && !this.isLoading && !this.error">
<div class="main term mb-2 text-center">
<p>
This replay is user-submitted. As such, it might have been edited.
</p>
<p>
Please take the displayed data with a grain of salt.
</p>
</div>
<div class="main term mb-2">
<div class="fade-stuff">
<div class="image-container">
<a href="https://osu.ppy.sh/beatmaps/{{ this.replayData.beatmap_id }}?mode=osu" target="_blank">
<img ngSrc="https://assets.ppy.sh/beatmaps/{{ this.replayData.beatmap_beatmapset_id }}/covers/cover.jpg" width="260" height="72"
alt="Beatmap Cover">
<div class="overlay">
<h4>
{{ this.replayData.beatmap_title }} <span class="text-muted">by</span> {{ this.replayData.beatmap_artist }}
</h4>
★{{ this.replayData.beatmap_star_rating | number: '1.0-2' }} {{ this.replayData.beatmap_version }}
</div>
</a>
</div>
<hr class="mt-2 mb-2">
<div class="badge-list">
<span class="badge" *ngFor="let mod of this.replayData.mods">
{{ mod }}
</span>
</div>
<h1>
{{ this.replayData.score | number }}
</h1>
<div style="display: flex; justify-content: space-between;">
<div style="flex: 1; padding-right: 10px;">
<ul style="line-height: 2.2">
<li>Played by: {{ this.replayData.username }}</li>
<li *ngIf="this.replayData.osu_replay_id">Link to score: <a class="btn" href="https://osu.ppy.sh/scores/osu/{{ this.replayData.osu_replay_id }}" target="_blank">osu!web</a></li>
</ul>
</div>
<div style="flex: 1; padding-left: 10px;">
<ul>
<li>
Max combo: {{ this.replayData.max_combo }}x
<span *ngIf="this.replayData.perfect" class="badge badge-green">perfect</span>
</li>
<li>Accuracy: {{ calculateAccuracy(this.replayData) | number: '1.2-2' }}%</li>
<li>300x: {{ this.replayData.count_300 }}</li>
<li>100x: {{ this.replayData.count_100 }}</li>
<li>50x: {{ this.replayData.count_50 }}</li>
<li>Misses: {{ this.replayData.count_miss }}</li>
</ul>
</div>
</div>
<div class="text-center mb-4 flex-container">
<div *ngIf="this.replayData.ur">
<h2 title="Converted Unstable Rate">cvUR</h2>
<div>{{ this.replayData.ur | number: '1.2-2' }}</div>
</div>
<div *ngIf="this.replayData.adjusted_ur">
<h2 title="Adjusted cvUR - filters outlier hits">Adj. cvUR</h2>
<div>{{ this.replayData.adjusted_ur | number: '1.2-2' }}</div>
</div>
<div *ngIf="this.replayData.frametime">
<h2 title="Median time between frames">Frametime</h2>
<div>{{ this.replayData.frametime | number: '1.0-2' }}ms</div>
</div>
<div *ngIf="this.replayData.hits">
<h2 title="Hits within <1px of the edge">Edge Hits</h2>
<div>{{ this.replayData.hits }}</div>
</div>
<div *ngIf="this.replayData.snaps">
<h2 title="Unusual snaps in the cursor movement">Snaps</h2>
<div>{{ this.replayData.snaps }}</div>
</div>
</div>
</div>
</div>
<div class="main term mb-2" *ngIf="this.replayData.similar_scores && this.replayData.similar_scores.length > 0">
<h1># similar replays</h1>
<table>
<thead>
<th class="text-center">Played by</th>
<th class="text-center">PP</th>
<th class="text-center">Date</th>
<th class="text-center">Similarity</th>
<th class="text-center">Correlation</th>
<th class="text-center"></th>
</thead>
<tbody>
<tr *ngFor="let score of this.replayData.similar_scores">
<td class="text-center">
<a [routerLink]="['/u/' + score.username]">{{ score.username }}</a>
</td>
<td class="text-center">
{{ score.pp | number: '1.2-2' }}
</td>
<td class="text-center">
{{ score.date }}
</td>
<td class="text-center">
{{ score.similarity | number: '1.2-3' }}
</td>
<td class="text-center">
{{ score.correlation | number: '1.2-4' }}
</td>
<td class="text-center">
<a class="btn" [routerLink]="['/s/' + score.replay_id]">details</a>
</td>
</tbody>
</table>
</div>
<div class="main term mb-2" *ngIf="this.replayData.mean_error">
<h1># nerd stats</h1>
<table>
<thead>
<th></th>
<th>
this replay
</th>
<th *ngIf="this.replayData.comparable_samples">
<span title="average values for this beatmap">
avg. (n={{ this.replayData.comparable_samples }})
</span>
</th>
</thead>
<tr>
<td>Mean error</td>
<td class="text-center">{{ this.replayData.mean_error | number: '1.2-2' }}</td>
<td *ngIf="this.replayData.comparable_samples" class="text-center">{{ this.replayData.comparable_mean_error | number: '1.2-2' }}</td>
</tr>
<tr>
<td>Error variance</td>
<td class="text-center">{{ this.replayData.error_variance | number: '1.2-2'}}</td>
<td *ngIf="this.replayData.comparable_samples" class="text-center">{{ this.replayData.comparable_error_variance | number: '1.2-2' }}</td>
</tr>
<tr>
<td>Error Std. deviation</td>
<td class="text-center">{{ this.replayData.error_standard_deviation | number: '1.2-2'}}</td>
<td *ngIf="this.replayData.comparable_samples" class="text-center">{{ this.replayData.comparable_error_standard_deviation | number: '1.2-2' }}</td>
</tr>
<tr>
<td>Min/max error</td>
<td class="text-center">[{{ this.replayData.minimum_error | number: '1.0-0' }}, {{ this.replayData.maximum_error | number: '1.0-0' }}]</td>
<td *ngIf="this.replayData.comparable_samples" class="text-center">[{{ this.replayData.comparable_minimum_error | number: '1.0-0' }}, {{ this.replayData.comparable_maximum_error | number: '1.0-0' }}]</td>
</tr>
<tr>
<td>Coefficient of variation</td>
<td class="text-center">{{ this.replayData.error_coefficient_of_variation | number: '1.2-2'}}</td>
<td *ngIf="this.replayData.comparable_samples" class="text-center">{{ this.replayData.comparable_error_coefficient_of_variation | number: '1.2-2' }}</td>
</tr>
<tr>
<td>Kurtosis</td>
<td class="text-center">{{ this.replayData.error_kurtosis | number: '1.2-2'}}</td>
<td *ngIf="this.replayData.comparable_samples" class="text-center">{{ this.replayData.comparable_error_kurtosis | number: '1.2-2' }}</td>
</tr>
<tr>
<td>Error skewness</td>
<td class="text-center">{{ this.replayData.error_skewness | number: '1.2-2'}}</td>
<td *ngIf="this.replayData.comparable_samples" class="text-center">{{ this.replayData.comparable_error_skewness | number: '1.2-2' }}</td>
</tr>
</table>
</div>
<ng-container *ngFor="let chart of this.replayData.charts">
<app-chart [title]="chart.title" [data]="chart.data"></app-chart>
</ng-container>
<div class="main term mb-2" *ngIf="this.replayData.error_distribution && Object.keys(this.replayData.error_distribution).length > 0">
<h1># hit distribution</h1>
<canvas baseChart
[data]="barChartData"
[options]="barChartOptions"
[plugins]="barChartPlugins"
[legend]="barChartLegend"
[type]="'bar'"
class="chart">
</canvas>
</div>
</ng-container>

View File

@ -0,0 +1,172 @@
import {Component, OnInit, ViewChild} from '@angular/core';
import {HttpClient} from "@angular/common/http";
import {ActivatedRoute, RouterLink} from "@angular/router";
import {Title} from "@angular/platform-browser";
import {DistributionEntry, UserReplayData} from "../replays";
import {environment} from "../../environments/environment";
import {catchError, throwError} from "rxjs";
import {ChartComponent} from "../../corelib/components/chart/chart.component";
import {BaseChartDirective, NgChartsModule} from "ng2-charts";
import {DecimalPipe, NgForOf, NgIf, NgOptimizedImage} from "@angular/common";
import {calculateAccuracy} from "../format";
import {ChartConfiguration} from "chart.js";
@Component({
selector: 'app-view-user-score',
standalone: true,
imports: [
ChartComponent,
NgChartsModule,
NgIf,
NgForOf,
RouterLink,
DecimalPipe,
NgOptimizedImage
],
templateUrl: './view-user-score.component.html',
styleUrl: './view-user-score.component.css'
})
export class ViewUserScoreComponent implements OnInit {
@ViewChild(BaseChartDirective)
public chart!: BaseChartDirective;
isLoading = false;
error: string | null = null;
replayData: UserReplayData | null = null;
replayId: number | null = null;
public barChartLegend = true;
public barChartPlugins = [];
public barChartData: ChartConfiguration<'bar'>['data'] = {
labels: [],
datasets: [
{ data: [], label: 'Miss (%)', backgroundColor: 'rgba(255,0,0,0.66)', borderRadius: 5 },
{ data: [], label: '50 (%)', backgroundColor: 'rgba(187,129,33,0.66)', borderRadius: 5 },
{ data: [], label: '100 (%)', backgroundColor: 'rgba(219,255,0,0.8)', borderRadius: 5 },
{ data: [], label: '300 (%)', backgroundColor: 'rgba(0,255,41,0.66)', borderRadius: 5 }
],
};
public barChartOptions: ChartConfiguration<'bar'>['options'] = {
responsive: true,
scales: {
x: {
stacked: true,
},
y: {
stacked: true
}
}
};
constructor(private http: HttpClient,
private activatedRoute: ActivatedRoute,
private title: Title
) {}
ngOnInit(): void {
this.activatedRoute.params.subscribe(params => {
this.replayId = params['replayId'];
if (this.replayId) {
this.loadScoreData();
}
});
}
private loadScoreData(): void {
this.isLoading = true;
this.http.get<UserReplayData>(`${environment.apiUrl}/user-score/${this.replayId}`).pipe(
catchError(error => {
this.isLoading = false;
return throwError(() => new Error('An error occurred with the request: ' + error.message));
})
).subscribe({
next: (response) => {
this.isLoading = false;
this.replayData = response;
this.title.setTitle(
`${this.replayData.username} on ${this.replayData.beatmap_title} (${this.replayData.beatmap_version})`
)
let errorDistribution = Object.entries(this.replayData.error_distribution);
if (errorDistribution.length >= 1) {
const sortedEntries = errorDistribution
.sort((a, b) => parseInt(a[0]) - parseInt(b[0]));
const chartData = this.generateChartData(sortedEntries);
this.barChartData.labels = chartData.labels;
for (let i = 0; i < 4; i++) {
this.barChartData.datasets[i].data = chartData.datasets[i];
}
}
},
error: (error) => {
this.error = error;
}
});
}
private getChartRange(entries: [string, DistributionEntry][]): [number, number] {
const keys = entries.map(([key, _]) => parseInt(key));
const minKey = Math.min(...keys);
const maxKey = Math.max(...keys);
return [minKey, maxKey];
}
private generateChartData(entries: [string, DistributionEntry][]): { labels: string[], datasets: number[][] } {
const range = this.getChartRange(entries);
const labels: string[] = [];
const datasets: number[][] = Array(4).fill(0).map(() => []);
const defaultPercentageValues: DistributionEntry = {
percentageMiss: 0,
percentage50: 0,
percentage100: 0,
percentage300: 0,
};
const entriesMap = new Map<number, DistributionEntry>(entries.map(([key, value]) => [parseInt(key), value]));
for (let key = range[0]; key <= range[1]; key += 2) {
const endKey = key + 2 <= range[1] ? key + 2 : key + 1;
labels.push(`${key}ms to ${endKey}ms`);
const currentEntry = entriesMap.get(key) || { ...defaultPercentageValues };
const nextEntry = key + 1 <= range[1] ? (entriesMap.get(key + 1) || { ...defaultPercentageValues }) : defaultPercentageValues;
const sumEntry: DistributionEntry = {
percentageMiss: currentEntry.percentageMiss + nextEntry.percentageMiss,
percentage50: currentEntry.percentage50 + nextEntry.percentage50,
percentage100: currentEntry.percentage100 + nextEntry.percentage100,
percentage300: currentEntry.percentage300 + nextEntry.percentage300,
};
datasets[0].push(sumEntry.percentageMiss);
datasets[1].push(sumEntry.percentage50);
datasets[2].push(sumEntry.percentage100);
datasets[3].push(sumEntry.percentage300);
}
// Handling the case for an odd last key if needed
if (range[1] % 2 !== range[0] % 2) {
const lastEntry = entriesMap.get(range[1]) || { ...defaultPercentageValues };
labels.push(`${range[1]}ms to ${range[1] + 1}ms`);
datasets[0].push(lastEntry.percentageMiss);
datasets[1].push(lastEntry.percentage50);
datasets[2].push(lastEntry.percentage100);
datasets[3].push(lastEntry.percentage300);
}
return { labels, datasets };
}
protected readonly Object = Object;
protected readonly calculateAccuracy = calculateAccuracy;
}