From 2adbc5c5d166a816246d9e48e283dcacb0d80c3c Mon Sep 17 00:00:00 2001 From: "nise.moe" Date: Fri, 16 Feb 2024 05:52:20 +0100 Subject: [PATCH] Keypress and slider end times calculation --- .../com/nisemoe/generated/tables/Scores.kt | 31 +++++++ .../generated/tables/records/ScoresRecord.kt | 32 ++++++- .../nise/integrations/CircleguardService.kt | 9 ++ .../nisemoe/nise/scheduler/FixOldScores.kt | 13 ++- .../nisemoe/nise/scheduler/ImportScores.kt | 2 +- .../db/migration/V0.0.1.014__alter_scores.sql | 7 ++ nise-circleguard/requirements.txt | 3 +- nise-circleguard/src/keypresses.py | 89 +++++++++++++++++++ nise-circleguard/src/main.py | 35 ++++++++ 9 files changed, 215 insertions(+), 6 deletions(-) create mode 100644 nise-backend/src/main/resources/db/migration/V0.0.1.014__alter_scores.sql create mode 100644 nise-circleguard/src/keypresses.py diff --git a/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/Scores.kt b/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/Scores.kt index ba74798..cc0be46 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/Scores.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/Scores.kt @@ -244,6 +244,37 @@ open class Scores( */ val VERSION: TableField = createField(DSL.name("version"), SQLDataType.INTEGER.defaultValue(DSL.field(DSL.raw("0"), SQLDataType.INTEGER)), this, "") + /** + * The column public.scores.keypresses_times. + */ + val KEYPRESSES_TIMES: TableField?> = createField(DSL.name("keypresses_times"), SQLDataType.FLOAT.array(), this, "") + + /** + * The column public.scores.keypresses_median. + */ + val KEYPRESSES_MEDIAN: TableField = createField(DSL.name("keypresses_median"), SQLDataType.DOUBLE, this, "") + + /** + * The column public.scores.keypresses_standard_deviation. + */ + val KEYPRESSES_STANDARD_DEVIATION: TableField = createField(DSL.name("keypresses_standard_deviation"), SQLDataType.DOUBLE, this, "") + + /** + * The column public.scores.sliderend_release_times. + */ + val SLIDEREND_RELEASE_TIMES: TableField?> = createField(DSL.name("sliderend_release_times"), SQLDataType.FLOAT.array(), this, "") + + /** + * The column public.scores.sliderend_release_median. + */ + val SLIDEREND_RELEASE_MEDIAN: TableField = createField(DSL.name("sliderend_release_median"), SQLDataType.DOUBLE, this, "") + + /** + * The column + * public.scores.sliderend_release_standard_deviation. + */ + val SLIDEREND_RELEASE_STANDARD_DEVIATION: TableField = createField(DSL.name("sliderend_release_standard_deviation"), SQLDataType.DOUBLE, this, "") + private constructor(alias: Name, aliased: Table?): this(alias, null, null, aliased, null) private constructor(alias: Name, aliased: Table?, parameters: Array?>?): this(alias, null, null, aliased, parameters) diff --git a/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/records/ScoresRecord.kt b/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/records/ScoresRecord.kt index 0276771..43f674c 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/records/ScoresRecord.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/records/ScoresRecord.kt @@ -161,6 +161,30 @@ open class ScoresRecord private constructor() : UpdatableRecordImpl? + set(value): Unit = set(35, value) + get(): Array? = get(35) as Array? + + 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? + set(value): Unit = set(38, value) + get(): Array? = get(38) as Array? + + 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? = null, keypressesMedian: Double? = null, keypressesStandardDeviation: Double? = null, sliderendReleaseTimes: Array? = 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?, + val keypresses_median: Double?, + val keypresses_standard_deviation: Double?, + + val sliderend_release_times: List?, + val sliderend_release_median: Double?, + val sliderend_release_standard_deviation: Double?, + val judgements: List ) diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/scheduler/FixOldScores.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/scheduler/FixOldScores.kt index 70c8ab7..ccae546 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/nise/scheduler/FixOldScores.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/scheduler/FixOldScores.kt @@ -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) diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/scheduler/ImportScores.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/scheduler/ImportScores.kt index c86b930..386af59 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/nise/scheduler/ImportScores.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/scheduler/ImportScores.kt @@ -64,7 +64,7 @@ class ImportScores( } } - val CURRENT_VERSION = 1 + val CURRENT_VERSION = 2 @Value("\${WEBHOOK_URL}") private lateinit var webhookUrl: String diff --git a/nise-backend/src/main/resources/db/migration/V0.0.1.014__alter_scores.sql b/nise-backend/src/main/resources/db/migration/V0.0.1.014__alter_scores.sql new file mode 100644 index 0000000..b216627 --- /dev/null +++ b/nise-backend/src/main/resources/db/migration/V0.0.1.014__alter_scores.sql @@ -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; \ No newline at end of file diff --git a/nise-circleguard/requirements.txt b/nise-circleguard/requirements.txt index 3405ada..9bb7745 100644 --- a/nise-circleguard/requirements.txt +++ b/nise-circleguard/requirements.txt @@ -1,3 +1,4 @@ ossapi==3.4.3 circleguard==5.4.1 -flask==3.0.2 \ No newline at end of file +flask==3.0.2 +brparser==1.0.4 \ No newline at end of file diff --git a/nise-circleguard/src/keypresses.py b/nise-circleguard/src/keypresses.py new file mode 100644 index 0000000..6e688af --- /dev/null +++ b/nise-circleguard/src/keypresses.py @@ -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 diff --git a/nise-circleguard/src/main.py b/nise-circleguard/src/main.py index 16372b2..4bfd7d4 100644 --- a/nise-circleguard/src/main.py +++ b/nise-circleguard/src/main.py @@ -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())