Keypress and slider end times calculation

This commit is contained in:
nise.moe 2024-02-16 05:52:20 +01:00
parent 2376fe733e
commit 2adbc5c5d1
9 changed files with 215 additions and 6 deletions

View File

@ -244,6 +244,37 @@ open class Scores(
*/
val VERSION: TableField<ScoresRecord, Int?> = createField(DSL.name("version"), SQLDataType.INTEGER.defaultValue(DSL.field(DSL.raw("0"), SQLDataType.INTEGER)), this, "")
/**
* The column <code>public.scores.keypresses_times</code>.
*/
val KEYPRESSES_TIMES: TableField<ScoresRecord, Array<Double?>?> = createField(DSL.name("keypresses_times"), SQLDataType.FLOAT.array(), this, "")
/**
* The column <code>public.scores.keypresses_median</code>.
*/
val KEYPRESSES_MEDIAN: TableField<ScoresRecord, Double?> = createField(DSL.name("keypresses_median"), SQLDataType.DOUBLE, this, "")
/**
* The column <code>public.scores.keypresses_standard_deviation</code>.
*/
val KEYPRESSES_STANDARD_DEVIATION: TableField<ScoresRecord, Double?> = createField(DSL.name("keypresses_standard_deviation"), SQLDataType.DOUBLE, this, "")
/**
* The column <code>public.scores.sliderend_release_times</code>.
*/
val SLIDEREND_RELEASE_TIMES: TableField<ScoresRecord, Array<Double?>?> = createField(DSL.name("sliderend_release_times"), SQLDataType.FLOAT.array(), this, "")
/**
* The column <code>public.scores.sliderend_release_median</code>.
*/
val SLIDEREND_RELEASE_MEDIAN: TableField<ScoresRecord, Double?> = createField(DSL.name("sliderend_release_median"), SQLDataType.DOUBLE, this, "")
/**
* The column
* <code>public.scores.sliderend_release_standard_deviation</code>.
*/
val SLIDEREND_RELEASE_STANDARD_DEVIATION: TableField<ScoresRecord, Double?> = createField(DSL.name("sliderend_release_standard_deviation"), SQLDataType.DOUBLE, this, "")
private constructor(alias: Name, aliased: Table<ScoresRecord>?): this(alias, null, null, aliased, null)
private constructor(alias: Name, aliased: Table<ScoresRecord>?, parameters: Array<Field<*>?>?): this(alias, null, null, aliased, parameters)

View File

@ -161,6 +161,30 @@ open class ScoresRecord private constructor() : UpdatableRecordImpl<ScoresRecord
set(value): Unit = set(34, value)
get(): Int? = get(34) as Int?
open var keypressesTimes: Array<Double?>?
set(value): Unit = set(35, value)
get(): Array<Double?>? = get(35) as Array<Double?>?
open var keypressesMedian: Double?
set(value): Unit = set(36, value)
get(): Double? = get(36) as Double?
open var keypressesStandardDeviation: Double?
set(value): Unit = set(37, value)
get(): Double? = get(37) as Double?
open var sliderendReleaseTimes: Array<Double?>?
set(value): Unit = set(38, value)
get(): Array<Double?>? = get(38) as Array<Double?>?
open var sliderendReleaseMedian: Double?
set(value): Unit = set(39, value)
get(): Double? = get(39) as Double?
open var sliderendReleaseStandardDeviation: Double?
set(value): Unit = set(40, value)
get(): Double? = get(40) as Double?
// -------------------------------------------------------------------------
// Primary key information
// -------------------------------------------------------------------------
@ -170,7 +194,7 @@ open class ScoresRecord private constructor() : UpdatableRecordImpl<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): 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): this() {
this.id = id
this.beatmapId = beatmapId
this.count_100 = count_100
@ -206,6 +230,12 @@ open class ScoresRecord private constructor() : UpdatableRecordImpl<ScoresRecord
this.sentDiscordNotification = sentDiscordNotification
this.addedAt = addedAt
this.version = version
this.keypressesTimes = keypressesTimes
this.keypressesMedian = keypressesMedian
this.keypressesStandardDeviation = keypressesStandardDeviation
this.sliderendReleaseTimes = sliderendReleaseTimes
this.sliderendReleaseMedian = sliderendReleaseMedian
this.sliderendReleaseStandardDeviation = sliderendReleaseStandardDeviation
resetChangedOnNotNull()
}
}

View File

