diff --git a/nise-backend/replay1.osr b/nise-backend/replay1.osr new file mode 100644 index 0000000..e830ce4 Binary files /dev/null and b/nise-backend/replay1.osr differ diff --git a/nise-backend/src/main/kotlin/com/nisemoe/generated/Public.kt b/nise-backend/src/main/kotlin/com/nisemoe/generated/Public.kt index f7fb720..c47cbeb 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/generated/Public.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/generated/Public.kt @@ -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 public.user_scores. + */ + val USER_SCORES: UserScores get() = UserScores.USER_SCORES + /** * The table public.users. */ @@ -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 ) } diff --git a/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/UserScores.kt b/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/UserScores.kt new file mode 100644 index 0000000..06a421f --- /dev/null +++ b/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/UserScores.kt @@ -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?, + path: ForeignKey?, + aliased: Table?, + parameters: Array?>? +): TableImpl( + alias, + Public.PUBLIC, + child, + path, + aliased, + parameters, + DSL.comment(""), + TableOptions.table() +) { + companion object { + + /** + * The reference instance of public.user_scores + */ + val USER_SCORES: UserScores = UserScores() + } + + /** + * The class holding records for this type + */ + override fun getRecordType(): Class = UserScoresRecord::class.java + + /** + * The column public.user_scores.id. + */ + val ID: TableField = createField(DSL.name("id"), SQLDataType.UUID.nullable(false).defaultValue(DSL.field(DSL.raw("gen_random_uuid()"), SQLDataType.UUID)), this, "") + + /** + * The column public.user_scores.added_at. + */ + val ADDED_AT: TableField = createField(DSL.name("added_at"), SQLDataType.TIMESTAMPWITHTIMEZONE(6).defaultValue(DSL.field(DSL.raw("CURRENT_TIMESTAMP"), SQLDataType.TIMESTAMPWITHTIMEZONE)), this, "") + + /** + * The column public.user_scores.beatmap_id. + */ + val BEATMAP_ID: TableField = createField(DSL.name("beatmap_id"), SQLDataType.INTEGER, this, "") + + /** + * The column public.user_scores.beatmap_hash. + */ + val BEATMAP_HASH: TableField = createField(DSL.name("beatmap_hash"), SQLDataType.VARCHAR(32), this, "") + + /** + * The column public.user_scores.game_mode. + */ + val GAME_MODE: TableField = createField(DSL.name("game_mode"), SQLDataType.INTEGER, this, "") + + /** + * The column public.user_scores.game_version. + */ + val GAME_VERSION: TableField = createField(DSL.name("game_version"), SQLDataType.INTEGER, this, "") + + /** + * The column public.user_scores.player_name. + */ + val PLAYER_NAME: TableField = createField(DSL.name("player_name"), SQLDataType.VARCHAR(256), this, "") + + /** + * The column public.user_scores.replay_hash. + */ + val REPLAY_HASH: TableField = createField(DSL.name("replay_hash"), SQLDataType.VARCHAR(32), this, "") + + /** + * The column public.user_scores.count_300. + */ + val COUNT_300: TableField = createField(DSL.name("count_300"), SQLDataType.SMALLINT, this, "") + + /** + * The column public.user_scores.count_100. + */ + val COUNT_100: TableField = createField(DSL.name("count_100"), SQLDataType.SMALLINT, this, "") + + /** + * The column public.user_scores.count_50. + */ + val COUNT_50: TableField = createField(DSL.name("count_50"), SQLDataType.SMALLINT, this, "") + + /** + * The column public.user_scores.count_geki. + */ + val COUNT_GEKI: TableField = createField(DSL.name("count_geki"), SQLDataType.SMALLINT, this, "") + + /** + * The column public.user_scores.count_katu. + */ + val COUNT_KATU: TableField = createField(DSL.name("count_katu"), SQLDataType.SMALLINT, this, "") + + /** + * The column public.user_scores.count_miss. + */ + val COUNT_MISS: TableField = createField(DSL.name("count_miss"), SQLDataType.SMALLINT, this, "") + + /** + * The column public.user_scores.total_score. + */ + val TOTAL_SCORE: TableField = createField(DSL.name("total_score"), SQLDataType.INTEGER, this, "") + + /** + * The column public.user_scores.max_combo. + */ + val MAX_COMBO: TableField = createField(DSL.name("max_combo"), SQLDataType.SMALLINT, this, "") + + /** + * The column public.user_scores.perfect. + */ + val PERFECT: TableField = createField(DSL.name("perfect"), SQLDataType.BOOLEAN, this, "") + + /** + * The column public.user_scores.mods. + */ + val MODS: TableField = createField(DSL.name("mods"), SQLDataType.INTEGER, this, "") + + /** + * The column public.user_scores.life_bar_graph. + */ + val LIFE_BAR_GRAPH: TableField = createField(DSL.name("life_bar_graph"), SQLDataType.CLOB, this, "") + + /** + * The column public.user_scores.timestamp. + */ + val TIMESTAMP: TableField = createField(DSL.name("timestamp"), SQLDataType.BIGINT, this, "") + + /** + * The column public.user_scores.replay_length. + */ + val REPLAY_LENGTH: TableField = createField(DSL.name("replay_length"), SQLDataType.INTEGER, this, "") + + /** + * The column public.user_scores.replay_data. + */ + val REPLAY_DATA: TableField = createField(DSL.name("replay_data"), SQLDataType.CLOB, this, "") + + /** + * The column public.user_scores.online_score_id. + */ + val ONLINE_SCORE_ID: TableField = createField(DSL.name("online_score_id"), SQLDataType.BIGINT, this, "") + + /** + * The column public.user_scores.additional_mod_info. + */ + val ADDITIONAL_MOD_INFO: TableField = createField(DSL.name("additional_mod_info"), SQLDataType.DOUBLE, this, "") + + /** + * The column public.user_scores.ur. + */ + val UR: TableField = createField(DSL.name("ur"), SQLDataType.DOUBLE, this, "") + + /** + * The column public.user_scores.frametime. + */ + val FRAMETIME: TableField = createField(DSL.name("frametime"), SQLDataType.DOUBLE, this, "") + + /** + * The column public.user_scores.edge_hits. + */ + val EDGE_HITS: TableField = createField(DSL.name("edge_hits"), SQLDataType.INTEGER, this, "") + + /** + * The column public.user_scores.snaps. + */ + val SNAPS: TableField = createField(DSL.name("snaps"), SQLDataType.INTEGER, this, "") + + /** + * The column public.user_scores.adjusted_ur. + */ + val ADJUSTED_UR: TableField = createField(DSL.name("adjusted_ur"), SQLDataType.DOUBLE, this, "") + + /** + * The column public.user_scores.mean_error. + */ + val MEAN_ERROR: TableField = createField(DSL.name("mean_error"), SQLDataType.DOUBLE, this, "") + + /** + * The column public.user_scores.error_variance. + */ + val ERROR_VARIANCE: TableField = createField(DSL.name("error_variance"), SQLDataType.DOUBLE, this, "") + + /** + * The column public.user_scores.error_standard_deviation. + */ + val ERROR_STANDARD_DEVIATION: TableField = createField(DSL.name("error_standard_deviation"), SQLDataType.DOUBLE, this, "") + + /** + * The column public.user_scores.minimum_error. + */ + val MINIMUM_ERROR: TableField = createField(DSL.name("minimum_error"), SQLDataType.DOUBLE, this, "") + + /** + * The column public.user_scores.maximum_error. + */ + val MAXIMUM_ERROR: TableField = createField(DSL.name("maximum_error"), SQLDataType.DOUBLE, this, "") + + /** + * The column public.user_scores.error_range. + */ + val ERROR_RANGE: TableField = createField(DSL.name("error_range"), SQLDataType.DOUBLE, this, "") + + /** + * The column + * public.user_scores.error_coefficient_of_variation. + */ + val ERROR_COEFFICIENT_OF_VARIATION: TableField = createField(DSL.name("error_coefficient_of_variation"), SQLDataType.DOUBLE, this, "") + + /** + * The column public.user_scores.error_kurtosis. + */ + val ERROR_KURTOSIS: TableField = createField(DSL.name("error_kurtosis"), SQLDataType.DOUBLE, this, "") + + /** + * The column public.user_scores.error_skewness. + */ + val ERROR_SKEWNESS: TableField = createField(DSL.name("error_skewness"), SQLDataType.DOUBLE, this, "") + + /** + * The column public.user_scores.keypresses_times. + */ + val KEYPRESSES_TIMES: TableField?> = createField(DSL.name("keypresses_times"), SQLDataType.FLOAT.array(), this, "") + + /** + * The column public.user_scores.keypresses_median. + */ + val KEYPRESSES_MEDIAN: TableField = createField(DSL.name("keypresses_median"), SQLDataType.DOUBLE, this, "") + + /** + * The column public.user_scores.keypresses_standard_deviation. + */ + val KEYPRESSES_STANDARD_DEVIATION: TableField = createField(DSL.name("keypresses_standard_deviation"), SQLDataType.DOUBLE, this, "") + + /** + * The column public.user_scores.sliderend_release_times. + */ + val SLIDEREND_RELEASE_TIMES: TableField?> = createField(DSL.name("sliderend_release_times"), SQLDataType.FLOAT.array(), this, "") + + /** + * The column public.user_scores.sliderend_release_median. + */ + val SLIDEREND_RELEASE_MEDIAN: TableField = createField(DSL.name("sliderend_release_median"), SQLDataType.DOUBLE, this, "") + + /** + * The column + * public.user_scores.sliderend_release_standard_deviation. + */ + val SLIDEREND_RELEASE_STANDARD_DEVIATION: TableField = createField(DSL.name("sliderend_release_standard_deviation"), SQLDataType.DOUBLE, this, "") + + /** + * The column public.user_scores.keypresses_median_adjusted. + */ + val KEYPRESSES_MEDIAN_ADJUSTED: TableField = createField(DSL.name("keypresses_median_adjusted"), SQLDataType.DOUBLE, this, "") + + /** + * The column + * public.user_scores.keypresses_standard_deviation_adjusted. + */ + val KEYPRESSES_STANDARD_DEVIATION_ADJUSTED: TableField = createField(DSL.name("keypresses_standard_deviation_adjusted"), SQLDataType.DOUBLE, this, "") + + /** + * The column + * public.user_scores.sliderend_release_median_adjusted. + */ + val SLIDEREND_RELEASE_MEDIAN_ADJUSTED: TableField = createField(DSL.name("sliderend_release_median_adjusted"), SQLDataType.DOUBLE, this, "") + + /** + * The column + * public.user_scores.sliderend_release_standard_deviation_adjusted. + */ + val SLIDEREND_RELEASE_STANDARD_DEVIATION_ADJUSTED: TableField = createField(DSL.name("sliderend_release_standard_deviation_adjusted"), SQLDataType.DOUBLE, this, "") + + /** + * The column public.user_scores.judgements. + */ + val JUDGEMENTS: TableField = createField(DSL.name("judgements"), SQLDataType.BLOB, this, "") + + private constructor(alias: Name, aliased: Table?): this(alias, null, null, aliased, null) + private constructor(alias: Name, aliased: Table?, parameters: Array?>?): this(alias, null, null, aliased, parameters) + + /** + * Create an aliased public.user_scores table reference + */ + constructor(alias: String): this(DSL.name(alias)) + + /** + * Create an aliased public.user_scores table reference + */ + constructor(alias: Name): this(alias, null) + + /** + * Create a public.user_scores table reference + */ + constructor(): this(DSL.name("user_scores"), null) + + constructor(child: Table, key: ForeignKey): this(Internal.createPathAlias(child, key), child, key, USER_SCORES, null) + override fun getSchema(): Schema? = if (aliased()) null else Public.PUBLIC + override fun getChecks(): List> = 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) +} diff --git a/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/records/UserScoresRecord.kt b/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/records/UserScoresRecord.kt new file mode 100644 index 0000000..bcf327f --- /dev/null +++ b/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/records/UserScoresRecord.kt @@ -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(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? + set(value): Unit = set(38, value) + get(): Array? = get(38) as Array? + + 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? + set(value): Unit = set(41, value) + get(): Array? = get(41) as Array? + + 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? = null, keypressesMedian: Double? = null, keypressesStandardDeviation: Double? = null, sliderendReleaseTimes: Array? = 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() + } +} diff --git a/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/references/Tables.kt b/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/references/Tables.kt index a521b55..f903481 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/references/Tables.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/references/Tables.kt @@ -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 public.user_scores. + */ +val USER_SCORES: UserScores = UserScores.USER_SCORES + /** * The table public.users. */ diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/controller/UploadReplayController.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/controller/UploadReplayController.kt new file mode 100644 index 0000000..fc85221 --- /dev/null +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/controller/UploadReplayController.kt @@ -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 { + // 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())) + } +} \ No newline at end of file diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/database/BeatmapService.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/database/BeatmapService.kt index 19d6ea3..3b2c1ea 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/nise/database/BeatmapService.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/database/BeatmapService.kt @@ -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) diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/osu/OsuApi.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/osu/OsuApi.kt index 7896a3b..b845d1c 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/nise/osu/OsuApi.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/osu/OsuApi.kt @@ -118,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. diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/osu/OsuReplay.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/osu/OsuReplay.kt new file mode 100644 index 0000000..bcfeac6 --- /dev/null +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/osu/OsuReplay.kt @@ -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 + } + +} \ No newline at end of file diff --git a/nise-backend/src/main/resources/db/migration/V0.0.1.027__create_user_scores.sql b/nise-backend/src/main/resources/db/migration/V0.0.1.027__create_user_scores.sql new file mode 100644 index 0000000..c768b44 --- /dev/null +++ b/nise-backend/src/main/resources/db/migration/V0.0.1.027__create_user_scores.sql @@ -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) +); \ No newline at end of file diff --git a/nise-backend/src/test/kotlin/com/nisemoe/nise/osu/OsuReplayTest.kt b/nise-backend/src/test/kotlin/com/nisemoe/nise/osu/OsuReplayTest.kt new file mode 100644 index 0000000..8372c17 --- /dev/null +++ b/nise-backend/src/test/kotlin/com/nisemoe/nise/osu/OsuReplayTest.kt @@ -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) + } + +} \ No newline at end of file diff --git a/nise-frontend/src/app/home/home.component.css b/nise-frontend/src/app/home/home.component.css index 943d3a8..13c849e 100644 --- a/nise-frontend/src/app/home/home.component.css +++ b/nise-frontend/src/app/home/home.component.css @@ -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; +} diff --git a/nise-frontend/src/app/home/home.component.html b/nise-frontend/src/app/home/home.component.html index 8a65828..c6f1e47 100644 --- a/nise-frontend/src/app/home/home.component.html +++ b/nise-frontend/src/app/home/home.component.html @@ -4,64 +4,77 @@
-
-

# Welcome to [nise.moe]

-

wtf is this?

-

This application will automatically crawl [osu!std] top scores and search for stolen replays or obvious relax/timewarp scores.

-

It started collecting replays on 2024-01-12

-

This website is not affiliated with the osu! game nor ppy. It is an unrelated, unaffiliated, 3rd party project.

-

If you have any suggestions or want to report bugs, feel free to join the Discord server below.

- -

# do you use rss? (nerd)

-

you can keep up with newly detected scores with the rss feed, subscribe to it using your favorite reader.

-
- - -
- +
-
-
- -
- new scores [live] - new scores [disconnected] -
- -
- -

- nothing yet...
- new scores will appear here. -

-
- +
+ + + + +
+
+ +
+ new scores [live] + new scores [disconnected] +
+ +
+ +

+ nothing yet...
+ new scores will appear here. +

+
+ + + +
diff --git a/nise-frontend/src/app/home/home.component.ts b/nise-frontend/src/app/home/home.component.ts index 24fdecf..b16911f 100644 --- a/nise-frontend/src/app/home/home.component.ts +++ b/nise-frontend/src/app/home/home.component.ts @@ -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(`${environment.apiUrl}/analyze`, formData).subscribe({ + next: (response) => { + this.loading = false; + this.router.navigate(['/c/' + response.id ]); + }, + error: (error) => { + this.loading = false; + }, + }); + } + } diff --git a/nise-frontend/src/assets/style.css b/nise-frontend/src/assets/style.css index f7b6571..26d98c1 100644 --- a/nise-frontend/src/assets/style.css +++ b/nise-frontend/src/assets/style.css @@ -59,7 +59,7 @@ html { @media screen and (min-width: 768px) { .main { - width: 820px !important; + width: 850px !important; } .header {