Keypress and slider end times calculation
This commit is contained in:
parent
2376fe733e
commit
2adbc5c5d1
@ -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, "")
|
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>?): this(alias, null, null, aliased, null)
|
||||||
private constructor(alias: Name, aliased: Table<ScoresRecord>?, parameters: Array<Field<*>?>?): this(alias, null, null, aliased, parameters)
|
private constructor(alias: Name, aliased: Table<ScoresRecord>?, parameters: Array<Field<*>?>?): this(alias, null, null, aliased, parameters)
|
||||||
|
|
||||||
|
|||||||
@ -161,6 +161,30 @@ open class ScoresRecord private constructor() : UpdatableRecordImpl<ScoresRecord
|
|||||||
set(value): Unit = set(34, value)
|
set(value): Unit = set(34, value)
|
||||||
get(): Int? = get(34) as Int?
|
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
|
// Primary key information
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
@ -170,7 +194,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): 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.id = id
|
||||||
this.beatmapId = beatmapId
|
this.beatmapId = beatmapId
|
||||||
this.count_100 = count_100
|
this.count_100 = count_100
|
||||||
@ -206,6 +230,12 @@ open class ScoresRecord private constructor() : UpdatableRecordImpl<ScoresRecord
|
|||||||
this.sentDiscordNotification = sentDiscordNotification
|
this.sentDiscordNotification = sentDiscordNotification
|
||||||
this.addedAt = addedAt
|
this.addedAt = addedAt
|
||||||
this.version = version
|
this.version = version
|
||||||
|
this.keypressesTimes = keypressesTimes
|
||||||
|
this.keypressesMedian = keypressesMedian
|
||||||
|
this.keypressesStandardDeviation = keypressesStandardDeviation
|
||||||
|
this.sliderendReleaseTimes = sliderendReleaseTimes
|
||||||
|
this.sliderendReleaseMedian = sliderendReleaseMedian
|
||||||
|
this.sliderendReleaseStandardDeviation = sliderendReleaseStandardDeviation
|
||||||
resetChangedOnNotNull()
|
resetChangedOnNotNull()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -74,6 +74,15 @@ class CircleguardService {
|
|||||||
var error_coefficient_of_variation: Double?,
|
var error_coefficient_of_variation: Double?,
|
||||||
var error_kurtosis: Double?,
|
var error_kurtosis: Double?,
|
||||||
var error_skewness: 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>
|
val judgements: List<ScoreJudgement>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -31,19 +31,19 @@ class FixOldScores(
|
|||||||
@Value("\${OLD_SCORES_PAGE_SIZE:5000}")
|
@Value("\${OLD_SCORES_PAGE_SIZE:5000}")
|
||||||
private var pageSize: Int = 5000
|
private var pageSize: Int = 5000
|
||||||
|
|
||||||
val CURRENT_VERSION = 1
|
val CURRENT_VERSION = 2
|
||||||
|
|
||||||
private val logger = LoggerFactory.getLogger(javaClass)
|
private val logger = LoggerFactory.getLogger(javaClass)
|
||||||
|
|
||||||
data class Task(val offset: Int, val limit: Int)
|
data class Task(val offset: Int, val limit: Int)
|
||||||
|
|
||||||
@Scheduled(fixedDelay = 40000, initialDelay = 0)
|
@Scheduled(fixedDelay = 120000, initialDelay = 0)
|
||||||
fun fixOldScores() {
|
fun fixOldScores() {
|
||||||
val condition = SCORES.REPLAY.isNotNull.and(SCORES.VERSION.lessThan(CURRENT_VERSION))
|
val condition = SCORES.REPLAY.isNotNull.and(SCORES.VERSION.lessThan(CURRENT_VERSION))
|
||||||
val totalRows = dslContext.fetchCount(SCORES, condition)
|
val totalRows = dslContext.fetchCount(SCORES, condition)
|
||||||
|
|
||||||
if(totalRows <= 0) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,6 +104,7 @@ class FixOldScores(
|
|||||||
).get()
|
).get()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
this.logger.error("Circleguard failed to process replay with score_id: ${score.id}")
|
this.logger.error("Circleguard failed to process replay with score_id: ${score.id}")
|
||||||
|
this.logger.error(e.stackTraceToString())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -128,6 +129,12 @@ class FixOldScores(
|
|||||||
.set(SCORES.ERROR_SKEWNESS, processedReplay.error_skewness)
|
.set(SCORES.ERROR_SKEWNESS, processedReplay.error_skewness)
|
||||||
.set(SCORES.SNAPS, processedReplay.snaps)
|
.set(SCORES.SNAPS, processedReplay.snaps)
|
||||||
.set(SCORES.EDGE_HITS, processedReplay.edge_hits)
|
.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))
|
.where(SCORES.REPLAY_ID.eq(score.replayId))
|
||||||
.returningResult(SCORES.ID)
|
.returningResult(SCORES.ID)
|
||||||
.fetchOne()?.getValue(SCORES.ID)
|
.fetchOne()?.getValue(SCORES.ID)
|
||||||
|
|||||||
@ -64,7 +64,7 @@ class ImportScores(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val CURRENT_VERSION = 1
|
val CURRENT_VERSION = 2
|
||||||
|
|
||||||
@Value("\${WEBHOOK_URL}")
|
@Value("\${WEBHOOK_URL}")
|
||||||
private lateinit var webhookUrl: String
|
private lateinit var webhookUrl: String
|
||||||
|
|||||||
@ -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;
|
||||||
@ -1,3 +1,4 @@
|
|||||||
ossapi==3.4.3
|
ossapi==3.4.3
|
||||||
circleguard==5.4.1
|
circleguard==5.4.1
|
||||||
flask==3.0.2
|
flask==3.0.2
|
||||||
|
brparser==1.0.4
|
||||||
89
nise-circleguard/src/keypresses.py
Normal file
89
nise-circleguard/src/keypresses.py
Normal 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
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
import base64
|
||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
from dataclasses import dataclass, asdict
|
from dataclasses import dataclass, asdict
|
||||||
@ -7,10 +8,12 @@ from typing import List, Iterable
|
|||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import scipy
|
import scipy
|
||||||
|
from brparser import Replay, BeatmapOsu, Mod
|
||||||
from circleguard import Circleguard, ReplayString, Hit
|
from circleguard import Circleguard, ReplayString, Hit
|
||||||
from flask import Flask, request, jsonify, abort
|
from flask import Flask, request, jsonify, abort
|
||||||
|
|
||||||
from src.WriteStreamWrapper import WriteStreamWrapper
|
from src.WriteStreamWrapper import WriteStreamWrapper
|
||||||
|
from src.keypresses import get_kp_sliders
|
||||||
|
|
||||||
# Circleguard
|
# Circleguard
|
||||||
cg = Circleguard(os.getenv("OSU_API_KEY"), db_path="./dbs/db.db", slider_dir="./dbs/")
|
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_kurtosis: float
|
||||||
error_skewness: 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]
|
judgements: List[Hit]
|
||||||
|
|
||||||
def to_dict(self):
|
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))
|
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))
|
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)
|
hits: Iterable[Hit] = cg.hits(replay=replay1, beatmap=cg_beatmap)
|
||||||
judgements: List[ScoreJudgement] = []
|
judgements: List[ScoreJudgement] = []
|
||||||
for hit in hits:
|
for hit in hits:
|
||||||
@ -143,6 +170,14 @@ def process_replay():
|
|||||||
error_kurtosis=kurtosis,
|
error_kurtosis=kurtosis,
|
||||||
error_skewness=skewness,
|
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
|
judgements=judgements
|
||||||
)
|
)
|
||||||
return jsonify(ur_response.to_dict())
|
return jsonify(ur_response.to_dict())
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user