Uploading user scores
This commit is contained in:
parent
48cf50d448
commit
6c477b7343
@ -56,3 +56,41 @@ fun compareReplaySet(replaySet: Array<Replay>,
|
|||||||
dispatcher.close()
|
dispatcher.close()
|
||||||
return@runBlocking result
|
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.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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|||||||
@ -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))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.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>.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -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(
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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 {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/'},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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']
|
||||||
})
|
})
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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