Uploading user scores
This commit is contained in:
parent
48cf50d448
commit
6c477b7343
@ -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()
|
||||
return@runBlocking result
|
||||
}
|
||||
@ -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 <code>public.user_scores_similarity</code>.
|
||||
*/
|
||||
val USER_SCORES_SIMILARITY: UserScoresSimilarity get() = UserScoresSimilarity.USER_SCORES_SIMILARITY
|
||||
|
||||
/**
|
||||
* The table <code>public.users</code>.
|
||||
*/
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@ -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<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 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 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)
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@ -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<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>?, 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)
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 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)}.
|
||||
*/
|
||||
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,
|
||||
* 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))
|
||||
}
|
||||
|
||||
@ -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))
|
||||
}
|
||||
@ -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<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?
|
||||
set(value): Unit = set(0, value)
|
||||
@ -65,6 +65,10 @@ open class BeatmapsRecord private constructor() : UpdatableRecordImpl<BeatmapsRe
|
||||
set(value): Unit = set(10, value)
|
||||
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
|
||||
// -------------------------------------------------------------------------
|
||||
@ -72,11 +76,11 @@ open class BeatmapsRecord private constructor() : UpdatableRecordImpl<BeatmapsRe
|
||||
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 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 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(): 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 field2(): Field<String?> = Beatmaps.BEATMAPS.ARTIST
|
||||
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 field10(): Field<String?> = Beatmaps.BEATMAPS.LAST_REPLAY_CHECK
|
||||
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 component2(): String? = artist
|
||||
override fun component3(): Int? = beatmapsetId
|
||||
@ -99,6 +104,7 @@ open class BeatmapsRecord private constructor() : UpdatableRecordImpl<BeatmapsRe
|
||||
override fun component9(): OffsetDateTime? = sysLastUpdate
|
||||
override fun component10(): String? = lastReplayCheck
|
||||
override fun component11(): String? = beatmapFile
|
||||
override fun component12(): String? = beatmapHash
|
||||
override fun value1(): Int? = beatmapId
|
||||
override fun value2(): String? = artist
|
||||
override fun value3(): Int? = beatmapsetId
|
||||
@ -110,6 +116,7 @@ open class BeatmapsRecord private constructor() : UpdatableRecordImpl<BeatmapsRe
|
||||
override fun value9(): OffsetDateTime? = sysLastUpdate
|
||||
override fun value10(): String? = lastReplayCheck
|
||||
override fun value11(): String? = beatmapFile
|
||||
override fun value12(): String? = beatmapHash
|
||||
|
||||
override fun value1(value: Int?): BeatmapsRecord {
|
||||
set(0, value)
|
||||
@ -166,7 +173,12 @@ open class BeatmapsRecord private constructor() : UpdatableRecordImpl<BeatmapsRe
|
||||
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.value2(value2)
|
||||
this.value3(value3)
|
||||
@ -178,13 +190,14 @@ open class BeatmapsRecord private constructor() : UpdatableRecordImpl<BeatmapsRe
|
||||
this.value9(value9)
|
||||
this.value10(value10)
|
||||
this.value11(value11)
|
||||
this.value12(value12)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.artist = artist
|
||||
this.beatmapsetId = beatmapsetId
|
||||
@ -196,6 +209,7 @@ open class BeatmapsRecord private constructor() : UpdatableRecordImpl<BeatmapsRe
|
||||
this.sysLastUpdate = sysLastUpdate
|
||||
this.lastReplayCheck = lastReplayCheck
|
||||
this.beatmapFile = beatmapFile
|
||||
this.beatmapHash = beatmapHash
|
||||
resetChangedOnNotNull()
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
@ -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 <code>public.user_scores_similarity</code>.
|
||||
*/
|
||||
val USER_SCORES_SIMILARITY: UserScoresSimilarity = UserScoresSimilarity.USER_SCORES_SIMILARITY
|
||||
|
||||
/**
|
||||
* The table <code>public.users</code>.
|
||||
*/
|
||||
|
||||
@ -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<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(
|
||||
val replay_id: Long,
|
||||
val user_id: Int,
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -177,6 +177,24 @@ class OsuApiModels {
|
||||
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
|
||||
data class ReplayResponse(
|
||||
val content: String
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -0,0 +1,2 @@
|
||||
ALTER TABLE public.beatmaps
|
||||
ADD COLUMN beatmap_hash varchar(32);
|
||||
@ -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)
|
||||
);
|
||||
@ -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/'},
|
||||
];
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -34,8 +34,14 @@
|
||||
<input type="file" id="fileUpload" style="display: none" (change)="this.uploadReplay($event)"
|
||||
accept=".osr">
|
||||
|
||||
<label for="fileUpload" class="btn pointer text-center" style="border: 2px solid #CCCCCC; display: block; margin-bottom: 20px; padding: 10px; color: white">
|
||||
Upload Replay!
|
||||
<label for="fileUpload" class="btn pointer text-center upload-button" [class.disabled]="this.loading">
|
||||
<ng-container *ngIf="this.loading; else buttonBody">
|
||||
Processing
|
||||
<app-cute-loading></app-cute-loading>
|
||||
</ng-container>
|
||||
<ng-template #buttonBody>
|
||||
Upload Replay!
|
||||
</ng-template>
|
||||
</label>
|
||||
|
||||
<div class="term">
|
||||
|
||||
@ -8,6 +8,7 @@ import {ReplayData} from "../replays";
|
||||
import {DecimalPipe, NgForOf, NgIf} from "@angular/common";
|
||||
import {Router, RouterLink} from "@angular/router";
|
||||
import {HttpClient} from "@angular/common/http";
|
||||
import {CuteLoadingComponent} from "../../corelib/components/cute-loading/cute-loading.component";
|
||||
|
||||
interface Statistics {
|
||||
total_beatmaps: number;
|
||||
@ -29,7 +30,8 @@ interface AnalyzeReplayResponse {
|
||||
DecimalPipe,
|
||||
RouterLink,
|
||||
NgIf,
|
||||
NgForOf
|
||||
NgForOf,
|
||||
CuteLoadingComponent
|
||||
],
|
||||
styleUrls: ['./home.component.css']
|
||||
})
|
||||
|
||||
@ -13,6 +13,59 @@ export interface ReplayDataSimilarScore {
|
||||
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 {
|
||||
replay_id: number | null;
|
||||
user_id: number;
|
||||
|
||||
@ -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%;
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
@ -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;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user