Compare commits
62 Commits
792b203255
...
ef1031fcf3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef1031fcf3 | ||
|
|
b25139367a | ||
|
|
b5bff780bb | ||
|
|
0ebf48dd29 | ||
|
|
136cf5f15b | ||
|
|
6e0e2c7864 | ||
|
|
ff30590fb5 | ||
|
|
52bf7d56b0 | ||
|
|
86f307b53b | ||
|
|
341cb269a4 | ||
|
|
777230af9b | ||
|
|
099c146f1b | ||
|
|
cd6774e354 | ||
|
|
a028e26c98 | ||
|
|
8cc50b244c | ||
|
|
a2acb0cc52 | ||
|
|
e91fa6dad2 | ||
|
|
0dc4f745a5 | ||
|
|
427654764d | ||
|
|
4c7ea03cc9 | ||
|
|
89f57c2ed9 | ||
|
|
8b1370bab3 | ||
|
|
9d30103d55 | ||
|
|
1faa80a1db | ||
|
|
b4824ce81f | ||
|
|
0dd385728c | ||
|
|
7d1fe66b99 | ||
|
|
bef1c72187 | ||
|
|
2452e0a2be | ||
|
|
5caa9ca14b | ||
|
|
35a9e9a00e | ||
|
|
573d642529 | ||
|
|
9e04397ac4 | ||
|
|
6b5ec34821 | ||
|
|
e177e46707 | ||
|
|
e848c975c8 | ||
|
|
c870c25afe | ||
|
|
db98f9e731 | ||
|
|
3a73942602 | ||
|
|
f798a791bf | ||
|
|
25e6b6b2dd | ||
|
|
73b002c7c7 | ||
|
|
c6dd723ccd | ||
|
|
c8ded05194 | ||
|
|
ad112e7b40 | ||
|
|
85ffbbf244 | ||
|
|
4f43edabc3 | ||
|
|
009633ea99 | ||
|
|
aef795c010 | ||
|
|
68ad9bb23a | ||
|
|
c63d6ecc00 | ||
|
|
1bc4326cff | ||
|
|
ae4cad89e1 | ||
|
|
d0ef99728e | ||
|
|
3ea6c2e8e4 | ||
|
|
cc10fa221e | ||
|
|
5e770a0f2c | ||
|
|
5472d418f5 | ||
|
|
c788e9b336 | ||
|
|
250227d60e | ||
|
|
9206de7308 | ||
|
|
306c05fcd7 |
@ -26,8 +26,12 @@ val IDX_REPLAY_IDS_PAIRS: Index = Internal.createIndex(DSL.name("idx_replay_ids_
|
|||||||
val IDX_SCORES_BEATMAP_ID: Index = Internal.createIndex(DSL.name("idx_scores_beatmap_id"), Scores.SCORES, arrayOf(Scores.SCORES.BEATMAP_ID), false)
|
val IDX_SCORES_BEATMAP_ID: Index = Internal.createIndex(DSL.name("idx_scores_beatmap_id"), Scores.SCORES, arrayOf(Scores.SCORES.BEATMAP_ID), false)
|
||||||
val IDX_SCORES_BEATMAP_ID_REPLAY_ID: Index = Internal.createIndex(DSL.name("idx_scores_beatmap_id_replay_id"), Scores.SCORES, arrayOf(Scores.SCORES.BEATMAP_ID, Scores.SCORES.REPLAY_ID), false)
|
val IDX_SCORES_BEATMAP_ID_REPLAY_ID: Index = Internal.createIndex(DSL.name("idx_scores_beatmap_id_replay_id"), Scores.SCORES, arrayOf(Scores.SCORES.BEATMAP_ID, Scores.SCORES.REPLAY_ID), false)
|
||||||
val IDX_SCORES_BEATMAP_ID_REPLAY_ID_UR: Index = Internal.createIndex(DSL.name("idx_scores_beatmap_id_replay_id_ur"), Scores.SCORES, arrayOf(Scores.SCORES.BEATMAP_ID, Scores.SCORES.REPLAY_ID, Scores.SCORES.UR), false)
|
val IDX_SCORES_BEATMAP_ID_REPLAY_ID_UR: Index = Internal.createIndex(DSL.name("idx_scores_beatmap_id_replay_id_ur"), Scores.SCORES, arrayOf(Scores.SCORES.BEATMAP_ID, Scores.SCORES.REPLAY_ID, Scores.SCORES.UR), false)
|
||||||
|
val IDX_SCORES_IS_BANNED: Index = Internal.createIndex(DSL.name("idx_scores_is_banned"), Scores.SCORES, arrayOf(Scores.SCORES.IS_BANNED), false)
|
||||||
val IDX_SCORES_JUDGEMENTS_SCORE_ID: Index = Internal.createIndex(DSL.name("idx_scores_judgements_score_id"), ScoresJudgements.SCORES_JUDGEMENTS, arrayOf(ScoresJudgements.SCORES_JUDGEMENTS.SCORE_ID), false)
|
val IDX_SCORES_JUDGEMENTS_SCORE_ID: Index = Internal.createIndex(DSL.name("idx_scores_judgements_score_id"), ScoresJudgements.SCORES_JUDGEMENTS, arrayOf(ScoresJudgements.SCORES_JUDGEMENTS.SCORE_ID), false)
|
||||||
|
val IDX_SCORES_KEYPRESSES_STANDARD_DEVIATION_ADJUSTED: Index = Internal.createIndex(DSL.name("idx_scores_keypresses_standard_deviation_adjusted"), Scores.SCORES, arrayOf(Scores.SCORES.KEYPRESSES_STANDARD_DEVIATION_ADJUSTED), false)
|
||||||
|
val IDX_SCORES_PP: Index = Internal.createIndex(DSL.name("idx_scores_pp"), Scores.SCORES, arrayOf(Scores.SCORES.PP), false)
|
||||||
val IDX_SCORES_REPLAY_ID: Index = Internal.createIndex(DSL.name("idx_scores_replay_id"), Scores.SCORES, arrayOf(Scores.SCORES.REPLAY_ID), false)
|
val IDX_SCORES_REPLAY_ID: Index = Internal.createIndex(DSL.name("idx_scores_replay_id"), Scores.SCORES, arrayOf(Scores.SCORES.REPLAY_ID), false)
|
||||||
|
val IDX_SCORES_SLIDEREND_RELEASE_STANDARD_DEVIATION_ADJUSTED: Index = Internal.createIndex(DSL.name("idx_scores_sliderend_release_standard_deviation_adjusted"), Scores.SCORES, arrayOf(Scores.SCORES.SLIDEREND_RELEASE_STANDARD_DEVIATION_ADJUSTED), false)
|
||||||
val IDX_SCORES_UR: Index = Internal.createIndex(DSL.name("idx_scores_ur"), Scores.SCORES, arrayOf(Scores.SCORES.UR), false)
|
val IDX_SCORES_UR: Index = Internal.createIndex(DSL.name("idx_scores_ur"), Scores.SCORES, arrayOf(Scores.SCORES.UR), false)
|
||||||
val IDX_SCORES_USER_ID: Index = Internal.createIndex(DSL.name("idx_scores_user_id"), Scores.SCORES, arrayOf(Scores.SCORES.USER_ID), false)
|
val IDX_SCORES_USER_ID: Index = Internal.createIndex(DSL.name("idx_scores_user_id"), Scores.SCORES, arrayOf(Scores.SCORES.USER_ID), false)
|
||||||
val IDX_USERS_IS_BANNED_FALSE: Index = Internal.createIndex(DSL.name("idx_users_is_banned_false"), Users.USERS, arrayOf(Users.USERS.IS_BANNED), false)
|
val IDX_USERS_IS_BANNED_FALSE: Index = Internal.createIndex(DSL.name("idx_users_is_banned_false"), Users.USERS, arrayOf(Users.USERS.IS_BANNED), false)
|
||||||
|
|||||||
@ -8,7 +8,11 @@ import com.nisemoe.generated.Public
|
|||||||
import com.nisemoe.generated.indexes.IDX_SCORES_BEATMAP_ID
|
import com.nisemoe.generated.indexes.IDX_SCORES_BEATMAP_ID
|
||||||
import com.nisemoe.generated.indexes.IDX_SCORES_BEATMAP_ID_REPLAY_ID
|
import com.nisemoe.generated.indexes.IDX_SCORES_BEATMAP_ID_REPLAY_ID
|
||||||
import com.nisemoe.generated.indexes.IDX_SCORES_BEATMAP_ID_REPLAY_ID_UR
|
import com.nisemoe.generated.indexes.IDX_SCORES_BEATMAP_ID_REPLAY_ID_UR
|
||||||
|
import com.nisemoe.generated.indexes.IDX_SCORES_IS_BANNED
|
||||||
|
import com.nisemoe.generated.indexes.IDX_SCORES_KEYPRESSES_STANDARD_DEVIATION_ADJUSTED
|
||||||
|
import com.nisemoe.generated.indexes.IDX_SCORES_PP
|
||||||
import com.nisemoe.generated.indexes.IDX_SCORES_REPLAY_ID
|
import com.nisemoe.generated.indexes.IDX_SCORES_REPLAY_ID
|
||||||
|
import com.nisemoe.generated.indexes.IDX_SCORES_SLIDEREND_RELEASE_STANDARD_DEVIATION_ADJUSTED
|
||||||
import com.nisemoe.generated.indexes.IDX_SCORES_UR
|
import com.nisemoe.generated.indexes.IDX_SCORES_UR
|
||||||
import com.nisemoe.generated.indexes.IDX_SCORES_USER_ID
|
import com.nisemoe.generated.indexes.IDX_SCORES_USER_ID
|
||||||
import com.nisemoe.generated.keys.REPLAY_ID_UNIQUE
|
import com.nisemoe.generated.keys.REPLAY_ID_UNIQUE
|
||||||
@ -317,6 +321,11 @@ open class Scores(
|
|||||||
*/
|
*/
|
||||||
val JUDGEMENTS: TableField<ScoresRecord, ByteArray?> = createField(DSL.name("judgements"), SQLDataType.BLOB, this, "")
|
val JUDGEMENTS: TableField<ScoresRecord, ByteArray?> = createField(DSL.name("judgements"), SQLDataType.BLOB, this, "")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The column <code>public.scores.leaderboard_rank</code>.
|
||||||
|
*/
|
||||||
|
val LEADERBOARD_RANK: TableField<ScoresRecord, Long?> = createField(DSL.name("leaderboard_rank"), SQLDataType.BIGINT, this, "")
|
||||||
|
|
||||||
private constructor(alias: Name, aliased: Table<ScoresRecord>?): this(alias, null, null, null, aliased, null, null)
|
private constructor(alias: Name, aliased: Table<ScoresRecord>?): this(alias, null, null, null, aliased, null, null)
|
||||||
private constructor(alias: Name, aliased: Table<ScoresRecord>?, parameters: Array<Field<*>?>?): this(alias, null, null, null, aliased, parameters, null)
|
private constructor(alias: Name, aliased: Table<ScoresRecord>?, parameters: Array<Field<*>?>?): this(alias, null, null, null, aliased, parameters, null)
|
||||||
private constructor(alias: Name, aliased: Table<ScoresRecord>?, where: Condition?): this(alias, null, null, null, aliased, null, where)
|
private constructor(alias: Name, aliased: Table<ScoresRecord>?, where: Condition?): this(alias, null, null, null, aliased, null, where)
|
||||||
@ -349,7 +358,7 @@ open class Scores(
|
|||||||
override fun `as`(alias: Table<*>): ScoresPath = ScoresPath(alias.qualifiedName, this)
|
override fun `as`(alias: Table<*>): ScoresPath = ScoresPath(alias.qualifiedName, this)
|
||||||
}
|
}
|
||||||
override fun getSchema(): Schema? = if (aliased()) null else Public.PUBLIC
|
override fun getSchema(): Schema? = if (aliased()) null else Public.PUBLIC
|
||||||
override fun getIndexes(): List<Index> = listOf(IDX_SCORES_BEATMAP_ID, IDX_SCORES_BEATMAP_ID_REPLAY_ID, IDX_SCORES_BEATMAP_ID_REPLAY_ID_UR, IDX_SCORES_REPLAY_ID, IDX_SCORES_UR, IDX_SCORES_USER_ID)
|
override fun getIndexes(): List<Index> = listOf(IDX_SCORES_BEATMAP_ID, IDX_SCORES_BEATMAP_ID_REPLAY_ID, IDX_SCORES_BEATMAP_ID_REPLAY_ID_UR, IDX_SCORES_IS_BANNED, IDX_SCORES_KEYPRESSES_STANDARD_DEVIATION_ADJUSTED, IDX_SCORES_PP, IDX_SCORES_REPLAY_ID, IDX_SCORES_SLIDEREND_RELEASE_STANDARD_DEVIATION_ADJUSTED, IDX_SCORES_UR, IDX_SCORES_USER_ID)
|
||||||
override fun getPrimaryKey(): UniqueKey<ScoresRecord> = SCORES_PKEY
|
override fun getPrimaryKey(): UniqueKey<ScoresRecord> = SCORES_PKEY
|
||||||
override fun getUniqueKeys(): List<UniqueKey<ScoresRecord>> = listOf(REPLAY_ID_UNIQUE)
|
override fun getUniqueKeys(): List<UniqueKey<ScoresRecord>> = listOf(REPLAY_ID_UNIQUE)
|
||||||
|
|
||||||
|
|||||||
@ -205,6 +205,10 @@ open class ScoresRecord private constructor() : UpdatableRecordImpl<ScoresRecord
|
|||||||
set(value): Unit = set(45, value)
|
set(value): Unit = set(45, value)
|
||||||
get(): ByteArray? = get(45) as ByteArray?
|
get(): ByteArray? = get(45) as ByteArray?
|
||||||
|
|
||||||
|
open var leaderboardRank: Long?
|
||||||
|
set(value): Unit = set(46, value)
|
||||||
|
get(): Long? = get(46) as Long?
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Primary key information
|
// Primary key information
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
@ -214,7 +218,7 @@ open class ScoresRecord private constructor() : UpdatableRecordImpl<ScoresRecord
|
|||||||
/**
|
/**
|
||||||
* Create a detached, initialised ScoresRecord
|
* Create a detached, initialised ScoresRecord
|
||||||
*/
|
*/
|
||||||
constructor(id: Int? = null, beatmapId: Int? = null, count_100: Int? = null, count_300: Int? = null, count_50: Int? = null, countMiss: Int? = null, date: LocalDateTime? = null, maxCombo: Int? = null, mods: Int? = null, perfect: Boolean? = null, pp: Double? = null, rank: String? = null, replayAvailable: Boolean? = null, replayId: Long? = null, score: Long? = null, userId: Long? = null, replay: ByteArray? = null, ur: Double? = null, frametime: Double? = null, edgeHits: Int? = null, snaps: Int? = null, isBanned: Boolean? = 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, sentDiscordNotification: Boolean? = null, addedAt: OffsetDateTime? = null, version: Int? = 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() {
|
constructor(id: Int? = null, beatmapId: Int? = null, count_100: Int? = null, count_300: Int? = null, count_50: Int? = null, countMiss: Int? = null, date: LocalDateTime? = null, maxCombo: Int? = null, mods: Int? = null, perfect: Boolean? = null, pp: Double? = null, rank: String? = null, replayAvailable: Boolean? = null, replayId: Long? = null, score: Long? = null, userId: Long? = null, replay: ByteArray? = null, ur: Double? = null, frametime: Double? = null, edgeHits: Int? = null, snaps: Int? = null, isBanned: Boolean? = 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, sentDiscordNotification: Boolean? = null, addedAt: OffsetDateTime? = null, version: Int? = 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, leaderboardRank: Long? = null): this() {
|
||||||
this.id = id
|
this.id = id
|
||||||
this.beatmapId = beatmapId
|
this.beatmapId = beatmapId
|
||||||
this.count_100 = count_100
|
this.count_100 = count_100
|
||||||
@ -261,6 +265,7 @@ open class ScoresRecord private constructor() : UpdatableRecordImpl<ScoresRecord
|
|||||||
this.sliderendReleaseMedianAdjusted = sliderendReleaseMedianAdjusted
|
this.sliderendReleaseMedianAdjusted = sliderendReleaseMedianAdjusted
|
||||||
this.sliderendReleaseStandardDeviationAdjusted = sliderendReleaseStandardDeviationAdjusted
|
this.sliderendReleaseStandardDeviationAdjusted = sliderendReleaseStandardDeviationAdjusted
|
||||||
this.judgements = judgements
|
this.judgements = judgements
|
||||||
|
this.leaderboardRank = leaderboardRank
|
||||||
resetChangedOnNotNull()
|
resetChangedOnNotNull()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -56,7 +56,8 @@ data class SuspiciousScoreEntry(
|
|||||||
val beatmap_star_rating: Double,
|
val beatmap_star_rating: Double,
|
||||||
val pp: Double,
|
val pp: Double,
|
||||||
val frametime: Double,
|
val frametime: Double,
|
||||||
val ur: Double
|
val ur: Double,
|
||||||
|
val leaderboard_rank: Long?,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class SimilarReplayEntry(
|
data class SimilarReplayEntry(
|
||||||
@ -64,6 +65,8 @@ data class SimilarReplayEntry(
|
|||||||
val replay_id_2: Long,
|
val replay_id_2: Long,
|
||||||
val user_id_1: Long,
|
val user_id_1: Long,
|
||||||
val user_id_2: Long,
|
val user_id_2: Long,
|
||||||
|
val user_banned_1: Boolean,
|
||||||
|
val user_banned_2: Boolean,
|
||||||
val username_1: String,
|
val username_1: String,
|
||||||
val username_2: String,
|
val username_2: String,
|
||||||
val beatmap_beatmapset_id: Long,
|
val beatmap_beatmapset_id: Long,
|
||||||
@ -71,6 +74,8 @@ data class SimilarReplayEntry(
|
|||||||
val replay_date_2: String,
|
val replay_date_2: String,
|
||||||
val replay_pp_1: Double,
|
val replay_pp_1: Double,
|
||||||
val replay_pp_2: Double,
|
val replay_pp_2: Double,
|
||||||
|
val replay_leaderboard_rank_1: Long?,
|
||||||
|
val replay_leaderboard_rank_2: Long?,
|
||||||
val beatmap_id: Long,
|
val beatmap_id: Long,
|
||||||
val beatmap_title: String,
|
val beatmap_title: String,
|
||||||
val beatmap_star_rating: Double,
|
val beatmap_star_rating: Double,
|
||||||
@ -175,6 +180,7 @@ data class ReplayData(
|
|||||||
val pp: Double?,
|
val pp: Double?,
|
||||||
val perfect: Boolean,
|
val perfect: Boolean,
|
||||||
val max_combo: Int,
|
val max_combo: Int,
|
||||||
|
val leaderboard_rank: Long?,
|
||||||
|
|
||||||
val count_300: Int,
|
val count_300: Int,
|
||||||
val count_100: Int,
|
val count_100: Int,
|
||||||
@ -198,6 +204,31 @@ data class ReplayData(
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Contains everything needed to reconstruct a replay (.osr) file.
|
||||||
|
// We currently do not store some of these - these have been marked.
|
||||||
|
data class EncodedReplayData(
|
||||||
|
val gameMode: Byte,
|
||||||
|
val gameVersion: Int,
|
||||||
|
val beatmapHash: String,
|
||||||
|
val username: String,
|
||||||
|
val replayHash: String, // We do not store - maybe we can reconstruct?
|
||||||
|
val count300: Short,
|
||||||
|
val count100: Short,
|
||||||
|
val count50: Short,
|
||||||
|
val countGeki: Short, // We do not store - probably should
|
||||||
|
val countKatu: Short, // We do not store - probably should
|
||||||
|
val countMisses: Short,
|
||||||
|
val totalScore: Int,
|
||||||
|
val greatestCombo: Short,
|
||||||
|
val perfect: Byte,
|
||||||
|
val mods: Int,
|
||||||
|
val lifeBar: String, // We do not store, and maybe shouldn't?
|
||||||
|
val timeStamp: Long,
|
||||||
|
val replayLength: ByteArray, // We do not store - could be calculated?
|
||||||
|
val scoreId: Long,
|
||||||
|
val additionalInformation: Double, // We do not store - probably not needed
|
||||||
|
)
|
||||||
|
|
||||||
data class DistributionEntry(
|
data class DistributionEntry(
|
||||||
val countMiss: Double,
|
val countMiss: Double,
|
||||||
val count300: Double,
|
val count300: Double,
|
||||||
|
|||||||
@ -76,7 +76,7 @@ class BanlistController(
|
|||||||
.where(USERS.IS_BANNED.eq(true))
|
.where(USERS.IS_BANNED.eq(true))
|
||||||
.orderBy(USERS.APPROX_BAN_DATE.desc())
|
.orderBy(USERS.APPROX_BAN_DATE.desc())
|
||||||
.limit(MAX_BANLIST_ENTRIES_PER_PAGE)
|
.limit(MAX_BANLIST_ENTRIES_PER_PAGE)
|
||||||
.offset((request.page - 1) * 10)
|
.offset((request.page - 1) * MAX_BANLIST_ENTRIES_PER_PAGE)
|
||||||
.fetch()
|
.fetch()
|
||||||
.map {
|
.map {
|
||||||
BanlistEntry(
|
BanlistEntry(
|
||||||
|
|||||||
@ -8,15 +8,15 @@ data class HealthResponse(
|
|||||||
val healthy: Boolean,
|
val healthy: Boolean,
|
||||||
)
|
)
|
||||||
|
|
||||||
val healthResponse = HealthResponse(
|
|
||||||
healthy = true,
|
|
||||||
)
|
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
class HealthController {
|
class HealthController {
|
||||||
@GetMapping("/health")
|
companion object {
|
||||||
fun healthCheck(): ResponseEntity<HealthResponse> {
|
val HEALTH = HealthResponse(
|
||||||
return ResponseEntity.ok(healthResponse)
|
healthy = true,
|
||||||
}
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/health")
|
||||||
|
fun healthCheck(): ResponseEntity<HealthResponse> = ResponseEntity.ok(HEALTH)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,12 +8,14 @@ data class VersionResponse(
|
|||||||
val version: String,
|
val version: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
val versionResponse = VersionResponse(
|
|
||||||
version = "v20250213",
|
|
||||||
)
|
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
class VersionController {
|
class VersionController {
|
||||||
@GetMapping("/version")
|
companion object {
|
||||||
fun getVersion(): ResponseEntity<VersionResponse> = ResponseEntity.ok(versionResponse)
|
val VERSION = VersionResponse(
|
||||||
|
version = "v20250702",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/version")
|
||||||
|
fun getVersion(): ResponseEntity<VersionResponse> = ResponseEntity.ok(VERSION)
|
||||||
}
|
}
|
||||||
@ -10,6 +10,7 @@ import com.nisemoe.nise.osu.OsuApi
|
|||||||
import com.nisemoe.nise.service.AuthService
|
import com.nisemoe.nise.service.AuthService
|
||||||
import com.nisemoe.nise.service.CompressReplay
|
import com.nisemoe.nise.service.CompressReplay
|
||||||
import org.apache.commons.compress.compressors.lzma.LZMACompressorInputStream
|
import org.apache.commons.compress.compressors.lzma.LZMACompressorInputStream
|
||||||
|
import org.jetbrains.kotlinx.dataframe.impl.asList
|
||||||
import org.jooq.Condition
|
import org.jooq.Condition
|
||||||
import org.jooq.DSLContext
|
import org.jooq.DSLContext
|
||||||
import org.jooq.Record
|
import org.jooq.Record
|
||||||
@ -39,6 +40,32 @@ class ScoreService(
|
|||||||
val osuUserAlias1 = USERS.`as`("osu_user_alias1")
|
val osuUserAlias1 = USERS.`as`("osu_user_alias1")
|
||||||
val osuUserAlias2 = USERS.`as`("osu_user_alias2")
|
val osuUserAlias2 = USERS.`as`("osu_user_alias2")
|
||||||
|
|
||||||
|
val SKIPPED_SIMILARITY_MAPS = arrayOf(
|
||||||
|
2635266, // - Death is just the beginning cs10
|
||||||
|
2528044, // - Genkaku Catastrophe cs10
|
||||||
|
2528067, // - Genkaku Catastrophe cs8
|
||||||
|
2665747, // - expand cs9
|
||||||
|
3063865, // - Kagayaku Hari no Kobitozoku ~ Little Princess 700 note stream
|
||||||
|
4641077, // - granat cs10
|
||||||
|
3616081, // - shop cs10
|
||||||
|
3533781, //- uwa cs10
|
||||||
|
3535358, // - uwa cs8
|
||||||
|
3208341, // - mizuumi cs8
|
||||||
|
4871320, // - youre a winner ninerik incident
|
||||||
|
3237399, // - dialtone cs10
|
||||||
|
4383507, // - another diff from the 700 note stream set
|
||||||
|
1606883, // - tabi no tochu cs7
|
||||||
|
4991602, // new bad map
|
||||||
|
4997662, // new bad map
|
||||||
|
1839017,
|
||||||
|
4467121,
|
||||||
|
4783487,
|
||||||
|
3772967,
|
||||||
|
3571032,
|
||||||
|
4443518,
|
||||||
|
5125702,
|
||||||
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCharts(db: Record): List<ReplayDataChart> {
|
fun getCharts(db: Record): List<ReplayDataChart> {
|
||||||
@ -165,7 +192,8 @@ class ScoreService(
|
|||||||
SCORES.ERROR_KURTOSIS,
|
SCORES.ERROR_KURTOSIS,
|
||||||
SCORES.ERROR_SKEWNESS,
|
SCORES.ERROR_SKEWNESS,
|
||||||
SCORES.SLIDEREND_RELEASE_TIMES,
|
SCORES.SLIDEREND_RELEASE_TIMES,
|
||||||
SCORES.KEYPRESSES_TIMES
|
SCORES.KEYPRESSES_TIMES,
|
||||||
|
SCORES.LEADERBOARD_RANK,
|
||||||
)
|
)
|
||||||
.from(SCORES)
|
.from(SCORES)
|
||||||
.join(USERS).on(SCORES.USER_ID.eq(USERS.USER_ID))
|
.join(USERS).on(SCORES.USER_ID.eq(USERS.USER_ID))
|
||||||
@ -228,7 +256,8 @@ class ScoreService(
|
|||||||
error_kurtosis = result.get(SCORES.ERROR_KURTOSIS, Double::class.java),
|
error_kurtosis = result.get(SCORES.ERROR_KURTOSIS, Double::class.java),
|
||||||
error_skewness = result.get(SCORES.ERROR_SKEWNESS, Double::class.java),
|
error_skewness = result.get(SCORES.ERROR_SKEWNESS, Double::class.java),
|
||||||
charts = charts,
|
charts = charts,
|
||||||
similar_scores = this.getSimilarScores(replayId)
|
similar_scores = this.getSimilarScores(replayId),
|
||||||
|
leaderboard_rank = result.get(SCORES.LEADERBOARD_RANK, Long::class.java)
|
||||||
)
|
)
|
||||||
this.loadComparableReplayData(replayData)
|
this.loadComparableReplayData(replayData)
|
||||||
return replayData
|
return replayData
|
||||||
@ -251,7 +280,8 @@ class ScoreService(
|
|||||||
BEATMAPS.STAR_RATING,
|
BEATMAPS.STAR_RATING,
|
||||||
SCORES.PP,
|
SCORES.PP,
|
||||||
SCORES.FRAMETIME,
|
SCORES.FRAMETIME,
|
||||||
SCORES.UR
|
SCORES.UR,
|
||||||
|
SCORES.LEADERBOARD_RANK,
|
||||||
)
|
)
|
||||||
.from(SCORES)
|
.from(SCORES)
|
||||||
.join(USERS).on(SCORES.USER_ID.eq(USERS.USER_ID))
|
.join(USERS).on(SCORES.USER_ID.eq(USERS.USER_ID))
|
||||||
@ -273,7 +303,8 @@ class ScoreService(
|
|||||||
beatmap_star_rating = it.get(BEATMAPS.STAR_RATING, Double::class.java),
|
beatmap_star_rating = it.get(BEATMAPS.STAR_RATING, Double::class.java),
|
||||||
pp = it.get(SCORES.PP, Double::class.java),
|
pp = it.get(SCORES.PP, Double::class.java),
|
||||||
frametime = it.get(SCORES.FRAMETIME, Double::class.java),
|
frametime = it.get(SCORES.FRAMETIME, Double::class.java),
|
||||||
ur = it.get(SCORES.UR, Double::class.java)
|
ur = it.get(SCORES.UR, Double::class.java),
|
||||||
|
leaderboard_rank = it.get(SCORES.LEADERBOARD_RANK, Long::class.java),
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -318,13 +349,17 @@ class ScoreService(
|
|||||||
osuScoreAlias1.REPLAY_ID,
|
osuScoreAlias1.REPLAY_ID,
|
||||||
osuScoreAlias1.USER_ID,
|
osuScoreAlias1.USER_ID,
|
||||||
osuUserAlias1.USERNAME,
|
osuUserAlias1.USERNAME,
|
||||||
|
osuUserAlias1.IS_BANNED,
|
||||||
osuScoreAlias1.DATE,
|
osuScoreAlias1.DATE,
|
||||||
osuScoreAlias1.PP,
|
osuScoreAlias1.PP,
|
||||||
|
osuScoreAlias1.LEADERBOARD_RANK,
|
||||||
osuScoreAlias2.REPLAY_ID,
|
osuScoreAlias2.REPLAY_ID,
|
||||||
osuScoreAlias2.USER_ID,
|
osuScoreAlias2.USER_ID,
|
||||||
osuUserAlias2.USERNAME,
|
osuUserAlias2.USERNAME,
|
||||||
|
osuUserAlias2.IS_BANNED,
|
||||||
osuScoreAlias2.DATE,
|
osuScoreAlias2.DATE,
|
||||||
osuScoreAlias2.PP,
|
osuScoreAlias2.PP,
|
||||||
|
osuScoreAlias2.LEADERBOARD_RANK,
|
||||||
BEATMAPS.BEATMAP_ID,
|
BEATMAPS.BEATMAP_ID,
|
||||||
BEATMAPS.TITLE,
|
BEATMAPS.TITLE,
|
||||||
BEATMAPS.STAR_RATING,
|
BEATMAPS.STAR_RATING,
|
||||||
@ -344,6 +379,10 @@ class ScoreService(
|
|||||||
and(osuScoreAlias2.IS_BANNED.eq(false))
|
and(osuScoreAlias2.IS_BANNED.eq(false))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Globally skip maps that are known to have false positives (eg. high CS)
|
||||||
|
.and(SCORES_SIMILARITY.BEATMAP_ID.notIn(*SKIPPED_SIMILARITY_MAPS))
|
||||||
|
// Skip maps that have high CS values (smaller circles mean the replays will be naturally similar)
|
||||||
|
.and(BEATMAPS.CS.lt(8.0))
|
||||||
.and(condition)
|
.and(condition)
|
||||||
.orderBy(osuScoreAlias2.DATE.desc(), SCORES_SIMILARITY.SIMILARITY.asc())
|
.orderBy(osuScoreAlias2.DATE.desc(), SCORES_SIMILARITY.SIMILARITY.asc())
|
||||||
.fetch()
|
.fetch()
|
||||||
@ -363,6 +402,8 @@ class ScoreService(
|
|||||||
fun getSimilarReplays(condition: Condition = DSL.noCondition()): List<SimilarReplayEntry> {
|
fun getSimilarReplays(condition: Condition = DSL.noCondition()): List<SimilarReplayEntry> {
|
||||||
val replays = getSimilarReplaysRecords(condition)
|
val replays = getSimilarReplaysRecords(condition)
|
||||||
return mapSimilarReplays(replays)
|
return mapSimilarReplays(replays)
|
||||||
|
// Filter scores where the imports have been out of order and the stolen replay's user has been banned
|
||||||
|
.filter { !it.user_banned_2 }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun mapSimilarReplays(replays: List<Record>) = replays.map {
|
private fun mapSimilarReplays(replays: List<Record>) = replays.map {
|
||||||
@ -373,6 +414,9 @@ class ScoreService(
|
|||||||
var userId1 = it.get(osuScoreAlias1.USER_ID, Long::class.java)
|
var userId1 = it.get(osuScoreAlias1.USER_ID, Long::class.java)
|
||||||
var userId2 = it.get(osuScoreAlias2.USER_ID, Long::class.java)
|
var userId2 = it.get(osuScoreAlias2.USER_ID, Long::class.java)
|
||||||
|
|
||||||
|
var userBanned1 = it.get(osuUserAlias1.IS_BANNED, Boolean::class.java)
|
||||||
|
var userBanned2 = it.get(osuUserAlias2.IS_BANNED, Boolean::class.java)
|
||||||
|
|
||||||
var username1 = it.get(osuUserAlias1.USERNAME, String::class.java)
|
var username1 = it.get(osuUserAlias1.USERNAME, String::class.java)
|
||||||
var username2 = it.get(osuUserAlias2.USERNAME, String::class.java)
|
var username2 = it.get(osuUserAlias2.USERNAME, String::class.java)
|
||||||
|
|
||||||
@ -382,6 +426,9 @@ class ScoreService(
|
|||||||
var replayPp1 = it.get(osuScoreAlias1.PP, Double::class.java)
|
var replayPp1 = it.get(osuScoreAlias1.PP, Double::class.java)
|
||||||
var replayPp2 = it.get(osuScoreAlias2.PP, Double::class.java)
|
var replayPp2 = it.get(osuScoreAlias2.PP, Double::class.java)
|
||||||
|
|
||||||
|
var replayLeaderboardRank1 = it.get(osuScoreAlias1.LEADERBOARD_RANK, Long::class.java)
|
||||||
|
var replayLeaderboardRank2 = it.get(osuScoreAlias2.LEADERBOARD_RANK, Long::class.java)
|
||||||
|
|
||||||
// Swap logic if replayDate1 is after replayDate2
|
// Swap logic if replayDate1 is after replayDate2
|
||||||
if (replayDate1.isAfter(replayDate2)) {
|
if (replayDate1.isAfter(replayDate2)) {
|
||||||
val tempReplayId = replayId1
|
val tempReplayId = replayId1
|
||||||
@ -392,6 +439,10 @@ class ScoreService(
|
|||||||
userId1 = userId2
|
userId1 = userId2
|
||||||
userId2 = tempUserId
|
userId2 = tempUserId
|
||||||
|
|
||||||
|
val tempUserBanned = userBanned1
|
||||||
|
userBanned1 = userBanned2
|
||||||
|
userBanned2 = tempUserBanned
|
||||||
|
|
||||||
val tempUsername = username1
|
val tempUsername = username1
|
||||||
username1 = username2
|
username1 = username2
|
||||||
username2 = tempUsername
|
username2 = tempUsername
|
||||||
@ -403,6 +454,10 @@ class ScoreService(
|
|||||||
val tempReplayPp = replayPp1
|
val tempReplayPp = replayPp1
|
||||||
replayPp1 = replayPp2
|
replayPp1 = replayPp2
|
||||||
replayPp2 = tempReplayPp
|
replayPp2 = tempReplayPp
|
||||||
|
|
||||||
|
val tempLeaderboardRank = replayLeaderboardRank1
|
||||||
|
replayLeaderboardRank1 = replayLeaderboardRank2
|
||||||
|
replayLeaderboardRank2 = tempLeaderboardRank
|
||||||
}
|
}
|
||||||
|
|
||||||
SimilarReplayEntry(
|
SimilarReplayEntry(
|
||||||
@ -410,6 +465,8 @@ class ScoreService(
|
|||||||
replay_id_2 = replayId2,
|
replay_id_2 = replayId2,
|
||||||
user_id_1 = userId1,
|
user_id_1 = userId1,
|
||||||
user_id_2 = userId2,
|
user_id_2 = userId2,
|
||||||
|
user_banned_1 = userBanned1,
|
||||||
|
user_banned_2 = userBanned2,
|
||||||
username_1 = username1,
|
username_1 = username1,
|
||||||
username_2 = username2,
|
username_2 = username2,
|
||||||
beatmap_beatmapset_id = it.get(BEATMAPS.BEATMAPSET_ID, Long::class.java),
|
beatmap_beatmapset_id = it.get(BEATMAPS.BEATMAPSET_ID, Long::class.java),
|
||||||
@ -417,10 +474,12 @@ class ScoreService(
|
|||||||
replay_date_2 = Format.formatLocalDateTime(replayDate2),
|
replay_date_2 = Format.formatLocalDateTime(replayDate2),
|
||||||
replay_pp_1 = replayPp1,
|
replay_pp_1 = replayPp1,
|
||||||
replay_pp_2 = replayPp2,
|
replay_pp_2 = replayPp2,
|
||||||
|
replay_leaderboard_rank_1 = replayLeaderboardRank1,
|
||||||
|
replay_leaderboard_rank_2 = replayLeaderboardRank2,
|
||||||
beatmap_id = it.get(BEATMAPS.BEATMAP_ID, Long::class.java),
|
beatmap_id = it.get(BEATMAPS.BEATMAP_ID, Long::class.java),
|
||||||
beatmap_title = it.get(BEATMAPS.TITLE, String::class.java),
|
beatmap_title = it.get(BEATMAPS.TITLE, String::class.java),
|
||||||
beatmap_star_rating = it.get(BEATMAPS.STAR_RATING, Double::class.java),
|
beatmap_star_rating = it.get(BEATMAPS.STAR_RATING, Double::class.java),
|
||||||
similarity = it.get(SCORES_SIMILARITY.SIMILARITY, Double::class.java)
|
similarity = it.get(SCORES_SIMILARITY.SIMILARITY, Double::class.java),
|
||||||
)
|
)
|
||||||
}.distinctBy {
|
}.distinctBy {
|
||||||
val (smallerId, largerId) = listOf(it.replay_id_1, it.replay_id_2).sorted()
|
val (smallerId, largerId) = listOf(it.replay_id_1, it.replay_id_2).sorted()
|
||||||
|
|||||||
@ -65,7 +65,7 @@ class UserScoreService(
|
|||||||
USER_SCORES.ERROR_SKEWNESS,
|
USER_SCORES.ERROR_SKEWNESS,
|
||||||
USER_SCORES.SLIDEREND_RELEASE_TIMES,
|
USER_SCORES.SLIDEREND_RELEASE_TIMES,
|
||||||
USER_SCORES.KEYPRESSES_TIMES,
|
USER_SCORES.KEYPRESSES_TIMES,
|
||||||
USER_SCORES.JUDGEMENTS
|
USER_SCORES.JUDGEMENTS,
|
||||||
)
|
)
|
||||||
.from(USER_SCORES)
|
.from(USER_SCORES)
|
||||||
.join(BEATMAPS).on(USER_SCORES.BEATMAP_ID.eq(BEATMAPS.BEATMAP_ID))
|
.join(BEATMAPS).on(USER_SCORES.BEATMAP_ID.eq(BEATMAPS.BEATMAP_ID))
|
||||||
@ -127,7 +127,8 @@ class UserScoreService(
|
|||||||
date = null,
|
date = null,
|
||||||
pp = null,
|
pp = null,
|
||||||
rank = null,
|
rank = null,
|
||||||
user_id = null
|
user_id = null,
|
||||||
|
leaderboard_rank = null,
|
||||||
)
|
)
|
||||||
this.scoreService.loadComparableReplayData(replayData)
|
this.scoreService.loadComparableReplayData(replayData)
|
||||||
return replayData
|
return replayData
|
||||||
|
|||||||
@ -61,7 +61,8 @@ class CircleguardService {
|
|||||||
val sliderend_release_standard_deviation: Double?,
|
val sliderend_release_standard_deviation: Double?,
|
||||||
val sliderend_release_standard_deviation_adjusted: Double?,
|
val sliderend_release_standard_deviation_adjusted: Double?,
|
||||||
|
|
||||||
val judgements: List<Judgement>
|
val judgements: List<Judgement>,
|
||||||
|
val hit_count: Int?,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun postProcessReplay(replayResponse: ReplayResponse, mods: Int = 0) {
|
fun postProcessReplay(replayResponse: ReplayResponse, mods: Int = 0) {
|
||||||
|
|||||||
@ -18,7 +18,7 @@ import java.util.*
|
|||||||
@Service
|
@Service
|
||||||
class MetabaseService : InitializingBean {
|
class MetabaseService : InitializingBean {
|
||||||
|
|
||||||
@Value("\${METABASE_API_KEY}")
|
@Value("\${METABASE_API_KEY:nil}")
|
||||||
private lateinit var metabaseApiKey: String
|
private lateinit var metabaseApiKey: String
|
||||||
|
|
||||||
@Value("\${METABASE_URL:https://neko.nise.moe}")
|
@Value("\${METABASE_URL:https://neko.nise.moe}")
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import org.slf4j.LoggerFactory
|
|||||||
import org.springframework.beans.factory.InitializingBean
|
import org.springframework.beans.factory.InitializingBean
|
||||||
import org.springframework.beans.factory.annotation.Value
|
import org.springframework.beans.factory.annotation.Value
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
|
import java.io.IOException
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
import java.net.http.HttpClient
|
import java.net.http.HttpClient
|
||||||
@ -241,6 +242,22 @@ class OsuApi(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getUserBeatmapScore(userId: Long, beatmapId: Int, scoreId: Long? = null): OsuApiModels.UserScore? {
|
||||||
|
val response = doRequest("https://osu.ppy.sh/api/v2/beatmaps/$beatmapId/scores/users/$userId", emptyMap())
|
||||||
|
|
||||||
|
if (response == null) {
|
||||||
|
this.logger.info("Error getting score on beatmap $beatmapId for user $userId")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val score = when (response.statusCode()) {
|
||||||
|
200 -> serializer.decodeFromString<OsuApiModels.UserScore>(response.body())
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (scoreId == null || score?.score?.id == scoreId) score else null
|
||||||
|
}
|
||||||
|
|
||||||
fun searchBeatmapsets(cursor: OsuApiModels.BeatmapsetSearchResultCursor?): OsuApiModels.BeatmapsetSearchResult? {
|
fun searchBeatmapsets(cursor: OsuApiModels.BeatmapsetSearchResultCursor?): OsuApiModels.BeatmapsetSearchResult? {
|
||||||
val queryParams = mutableMapOf(
|
val queryParams = mutableMapOf(
|
||||||
"s" to "ranked", // Status [only ranked]
|
"s" to "ranked", // Status [only ranked]
|
||||||
@ -346,7 +363,12 @@ class OsuApi(
|
|||||||
val waitTimes = listOf(15L, 30L, 60L)
|
val waitTimes = listOf(15L, 30L, 60L)
|
||||||
|
|
||||||
for (waitTime in waitTimes) {
|
for (waitTime in waitTimes) {
|
||||||
val response = httpClient.send(request, HttpResponse.BodyHandlers.ofString())
|
val response = try {
|
||||||
|
httpClient.send(request, HttpResponse.BodyHandlers.ofString())
|
||||||
|
} catch (ex: IOException) {
|
||||||
|
// Some transport level exception might be thrown, continue with the retry backoff and see if it fixes itself
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
this.logger.debug("Request: {}", request.uri())
|
this.logger.debug("Request: {}", request.uri())
|
||||||
this.logger.debug("Result: {}", response.statusCode())
|
this.logger.debug("Result: {}", response.statusCode())
|
||||||
|
|||||||
@ -140,6 +140,12 @@ class OsuApiModels {
|
|||||||
val scores: List<Score>
|
val scores: List<Score>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class UserScore(
|
||||||
|
val score: Score,
|
||||||
|
val position: Long,
|
||||||
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
enum class Grade {
|
enum class Grade {
|
||||||
@SerialName("XH")
|
@SerialName("XH")
|
||||||
|
|||||||
283
nise-backend/src/main/kotlin/com/nisemoe/nise/replays/Wtc.kt
Normal file
283
nise-backend/src/main/kotlin/com/nisemoe/nise/replays/Wtc.kt
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
package com.nisemoe.nise.replays
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.ByteOrder
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.round
|
||||||
|
|
||||||
|
// JVM implementation of https://github.com/circleguard/wtc-lzma-compressor/tree/master
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class WTC {
|
||||||
|
companion object {
|
||||||
|
private const val CURRENT_VERSION_HEADER: Short = 1
|
||||||
|
|
||||||
|
private val VERSION_HEADER_BYTE_ARRAY = byteArrayOf((CURRENT_VERSION_HEADER.toInt() and 0xFF).toByte(), ((CURRENT_VERSION_HEADER.toInt() shr 8) and 0xFF).toByte())
|
||||||
|
|
||||||
|
fun compress(stream: String): ByteArray {
|
||||||
|
val lists = separate(stream)
|
||||||
|
|
||||||
|
val xs = unsortedDiffPackShortsToBytes(lists.x)
|
||||||
|
val ys = unsortedDiffPackShortsToBytes(lists.y)
|
||||||
|
|
||||||
|
val ws = packIntsToBytes(lists.w)
|
||||||
|
val zs = lists.z
|
||||||
|
|
||||||
|
fun packBytes(arr: ByteArray): ByteArray {
|
||||||
|
val length = arr.size
|
||||||
|
val buffer = ByteBuffer.allocate(4 + length).order(ByteOrder.LITTLE_ENDIAN)
|
||||||
|
|
||||||
|
buffer.putInt(length)
|
||||||
|
for (byte in arr) {
|
||||||
|
buffer.put(byte)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer.array()
|
||||||
|
}
|
||||||
|
|
||||||
|
val byteStream = ByteArrayOutputStream()
|
||||||
|
|
||||||
|
byteStream.writeBytes(VERSION_HEADER_BYTE_ARRAY)
|
||||||
|
byteStream.writeBytes(packBytes(xs))
|
||||||
|
byteStream.writeBytes(packBytes(ys))
|
||||||
|
byteStream.writeBytes(packBytes(zs))
|
||||||
|
byteStream.writeBytes(packBytes(ws))
|
||||||
|
|
||||||
|
return byteStream.toByteArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decompress(data: ByteArray, hasVersionHeader: Boolean = true): String {
|
||||||
|
val buffer = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN)
|
||||||
|
|
||||||
|
fun unpackBytes(): ByteArray {
|
||||||
|
val size = buffer.getInt()
|
||||||
|
|
||||||
|
val bytes = ByteArray(size)
|
||||||
|
buffer.get(bytes, 0, size)
|
||||||
|
|
||||||
|
return bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasVersionHeader) {
|
||||||
|
buffer.getShort() // Version - may be used in the future
|
||||||
|
}
|
||||||
|
|
||||||
|
val xs = unpackBytes()
|
||||||
|
val ys = unpackBytes()
|
||||||
|
val zs = unpackBytes()
|
||||||
|
val ws = unpackBytes()
|
||||||
|
|
||||||
|
val xxs = unsortedDiffUnpackBytesToShorts(xs)
|
||||||
|
val yys = unsortedDiffUnpackBytesToShorts(ys)
|
||||||
|
|
||||||
|
val wws = unpackBytesToInts(ws)
|
||||||
|
|
||||||
|
return combine(FrameLists(
|
||||||
|
x = xxs,
|
||||||
|
y = yys,
|
||||||
|
z = zs,
|
||||||
|
w = wws,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
data class FrameLists(
|
||||||
|
val x: ShortArray,
|
||||||
|
val y: ShortArray,
|
||||||
|
val z: ByteArray,
|
||||||
|
val w: IntArray,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun unsortedDiffPackShortsToBytes(shorts: ShortArray): ByteArray {
|
||||||
|
val start = shorts.first()
|
||||||
|
val diff = arrayDiff(shorts)
|
||||||
|
val packed = mutableListOf<Byte>()
|
||||||
|
|
||||||
|
fun pack(word: Short) {
|
||||||
|
if (abs(word.toInt()) <= Byte.MAX_VALUE) {
|
||||||
|
packed.add(word.toByte())
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
packed.add(Byte.MIN_VALUE)
|
||||||
|
packed.add((word.toInt() and 0xFF).toByte())
|
||||||
|
packed.add((word.toInt() shr 8).toByte())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pack(start)
|
||||||
|
for (word in diff) {
|
||||||
|
pack(word)
|
||||||
|
}
|
||||||
|
|
||||||
|
return packed.toByteArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun unsortedDiffUnpackBytesToShorts(int8s: ByteArray): ShortArray {
|
||||||
|
val decoded = mutableListOf<Short>()
|
||||||
|
|
||||||
|
var i = 0
|
||||||
|
while (i < int8s.size) {
|
||||||
|
val byte = int8s[i]
|
||||||
|
|
||||||
|
if (byte == Byte.MIN_VALUE) {
|
||||||
|
i++
|
||||||
|
var word = int8s[i].toInt() and 0xFF
|
||||||
|
i++
|
||||||
|
word += int8s[i].toInt() shl 8
|
||||||
|
decoded.add(word.toShort())
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
decoded.add(byte.toShort())
|
||||||
|
}
|
||||||
|
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
return cumSum(decoded.toShortArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun packIntsToBytes(int32s: IntArray): ByteArray {
|
||||||
|
val packed = mutableListOf<Byte>()
|
||||||
|
|
||||||
|
for (dw in int32s) {
|
||||||
|
var dword = dw
|
||||||
|
if (abs(dword) <= Byte.MAX_VALUE) {
|
||||||
|
packed.add(dword.toByte())
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
packed.add(Byte.MIN_VALUE)
|
||||||
|
packed.add((dword and 0xFF).toByte())
|
||||||
|
dword = dword shr 8
|
||||||
|
packed.add((dword and 0xFF).toByte())
|
||||||
|
dword = dword shr 8
|
||||||
|
packed.add((dword and 0xFF).toByte())
|
||||||
|
dword = dword shr 8
|
||||||
|
packed.add(dword.toByte())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return packed.toByteArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun unpackBytesToInts(int8s: ByteArray): IntArray {
|
||||||
|
val unpacked = mutableListOf<Int>()
|
||||||
|
|
||||||
|
var i = 0
|
||||||
|
while (i < int8s.size) {
|
||||||
|
val byte = int8s[i]
|
||||||
|
|
||||||
|
if (byte == Byte.MIN_VALUE) {
|
||||||
|
i++
|
||||||
|
var dword = int8s[i].toInt() and 0xFF
|
||||||
|
i++
|
||||||
|
dword += (int8s[i].toInt() shl 8) and 0xFF00
|
||||||
|
i++
|
||||||
|
dword += (int8s[i].toInt() shl 16) and 0xFF0000
|
||||||
|
i++
|
||||||
|
dword += int8s[i].toInt() shl 24
|
||||||
|
unpacked.add(dword)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
unpacked.add(byte.toInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
return unpacked.toIntArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun separate(stream: String): FrameLists {
|
||||||
|
val frames = stream.split(',')
|
||||||
|
val frameCount = frames.size
|
||||||
|
|
||||||
|
val ws = IntArray(frameCount)
|
||||||
|
val xs = ShortArray(frameCount)
|
||||||
|
val ys = ShortArray(frameCount)
|
||||||
|
val zs = ByteArray(frameCount)
|
||||||
|
|
||||||
|
for ((i, frame) in frames.withIndex()) {
|
||||||
|
if (frame.isEmpty()) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
val splitFrame = frame.split('|')
|
||||||
|
val w = splitFrame[0].toInt()
|
||||||
|
val x = splitFrame[1].toFloat()
|
||||||
|
val y = splitFrame[2].toFloat()
|
||||||
|
val z = splitFrame[3].toInt()
|
||||||
|
|
||||||
|
val zz = z and 0xFF
|
||||||
|
|
||||||
|
var xx = round(x * 16).toInt()
|
||||||
|
var yy = round(y * 16).toInt()
|
||||||
|
|
||||||
|
if (xx <= -0x8000) xx = -0x8000
|
||||||
|
else if (xx >= 0x7FFF) xx = 0x7FFF
|
||||||
|
|
||||||
|
if (yy <= -0x8000) yy = -0x8000
|
||||||
|
else if (yy >= 0x7FFF) yy = 0x7FFF
|
||||||
|
|
||||||
|
ws[i] = w
|
||||||
|
xs[i] = xx.toShort()
|
||||||
|
ys[i] = yy.toShort()
|
||||||
|
zs[i] = zz.toByte()
|
||||||
|
}
|
||||||
|
|
||||||
|
return FrameLists(
|
||||||
|
x = xs,
|
||||||
|
y = ys,
|
||||||
|
z = zs,
|
||||||
|
w = ws,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun combine(lists: FrameLists): String {
|
||||||
|
val xArr = lists.x.map { it.toFloat() / 16 }
|
||||||
|
|
||||||
|
val yArr = lists.y.map { it.toFloat() / 16 }
|
||||||
|
|
||||||
|
val frames = arrayOfNulls<String>(xArr.size)
|
||||||
|
|
||||||
|
for (i in xArr.indices) {
|
||||||
|
val x = xArr[i]
|
||||||
|
val y = yArr[i]
|
||||||
|
val z = lists.z[i]
|
||||||
|
val w = lists.w[i]
|
||||||
|
frames[i] = "$w|$x|$y|$z"
|
||||||
|
}
|
||||||
|
|
||||||
|
return frames.joinToString(",")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun arrayDiff(arr: ShortArray): ShortArray {
|
||||||
|
if (arr.isEmpty()) {
|
||||||
|
return emptyArray<Short>().toShortArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
val diffed = ShortArray(arr.size - 1)
|
||||||
|
|
||||||
|
for (index in 1..<arr.size) {
|
||||||
|
diffed[index - 1] = (arr[index] - arr[index - 1]).toShort()
|
||||||
|
}
|
||||||
|
|
||||||
|
return diffed
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cumSum(arr: ShortArray): ShortArray {
|
||||||
|
if (arr.isEmpty()) {
|
||||||
|
return emptyArray<Short>().toShortArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
val cumArr = ShortArray(arr.size)
|
||||||
|
|
||||||
|
cumArr[0] = arr.first()
|
||||||
|
for (index in 1..<arr.size) {
|
||||||
|
cumArr[index] = (arr[index] + cumArr[index - 1]).toShort()
|
||||||
|
}
|
||||||
|
|
||||||
|
return cumArr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -28,9 +28,9 @@ class RssService(
|
|||||||
items.subList(0, items.size.coerceAtMost(50))
|
items.subList(0, items.size.coerceAtMost(50))
|
||||||
|
|
||||||
val channel = Channel(
|
val channel = Channel(
|
||||||
title = "nise.moe's feed and sneed",
|
title = "nise.stedos.dev's feed and sneed",
|
||||||
link = "https://nise.moe/rss",
|
link = "https://nise.stedos.dev/rss",
|
||||||
description = "Feed of *sus* scores for osu!std - /nise.moe/",
|
description = "Feed of *sus* scores for osu!std - /nise.stedos.dev/",
|
||||||
lastBuildDate = Date().toInstant().atZone(ZoneOffset.UTC).format(DateTimeFormatter.RFC_1123_DATE_TIME),
|
lastBuildDate = Date().toInstant().atZone(ZoneOffset.UTC).format(DateTimeFormatter.RFC_1123_DATE_TIME),
|
||||||
item = items.map {
|
item = items.map {
|
||||||
Item(
|
Item(
|
||||||
@ -42,7 +42,7 @@ class RssService(
|
|||||||
)
|
)
|
||||||
|
|
||||||
},
|
},
|
||||||
atomLink = AtomLink(href = "https://nise.moe/rss.xml")
|
atomLink = AtomLink(href = "https://nise.stedos.dev/rss.xml")
|
||||||
)
|
)
|
||||||
|
|
||||||
return RssFeed(channel = channel)
|
return RssFeed(channel = channel)
|
||||||
@ -74,8 +74,8 @@ class RssService(
|
|||||||
if(score1.addedAt != null) {
|
if(score1.addedAt != null) {
|
||||||
val item = RssFeedController.IntermeriaryFeedItem(
|
val item = RssFeedController.IntermeriaryFeedItem(
|
||||||
title = "Possible stolen replay",
|
title = "Possible stolen replay",
|
||||||
guid = "https://nise.moe/p/${score1.replayId}/${score2.replayId}",
|
guid = "https://nise.stedos.dev/p/${score1.replayId}/${score2.replayId}",
|
||||||
link = "https://nise.moe/p/${score1.replayId}/${score2.replayId}",
|
link = "https://nise.stedos.dev/p/${score1.replayId}/${score2.replayId}",
|
||||||
description = "Similarity: ${result[SCORES_SIMILARITY.SIMILARITY]}%\n" +
|
description = "Similarity: ${result[SCORES_SIMILARITY.SIMILARITY]}%\n" +
|
||||||
"Replay by ${user1.username} on ${beatmap.artist} - ${beatmap.title} [${beatmap.version}] (${beatmap.starRating} stars)\n" +
|
"Replay by ${user1.username} on ${beatmap.artist} - ${beatmap.title} [${beatmap.version}] (${beatmap.starRating} stars)\n" +
|
||||||
"Replay by ${user2.username} on ${beatmap.artist} - ${beatmap.title} [${beatmap.version}] (${beatmap.starRating} stars)",
|
"Replay by ${user2.username} on ${beatmap.artist} - ${beatmap.title} [${beatmap.version}] (${beatmap.starRating} stars)",
|
||||||
@ -113,8 +113,8 @@ class RssService(
|
|||||||
if(score.addedAt != null) {
|
if(score.addedAt != null) {
|
||||||
val item = RssFeedController.IntermeriaryFeedItem(
|
val item = RssFeedController.IntermeriaryFeedItem(
|
||||||
title = "Suspicious score by ${user.username}",
|
title = "Suspicious score by ${user.username}",
|
||||||
guid = "https://nise.moe/s/${score.replayId}",
|
guid = "https://nise.stedos.dev/s/${score.replayId}",
|
||||||
link = "https://nise.moe/s/${score.replayId}",
|
link = "https://nise.stedos.dev/s/${score.replayId}",
|
||||||
description = "New score by ${user.username} on ${beatmap.artist} - ${beatmap.title} [${beatmap.version}] (${beatmap.starRating} stars)",
|
description = "New score by ${user.username} on ${beatmap.artist} - ${beatmap.title} [${beatmap.version}] (${beatmap.starRating} stars)",
|
||||||
pubDate = score.addedAt!!
|
pubDate = score.addedAt!!
|
||||||
)
|
)
|
||||||
|
|||||||
@ -13,7 +13,6 @@ import org.slf4j.LoggerFactory
|
|||||||
import org.springframework.scheduling.annotation.Scheduled
|
import org.springframework.scheduling.annotation.Scheduled
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import org.springframework.util.StopWatch
|
import org.springframework.util.StopWatch
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
class GlobalCache(
|
class GlobalCache(
|
||||||
@ -34,7 +33,7 @@ class GlobalCache(
|
|||||||
fun updateCaches() {
|
fun updateCaches() {
|
||||||
val stopwatch = StopWatch()
|
val stopwatch = StopWatch()
|
||||||
stopwatch.start()
|
stopwatch.start()
|
||||||
logger.info("Updating the cache!")
|
logger.info("Updating the cache...")
|
||||||
|
|
||||||
runBlocking {
|
runBlocking {
|
||||||
val rssFeedDeferred = async { rssService.generateFeed() }
|
val rssFeedDeferred = async { rssService.generateFeed() }
|
||||||
@ -50,6 +49,8 @@ class GlobalCache(
|
|||||||
|
|
||||||
stopwatch.stop()
|
stopwatch.stop()
|
||||||
logger.info("Cache updated in {} seconds", String.format("%.2f", stopwatch.totalTimeSeconds))
|
logger.info("Cache updated in {} seconds", String.format("%.2f", stopwatch.totalTimeSeconds))
|
||||||
|
logger.info("[CACHE]: Similar replays count: {}", similarReplays?.size)
|
||||||
|
logger.info("[CACHE]: Suspicious scores count: {}", suspiciousScores?.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -293,7 +293,7 @@ class ImportScores(
|
|||||||
dslContext.update(BEATMAPS)
|
dslContext.update(BEATMAPS)
|
||||||
.set(BEATMAPS.BEATMAP_HASH, topScore.beatmap.checksum)
|
.set(BEATMAPS.BEATMAP_HASH, topScore.beatmap.checksum)
|
||||||
.set(BEATMAPS.STAR_RATING, topScore.beatmap.difficulty_rating)
|
.set(BEATMAPS.STAR_RATING, topScore.beatmap.difficulty_rating)
|
||||||
.set(BEATMAPS.VERSION, beatmap.version)
|
.set(BEATMAPS.VERSION, topScore.beatmap.version)
|
||||||
.set(BEATMAPS.ARTIST, topScore.beatmapset!!.artist)
|
.set(BEATMAPS.ARTIST, topScore.beatmapset!!.artist)
|
||||||
.set(BEATMAPS.SOURCE, topScore.beatmapset.source)
|
.set(BEATMAPS.SOURCE, topScore.beatmapset.source)
|
||||||
.set(BEATMAPS.TITLE, topScore.beatmapset.title)
|
.set(BEATMAPS.TITLE, topScore.beatmapset.title)
|
||||||
@ -727,6 +727,8 @@ class ImportScores(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val topScore = osuApi.getUserBeatmapScore(score.user_id, beatmapId, score.best_id)
|
||||||
|
|
||||||
dslContext.insertInto(SCORES)
|
dslContext.insertInto(SCORES)
|
||||||
.set(SCORES.BEATMAP_ID, beatmapId)
|
.set(SCORES.BEATMAP_ID, beatmapId)
|
||||||
.set(SCORES.COUNT_300, score.statistics.count_300)
|
.set(SCORES.COUNT_300, score.statistics.count_300)
|
||||||
@ -745,6 +747,7 @@ class ImportScores(
|
|||||||
.set(SCORES.REPLAY_ID, score.best_id)
|
.set(SCORES.REPLAY_ID, score.best_id)
|
||||||
.set(SCORES.USER_ID, score.user_id)
|
.set(SCORES.USER_ID, score.user_id)
|
||||||
.set(SCORES.VERSION, CURRENT_VERSION)
|
.set(SCORES.VERSION, CURRENT_VERSION)
|
||||||
|
.set(SCORES.LEADERBOARD_RANK, topScore?.position)
|
||||||
.execute()
|
.execute()
|
||||||
|
|
||||||
this.statistics.scoresAddedToDatabase++
|
this.statistics.scoresAddedToDatabase++
|
||||||
@ -798,6 +801,12 @@ class ImportScores(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the score has a low amount of hits the UR calculation will be inaccurate, skip these plays
|
||||||
|
if (processedReplay.hit_count == null || processedReplay.hit_count < 10) {
|
||||||
|
this.logger.warn("Processed play has less than 10 hits, skipping score ${score.id}")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val compressedReplay = CompressReplay.compressReplay(scoreReplay.content.toByteArray())
|
val compressedReplay = CompressReplay.compressReplay(scoreReplay.content.toByteArray())
|
||||||
|
|
||||||
val scoreId = dslContext.update(SCORES)
|
val scoreId = dslContext.update(SCORES)
|
||||||
|
|||||||
@ -78,6 +78,7 @@ class ScoreSearchController(
|
|||||||
val perfect: Boolean?,
|
val perfect: Boolean?,
|
||||||
val pp: Double?,
|
val pp: Double?,
|
||||||
val rank: String?,
|
val rank: String?,
|
||||||
|
val leaderboard_rank: Long?,
|
||||||
val replay_id: Long?,
|
val replay_id: Long?,
|
||||||
val score: Long?,
|
val score: Long?,
|
||||||
val ur: Double?,
|
val ur: Double?,
|
||||||
|
|||||||
@ -51,6 +51,7 @@ class ScoreSearchSchemaController(
|
|||||||
InternalSchemaField("perfect", "Perfect", Category.score, Type.boolean, false, "if score is a full combo", databaseField = SCORES.PERFECT),
|
InternalSchemaField("perfect", "Perfect", Category.score, Type.boolean, false, "if score is a full combo", databaseField = SCORES.PERFECT),
|
||||||
InternalSchemaField("pp", "Score PP", Category.score, Type.number, true, "performance points for score", databaseField = SCORES.PP),
|
InternalSchemaField("pp", "Score PP", Category.score, Type.number, true, "performance points for score", databaseField = SCORES.PP),
|
||||||
InternalSchemaField("rank", "Rank", Category.score, Type.grade, false, "score grade", databaseField = SCORES.RANK),
|
InternalSchemaField("rank", "Rank", Category.score, Type.grade, false, "score grade", databaseField = SCORES.RANK),
|
||||||
|
InternalSchemaField("leaderboard_rank", "Leaderboard Rank", Category.score, Type.number, false, "leaderboard position of the play at import", databaseField = SCORES.LEADERBOARD_RANK),
|
||||||
InternalSchemaField("replay_id", "Replay ID", Category.score, Type.number, false, "identifier for replay", databaseField = SCORES.REPLAY_ID),
|
InternalSchemaField("replay_id", "Replay ID", Category.score, Type.number, false, "identifier for replay", databaseField = SCORES.REPLAY_ID),
|
||||||
InternalSchemaField("score", "Score", Category.score, Type.number, false, "score value", databaseField = SCORES.SCORE),
|
InternalSchemaField("score", "Score", Category.score, Type.number, false, "score value", databaseField = SCORES.SCORE),
|
||||||
InternalSchemaField("ur", "UR", Category.metrics, Type.number, false, "unstable rate", databaseField = SCORES.UR),
|
InternalSchemaField("ur", "UR", Category.metrics, Type.number, false, "unstable rate", databaseField = SCORES.UR),
|
||||||
|
|||||||
@ -83,6 +83,7 @@ class ScoreSearchService(
|
|||||||
SCORES.ERROR_COEFFICIENT_OF_VARIATION,
|
SCORES.ERROR_COEFFICIENT_OF_VARIATION,
|
||||||
SCORES.ERROR_KURTOSIS,
|
SCORES.ERROR_KURTOSIS,
|
||||||
SCORES.ERROR_SKEWNESS,
|
SCORES.ERROR_SKEWNESS,
|
||||||
|
SCORES.LEADERBOARD_RANK,
|
||||||
|
|
||||||
// Beatmaps fields
|
// Beatmaps fields
|
||||||
BEATMAPS.ARTIST,
|
BEATMAPS.ARTIST,
|
||||||
@ -186,6 +187,7 @@ class ScoreSearchService(
|
|||||||
perfect = it.get(SCORES.PERFECT),
|
perfect = it.get(SCORES.PERFECT),
|
||||||
pp = it.get(SCORES.PP)?.roundToInt()?.toDouble(),
|
pp = it.get(SCORES.PP)?.roundToInt()?.toDouble(),
|
||||||
rank = it.get(SCORES.RANK),
|
rank = it.get(SCORES.RANK),
|
||||||
|
leaderboard_rank = it.get(SCORES.LEADERBOARD_RANK),
|
||||||
replay_id = it.get(SCORES.REPLAY_ID),
|
replay_id = it.get(SCORES.REPLAY_ID),
|
||||||
score = it.get(SCORES.SCORE),
|
score = it.get(SCORES.SCORE),
|
||||||
ur = it.get(SCORES.UR),
|
ur = it.get(SCORES.UR),
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE public.scores ADD COLUMN leaderboard_rank BIGINT;
|
||||||
43
nise-backend/src/test/kotlin/com/nisemoe/nise/osu/WtcTest.kt
Normal file
43
nise-backend/src/test/kotlin/com/nisemoe/nise/osu/WtcTest.kt
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package com.nisemoe.nise.osu
|
||||||
|
|
||||||
|
import com.nisemoe.nise.replays.WTC
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest
|
||||||
|
import org.junit.jupiter.params.provider.ValueSource
|
||||||
|
import org.junit.jupiter.api.Assertions
|
||||||
|
import org.testcontainers.shaded.org.bouncycastle.util.Arrays
|
||||||
|
import java.nio.file.Paths
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class WtcTest {
|
||||||
|
private val resourcesPath = Paths.get("src", "test", "resources", "wtc")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares the bytes produced from the `wtcCompress()` function with bytes produced from the Python implementation of WTC.
|
||||||
|
*/
|
||||||
|
@ParameterizedTest
|
||||||
|
@ValueSource(strings = ["replay_1", "replay_2", "replay_3", "replay_4"])
|
||||||
|
fun wtcCompressReturnsCorrectBytes(replayName: String) {
|
||||||
|
val expected = resourcesPath.resolve("${replayName}_compressed.dat").toFile().readBytes()
|
||||||
|
val replayEvents = resourcesPath.resolve("${replayName}_events.txt").toFile().readText()
|
||||||
|
|
||||||
|
val wtcCompressed = WTC.compress(replayEvents)
|
||||||
|
|
||||||
|
// We include a version header at the start of the compressed byte array - create a new array excluding these
|
||||||
|
// so we can compare just the raw data with the Python WTC implementation's output.
|
||||||
|
// (remove the first two bytes since the version header is a Short)
|
||||||
|
val wtcCompressedWithoutVersionHeader = Arrays.copyOfRange(wtcCompressed, 2, wtcCompressed.size)
|
||||||
|
|
||||||
|
Assertions.assertArrayEquals(expected, wtcCompressedWithoutVersionHeader)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@ValueSource(strings = ["replay_1", "replay_2", "replay_3", "replay_4"])
|
||||||
|
fun wtcDecompressReturnsCorrectReplayFrames(replayName: String) {
|
||||||
|
val expected = resourcesPath.resolve("${replayName}_decompressed.txt").toFile().readText()
|
||||||
|
val compressedReplay = resourcesPath.resolve("${replayName}_compressed.dat").toFile().readBytes()
|
||||||
|
|
||||||
|
val wtcDecompressed = WTC.decompress(compressedReplay, false)
|
||||||
|
|
||||||
|
assertEquals(expected, wtcDecompressed)
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
nise-backend/src/test/resources/wtc/replay_1_compressed.dat
Normal file
BIN
nise-backend/src/test/resources/wtc/replay_1_compressed.dat
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
1
nise-backend/src/test/resources/wtc/replay_1_events.txt
Normal file
1
nise-backend/src/test/resources/wtc/replay_1_events.txt
Normal file
File diff suppressed because one or more lines are too long
BIN
nise-backend/src/test/resources/wtc/replay_2_compressed.dat
Normal file
BIN
nise-backend/src/test/resources/wtc/replay_2_compressed.dat
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
1
nise-backend/src/test/resources/wtc/replay_2_events.txt
Normal file
1
nise-backend/src/test/resources/wtc/replay_2_events.txt
Normal file
File diff suppressed because one or more lines are too long
BIN
nise-backend/src/test/resources/wtc/replay_3_compressed.dat
Normal file
BIN
nise-backend/src/test/resources/wtc/replay_3_compressed.dat
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
1
nise-backend/src/test/resources/wtc/replay_3_events.txt
Normal file
1
nise-backend/src/test/resources/wtc/replay_3_events.txt
Normal file
File diff suppressed because one or more lines are too long
BIN
nise-backend/src/test/resources/wtc/replay_4_compressed.dat
Normal file
BIN
nise-backend/src/test/resources/wtc/replay_4_compressed.dat
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
1
nise-backend/src/test/resources/wtc/replay_4_events.txt
Normal file
1
nise-backend/src/test/resources/wtc/replay_4_events.txt
Normal file
File diff suppressed because one or more lines are too long
@ -102,6 +102,7 @@ class ReplayResponse:
|
|||||||
sliderend_release_standard_deviation_adjusted: float
|
sliderend_release_standard_deviation_adjusted: float
|
||||||
|
|
||||||
judgements: List[Hit]
|
judgements: List[Hit]
|
||||||
|
hit_count: int
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
d = asdict(self)
|
d = asdict(self)
|
||||||
@ -217,7 +218,8 @@ async def process_replay(request: Request):
|
|||||||
sliderend_release_standard_deviation=np.std(se, ddof=1),
|
sliderend_release_standard_deviation=np.std(se, ddof=1),
|
||||||
sliderend_release_standard_deviation_adjusted=np.std(my_filter_outliers(se), ddof=1),
|
sliderend_release_standard_deviation_adjusted=np.std(my_filter_outliers(se), ddof=1),
|
||||||
|
|
||||||
judgements=judgements
|
judgements=judgements,
|
||||||
|
hit_count=len(hits)
|
||||||
)
|
)
|
||||||
return json(ur_response.to_dict())
|
return json(ur_response.to_dict())
|
||||||
|
|
||||||
|
|||||||
@ -34,5 +34,5 @@
|
|||||||
|
|
||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
<div class="text-center version">
|
<div class="text-center version">
|
||||||
v20250213
|
v20250622
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -97,9 +97,10 @@ export interface ReplayData {
|
|||||||
snaps: number;
|
snaps: number;
|
||||||
hits: number;
|
hits: number;
|
||||||
|
|
||||||
pp: number,
|
pp: number;
|
||||||
perfect: boolean;
|
perfect: boolean;
|
||||||
max_combo: number,
|
max_combo: number;
|
||||||
|
leaderboard_rank?: number;
|
||||||
|
|
||||||
mean_error?: number,
|
mean_error?: number,
|
||||||
error_variance?: number,
|
error_variance?: number,
|
||||||
@ -156,6 +157,7 @@ export interface SuspiciousScore {
|
|||||||
pp: number;
|
pp: number;
|
||||||
frametime: number;
|
frametime: number;
|
||||||
ur: number;
|
ur: number;
|
||||||
|
leaderboard_rank: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SimilarReplay {
|
export interface SimilarReplay {
|
||||||
@ -168,6 +170,8 @@ export interface SimilarReplay {
|
|||||||
replay_date_2: string;
|
replay_date_2: string;
|
||||||
replay_pp_1: number;
|
replay_pp_1: number;
|
||||||
replay_pp_2: number;
|
replay_pp_2: number;
|
||||||
|
replay_leaderboard_rank_1: number;
|
||||||
|
replay_leaderboard_rank_2: number;
|
||||||
beatmap_id: number;
|
beatmap_id: number;
|
||||||
beatmap_title: string;
|
beatmap_title: string;
|
||||||
beatmap_star_rating: number;
|
beatmap_star_rating: number;
|
||||||
|
|||||||
@ -129,7 +129,7 @@
|
|||||||
<td *ngFor="let column of fields" [hidden]="!column.active" class="text-center" style="line-height: 32px">
|
<td *ngFor="let column of fields" [hidden]="!column.active" class="text-center" style="line-height: 32px">
|
||||||
<ng-container *ngIf="getValue(entry, column.name) !== null; else nullDisplay">
|
<ng-container *ngIf="getValue(entry, column.name) !== null; else nullDisplay">
|
||||||
<ng-container *ngIf="column.type == 'number'">
|
<ng-container *ngIf="column.type == 'number'">
|
||||||
{{ getValue(entry, column.name) | number }}
|
{{ isFieldId(column.name) ? getValue(entry, column.name) : (getValue(entry, column.name) | number) }}
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="column.type == 'flag'">
|
<ng-container *ngIf="column.type == 'flag'">
|
||||||
<span class="flag" [title]="getValue(entry, column.name)">{{ countryCodeToFlag(getValue(entry, column.name)) }}</span>
|
<span class="flag" [title]="getValue(entry, column.name)">{{ countryCodeToFlag(getValue(entry, column.name)) }}</span>
|
||||||
|
|||||||
@ -352,6 +352,8 @@ export class SearchComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isFieldId = (field: string): boolean => field === 'id' || field.includes("_id");
|
||||||
|
|
||||||
protected readonly countryCodeToFlag = countryCodeToFlag;
|
protected readonly countryCodeToFlag = countryCodeToFlag;
|
||||||
protected readonly Math = Math;
|
protected readonly Math = Math;
|
||||||
protected readonly formatDuration = formatDuration;
|
protected readonly formatDuration = formatDuration;
|
||||||
|
|||||||
@ -32,7 +32,7 @@ export class TextReportService {
|
|||||||
report += `\n\n${this.getStealingReport(similarReplay)}\n`;
|
report += `\n\n${this.getStealingReport(similarReplay)}\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
report += `\n\nGenerated on ${site} - [${userDetails.username} on ${site}](${environment.webUrl}/u/${userDetails.user_id})`;
|
report += `\n\nGenerated on ${site} - [${userDetails.username} profile](${environment.webUrl}/u/${userDetails.user_id})`;
|
||||||
|
|
||||||
return report;
|
return report;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
<ng-container *ngIf="this.isError">
|
<ng-container *ngIf="this.isError">
|
||||||
<div class="main term">
|
<div class="main term">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
An error occured. Maybe try again in a bit?
|
An error occurred. Maybe try again in a bit?
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
@ -29,9 +29,9 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- <div class="text-center mt-2">
|
<div class="text-center mt-2">
|
||||||
<a class="btn" [href]="'https://replay.nise.moe/' + this.pair.replays[0].replay_id + '/' + this.pair.replays[1].replay_id" target="_blank">Open in Replay Viewer</a>
|
<a class="btn" [href]="'https://nise.stedos.dev/replay-viewer/' + this.pair.replays[0].replay_id + '/' + this.pair.replays[1].replay_id" target="_blank">Open in Replay Viewer</a>
|
||||||
</div> -->
|
</div>
|
||||||
|
|
||||||
<div class="some-page-wrapper text-center">
|
<div class="some-page-wrapper text-center">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
<ng-container *ngIf="this.error">
|
<ng-container *ngIf="this.error">
|
||||||
<div class="main term">
|
<div class="main term">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
An error occured. Maybe try again in a bit?
|
An error occurred. Maybe try again in a bit?
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
@ -53,9 +53,9 @@
|
|||||||
Open in CircleGuard
|
Open in CircleGuard
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<!-- <a style="flex: 1" class="text-center" [href]="'https://replay.nise.moe/' + this.replayData.replay_id" target="_blank" [class.disabled]="!hasReplay()">
|
<a style="flex: 1" class="text-center" [href]="'https://nise.stedos.dev/replay-viewer/' + this.replayData.replay_id" target="_blank" [class.disabled]="!hasReplay()">
|
||||||
Open in Replay Viewer
|
Open in Replay Viewer
|
||||||
</a> -->
|
</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -136,6 +136,10 @@
|
|||||||
<span class="stat-label">PP</span>
|
<span class="stat-label">PP</span>
|
||||||
<span class="stat-value">{{ this.replayData.pp | number: '1.0-0' }}</span>
|
<span class="stat-value">{{ this.replayData.pp | number: '1.0-0' }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="stat" *ngIf="this.replayData.leaderboard_rank">
|
||||||
|
<span class="stat-label">Rank</span>
|
||||||
|
<span class="stat-value">{{ this.replayData.leaderboard_rank }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stats-container">
|
<div class="stats-container">
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
|
|||||||
@ -51,6 +51,20 @@
|
|||||||
<input class="form-control" type="text" id="searchBeatmap" [(ngModel)]="this.filterManager.filters.searchBeatmap" (input)="filterScores()"
|
<input class="form-control" type="text" id="searchBeatmap" [(ngModel)]="this.filterManager.filters.searchBeatmap" (input)="filterScores()"
|
||||||
[readOnly]="this.isUrlFilters" [disabled]="this.isUrlFilters">
|
[readOnly]="this.isUrlFilters" [disabled]="this.isUrlFilters">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<!-- Min Rank -->
|
||||||
|
<p>
|
||||||
|
<label for="minRank" class="form-label">Min Rank (of stolen)</label>
|
||||||
|
<input class="form-control" type="number" id="minRank" [(ngModel)]="this.filterManager.filters.minRank" (input)="filterScores()"
|
||||||
|
[readOnly]="this.isUrlFilters" [disabled]="this.isUrlFilters">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Max Rank -->
|
||||||
|
<p>
|
||||||
|
<label for="maxRank" class="form-label">Max Rank (of stolen)</label>
|
||||||
|
<input class="form-control" type="number" id="maxRank" [(ngModel)]="this.filterManager.filters.maxRank" (input)="filterScores()"
|
||||||
|
[readOnly]="this.isUrlFilters" [disabled]="this.isUrlFilters">
|
||||||
|
</p>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<div *ngIf="getTotalPages() > 1" style="padding: 20px">
|
<div *ngIf="getTotalPages() > 1" style="padding: 20px">
|
||||||
|
|||||||
@ -19,6 +19,9 @@ export interface FilterStolenReplays {
|
|||||||
|
|
||||||
minSimilarity?: number;
|
minSimilarity?: number;
|
||||||
maxSimilarity?: number;
|
maxSimilarity?: number;
|
||||||
|
|
||||||
|
minRank?: number;
|
||||||
|
maxRank?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -133,7 +136,12 @@ export class ViewSimilarReplaysComponent implements OnInit {
|
|||||||
const similarityMatch = (filters.minSimilarity !== undefined ? score.similarity >= filters.minSimilarity : true) &&
|
const similarityMatch = (filters.minSimilarity !== undefined ? score.similarity >= filters.minSimilarity : true) &&
|
||||||
(filters.maxSimilarity !== undefined ? score.similarity <= filters.maxSimilarity : true);
|
(filters.maxSimilarity !== undefined ? score.similarity <= filters.maxSimilarity : true);
|
||||||
|
|
||||||
return usernameMatch && beatmapMatch && ppMatch && similarityMatch;
|
const scoreHasRank = score.replay_leaderboard_rank_2 > 0;
|
||||||
|
|
||||||
|
const rankMatch = (filters.minRank == null || (score.replay_leaderboard_rank_2 <= filters.minRank && scoreHasRank)) &&
|
||||||
|
(filters.maxRank == null || score.replay_leaderboard_rank_2 >= filters.maxRank && scoreHasRank);
|
||||||
|
|
||||||
|
return usernameMatch && beatmapMatch && ppMatch && similarityMatch && rankMatch;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.filterManager.persistToLocalStorage();
|
this.filterManager.persistToLocalStorage();
|
||||||
|
|||||||
@ -24,7 +24,7 @@
|
|||||||
<input class="form-control" type="number" id="maxPP" [(ngModel)]="this.filterManager.filters.maxPP" (input)="filterScores()"
|
<input class="form-control" type="number" id="maxPP" [(ngModel)]="this.filterManager.filters.maxPP" (input)="filterScores()"
|
||||||
[readOnly]="this.isUrlFilters" [disabled]="this.isUrlFilters">
|
[readOnly]="this.isUrlFilters" [disabled]="this.isUrlFilters">
|
||||||
</p>
|
</p>
|
||||||
`
|
|
||||||
<!-- Min cvUR -->
|
<!-- Min cvUR -->
|
||||||
<p>
|
<p>
|
||||||
<label for="minUR" class="form-label">Min cvUR</label>
|
<label for="minUR" class="form-label">Min cvUR</label>
|
||||||
@ -52,6 +52,20 @@
|
|||||||
<input class="form-control" type="text" id="searchBeatmap" [(ngModel)]="this.filterManager.filters.searchBeatmap" (input)="filterScores()"
|
<input class="form-control" type="text" id="searchBeatmap" [(ngModel)]="this.filterManager.filters.searchBeatmap" (input)="filterScores()"
|
||||||
[readOnly]="this.isUrlFilters" [disabled]="this.isUrlFilters">
|
[readOnly]="this.isUrlFilters" [disabled]="this.isUrlFilters">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<!-- Min Rank -->
|
||||||
|
<p>
|
||||||
|
<label for="minRank" class="form-label">Min Rank</label>
|
||||||
|
<input class="form-control" type="number" id="minRank" [(ngModel)]="this.filterManager.filters.minRank" (input)="filterScores()"
|
||||||
|
[readOnly]="this.isUrlFilters" [disabled]="this.isUrlFilters">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Max Rank -->
|
||||||
|
<p>
|
||||||
|
<label for="maxRank" class="form-label">Max Rank</label>
|
||||||
|
<input class="form-control" type="number" id="maxRank" [(ngModel)]="this.filterManager.filters.maxRank" (input)="filterScores()"
|
||||||
|
[readOnly]="this.isUrlFilters" [disabled]="this.isUrlFilters">
|
||||||
|
</p>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<div *ngIf="getTotalPages() > 1" style="padding: 20px">
|
<div *ngIf="getTotalPages() > 1" style="padding: 20px">
|
||||||
|
|||||||
@ -19,6 +19,9 @@ export interface SuspiciousScoresFilter {
|
|||||||
|
|
||||||
searchUsername?: string;
|
searchUsername?: string;
|
||||||
searchBeatmap?: string;
|
searchBeatmap?: string;
|
||||||
|
|
||||||
|
minRank?: number;
|
||||||
|
maxRank?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -152,7 +155,12 @@ export class ViewSuspiciousScoresComponent implements OnInit, OnDestroy {
|
|||||||
const urMatch = (filters.minUR == null || score.ur >= filters.minUR) &&
|
const urMatch = (filters.minUR == null || score.ur >= filters.minUR) &&
|
||||||
(filters.maxUR == null || score.ur <= filters.maxUR);
|
(filters.maxUR == null || score.ur <= filters.maxUR);
|
||||||
|
|
||||||
return usernameMatch && beatmapMatch && ppMatch && urMatch;
|
const scoreHasRank = score.leaderboard_rank > 0;
|
||||||
|
|
||||||
|
const rankMatch = (filters.minRank == null || (score.leaderboard_rank <= filters.minRank && scoreHasRank)) &&
|
||||||
|
(filters.maxRank == null || score.leaderboard_rank >= filters.maxRank && scoreHasRank);
|
||||||
|
|
||||||
|
return usernameMatch && beatmapMatch && ppMatch && urMatch && rankMatch;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Presumably persists the current state of filters for future sessions
|
// Presumably persists the current state of filters for future sessions
|
||||||
|
|||||||
@ -298,7 +298,6 @@ fieldset button:not(:last-child) {
|
|||||||
|
|
||||||
fieldset p label {
|
fieldset p label {
|
||||||
display: block;
|
display: block;
|
||||||
margin-right: 100px !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge.mod {
|
.badge.mod {
|
||||||
|
|||||||
@ -18,12 +18,24 @@ export class DownloadFilesService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
downloadCSV(input: Object[], columns: string[], fileName: string = 'data') {
|
downloadCSV(input: Object[], columns: string[], fileName: string = 'data') {
|
||||||
const header = columns.join(',') + '\n';
|
let csvData = columns.join(',') + '\n';
|
||||||
|
|
||||||
let csvData = input.map(row =>
|
for (const row of input) {
|
||||||
input.map(row => Object.values(row).join(',')).join('\n')
|
let rowData: string[] = [];
|
||||||
).join('\n');
|
for (const column of columns) {
|
||||||
csvData = header + csvData;
|
let value = (row as Record<string, any>)[column];
|
||||||
|
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
value = value.replaceAll(',', ';');
|
||||||
|
} else if (Array.isArray(value)) {
|
||||||
|
value = value.join(';');
|
||||||
|
}
|
||||||
|
|
||||||
|
rowData.push(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
csvData += rowData.join(',') + '\n';
|
||||||
|
}
|
||||||
|
|
||||||
const blob = new Blob([csvData], { type: 'text/csv' });
|
const blob = new Blob([csvData], { type: 'text/csv' });
|
||||||
const url = window.URL.createObjectURL(blob);
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
|||||||
@ -63,6 +63,7 @@ services:
|
|||||||
POSTGRES_DB: ${DB_NAME}
|
POSTGRES_DB: ${DB_NAME}
|
||||||
# redis
|
# redis
|
||||||
REDIS_DB: 4
|
REDIS_DB: 4
|
||||||
|
REDIS_HOST: "redis"
|
||||||
# Discord
|
# Discord
|
||||||
WEBHOOK_URL: ${WEBHOOK_URL}
|
WEBHOOK_URL: ${WEBHOOK_URL}
|
||||||
SCORES_WEBHOOK_URL: ${SCORES_WEBHOOK_URL}
|
SCORES_WEBHOOK_URL: ${SCORES_WEBHOOK_URL}
|
||||||
@ -100,6 +101,10 @@ services:
|
|||||||
container_name: nise-frontend
|
container_name: nise-frontend
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
|
nise-replay-viewer:
|
||||||
|
image: code.stedos.dev/stedos/nise-replay-viewer:latest
|
||||||
|
container_name: nise-replay-viewer
|
||||||
|
restart: always
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres-data:
|
postgres-data:
|
||||||
@ -35,6 +35,12 @@ http {
|
|||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection "upgrade";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location /replay-viewer/ {
|
||||||
|
proxy_pass http://nise-replay-viewer/;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
FROM openresty/openresty:focal
|
FROM nginx:1.27.0-alpine
|
||||||
|
|
||||||
RUN rm -rf /usr/share/nginx/html/*
|
RUN rm -rf /usr/share/nginx/html/*
|
||||||
|
|
||||||
|
|||||||
@ -4,17 +4,17 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>/replay/ - nise.moe</title>
|
<title>/replay/ - nise.stedos.dev</title>
|
||||||
<link rel="icon" type="image/x-icon" href="https://nise.moe/assets/favicon.ico">
|
<link rel="icon" type="image/x-icon" href="https:/nise.stedos.dev/assets/favicon.ico">
|
||||||
<!-- Embed data -->
|
<!-- Embed data -->
|
||||||
<meta property="og:title" content="/nise.moe/ - osu!cheaters finder">
|
<meta property="og:title" content="/nise.stedos.dev/ - osu!cheaters finder">
|
||||||
<meta property="og:description" content="crawls osu!std replays and tries to find naughty boys.">
|
<meta property="og:description" content="crawls osu!std replays and tries to find naughty boys.">
|
||||||
<meta property="og:url" content="https://nise.moe">
|
<meta property="og:url" content="https://nise.stedos.dev">
|
||||||
<meta property="og:image" content="https://nise.moe/assets/banner.png">
|
<meta property="og:image" content="https://nise.stedos.dev/assets/banner.png">
|
||||||
<meta property="og:type" content="website">
|
<meta property="og:type" content="website">
|
||||||
<meta name="theme-color" content="#151515">
|
<meta name="theme-color" content="#151515">
|
||||||
<meta name="twitter:card" content="summary_large_image">
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
<meta name="twitter:image:src" content="https://nise.moe/assets/banner.png">
|
<meta name="twitter:image:src" content="https://nise.stedos.dev/assets/banner.png">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@ -18,9 +18,12 @@ export function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// We might be beind a reverse proxy - remove the proxied URL
|
||||||
|
const path = location.pathname.replace("/replay-viewer", "");
|
||||||
|
|
||||||
// This pattern matches one or more digits followed by an optional slash and any characters (non-greedy)
|
// This pattern matches one or more digits followed by an optional slash and any characters (non-greedy)
|
||||||
const pathRegex = /^\/(\d+)(?:\/(\d+))?/;
|
const pathRegex = /^\/(\d+)(?:\/(\d+))?/;
|
||||||
const match = location.pathname.match(pathRegex);
|
const match = path.match(pathRegex);
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
// match[1] will contain the first ID, match[2] (if present) will contain the second ID
|
// match[1] will contain the first ID, match[2] (if present) will contain the second ID
|
||||||
|
|||||||
@ -8,7 +8,7 @@ export function Navbar() {
|
|||||||
<Menubar className="rounded-none border-x-0 border-t-0 flex justify-between px-4">
|
<Menubar className="rounded-none border-x-0 border-t-0 flex justify-between px-4">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<img src={"https://nise.moe/assets/keisatsu-chan.png"} width={48}/>
|
<img src={"https://nise.stedos.dev/assets/keisatsu-chan.png"} width={48}/>
|
||||||
<h3 className="scroll-m-20 text-lg font-semibold tracking-tight">
|
<h3 className="scroll-m-20 text-lg font-semibold tracking-tight">
|
||||||
/replay/
|
/replay/
|
||||||
</h3>
|
</h3>
|
||||||
@ -24,14 +24,14 @@ export function Navbar() {
|
|||||||
{OsuRenderer.beatmap && (
|
{OsuRenderer.beatmap && (
|
||||||
<>
|
<>
|
||||||
{OsuRenderer.replay2 == null && (
|
{OsuRenderer.replay2 == null && (
|
||||||
<a href={"https://nise.moe/s/" + OsuRenderer.replay.info.id} target="_blank"
|
<a href={"https://nise.stedos.dev/s/" + OsuRenderer.replay.info.id} target="_blank"
|
||||||
className="flex items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none focus:bg-accent hover:bg-accent">
|
className="flex items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none focus:bg-accent hover:bg-accent">
|
||||||
View on nise.moe
|
View on nise.stedos.dev
|
||||||
</a>)}
|
</a>)}
|
||||||
{OsuRenderer.replay2 && (
|
{OsuRenderer.replay2 && (
|
||||||
<a href={"https://nise.moe/p/" + OsuRenderer.replay.info.id + "/" + OsuRenderer.replay2.info.id} target="_blank"
|
<a href={"https://nise.stedos.dev/p/" + OsuRenderer.replay.info.id + "/" + OsuRenderer.replay2.info.id} target="_blank"
|
||||||
className="flex items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none focus:bg-accent hover:bg-accent">
|
className="flex items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none focus:bg-accent hover:bg-accent">
|
||||||
View on nise.moe
|
View on nise.stedos.dev
|
||||||
</a>)}
|
</a>)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -39,7 +39,7 @@ export class Drawer {
|
|||||||
|
|
||||||
static async loadDefaultImages() {
|
static async loadDefaultImages() {
|
||||||
const imageLoadPromises = Object.keys(Drawer.images).map(imageName =>
|
const imageLoadPromises = Object.keys(Drawer.images).map(imageName =>
|
||||||
loadImageAsync(`/${imageName}.png`).then(
|
loadImageAsync(`/replay-viewer/${imageName}.png`).then(
|
||||||
image => {
|
image => {
|
||||||
Drawer.images[imageName as keyof typeof Drawer.images] = image;
|
Drawer.images[imageName as keyof typeof Drawer.images] = image;
|
||||||
},
|
},
|
||||||
|
|||||||
@ -231,7 +231,7 @@ export class OsuRenderer {
|
|||||||
static getApiUrl(): string {
|
static getApiUrl(): string {
|
||||||
return document.location.hostname === "localhost"
|
return document.location.hostname === "localhost"
|
||||||
? `http://localhost:8080`
|
? `http://localhost:8080`
|
||||||
: `https://nise.moe/api`;
|
: `https://nise.stedos.dev/api`;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async loadReplayPairFromUrl(replayId1: number, replayId2: number) {
|
static async loadReplayPairFromUrl(replayId1: number, replayId2: number) {
|
||||||
|
|||||||
@ -9,4 +9,5 @@ export default defineConfig({
|
|||||||
"@": path.resolve(__dirname, "./src"),
|
"@": path.resolve(__dirname, "./src"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
base: "/replay-viewer/",
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user