Basic work on replay upload
This commit is contained in:
parent
a4edf5d0c2
commit
48cf50d448
BIN
nise-backend/replay1.osr
Normal file
BIN
nise-backend/replay1.osr
Normal file
Binary file not shown.
@ -22,6 +22,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.UserScores
|
||||
import com.nisemoe.generated.tables.Users
|
||||
|
||||
import kotlin.collections.List
|
||||
@ -85,6 +86,11 @@ open class Public : SchemaImpl("public", DefaultCatalog.DEFAULT_CATALOG) {
|
||||
*/
|
||||
val UPDATE_USER_QUEUE: UpdateUserQueue get() = UpdateUserQueue.UPDATE_USER_QUEUE
|
||||
|
||||
/**
|
||||
* The table <code>public.user_scores</code>.
|
||||
*/
|
||||
val USER_SCORES: UserScores get() = UserScores.USER_SCORES
|
||||
|
||||
/**
|
||||
* The table <code>public.users</code>.
|
||||
*/
|
||||
@ -114,6 +120,7 @@ open class Public : SchemaImpl("public", DefaultCatalog.DEFAULT_CATALOG) {
|
||||
ScoresJudgements.SCORES_JUDGEMENTS,
|
||||
ScoresSimilarity.SCORES_SIMILARITY,
|
||||
UpdateUserQueue.UPDATE_USER_QUEUE,
|
||||
UserScores.USER_SCORES,
|
||||
Users.USERS
|
||||
)
|
||||
}
|
||||
|
||||
@ -0,0 +1,355 @@
|
||||
/*
|
||||
* This file is generated by jOOQ.
|
||||
*/
|
||||
package com.nisemoe.generated.tables
|
||||
|
||||
|
||||
import com.nisemoe.generated.Public
|
||||
import com.nisemoe.generated.tables.records.UserScoresRecord
|
||||
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.UUID
|
||||
|
||||
import kotlin.collections.List
|
||||
|
||||
import org.jooq.Check
|
||||
import org.jooq.Field
|
||||
import org.jooq.ForeignKey
|
||||
import org.jooq.Name
|
||||
import org.jooq.Record
|
||||
import org.jooq.Schema
|
||||
import org.jooq.Table
|
||||
import org.jooq.TableField
|
||||
import org.jooq.TableOptions
|
||||
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 UserScores(
|
||||
alias: Name,
|
||||
child: Table<out Record>?,
|
||||
path: ForeignKey<out Record, UserScoresRecord>?,
|
||||
aliased: Table<UserScoresRecord>?,
|
||||
parameters: Array<Field<*>?>?
|
||||
): TableImpl<UserScoresRecord>(
|
||||
alias,
|
||||
Public.PUBLIC,
|
||||
child,
|
||||
path,
|
||||
aliased,
|
||||
parameters,
|
||||
DSL.comment(""),
|
||||
TableOptions.table()
|
||||
) {
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* The reference instance of <code>public.user_scores</code>
|
||||
*/
|
||||
val USER_SCORES: UserScores = UserScores()
|
||||
}
|
||||
|
||||
/**
|
||||
* The class holding records for this type
|
||||
*/
|
||||
override fun getRecordType(): Class<UserScoresRecord> = UserScoresRecord::class.java
|
||||
|
||||
/**
|
||||
* The column <code>public.user_scores.id</code>.
|
||||
*/
|
||||
val ID: TableField<UserScoresRecord, UUID?> = createField(DSL.name("id"), SQLDataType.UUID.nullable(false).defaultValue(DSL.field(DSL.raw("gen_random_uuid()"), SQLDataType.UUID)), this, "")
|
||||
|
||||
/**
|
||||
* The column <code>public.user_scores.added_at</code>.
|
||||
*/
|
||||
val ADDED_AT: TableField<UserScoresRecord, OffsetDateTime?> = createField(DSL.name("added_at"), SQLDataType.TIMESTAMPWITHTIMEZONE(6).defaultValue(DSL.field(DSL.raw("CURRENT_TIMESTAMP"), SQLDataType.TIMESTAMPWITHTIMEZONE)), this, "")
|
||||
|
||||
/**
|
||||
* The column <code>public.user_scores.beatmap_id</code>.
|
||||
*/
|
||||
val BEATMAP_ID: TableField<UserScoresRecord, Int?> = createField(DSL.name("beatmap_id"), SQLDataType.INTEGER, this, "")
|
||||
|
||||
/**
|
||||
* The column <code>public.user_scores.beatmap_hash</code>.
|
||||
*/
|
||||
val BEATMAP_HASH: TableField<UserScoresRecord, String?> = createField(DSL.name("beatmap_hash"), SQLDataType.VARCHAR(32), this, "")
|
||||
|
||||
/**
|
||||
* The column <code>public.user_scores.game_mode</code>.
|
||||
*/
|
||||
val GAME_MODE: TableField<UserScoresRecord, Int?> = createField(DSL.name("game_mode"), SQLDataType.INTEGER, this, "")
|
||||
|
||||
/**
|
||||
* The column <code>public.user_scores.game_version</code>.
|
||||
*/
|
||||
val GAME_VERSION: TableField<UserScoresRecord, Int?> = createField(DSL.name("game_version"), SQLDataType.INTEGER, this, "")
|
||||
|
||||
/**
|
||||
* The column <code>public.user_scores.player_name</code>.
|
||||
*/
|
||||
val PLAYER_NAME: TableField<UserScoresRecord, String?> = createField(DSL.name("player_name"), SQLDataType.VARCHAR(256), this, "")
|
||||
|
||||
/**
|
||||
* The column <code>public.user_scores.replay_hash</code>.
|
||||
*/
|
||||
val REPLAY_HASH: TableField<UserScoresRecord, String?> = createField(DSL.name("replay_hash"), SQLDataType.VARCHAR(32), this, "")
|
||||
|
||||
/**
|
||||
* The column <code>public.user_scores.count_300</code>.
|
||||
*/
|
||||
val COUNT_300: TableField<UserScoresRecord, Short?> = createField(DSL.name("count_300"), SQLDataType.SMALLINT, this, "")
|
||||
|
||||
/**
|
||||
* The column <code>public.user_scores.count_100</code>.
|
||||
*/
|
||||
val COUNT_100: TableField<UserScoresRecord, Short?> = createField(DSL.name("count_100"), SQLDataType.SMALLINT, this, "")
|
||||
|
||||
/**
|
||||
* The column <code>public.user_scores.count_50</code>.
|
||||
*/
|
||||
val COUNT_50: TableField<UserScoresRecord, Short?> = createField(DSL.name("count_50"), SQLDataType.SMALLINT, this, "")
|
||||
|
||||
/**
|
||||
* The column <code>public.user_scores.count_geki</code>.
|
||||
*/
|
||||
val COUNT_GEKI: TableField<UserScoresRecord, Short?> = createField(DSL.name("count_geki"), SQLDataType.SMALLINT, this, "")
|
||||
|
||||
/**
|
||||
* The column <code>public.user_scores.count_katu</code>.
|
||||
*/
|
||||
val COUNT_KATU: TableField<UserScoresRecord, Short?> = createField(DSL.name("count_katu"), SQLDataType.SMALLINT, this, "")
|
||||
|
||||
/**
|
||||
* The column <code>public.user_scores.count_miss</code>.
|
||||
*/
|
||||
val COUNT_MISS: TableField<UserScoresRecord, Short?> = createField(DSL.name("count_miss"), SQLDataType.SMALLINT, this, "")
|
||||
|
||||
/**
|
||||
* The column <code>public.user_scores.total_score</code>.
|
||||
*/
|
||||
val TOTAL_SCORE: TableField<UserScoresRecord, Int?> = createField(DSL.name("total_score"), SQLDataType.INTEGER, this, "")
|
||||
|
||||
/**
|
||||
* The column <code>public.user_scores.max_combo</code>.
|
||||
*/
|
||||
val MAX_COMBO: TableField<UserScoresRecord, Short?> = createField(DSL.name("max_combo"), SQLDataType.SMALLINT, this, "")
|
||||
|
||||
/**
|
||||
* The column <code>public.user_scores.perfect</code>.
|
||||
*/
|
||||
val PERFECT: TableField<UserScoresRecord, Boolean?> = createField(DSL.name("perfect"), SQLDataType.BOOLEAN, this, "")
|
||||
|
||||
/**
|
||||
* The column <code>public.user_scores.mods</code>.
|
||||
*/
|
||||
val MODS: TableField<UserScoresRecord, Int?> = createField(DSL.name("mods"), SQLDataType.INTEGER, this, "")
|
||||
|
||||
/**
|
||||
* The column <code>public.user_scores.life_bar_graph</code>.
|
||||
*/
|
||||
val LIFE_BAR_GRAPH: TableField<UserScoresRecord, String?> = createField(DSL.name("life_bar_graph"), SQLDataType.CLOB, this, "")
|
||||
|
||||
/**
|
||||
* The column <code>public.user_scores.timestamp</code>.
|
||||
*/
|
||||
val TIMESTAMP: TableField<UserScoresRecord, Long?> = createField(DSL.name("timestamp"), SQLDataType.BIGINT, this, "")
|
||||
|
||||
/**
|
||||
* The column <code>public.user_scores.replay_length</code>.
|
||||
*/
|
||||
val REPLAY_LENGTH: TableField<UserScoresRecord, Int?> = createField(DSL.name("replay_length"), SQLDataType.INTEGER, this, "")
|
||||
|
||||
/**
|
||||
* The column <code>public.user_scores.replay_data</code>.
|
||||
*/
|
||||
val REPLAY_DATA: TableField<UserScoresRecord, String?> = createField(DSL.name("replay_data"), SQLDataType.CLOB, this, "")
|
||||
|
||||
/**
|
||||
* The column <code>public.user_scores.online_score_id</code>.
|
||||
*/
|
||||
val ONLINE_SCORE_ID: TableField<UserScoresRecord, Long?> = createField(DSL.name("online_score_id"), SQLDataType.BIGINT, this, "")
|
||||
|
||||
/**
|
||||
* The column <code>public.user_scores.additional_mod_info</code>.
|
||||
*/
|
||||
val ADDITIONAL_MOD_INFO: TableField<UserScoresRecord, Double?> = createField(DSL.name("additional_mod_info"), SQLDataType.DOUBLE, this, "")
|
||||
|
||||
/**
|
||||
* The column <code>public.user_scores.ur</code>.
|
||||
*/
|
||||
val UR: TableField<UserScoresRecord, Double?> = createField(DSL.name("ur"), SQLDataType.DOUBLE, this, "")
|
||||
|
||||
/**
|
||||
* The column <code>public.user_scores.frametime</code>.
|
||||
*/
|
||||
val FRAMETIME: TableField<UserScoresRecord, Double?> = createField(DSL.name("frametime"), SQLDataType.DOUBLE, this, "")
|
||||
|
||||
/**
|
||||
* The column <code>public.user_scores.edge_hits</code>.
|
||||
*/
|
||||
val EDGE_HITS: TableField<UserScoresRecord, Int?> = createField(DSL.name("edge_hits"), SQLDataType.INTEGER, this, "")
|
||||
|
||||
/**
|
||||
* The column <code>public.user_scores.snaps</code>.
|
||||
*/
|
||||
val SNAPS: TableField<UserScoresRecord, Int?> = createField(DSL.name("snaps"), SQLDataType.INTEGER, this, "")
|
||||
|
||||
/**
|
||||
* The column <code>public.user_scores.adjusted_ur</code>.
|
||||
*/
|
||||
val ADJUSTED_UR: TableField<UserScoresRecord, Double?> = createField(DSL.name("adjusted_ur"), SQLDataType.DOUBLE, this, "")
|
||||
|
||||
/**
|
||||
* The column <code>public.user_scores.mean_error</code>.
|
||||
*/
|
||||
val MEAN_ERROR: TableField<UserScoresRecord, Double?> = createField(DSL.name("mean_error"), SQLDataType.DOUBLE, this, "")
|
||||
|
||||
/**
|
||||
* The column <code>public.user_scores.error_variance</code>.
|
||||
*/
|
||||
val ERROR_VARIANCE: TableField<UserScoresRecord, Double?> = createField(DSL.name("error_variance"), SQLDataType.DOUBLE, this, "")
|
||||
|
||||
/**
|
||||
* The column <code>public.user_scores.error_standard_deviation</code>.
|
||||
*/
|
||||
val ERROR_STANDARD_DEVIATION: TableField<UserScoresRecord, Double?> = createField(DSL.name("error_standard_deviation"), SQLDataType.DOUBLE, this, "")
|
||||
|
||||
/**
|
||||
* The column <code>public.user_scores.minimum_error</code>.
|
||||
*/
|
||||
val MINIMUM_ERROR: TableField<UserScoresRecord, Double?> = createField(DSL.name("minimum_error"), SQLDataType.DOUBLE, this, "")
|
||||
|
||||
/**
|
||||
* The column <code>public.user_scores.maximum_error</code>.
|
||||
*/
|
||||
val MAXIMUM_ERROR: TableField<UserScoresRecord, Double?> = createField(DSL.name("maximum_error"), SQLDataType.DOUBLE, this, "")
|
||||
|
||||
/**
|
||||
* The column <code>public.user_scores.error_range</code>.
|
||||
*/
|
||||
val ERROR_RANGE: TableField<UserScoresRecord, Double?> = createField(DSL.name("error_range"), SQLDataType.DOUBLE, this, "")
|
||||
|
||||
/**
|
||||
* The column
|
||||
* <code>public.user_scores.error_coefficient_of_variation</code>.
|
||||
*/
|
||||
val ERROR_COEFFICIENT_OF_VARIATION: TableField<UserScoresRecord, Double?> = createField(DSL.name("error_coefficient_of_variation"), SQLDataType.DOUBLE, this, "")
|
||||
|
||||
/**
|
||||
* The column <code>public.user_scores.error_kurtosis</code>.
|
||||
*/
|
||||
val ERROR_KURTOSIS: TableField<UserScoresRecord, Double?> = createField(DSL.name("error_kurtosis"), SQLDataType.DOUBLE, this, "")
|
||||
|
||||
/**
|
||||
* The column <code>public.user_scores.error_skewness</code>.
|
||||
*/
|
||||
val ERROR_SKEWNESS: TableField<UserScoresRecord, Double?> = createField(DSL.name("error_skewness"), SQLDataType.DOUBLE, this, "")
|
||||
|
||||
/**
|
||||
* The column <code>public.user_scores.keypresses_times</code>.
|
||||
*/
|
||||
val KEYPRESSES_TIMES: TableField<UserScoresRecord, Array<Double?>?> = createField(DSL.name("keypresses_times"), SQLDataType.FLOAT.array(), this, "")
|
||||
|
||||
/**
|
||||
* The column <code>public.user_scores.keypresses_median</code>.
|
||||
*/
|
||||
val KEYPRESSES_MEDIAN: TableField<UserScoresRecord, Double?> = createField(DSL.name("keypresses_median"), SQLDataType.DOUBLE, this, "")
|
||||
|
||||
/**
|
||||
* The column <code>public.user_scores.keypresses_standard_deviation</code>.
|
||||
*/
|
||||
val KEYPRESSES_STANDARD_DEVIATION: TableField<UserScoresRecord, Double?> = createField(DSL.name("keypresses_standard_deviation"), SQLDataType.DOUBLE, this, "")
|
||||
|
||||
/**
|
||||
* The column <code>public.user_scores.sliderend_release_times</code>.
|
||||
*/
|
||||
val SLIDEREND_RELEASE_TIMES: TableField<UserScoresRecord, Array<Double?>?> = createField(DSL.name("sliderend_release_times"), SQLDataType.FLOAT.array(), this, "")
|
||||
|
||||
/**
|
||||
* The column <code>public.user_scores.sliderend_release_median</code>.
|
||||
*/
|
||||
val SLIDEREND_RELEASE_MEDIAN: TableField<UserScoresRecord, Double?> = createField(DSL.name("sliderend_release_median"), SQLDataType.DOUBLE, this, "")
|
||||
|
||||
/**
|
||||
* The column
|
||||
* <code>public.user_scores.sliderend_release_standard_deviation</code>.
|
||||
*/
|
||||
val SLIDEREND_RELEASE_STANDARD_DEVIATION: TableField<UserScoresRecord, Double?> = createField(DSL.name("sliderend_release_standard_deviation"), SQLDataType.DOUBLE, this, "")
|
||||
|
||||
/**
|
||||
* The column <code>public.user_scores.keypresses_median_adjusted</code>.
|
||||
*/
|
||||
val KEYPRESSES_MEDIAN_ADJUSTED: TableField<UserScoresRecord, Double?> = createField(DSL.name("keypresses_median_adjusted"), SQLDataType.DOUBLE, this, "")
|
||||
|
||||
/**
|
||||
* The column
|
||||
* <code>public.user_scores.keypresses_standard_deviation_adjusted</code>.
|
||||
*/
|
||||
val KEYPRESSES_STANDARD_DEVIATION_ADJUSTED: TableField<UserScoresRecord, Double?> = createField(DSL.name("keypresses_standard_deviation_adjusted"), SQLDataType.DOUBLE, this, "")
|
||||
|
||||
/**
|
||||
* The column
|
||||
* <code>public.user_scores.sliderend_release_median_adjusted</code>.
|
||||
*/
|
||||
val SLIDEREND_RELEASE_MEDIAN_ADJUSTED: TableField<UserScoresRecord, Double?> = createField(DSL.name("sliderend_release_median_adjusted"), SQLDataType.DOUBLE, this, "")
|
||||
|
||||
/**
|
||||
* The column
|
||||
* <code>public.user_scores.sliderend_release_standard_deviation_adjusted</code>.
|
||||
*/
|
||||
val SLIDEREND_RELEASE_STANDARD_DEVIATION_ADJUSTED: TableField<UserScoresRecord, Double?> = createField(DSL.name("sliderend_release_standard_deviation_adjusted"), SQLDataType.DOUBLE, this, "")
|
||||
|
||||
/**
|
||||
* The column <code>public.user_scores.judgements</code>.
|
||||
*/
|
||||
val JUDGEMENTS: TableField<UserScoresRecord, ByteArray?> = createField(DSL.name("judgements"), SQLDataType.BLOB, this, "")
|
||||
|
||||
private constructor(alias: Name, aliased: Table<UserScoresRecord>?): this(alias, null, null, aliased, null)
|
||||
private constructor(alias: Name, aliased: Table<UserScoresRecord>?, parameters: Array<Field<*>?>?): this(alias, null, null, aliased, parameters)
|
||||
|
||||
/**
|
||||
* Create an aliased <code>public.user_scores</code> table reference
|
||||
*/
|
||||
constructor(alias: String): this(DSL.name(alias))
|
||||
|
||||
/**
|
||||
* Create an aliased <code>public.user_scores</code> table reference
|
||||
*/
|
||||
constructor(alias: Name): this(alias, null)
|
||||
|
||||
/**
|
||||
* Create a <code>public.user_scores</code> table reference
|
||||
*/
|
||||
constructor(): this(DSL.name("user_scores"), null)
|
||||
|
||||
constructor(child: Table<out Record>, key: ForeignKey<out Record, UserScoresRecord>): this(Internal.createPathAlias(child, key), child, key, USER_SCORES, null)
|
||||
override fun getSchema(): Schema? = if (aliased()) null else Public.PUBLIC
|
||||
override fun getChecks(): List<Check<UserScoresRecord>> = listOf(
|
||||
Internal.createCheck(this, DSL.name("life_bar_graph_check"), "((octet_length(life_bar_graph) <= 524288))", true),
|
||||
Internal.createCheck(this, DSL.name("replay_data_check"), "((octet_length(replay_data) <= 524288))", true)
|
||||
)
|
||||
override fun `as`(alias: String): UserScores = UserScores(DSL.name(alias), this)
|
||||
override fun `as`(alias: Name): UserScores = UserScores(alias, this)
|
||||
override fun `as`(alias: Table<*>): UserScores = UserScores(alias.getQualifiedName(), this)
|
||||
|
||||
/**
|
||||
* Rename this table
|
||||
*/
|
||||
override fun rename(name: String): UserScores = UserScores(DSL.name(name), null)
|
||||
|
||||
/**
|
||||
* Rename this table
|
||||
*/
|
||||
override fun rename(name: Name): UserScores = UserScores(name, null)
|
||||
|
||||
/**
|
||||
* Rename this table
|
||||
*/
|
||||
override fun rename(name: Table<*>): UserScores = UserScores(name.getQualifiedName(), null)
|
||||
}
|
||||
@ -0,0 +1,272 @@
|
||||
/*
|
||||
* This file is generated by jOOQ.
|
||||
*/
|
||||
package com.nisemoe.generated.tables.records
|
||||
|
||||
|
||||
import com.nisemoe.generated.tables.UserScores
|
||||
|
||||
import java.time.OffsetDateTime
|
||||
import java.util.UUID
|
||||
|
||||
import org.jooq.impl.TableRecordImpl
|
||||
|
||||
|
||||
/**
|
||||
* This class is generated by jOOQ.
|
||||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
open class UserScoresRecord private constructor() : TableRecordImpl<UserScoresRecord>(UserScores.USER_SCORES) {
|
||||
|
||||
open var id: UUID?
|
||||
set(value): Unit = set(0, value)
|
||||
get(): UUID? = get(0) as UUID?
|
||||
|
||||
open var addedAt: OffsetDateTime?
|
||||
set(value): Unit = set(1, value)
|
||||
get(): OffsetDateTime? = get(1) as OffsetDateTime?
|
||||
|
||||
open var beatmapId: Int?
|
||||
set(value): Unit = set(2, value)
|
||||
get(): Int? = get(2) as Int?
|
||||
|
||||
open var beatmapHash: String?
|
||||
set(value): Unit = set(3, value)
|
||||
get(): String? = get(3) as String?
|
||||
|
||||
open var gameMode: Int?
|
||||
set(value): Unit = set(4, value)
|
||||
get(): Int? = get(4) as Int?
|
||||
|
||||
open var gameVersion: Int?
|
||||
set(value): Unit = set(5, value)
|
||||
get(): Int? = get(5) as Int?
|
||||
|
||||
open var playerName: String?
|
||||
set(value): Unit = set(6, value)
|
||||
get(): String? = get(6) as String?
|
||||
|
||||
open var replayHash: String?
|
||||
set(value): Unit = set(7, value)
|
||||
get(): String? = get(7) as String?
|
||||
|
||||
open var count_300: Short?
|
||||
set(value): Unit = set(8, value)
|
||||
get(): Short? = get(8) as Short?
|
||||
|
||||
open var count_100: Short?
|
||||
set(value): Unit = set(9, value)
|
||||
get(): Short? = get(9) as Short?
|
||||
|
||||
open var count_50: Short?
|
||||
set(value): Unit = set(10, value)
|
||||
get(): Short? = get(10) as Short?
|
||||
|
||||
open var countGeki: Short?
|
||||
set(value): Unit = set(11, value)
|
||||
get(): Short? = get(11) as Short?
|
||||
|
||||
open var countKatu: Short?
|
||||
set(value): Unit = set(12, value)
|
||||
get(): Short? = get(12) as Short?
|
||||
|
||||
open var countMiss: Short?
|
||||
set(value): Unit = set(13, value)
|
||||
get(): Short? = get(13) as Short?
|
||||
|
||||
open var totalScore: Int?
|
||||
set(value): Unit = set(14, value)
|
||||
get(): Int? = get(14) as Int?
|
||||
|
||||
open var maxCombo: Short?
|
||||
set(value): Unit = set(15, value)
|
||||
get(): Short? = get(15) as Short?
|
||||
|
||||
open var perfect: Boolean?
|
||||
set(value): Unit = set(16, value)
|
||||
get(): Boolean? = get(16) as Boolean?
|
||||
|
||||
open var mods: Int?
|
||||
set(value): Unit = set(17, value)
|
||||
get(): Int? = get(17) as Int?
|
||||
|
||||
open var lifeBarGraph: String?
|
||||
set(value): Unit = set(18, value)
|
||||
get(): String? = get(18) as String?
|
||||
|
||||
open var timestamp: Long?
|
||||
set(value): Unit = set(19, value)
|
||||
get(): Long? = get(19) as Long?
|
||||
|
||||
open var replayLength: Int?
|
||||
set(value): Unit = set(20, value)
|
||||
get(): Int? = get(20) as Int?
|
||||
|
||||
open var replayData: String?
|
||||
set(value): Unit = set(21, value)
|
||||
get(): String? = get(21) as String?
|
||||
|
||||
open var onlineScoreId: Long?
|
||||
set(value): Unit = set(22, value)
|
||||
get(): Long? = get(22) as Long?
|
||||
|
||||
open var additionalModInfo: Double?
|
||||
set(value): Unit = set(23, value)
|
||||
get(): Double? = get(23) as Double?
|
||||
|
||||
open var ur: Double?
|
||||
set(value): Unit = set(24, value)
|
||||
get(): Double? = get(24) as Double?
|
||||
|
||||
open var frametime: Double?
|
||||
set(value): Unit = set(25, value)
|
||||
get(): Double? = get(25) as Double?
|
||||
|
||||
open var edgeHits: Int?
|
||||
set(value): Unit = set(26, value)
|
||||
get(): Int? = get(26) as Int?
|
||||
|
||||
open var snaps: Int?
|
||||
set(value): Unit = set(27, value)
|
||||
get(): Int? = get(27) as Int?
|
||||
|
||||
open var adjustedUr: Double?
|
||||
set(value): Unit = set(28, value)
|
||||
get(): Double? = get(28) as Double?
|
||||
|
||||
open var meanError: Double?
|
||||
set(value): Unit = set(29, value)
|
||||
get(): Double? = get(29) as Double?
|
||||
|
||||
open var errorVariance: Double?
|
||||
set(value): Unit = set(30, value)
|
||||
get(): Double? = get(30) as Double?
|
||||
|
||||
open var errorStandardDeviation: Double?
|
||||
set(value): Unit = set(31, value)
|
||||
get(): Double? = get(31) as Double?
|
||||
|
||||
open var minimumError: Double?
|
||||
set(value): Unit = set(32, value)
|
||||
get(): Double? = get(32) as Double?
|
||||
|
||||
open var maximumError: Double?
|
||||
set(value): Unit = set(33, value)
|
||||
get(): Double? = get(33) as Double?
|
||||
|
||||
open var errorRange: Double?
|
||||
set(value): Unit = set(34, value)
|
||||
get(): Double? = get(34) as Double?
|
||||
|
||||
open var errorCoefficientOfVariation: Double?
|
||||
set(value): Unit = set(35, value)
|
||||
get(): Double? = get(35) as Double?
|
||||
|
||||
open var errorKurtosis: Double?
|
||||
set(value): Unit = set(36, value)
|
||||
get(): Double? = get(36) as Double?
|
||||
|
||||
open var errorSkewness: Double?
|
||||
set(value): Unit = set(37, value)
|
||||
get(): Double? = get(37) as Double?
|
||||
|
||||
open var keypressesTimes: Array<Double?>?
|
||||
set(value): Unit = set(38, value)
|
||||
get(): Array<Double?>? = get(38) as Array<Double?>?
|
||||
|
||||
open var keypressesMedian: Double?
|
||||
set(value): Unit = set(39, value)
|
||||
get(): Double? = get(39) as Double?
|
||||
|
||||
open var keypressesStandardDeviation: Double?
|
||||
set(value): Unit = set(40, value)
|
||||
get(): Double? = get(40) as Double?
|
||||
|
||||
open var sliderendReleaseTimes: Array<Double?>?
|
||||
set(value): Unit = set(41, value)
|
||||
get(): Array<Double?>? = get(41) as Array<Double?>?
|
||||
|
||||
open var sliderendReleaseMedian: Double?
|
||||
set(value): Unit = set(42, value)
|
||||
get(): Double? = get(42) as Double?
|
||||
|
||||
open var sliderendReleaseStandardDeviation: Double?
|
||||
set(value): Unit = set(43, value)
|
||||
get(): Double? = get(43) as Double?
|
||||
|
||||
open var keypressesMedianAdjusted: Double?
|
||||
set(value): Unit = set(44, value)
|
||||
get(): Double? = get(44) as Double?
|
||||
|
||||
open var keypressesStandardDeviationAdjusted: Double?
|
||||
set(value): Unit = set(45, value)
|
||||
get(): Double? = get(45) as Double?
|
||||
|
||||
open var sliderendReleaseMedianAdjusted: Double?
|
||||
set(value): Unit = set(46, value)
|
||||
get(): Double? = get(46) as Double?
|
||||
|
||||
open var sliderendReleaseStandardDeviationAdjusted: Double?
|
||||
set(value): Unit = set(47, value)
|
||||
get(): Double? = get(47) as Double?
|
||||
|
||||
open var judgements: ByteArray?
|
||||
set(value): Unit = set(48, value)
|
||||
get(): ByteArray? = get(48) as ByteArray?
|
||||
|
||||
/**
|
||||
* Create a detached, initialised UserScoresRecord
|
||||
*/
|
||||
constructor(id: UUID? = null, addedAt: OffsetDateTime? = null, beatmapId: Int? = null, beatmapHash: String? = null, gameMode: Int? = null, gameVersion: Int? = null, playerName: String? = null, replayHash: String? = null, count_300: Short? = null, count_100: Short? = null, count_50: Short? = null, countGeki: Short? = null, countKatu: Short? = null, countMiss: Short? = null, totalScore: Int? = null, maxCombo: Short? = null, perfect: Boolean? = null, mods: Int? = null, lifeBarGraph: String? = null, timestamp: Long? = null, replayLength: Int? = null, replayData: String? = null, onlineScoreId: Long? = null, additionalModInfo: Double? = null, ur: Double? = null, frametime: Double? = null, edgeHits: Int? = null, snaps: Int? = null, adjustedUr: Double? = null, meanError: Double? = null, errorVariance: Double? = null, errorStandardDeviation: Double? = null, minimumError: Double? = null, maximumError: Double? = null, errorRange: Double? = null, errorCoefficientOfVariation: Double? = null, errorKurtosis: Double? = null, errorSkewness: Double? = null, keypressesTimes: Array<Double?>? = null, keypressesMedian: Double? = null, keypressesStandardDeviation: Double? = null, sliderendReleaseTimes: Array<Double?>? = null, sliderendReleaseMedian: Double? = null, sliderendReleaseStandardDeviation: Double? = null, keypressesMedianAdjusted: Double? = null, keypressesStandardDeviationAdjusted: Double? = null, sliderendReleaseMedianAdjusted: Double? = null, sliderendReleaseStandardDeviationAdjusted: Double? = null, judgements: ByteArray? = null): this() {
|
||||
this.id = id
|
||||
this.addedAt = addedAt
|
||||
this.beatmapId = beatmapId
|
||||
this.beatmapHash = beatmapHash
|
||||
this.gameMode = gameMode
|
||||
this.gameVersion = gameVersion
|
||||
this.playerName = playerName
|
||||
this.replayHash = replayHash
|
||||
this.count_300 = count_300
|
||||
this.count_100 = count_100
|
||||
this.count_50 = count_50
|
||||
this.countGeki = countGeki
|
||||
this.countKatu = countKatu
|
||||
this.countMiss = countMiss
|
||||
this.totalScore = totalScore
|
||||
this.maxCombo = maxCombo
|
||||
this.perfect = perfect
|
||||
this.mods = mods
|
||||
this.lifeBarGraph = lifeBarGraph
|
||||
this.timestamp = timestamp
|
||||
this.replayLength = replayLength
|
||||
this.replayData = replayData
|
||||
this.onlineScoreId = onlineScoreId
|
||||
this.additionalModInfo = additionalModInfo
|
||||
this.ur = ur
|
||||
this.frametime = frametime
|
||||
this.edgeHits = edgeHits
|
||||
this.snaps = snaps
|
||||
this.adjustedUr = adjustedUr
|
||||
this.meanError = meanError
|
||||
this.errorVariance = errorVariance
|
||||
this.errorStandardDeviation = errorStandardDeviation
|
||||
this.minimumError = minimumError
|
||||
this.maximumError = maximumError
|
||||
this.errorRange = errorRange
|
||||
this.errorCoefficientOfVariation = errorCoefficientOfVariation
|
||||
this.errorKurtosis = errorKurtosis
|
||||
this.errorSkewness = errorSkewness
|
||||
this.keypressesTimes = keypressesTimes
|
||||
this.keypressesMedian = keypressesMedian
|
||||
this.keypressesStandardDeviation = keypressesStandardDeviation
|
||||
this.sliderendReleaseTimes = sliderendReleaseTimes
|
||||
this.sliderendReleaseMedian = sliderendReleaseMedian
|
||||
this.sliderendReleaseStandardDeviation = sliderendReleaseStandardDeviation
|
||||
this.keypressesMedianAdjusted = keypressesMedianAdjusted
|
||||
this.keypressesStandardDeviationAdjusted = keypressesStandardDeviationAdjusted
|
||||
this.sliderendReleaseMedianAdjusted = sliderendReleaseMedianAdjusted
|
||||
this.sliderendReleaseStandardDeviationAdjusted = sliderendReleaseStandardDeviationAdjusted
|
||||
this.judgements = judgements
|
||||
resetChangedOnNotNull()
|
||||
}
|
||||
}
|
||||
@ -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.UserScores
|
||||
import com.nisemoe.generated.tables.Users
|
||||
|
||||
|
||||
@ -56,6 +57,11 @@ val SCORES_SIMILARITY: ScoresSimilarity = ScoresSimilarity.SCORES_SIMILARITY
|
||||
*/
|
||||
val UPDATE_USER_QUEUE: UpdateUserQueue = UpdateUserQueue.UPDATE_USER_QUEUE
|
||||
|
||||
/**
|
||||
* The table <code>public.user_scores</code>.
|
||||
*/
|
||||
val USER_SCORES: UserScores = UserScores.USER_SCORES
|
||||
|
||||
/**
|
||||
* The table <code>public.users</code>.
|
||||
*/
|
||||
|
||||
@ -0,0 +1,122 @@
|
||||
package com.nisemoe.nise.controller
|
||||
|
||||
import com.nisemoe.generated.tables.references.SCORES
|
||||
import com.nisemoe.generated.tables.references.USER_SCORES
|
||||
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.service.CompressJudgements
|
||||
import org.jooq.DSLContext
|
||||
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
|
||||
|
||||
@RestController
|
||||
class UploadReplayController(
|
||||
private val dslContext: DSLContext,
|
||||
private val beatmapService: BeatmapService,
|
||||
private val compressJudgements: CompressJudgements,
|
||||
private val circleguardService: CircleguardService,
|
||||
private val osuApi: OsuApi
|
||||
) {
|
||||
|
||||
private val maxFileSize: Long = 4 * 1024 * 1024 // ~4MB
|
||||
|
||||
data class AnalyzeReplayResult(
|
||||
val id: String
|
||||
)
|
||||
|
||||
@PostMapping("analyze")
|
||||
fun analyzeReplay(@RequestParam("replay") replayFile: MultipartFile): ResponseEntity<AnalyzeReplayResult> {
|
||||
// Basic pre-flights checks
|
||||
if (replayFile.size > maxFileSize) {
|
||||
return ResponseEntity.badRequest().build()
|
||||
}
|
||||
|
||||
// Create an OsuReplay instance and decode the file
|
||||
val replay = OsuReplay(replayFile.bytes)
|
||||
|
||||
if(replay.gameMode != 0 || replay.beatmapHash.isNullOrBlank()) {
|
||||
return ResponseEntity.badRequest().build()
|
||||
}
|
||||
|
||||
// Fetch the beatmap id
|
||||
val beatmapId = this.osuApi.getBeatmapIdFromHash(replay.beatmapHash!!)
|
||||
?: return ResponseEntity.badRequest().build()
|
||||
|
||||
// TODO: Add beatmap to database if it doesn't exist
|
||||
|
||||
val beatmapFile = this.beatmapService.getBeatmapFile(beatmapId)
|
||||
?: return ResponseEntity.badRequest().build()
|
||||
|
||||
// Analyze the replay
|
||||
val analysis = this.circleguardService.processReplay(
|
||||
replayData = replay.replayData!!,
|
||||
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)
|
||||
.set(USER_SCORES.GAME_MODE, replay.gameMode)
|
||||
.set(USER_SCORES.GAME_VERSION, replay.gameVersion)
|
||||
.set(USER_SCORES.PLAYER_NAME, replay.playerName)
|
||||
.set(USER_SCORES.REPLAY_HASH, replay.replayHash)
|
||||
.set(USER_SCORES.COUNT_300, replay.numberOf300s)
|
||||
.set(USER_SCORES.COUNT_100, replay.numberOf100s)
|
||||
.set(USER_SCORES.COUNT_50, replay.numberOf50s)
|
||||
.set(USER_SCORES.COUNT_GEKI, replay.numberOfGekis)
|
||||
.set(USER_SCORES.COUNT_KATU, replay.numberOfKatus)
|
||||
.set(USER_SCORES.COUNT_MISS, replay.numberOfMisses)
|
||||
.set(USER_SCORES.TOTAL_SCORE, replay.totalScore)
|
||||
.set(USER_SCORES.MAX_COMBO, replay.greatestCombo)
|
||||
.set(USER_SCORES.PERFECT, replay.perfectCombo)
|
||||
.set(USER_SCORES.MODS, replay.modsUsed)
|
||||
.set(USER_SCORES.LIFE_BAR_GRAPH, replay.lifeBarGraph)
|
||||
.set(USER_SCORES.TIMESTAMP, replay.timestamp)
|
||||
.set(USER_SCORES.REPLAY_LENGTH, replay.replayLength)
|
||||
.set(USER_SCORES.REPLAY_DATA, replay.replayData)
|
||||
.set(USER_SCORES.ONLINE_SCORE_ID, replay.onlineScoreID)
|
||||
.set(USER_SCORES.ADDITIONAL_MOD_INFO, replay.additionalModInfo)
|
||||
.set(USER_SCORES.UR, analysis.ur)
|
||||
.set(USER_SCORES.FRAMETIME, analysis.frametime)
|
||||
.set(USER_SCORES.EDGE_HITS, analysis.edge_hits)
|
||||
.set(USER_SCORES.SNAPS, analysis.snaps)
|
||||
.set(USER_SCORES.ADJUSTED_UR, analysis.adjusted_ur)
|
||||
.set(USER_SCORES.MEAN_ERROR, analysis.mean_error)
|
||||
.set(USER_SCORES.ERROR_VARIANCE, analysis.error_variance)
|
||||
.set(USER_SCORES.ERROR_STANDARD_DEVIATION, analysis.error_standard_deviation)
|
||||
.set(USER_SCORES.MINIMUM_ERROR, analysis.minimum_error)
|
||||
.set(USER_SCORES.MAXIMUM_ERROR, analysis.maximum_error)
|
||||
.set(USER_SCORES.ERROR_RANGE, analysis.error_range)
|
||||
.set(USER_SCORES.ERROR_COEFFICIENT_OF_VARIATION, analysis.error_coefficient_of_variation)
|
||||
.set(USER_SCORES.ERROR_KURTOSIS, analysis.error_kurtosis)
|
||||
.set(USER_SCORES.ERROR_SKEWNESS, analysis.error_skewness)
|
||||
.set(USER_SCORES.KEYPRESSES_TIMES, analysis.keypresses_times?.toTypedArray())
|
||||
.set(USER_SCORES.KEYPRESSES_MEDIAN, analysis.keypresses_median)
|
||||
.set(USER_SCORES.KEYPRESSES_STANDARD_DEVIATION, analysis.keypresses_standard_deviation)
|
||||
.set(USER_SCORES.SLIDEREND_RELEASE_TIMES, analysis.sliderend_release_times?.toTypedArray())
|
||||
.set(USER_SCORES.SLIDEREND_RELEASE_MEDIAN, analysis.sliderend_release_median)
|
||||
.set(USER_SCORES.SLIDEREND_RELEASE_STANDARD_DEVIATION, analysis.sliderend_release_standard_deviation)
|
||||
.set(USER_SCORES.KEYPRESSES_MEDIAN_ADJUSTED, analysis.keypresses_median_adjusted)
|
||||
.set(USER_SCORES.KEYPRESSES_STANDARD_DEVIATION_ADJUSTED, analysis.keypresses_standard_deviation_adjusted)
|
||||
.set(USER_SCORES.SLIDEREND_RELEASE_MEDIAN_ADJUSTED, analysis.sliderend_release_median_adjusted)
|
||||
.set(USER_SCORES.SLIDEREND_RELEASE_STANDARD_DEVIATION_ADJUSTED, analysis.sliderend_release_standard_deviation_adjusted)
|
||||
.set(SCORES.JUDGEMENTS, compressJudgements.serialize(analysis.judgements))
|
||||
.returning(USER_SCORES.ID)
|
||||
.fetchOne()
|
||||
|
||||
if(newUserReplayId == null) {
|
||||
return ResponseEntity.internalServerError().build()
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(AnalyzeReplayResult(newUserReplayId.get(USER_SCORES.ID).toString()))
|
||||
}
|
||||
}
|
||||
@ -1,12 +1,47 @@
|
||||
package com.nisemoe.nise.database
|
||||
|
||||
import com.nisemoe.generated.tables.references.BEATMAPS
|
||||
import com.nisemoe.generated.tables.references.SCORES
|
||||
import com.nisemoe.nise.osu.OsuApi
|
||||
import org.jooq.DSLContext
|
||||
import org.jooq.impl.DSL.avg
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
@Service
|
||||
class BeatmapService(private val dslContext: DSLContext) {
|
||||
class BeatmapService(
|
||||
private val dslContext: DSLContext,
|
||||
private val osuApi: OsuApi
|
||||
) {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(javaClass)
|
||||
|
||||
fun getBeatmapFile(beatmapId: Int): String? {
|
||||
// Fetch the beatmap file from database
|
||||
var beatmapFile = dslContext.select(BEATMAPS.BEATMAP_FILE)
|
||||
.from(BEATMAPS)
|
||||
.where(BEATMAPS.BEATMAP_ID.eq(beatmapId))
|
||||
.fetchOneInto(String::class.java)
|
||||
|
||||
if(!beatmapFile.isNullOrBlank()) {
|
||||
return beatmapFile
|
||||
}
|
||||
|
||||
this.logger.warn("Failed to fetch beatmap file for beatmap_id = $beatmapId from database")
|
||||
|
||||
beatmapFile = this.osuApi.getBeatmapFile(beatmapId = beatmapId)
|
||||
|
||||
if(beatmapFile == null) {
|
||||
this.logger.error("Failed to fetch beatmap file for beatmap_id = $beatmapId from osu!api")
|
||||
return null
|
||||
} else {
|
||||
dslContext.update(BEATMAPS)
|
||||
.set(BEATMAPS.BEATMAP_FILE, beatmapFile)
|
||||
.where(BEATMAPS.BEATMAP_ID.eq(beatmapId))
|
||||
.execute()
|
||||
return beatmapFile
|
||||
}
|
||||
}
|
||||
|
||||
fun getAverageUR(beatmapId: Int, excludeReplayId: Long): Double? {
|
||||
val condition = SCORES.BEATMAP_ID.eq(beatmapId)
|
||||
|
||||
@ -118,6 +118,25 @@ class OsuApi(
|
||||
}
|
||||
}
|
||||
|
||||
fun getBeatmapIdFromHash(beatmapHash: String): Int? {
|
||||
val queryParams = mapOf(
|
||||
"checksum" to beatmapHash
|
||||
)
|
||||
|
||||
val response = doRequest("https://osu.ppy.sh/api/v2/beatmaps/lookup?", queryParams)
|
||||
if(response == null) {
|
||||
this.logger.info("Error loading beatmap")
|
||||
return null
|
||||
}
|
||||
|
||||
return if (response.statusCode() == 200) {
|
||||
val beatmap = serializer.decodeFromString(OsuApiModels.BeatmapCompact.serializer(), response.body())
|
||||
beatmap.id ?: null
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the replay data for a given score ID from the osu!api.
|
||||
* Efficiently cycles through the API keys to avoid rate limiting.
|
||||
|
||||
209
nise-backend/src/main/kotlin/com/nisemoe/nise/osu/OsuReplay.kt
Normal file
209
nise-backend/src/main/kotlin/com/nisemoe/nise/osu/OsuReplay.kt
Normal file
@ -0,0 +1,209 @@
|
||||
package com.nisemoe.nise.osu
|
||||
|
||||
import org.apache.commons.compress.compressors.lzma.LZMACompressorInputStream
|
||||
import org.apache.commons.compress.compressors.lzma.LZMACompressorOutputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.DataInputStream
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.*
|
||||
|
||||
class OsuReplay(fileContent: ByteArray) {
|
||||
|
||||
companion object {
|
||||
|
||||
// ~4mb
|
||||
private val EXPECTED_FILE_SIZE = 0 .. 4194304
|
||||
|
||||
// ~512kb
|
||||
private const val MAX_STRING_LENGTH = 512000
|
||||
|
||||
private val EXPECTED_STRING_LENGTH = 0 .. MAX_STRING_LENGTH
|
||||
|
||||
private val EXPECTED_INT_RANGE = Int.MIN_VALUE..Int.MAX_VALUE
|
||||
|
||||
private val EXPECTED_LONG_RANGE = Long.MIN_VALUE..Long.MAX_VALUE
|
||||
|
||||
private val EXPECTED_DOUBLE_RANGE = Double.MIN_VALUE..Double.MAX_VALUE
|
||||
|
||||
}
|
||||
|
||||
private val dis = DataInputStream(fileContent.inputStream())
|
||||
|
||||
var gameMode: Int = 0
|
||||
var gameVersion: Int = 0
|
||||
var beatmapHash: String? = null
|
||||
var playerName: String? = null
|
||||
var replayHash: String? = null
|
||||
var numberOf300s: Short = 0
|
||||
var numberOf100s: Short = 0
|
||||
var numberOf50s: Short = 0
|
||||
var numberOfGekis: Short = 0
|
||||
var numberOfKatus: Short = 0
|
||||
var numberOfMisses: Short = 0
|
||||
var totalScore: Int = 0
|
||||
var greatestCombo: Short = 0
|
||||
var perfectCombo: Boolean = false
|
||||
var modsUsed: Int = 0
|
||||
var lifeBarGraph: String? = null
|
||||
var timestamp: Long = 0
|
||||
var replayLength: Int = 0
|
||||
var replayData: String? = null
|
||||
var onlineScoreID: Long = 0
|
||||
var additionalModInfo: Double = 0.0
|
||||
|
||||
init {
|
||||
if (fileContent.size !in EXPECTED_FILE_SIZE) {
|
||||
throw SecurityException("File size out of expected bounds")
|
||||
}
|
||||
|
||||
decode()
|
||||
}
|
||||
|
||||
private fun decode() {
|
||||
try {
|
||||
gameMode = dis.readByte().toInt()
|
||||
if(gameMode != 0) {
|
||||
throw SecurityException("Invalid game mode")
|
||||
}
|
||||
|
||||
gameVersion = readIntLittleEndian()
|
||||
beatmapHash = dis.readCompressedReplayData()
|
||||
playerName = dis.readCompressedReplayData()
|
||||
replayHash = dis.readCompressedReplayData()
|
||||
numberOf300s = readShortLittleEndian()
|
||||
numberOf100s = readShortLittleEndian()
|
||||
numberOf50s = readShortLittleEndian()
|
||||
numberOfGekis = readShortLittleEndian()
|
||||
numberOfKatus = readShortLittleEndian()
|
||||
numberOfMisses = readShortLittleEndian()
|
||||
totalScore = readIntLittleEndian()
|
||||
greatestCombo = readShortLittleEndian()
|
||||
perfectCombo = dis.readByte() != 0.toByte()
|
||||
modsUsed = readIntLittleEndian()
|
||||
lifeBarGraph = dis.readCompressedReplayData()
|
||||
timestamp = readLongLittleEndian()
|
||||
replayLength = readIntLittleEndian()
|
||||
replayData = dis.readCompressedReplayData(replayLength)
|
||||
onlineScoreID = readLongLittleEndian()
|
||||
if ((modsUsed and (1 shl 24)) != 0) {
|
||||
additionalModInfo = readDoubleLittleEndian()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("Failed to decode .osr file content: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun DataInputStream.readCompressedReplayData(): String? {
|
||||
return when (readByte()) {
|
||||
0x0b.toByte() -> {
|
||||
val length = readULEB128()
|
||||
if (length !in EXPECTED_STRING_LENGTH) {
|
||||
throw SecurityException("String length out of expected bounds")
|
||||
}
|
||||
ByteArray(length.toInt()).also { readFully(it) }.toString(StandardCharsets.UTF_8)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun DataInputStream.readCompressedReplayData(length: Int): String {
|
||||
// Read the compressed data
|
||||
val compressedData = ByteArray(length)
|
||||
readFully(compressedData)
|
||||
|
||||
// Decompress the data
|
||||
val decompressedStream = LZMACompressorInputStream(compressedData.inputStream())
|
||||
val decompressedData = decompressedStream.readBytes()
|
||||
decompressedStream.close()
|
||||
|
||||
// Compress the decompressed data
|
||||
val compressedOutputStream = ByteArrayOutputStream()
|
||||
val lzmaCompressorOutputStream = LZMACompressorOutputStream(compressedOutputStream)
|
||||
lzmaCompressorOutputStream.write(decompressedData)
|
||||
lzmaCompressorOutputStream.close()
|
||||
|
||||
// Now encode the re-compressed data to Base64
|
||||
return Base64.getEncoder().encodeToString(compressedOutputStream.toByteArray())
|
||||
}
|
||||
|
||||
private fun DataInputStream.readULEB128(): Long {
|
||||
var result = 0L
|
||||
var shift = 0
|
||||
var size = 0
|
||||
|
||||
do {
|
||||
if (size == 10) { // Prevent reading more than 10 bytes, the maximum needed for a 64-bit number
|
||||
throw SecurityException("Invalid LEB128 sequence.")
|
||||
}
|
||||
|
||||
val byte = readByte()
|
||||
size++
|
||||
|
||||
// Check for overflow: If we're on the last byte (10th), it should not have more than 1 bit before the continuation bit
|
||||
if (size == 10 && byte.toInt() and 0x7F > 1) {
|
||||
throw SecurityException("LEB128 sequence overflow.")
|
||||
}
|
||||
|
||||
val value = (byte.toInt() and 0x7F)
|
||||
if (shift >= 63 && value > 0) { // prevent shifting into oblivion
|
||||
throw SecurityException("LEB128 sequence overflow.")
|
||||
}
|
||||
|
||||
result = result or (value.toLong() shl shift)
|
||||
shift += 7
|
||||
} while (byte.toInt() and 0x80 > 0)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private fun readShortLittleEndian(): Short {
|
||||
if (dis.available() < Short.SIZE_BYTES) {
|
||||
throw SecurityException("Insufficient data available to read short")
|
||||
}
|
||||
val bytes = ByteArray(Short.SIZE_BYTES)
|
||||
dis.readFully(bytes)
|
||||
return ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).short
|
||||
}
|
||||
|
||||
private fun readIntLittleEndian(): Int {
|
||||
if (dis.available() < Int.SIZE_BYTES) {
|
||||
throw SecurityException("Insufficient data available to read int")
|
||||
}
|
||||
val bytes = ByteArray(Int.SIZE_BYTES)
|
||||
dis.readFully(bytes)
|
||||
val value = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).int
|
||||
if (value !in EXPECTED_INT_RANGE) {
|
||||
throw SecurityException("Decoded integer value out of expected bounds")
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
private fun readLongLittleEndian(): Long {
|
||||
if (dis.available() < Long.SIZE_BYTES) {
|
||||
throw SecurityException("Insufficient data available to read long")
|
||||
}
|
||||
val bytes = ByteArray(Long.SIZE_BYTES)
|
||||
dis.readFully(bytes)
|
||||
val value = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).long
|
||||
if (value !in EXPECTED_LONG_RANGE) {
|
||||
throw SecurityException("Decoded long value out of expected bounds")
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
private fun readDoubleLittleEndian(): Double {
|
||||
if (dis.available() < Double.SIZE_BYTES) {
|
||||
throw SecurityException("Insufficient data available to read double")
|
||||
}
|
||||
val bytes = ByteArray(Double.SIZE_BYTES)
|
||||
dis.readFully(bytes)
|
||||
val value = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).double
|
||||
if (value !in EXPECTED_DOUBLE_RANGE) {
|
||||
throw SecurityException("Decoded double value out of expected bounds")
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
CREATE TABLE public.user_scores
|
||||
(
|
||||
id uuid not null default gen_random_uuid(),
|
||||
added_at timestamptz DEFAULT CURRENT_TIMESTAMP NULL,
|
||||
beatmap_id int4 NULL,
|
||||
beatmap_hash varchar(32) NULL,
|
||||
game_mode int4 NULL,
|
||||
game_version int4 NULL,
|
||||
player_name varchar(256) NULL,
|
||||
replay_hash varchar(32) NULL,
|
||||
count_300 int2 NULL,
|
||||
count_100 int2 NULL,
|
||||
count_50 int2 NULL,
|
||||
count_geki int2 NULL,
|
||||
count_katu int2 NULL,
|
||||
count_miss int2 NULL,
|
||||
total_score int4 NULL,
|
||||
max_combo int2 NULL,
|
||||
perfect bool NULL,
|
||||
mods int4 NULL,
|
||||
life_bar_graph text NULL,
|
||||
timestamp int8 NULL,
|
||||
replay_length int4 NULL,
|
||||
replay_data text NULL,
|
||||
online_score_id int8 NULL,
|
||||
additional_mod_info float8 NULL,
|
||||
|
||||
ur float8 NULL,
|
||||
frametime float8 NULL,
|
||||
edge_hits int4 NULL,
|
||||
snaps int4 NULL,
|
||||
adjusted_ur float8 NULL,
|
||||
mean_error float8 NULL,
|
||||
error_variance float8 NULL,
|
||||
error_standard_deviation float8 NULL,
|
||||
minimum_error float8 NULL,
|
||||
maximum_error float8 NULL,
|
||||
error_range float8 NULL,
|
||||
error_coefficient_of_variation float8 NULL,
|
||||
error_kurtosis float8 NULL,
|
||||
error_skewness float8 NULL,
|
||||
keypresses_times float8[] NULL,
|
||||
keypresses_median float8 NULL,
|
||||
keypresses_standard_deviation float8 NULL,
|
||||
sliderend_release_times float8[] NULL,
|
||||
sliderend_release_median float8 NULL,
|
||||
sliderend_release_standard_deviation float8 NULL,
|
||||
keypresses_median_adjusted float8 NULL,
|
||||
keypresses_standard_deviation_adjusted float8 NULL,
|
||||
sliderend_release_median_adjusted float8 NULL,
|
||||
sliderend_release_standard_deviation_adjusted float8 NULL,
|
||||
judgements bytea NULL,
|
||||
|
||||
CONSTRAINT life_bar_graph_check CHECK (OCTET_LENGTH(life_bar_graph) <= 524288),
|
||||
CONSTRAINT replay_data_check CHECK (OCTET_LENGTH(replay_data) <= 524288)
|
||||
);
|
||||
@ -0,0 +1,23 @@
|
||||
package com.nisemoe.nise.osu
|
||||
|
||||
import org.junit.jupiter.api.Assertions.*
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.io.FileInputStream
|
||||
|
||||
class OsuReplayTest {
|
||||
|
||||
@Test
|
||||
fun testDecode() {
|
||||
// Read the .osr file into a ByteArray
|
||||
val filePath = "replay1.osr"
|
||||
val fileByteArray = FileInputStream(filePath).readBytes()
|
||||
|
||||
// Create an OsuReplay instance and decode the file
|
||||
val replay = OsuReplay(fileByteArray)
|
||||
|
||||
// Assert the decoded properties
|
||||
assertEquals(0, replay.gameMode)
|
||||
assertEquals("jup1terrosu", replay.playerName)
|
||||
}
|
||||
|
||||
}
|
||||
@ -4,11 +4,15 @@
|
||||
box-sizing: border-box; /* Includes padding and border in the element's total width and height */
|
||||
}
|
||||
|
||||
.main .term:nth-child(1) {
|
||||
.main .subcontainer:nth-child(1) {
|
||||
width: 70%;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.main .term:nth-child(2) {
|
||||
.main .subcontainer:nth-child(2) {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
.subcontainer .term {
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
@ -4,64 +4,77 @@
|
||||
|
||||
<div class="main container">
|
||||
|
||||
<div class="term">
|
||||
<h1># Welcome to [nise.moe]</h1>
|
||||
<h3>wtf is this?</h3>
|
||||
<p>This application will automatically crawl [osu!std] top scores and search for stolen replays or obvious relax/timewarp scores.</p>
|
||||
<p>It started collecting replays on <i>2024-01-12</i></p>
|
||||
<p>This website is not affiliated with the osu! game nor ppy. It is an unrelated, unaffiliated, 3rd party project.</p>
|
||||
<p>If you have any suggestions or want to report bugs, feel free to join the Discord server below.</p>
|
||||
<div class="text-center mt-4">
|
||||
<a href="https://discord.gg/wn4gWpA36w" target="_blank" class="btn">Join the Discord!</a>
|
||||
</div>
|
||||
<h3 class="mt-4"># do you use rss? (nerd)</h3>
|
||||
<p>you can keep up with newly detected scores with the rss feed, subscribe to it using your favorite reader.</p>
|
||||
<div class="text-center">
|
||||
<a href="https://nise.moe/api/rss.xml" target="_blank">
|
||||
<img title="rss-chan!" src="/assets/rss.png" width="64" style="filter: grayscale(40%) sepia(10%) brightness(90%);">
|
||||
<br>
|
||||
<span style="padding: 2px; border: 1px dotted #b3b8c3;">
|
||||
<div class="subcontainer">
|
||||
<div class="term">
|
||||
<h1># Welcome to [nise.moe]</h1>
|
||||
<h3>wtf is this?</h3>
|
||||
<p>This application will automatically crawl [osu!std] top scores and search for stolen replays or obvious relax/timewarp scores.</p>
|
||||
<p>It started collecting replays on <i>2024-01-12</i></p>
|
||||
<p>This website is not affiliated with the osu! game nor ppy. It is an unrelated, unaffiliated, 3rd party project.</p>
|
||||
<p>If you have any suggestions or want to report bugs, feel free to join the Discord server below.</p>
|
||||
<div class="text-center mt-4">
|
||||
<a href="https://discord.gg/wn4gWpA36w" target="_blank" class="btn">Join the Discord!</a>
|
||||
</div>
|
||||
<h3 class="mt-4"># do you use rss? (nerd)</h3>
|
||||
<p>you can keep up with newly detected scores with the rss feed, subscribe to it using your favorite reader.</p>
|
||||
<div class="text-center">
|
||||
<a href="https://nise.moe/api/rss.xml" target="_blank">
|
||||
<img title="rss-chan!" src="/assets/rss.png" width="64" style="filter: grayscale(40%) sepia(10%) brightness(90%);">
|
||||
<br>
|
||||
<span style="padding: 2px; border: 1px dotted #b3b8c3;">
|
||||
Get the feed
|
||||
</span>
|
||||
</a>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="term">
|
||||
<div class="text-center" style="font-weight: bold; font-size: 14px; padding-bottom: 10px">
|
||||
<img src="/assets/new.png" width="48" style=" filter: grayscale(30%) sepia(20%) brightness(90%);">
|
||||
<br>
|
||||
<ng-container *ngIf="this.wantsConnection">new <span style="margin-right: 4px" class="text-has-info " title="this feed includes any score - this does not mean they're cheating.">scores</span> <span style="color: rgba(36,255,114,0.65); filter: grayscale(40%)">[live]</span></ng-container>
|
||||
<ng-container *ngIf="!this.wantsConnection">new <span style="margin-right: 4px" class="text-has-info " title="this feed includes any score - this does not mean they're cheating.">scores</span> <span style="color: rgba(234,64,29,0.92); filter: grayscale(40%)">[disconnected]</span></ng-container>
|
||||
<br>
|
||||
<button (click)="this.toggleConnection()" style="margin-top: 4px;">
|
||||
<ng-container *ngIf="this.wantsConnection">Disconnect</ng-container>
|
||||
<ng-container *ngIf="!this.wantsConnection">Connect</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
<ng-container *ngIf="this.liveScores.length <= 0">
|
||||
<p class="text-center text-muted">
|
||||
nothing yet...<br>
|
||||
new scores will appear here.
|
||||
</p>
|
||||
</ng-container>
|
||||
<div style="overflow-y: scroll; max-height: 565px">
|
||||
<ng-container *ngFor="let replay of this.liveScores">
|
||||
<div style="margin-bottom: 6px">
|
||||
<a [routerLink]="['/s/' + replay.replay_id]">
|
||||
<div style="border: 1px dotted rgba(204,204,204,0.4); padding: 4px; margin-right: 16px; font-size: 13px">
|
||||
<ul style="list-style: none; padding-left: 10px;">
|
||||
<li>User: {{ replay.username }}</li>
|
||||
<li>Date: {{ replay.date }}</li>
|
||||
<li>PP: {{ replay.pp | number: '1.0-0' }}</li>
|
||||
<li>cvUR: {{ replay.ur | number: '1.2-2' }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="subcontainer">
|
||||
|
||||
<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>
|
||||
|
||||
<div class="term">
|
||||
<div class="text-center" style="font-weight: bold; font-size: 14px; padding-bottom: 10px">
|
||||
<img src="/assets/new.png" width="48" style=" filter: grayscale(30%) sepia(20%) brightness(90%);">
|
||||
<br>
|
||||
<ng-container *ngIf="this.wantsConnection">new <span style="margin-right: 4px" class="text-has-info " title="this feed includes any score - this does not mean they're cheating.">scores</span> <span style="color: rgba(36,255,114,0.65); filter: grayscale(40%)">[live]</span></ng-container>
|
||||
<ng-container *ngIf="!this.wantsConnection">new <span style="margin-right: 4px" class="text-has-info " title="this feed includes any score - this does not mean they're cheating.">scores</span> <span style="color: rgba(234,64,29,0.92); filter: grayscale(40%)">[disconnected]</span></ng-container>
|
||||
<br>
|
||||
<button (click)="this.toggleConnection()" style="margin-top: 4px;">
|
||||
<ng-container *ngIf="this.wantsConnection">Disconnect</ng-container>
|
||||
<ng-container *ngIf="!this.wantsConnection">Connect</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
<ng-container *ngIf="this.liveScores.length <= 0">
|
||||
<p class="text-center text-muted">
|
||||
nothing yet...<br>
|
||||
new scores will appear here.
|
||||
</p>
|
||||
</ng-container>
|
||||
<div style="overflow-y: scroll; max-height: 565px">
|
||||
<ng-container *ngFor="let replay of this.liveScores">
|
||||
<div style="margin-bottom: 6px">
|
||||
<a [routerLink]="['/s/' + replay.replay_id]">
|
||||
<div style="border: 1px dotted rgba(204,204,204,0.4); padding: 4px; margin-right: 16px; font-size: 13px">
|
||||
<ul style="list-style: none; padding-left: 10px;">
|
||||
<li>User: {{ replay.username }}</li>
|
||||
<li>Date: {{ replay.date }}</li>
|
||||
<li>PP: {{ replay.pp | number: '1.0-0' }}</li>
|
||||
<li>cvUR: {{ replay.ur | number: '1.2-2' }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@ -6,7 +6,8 @@ import {RxStompService} from "../../corelib/stomp/stomp.service";
|
||||
import {Message} from "@stomp/stompjs/esm6";
|
||||
import {ReplayData} from "../replays";
|
||||
import {DecimalPipe, NgForOf, NgIf} from "@angular/common";
|
||||
import {RouterLink} from "@angular/router";
|
||||
import {Router, RouterLink} from "@angular/router";
|
||||
import {HttpClient} from "@angular/common/http";
|
||||
|
||||
interface Statistics {
|
||||
total_beatmaps: number;
|
||||
@ -16,6 +17,10 @@ interface Statistics {
|
||||
total_replay_similarity: number;
|
||||
}
|
||||
|
||||
interface AnalyzeReplayResponse {
|
||||
id: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
standalone: true,
|
||||
@ -36,8 +41,12 @@ export class HomeComponent implements OnInit, OnDestroy {
|
||||
statistics: Statistics | null = null;
|
||||
wantsConnection: boolean = true;
|
||||
|
||||
loading = false;
|
||||
|
||||
constructor(
|
||||
private localCacheService: LocalCacheService,
|
||||
private router: Router,
|
||||
private httpClient: HttpClient,
|
||||
private rxStompService: RxStompService,
|
||||
) { }
|
||||
|
||||
@ -92,4 +101,27 @@ export class HomeComponent implements OnInit, OnDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
uploadReplay(event: any) {
|
||||
if (event.target.files.length <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
|
||||
const file: File = event.target.files[0];
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('replay', file);
|
||||
|
||||
this.httpClient.post<AnalyzeReplayResponse>(`${environment.apiUrl}/analyze`, formData).subscribe({
|
||||
next: (response) => {
|
||||
this.loading = false;
|
||||
this.router.navigate(['/c/' + response.id ]);
|
||||
},
|
||||
error: (error) => {
|
||||
this.loading = false;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -59,7 +59,7 @@ html {
|
||||
@media screen and (min-width: 768px) {
|
||||
|
||||
.main {
|
||||
width: 820px !important;
|
||||
width: 850px !important;
|
||||
}
|
||||
|
||||
.header {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user