@ -74,6 +74,15 @@ class CircleguardService {
var error_coefficient_of_variation: Double?,
var error_kurtosis: Double?,
var error_skewness: Double?,
val keypresses_times: List<Double>?,
val keypresses_median: Double?,
val keypresses_standard_deviation: Double?,
val sliderend_release_times: List<Double>?,
val sliderend_release_median: Double?,
val sliderend_release_standard_deviation: Double?,
val judgements: List<ScoreJudgement>
)

View File

@ -31,19 +31,19 @@ class FixOldScores(
@Value("\${OLD_SCORES_PAGE_SIZE:5000}")
private var pageSize: Int = 5000
val CURRENT_VERSION = 1
val CURRENT_VERSION = 2
private val logger = LoggerFactory.getLogger(javaClass)
data class Task(val offset: Int, val limit: Int)
@Scheduled(fixedDelay = 40000, initialDelay = 0)
@Scheduled(fixedDelay = 120000, initialDelay = 0)
fun fixOldScores() {
val condition = SCORES.REPLAY.isNotNull.and(SCORES.VERSION.lessThan(CURRENT_VERSION))
val totalRows = dslContext.fetchCount(SCORES, condition)
if(totalRows <= 0) {
this.logger.warn("Fixing old scores but there are none, total rows: $totalRows")
this.logger.debug("Fixing old scores but there are none, total rows: $totalRows")
return
}
@ -104,6 +104,7 @@ class FixOldScores(
).get()
} catch (e: Exception) {
this.logger.error("Circleguard failed to process replay with score_id: ${score.id}")
this.logger.error(e.stackTraceToString())
return
}
@ -128,6 +129,12 @@ class FixOldScores(
.set(SCORES.ERROR_SKEWNESS, processedReplay.error_skewness)
.set(SCORES.SNAPS, processedReplay.snaps)
.set(SCORES.EDGE_HITS, processedReplay.edge_hits)
.set(SCORES.KEYPRESSES_TIMES, processedReplay.keypresses_times?.toTypedArray())
.set(SCORES.KEYPRESSES_MEDIAN, processedReplay.keypresses_median)
.set(SCORES.KEYPRESSES_STANDARD_DEVIATION, processedReplay.keypresses_standard_deviation)
.set(SCORES.SLIDEREND_RELEASE_TIMES, processedReplay.sliderend_release_times?.toTypedArray())
.set(SCORES.SLIDEREND_RELEASE_MEDIAN, processedReplay.sliderend_release_median)
.set(SCORES.SLIDEREND_RELEASE_STANDARD_DEVIATION, processedReplay.sliderend_release_standard_deviation)
.where(SCORES.REPLAY_ID.eq(score.replayId))
.returningResult(SCORES.ID)
.fetchOne()?.getValue(SCORES.ID)

View File

@ -64,7 +64,7 @@ class ImportScores(
}
}
val CURRENT_VERSION = 1
val CURRENT_VERSION = 2
@Value("\${WEBHOOK_URL}")
private lateinit var webhookUrl: String

View File

@ -0,0 +1,7 @@
ALTER TABLE public.scores
ADD COLUMN keypresses_times float8[],
ADD COLUMN keypresses_median float8,
ADD COLUMN keypresses_standard_deviation float8,
ADD COLUMN sliderend_release_times float8[],
ADD COLUMN sliderend_release_median float8,
ADD COLUMN sliderend_release_standard_deviation float8;

View File

@ -1,3 +1,4 @@
ossapi==3.4.3
circleguard==5.4.1
flask==3.0.2
brparser==1.0.4

View File

@ -0,0 +1,89 @@
from brparser import Keypress, Key, apply_mods_to_time, diff_range, HitCircleOsu, SliderOsu, SpinnerOsu
def get_keypresses(replay):
keypresses = []
button_1 = False
button_2 = False
for event in replay.replay_data:
if (event.keys & Key.M1):
if not button_1:
button_1 = True
init_1 = event.time
else:
if button_1:
button_1 = False
keypresses.append(Keypress(init_1, event.time))
if (event.keys & Key.M2):
if not button_2:
button_2 = True
init_2 = event.time
else:
if button_2:
button_2 = False
keypresses.append(Keypress(init_2, event.time))
return keypresses
def get_kp_sliders(replay, beatmap):
keypresses = get_keypresses(replay)
hw_50 = int(diff_range(beatmap.od, 200, 150, 100, replay.mods))
keypress_times = []
sliderend_release_times = []
hit_objs = beatmap.hit_objects
kp_i = 0
ho_i = 0
while ho_i < len(hit_objs) and kp_i < len(keypresses):
ho = hit_objs[ho_i]
kp = keypresses[kp_i]
if isinstance(ho, HitCircleOsu):
end_time = ho.start_time + hw_50 + 1
else:
end_time = ho.end_time
if kp.key_down < ho.start_time - 400:
kp_i += 1
continue
if kp.key_down <= ho.start_time - hw_50:
if isinstance(ho, SliderOsu):
release_time = kp.key_up - ho.end_time
sliderend_release_times.append(
apply_mods_to_time(release_time, replay.mods))
while keypresses[kp_i].key_down < ho.end_time:
kp_i += 1
if kp_i >= len(keypresses):
break
else:
if isinstance(ho, HitCircleOsu):
keypress_time = kp.key_up - kp.key_down
keypress_times.append(apply_mods_to_time(keypress_time,
replay.mods))
kp_i += 1
ho_i += 1
elif kp.key_down >= end_time:
ho_i += 1
else:
if kp.key_down < ho.start_time + hw_50 and not isinstance(ho, SpinnerOsu):
if isinstance(ho, SliderOsu):
release_time = kp.key_up - ho.end_time
sliderend_release_times.append(
apply_mods_to_time(release_time, replay.mods))
while keypresses[kp_i].key_down < ho.end_time:
kp_i += 1
if kp_i >= len(keypresses):
break
else:
keypress_time = kp.key_up - kp.key_down
keypress_times.append(apply_mods_to_time(keypress_time,
replay.mods))
kp_i += 1
ho_i += 1
else:
kp_i += 1
return keypress_times, sliderend_release_times

View File

@ -1,3 +1,4 @@
import base64
import io
import os
from dataclasses import dataclass, asdict
@ -7,10 +8,12 @@ from typing import List, Iterable
import numpy as np
import scipy
from brparser import Replay, BeatmapOsu, Mod
from circleguard import Circleguard, ReplayString, Hit
from flask import Flask, request, jsonify, abort
from src.WriteStreamWrapper import WriteStreamWrapper
from src.keypresses import get_kp_sliders
# Circleguard
cg = Circleguard(os.getenv("OSU_API_KEY"), db_path="./dbs/db.db", slider_dir="./dbs/")
@ -55,6 +58,14 @@ class ReplayResponse:
error_kurtosis: float
error_skewness: float
keypresses_times: List[int]
keypresses_median: float
keypresses_standard_deviation: float
sliderend_release_times: List[int]
sliderend_release_median: float
sliderend_release_standard_deviation: float
judgements: List[Hit]
def to_dict(self):
@ -100,6 +111,22 @@ def process_replay():
edge_hits = sum(1 for _ in cg.hits(replay=replay1, within=1, beatmap=cg_beatmap))
snaps = sum(1 for _ in cg.snaps(replay=replay1, beatmap=cg_beatmap))
#
# Decode the base64 string
decoded_data = base64.b64decode(replay_request.replay_data)
# Pass the decoded data to the Replay class
replay = Replay(decoded_data, pure_lzma=True)
replay.mods = Mod(replay_request.mods)
beatmap_file = f'dbs/{cg_beatmap.artist} - {cg_beatmap.title} ({cg_beatmap.creator})[{cg_beatmap.version}].osu'
if not os.path.exists(beatmap_file):
print(f'Map not found @ {beatmap_file}', flush=True)
return 400, "Map not found"
beatmap = BeatmapOsu(f'dbs/{cg_beatmap.artist} - {cg_beatmap.title} ({cg_beatmap.creator})[{cg_beatmap.version}].osu')
kp, se = get_kp_sliders(replay, beatmap)
hits: Iterable[Hit] = cg.hits(replay=replay1, beatmap=cg_beatmap)
judgements: List[ScoreJudgement] = []
for hit in hits:
@ -143,6 +170,14 @@ def process_replay():
error_kurtosis=kurtosis,
error_skewness=skewness,
keypresses_times=kp,
keypresses_median=np.median(kp),
keypresses_standard_deviation=np.std(kp, ddof=1),
sliderend_release_times=se,
sliderend_release_median=np.median(se),
sliderend_release_standard_deviation=np.std(se, ddof=1),
judgements=judgements
)
return jsonify(ur_response.to_dict())