Generalize code to more scoring systems
This commit is contained in:
parent
62949004f2
commit
4ed3fe276b
15
schema.sql
15
schema.sql
|
@ -1,10 +1,10 @@
|
||||||
# {{{ users
|
# {{{ users
|
||||||
# }}}
|
|
||||||
create table IF NOT EXISTS users (
|
create table IF NOT EXISTS users (
|
||||||
id INTEGER NOT NULL PRIMARY KEY,
|
id INTEGER NOT NULL PRIMARY KEY,
|
||||||
discord_id TEXT UNIQUE NOT NULL,
|
discord_id TEXT UNIQUE NOT NULL,
|
||||||
is_pookie BOOL NOT NULL DEFAULT 0
|
is_pookie BOOL NOT NULL DEFAULT 0
|
||||||
);
|
);
|
||||||
|
# }}}
|
||||||
# {{{ songs
|
# {{{ songs
|
||||||
CREATE TABLE IF NOT EXISTS songs (
|
CREATE TABLE IF NOT EXISTS songs (
|
||||||
id INTEGER NOT NULL PRIMARY KEY,
|
id INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
@ -55,5 +55,18 @@ CREATE TABLE IF NOT EXISTS plays (
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||||
);
|
);
|
||||||
# }}}
|
# }}}
|
||||||
|
# {{{ scores
|
||||||
|
CREATE TABLE IF NOT EXISTS scores (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
play_id INTEGER NOT NULL,
|
||||||
|
|
||||||
|
score INTEGER NOT NULL,
|
||||||
|
creation_ptt INTEGER,
|
||||||
|
scoring_system NOT NULL CHECK (scoring_system IN ('standard', 'ex')),
|
||||||
|
|
||||||
|
FOREIGN KEY (play_id) REFERENCES plays(id),
|
||||||
|
UNIQUE(play_id, scoring_system)
|
||||||
|
)
|
||||||
|
# }}}
|
||||||
|
|
||||||
insert into users(discord_id) values (385759924917108740);
|
insert into users(discord_id) values (385759924917108740);
|
||||||
|
|
|
@ -125,10 +125,11 @@ impl GoalStats {
|
||||||
let plays = get_best_plays(
|
let plays = get_best_plays(
|
||||||
&ctx.db,
|
&ctx.db,
|
||||||
&ctx.song_cache,
|
&ctx.song_cache,
|
||||||
user,
|
user.id,
|
||||||
scoring_system,
|
scoring_system,
|
||||||
0,
|
0,
|
||||||
usize::MAX,
|
usize::MAX,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await??;
|
.await??;
|
||||||
|
|
||||||
|
@ -150,22 +151,20 @@ impl GoalStats {
|
||||||
.count as usize;
|
.count as usize;
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Peak ptt
|
// {{{ Peak ptt
|
||||||
let peak_ptt = {
|
let peak_ptt = query!(
|
||||||
let record = query!(
|
|
||||||
"
|
|
||||||
SELECT
|
|
||||||
max(creation_ptt) as standard,
|
|
||||||
max(creation_zeta_ptt) as ex
|
|
||||||
FROM plays
|
|
||||||
"
|
"
|
||||||
|
SELECT s.creation_ptt
|
||||||
|
FROM plays p
|
||||||
|
JOIN scores s ON s.play_id = p.id
|
||||||
|
WHERE user_id = ?
|
||||||
|
AND scoring_system = ?
|
||||||
|
",
|
||||||
|
user.id,
|
||||||
|
ScoringSystem::SCORING_SYSTEM_DB_STRINGS[scoring_system.to_index()]
|
||||||
)
|
)
|
||||||
.fetch_one(&ctx.db)
|
.fetch_one(&ctx.db)
|
||||||
.await?;
|
.await?
|
||||||
match scoring_system {
|
.creation_ptt
|
||||||
ScoringSystem::Standard => record.standard,
|
|
||||||
ScoringSystem::EX => record.ex,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.ok_or_else(|| "No ptt history data found")? as u32;
|
.ok_or_else(|| "No ptt history data found")? as u32;
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Peak PM relay
|
// {{{ Peak PM relay
|
||||||
|
|
|
@ -2,4 +2,5 @@ pub mod achievement;
|
||||||
pub mod chart;
|
pub mod chart;
|
||||||
pub mod jacket;
|
pub mod jacket;
|
||||||
pub mod play;
|
pub mod play;
|
||||||
|
pub mod rating;
|
||||||
pub mod score;
|
pub mod score;
|
||||||
|
|
|
@ -1,41 +1,42 @@
|
||||||
use std::str::FromStr;
|
use std::array;
|
||||||
|
|
||||||
|
use chrono::NaiveDateTime;
|
||||||
|
use chrono::Utc;
|
||||||
use num::traits::Euclid;
|
use num::traits::Euclid;
|
||||||
|
use num::CheckedDiv;
|
||||||
|
use num::Rational32;
|
||||||
|
use num::Zero;
|
||||||
use poise::serenity_prelude::{
|
use poise::serenity_prelude::{
|
||||||
Attachment, AttachmentId, CreateAttachment, CreateEmbed, CreateEmbedAuthor, Timestamp,
|
Attachment, AttachmentId, CreateAttachment, CreateEmbed, CreateEmbedAuthor, Timestamp,
|
||||||
};
|
};
|
||||||
use sqlx::{query, query_as, SqlitePool};
|
use sqlx::query_as;
|
||||||
|
use sqlx::{query, SqlitePool};
|
||||||
|
|
||||||
use crate::arcaea::chart::{Chart, Song};
|
use crate::arcaea::chart::{Chart, Song};
|
||||||
use crate::context::{Error, UserContext};
|
use crate::context::{Error, UserContext};
|
||||||
use crate::user::User;
|
use crate::user::User;
|
||||||
|
|
||||||
use super::chart::SongCache;
|
use super::chart::SongCache;
|
||||||
|
use super::rating::{rating_as_fixed, rating_as_float};
|
||||||
use super::score::{Score, ScoringSystem};
|
use super::score::{Score, ScoringSystem};
|
||||||
|
|
||||||
// {{{ Create play
|
// {{{ Create play
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct CreatePlay {
|
pub struct CreatePlay {
|
||||||
chart_id: u32,
|
|
||||||
discord_attachment_id: Option<AttachmentId>,
|
discord_attachment_id: Option<AttachmentId>,
|
||||||
|
|
||||||
// Actual score data
|
// Scoring details
|
||||||
score: Score,
|
score: Score,
|
||||||
zeta_score: Score,
|
|
||||||
|
|
||||||
// Optional score details
|
|
||||||
max_recall: Option<u32>,
|
max_recall: Option<u32>,
|
||||||
far_notes: Option<u32>,
|
far_notes: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CreatePlay {
|
impl CreatePlay {
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn new(score: Score, chart: &Chart) -> Self {
|
pub fn new(score: Score) -> Self {
|
||||||
Self {
|
Self {
|
||||||
chart_id: chart.id,
|
|
||||||
discord_attachment_id: None,
|
discord_attachment_id: None,
|
||||||
score,
|
score,
|
||||||
zeta_score: score.to_zeta(chart.note_count as u32),
|
|
||||||
max_recall: None,
|
max_recall: None,
|
||||||
far_notes: None,
|
far_notes: None,
|
||||||
}
|
}
|
||||||
|
@ -60,7 +61,7 @@ impl CreatePlay {
|
||||||
}
|
}
|
||||||
|
|
||||||
// {{{ Save
|
// {{{ Save
|
||||||
pub async fn save(self, ctx: &UserContext, user: &User) -> Result<Play, Error> {
|
pub async fn save(self, ctx: &UserContext, user: &User, chart: &Chart) -> Result<Play, Error> {
|
||||||
let attachment_id = self.discord_attachment_id.map(|i| i.get() as i64);
|
let attachment_id = self.discord_attachment_id.map(|i| i.get() as i64);
|
||||||
|
|
||||||
// {{{ Save current data to play
|
// {{{ Save current data to play
|
||||||
|
@ -68,16 +69,14 @@ impl CreatePlay {
|
||||||
"
|
"
|
||||||
INSERT INTO plays(
|
INSERT INTO plays(
|
||||||
user_id,chart_id,discord_attachment_id,
|
user_id,chart_id,discord_attachment_id,
|
||||||
score,zeta_score,max_recall,far_notes
|
max_recall,far_notes
|
||||||
)
|
)
|
||||||
VALUES(?,?,?,?,?,?,?)
|
VALUES(?,?,?,?,?)
|
||||||
RETURNING id, created_at
|
RETURNING id, created_at
|
||||||
",
|
",
|
||||||
user.id,
|
user.id,
|
||||||
self.chart_id,
|
chart.id,
|
||||||
attachment_id,
|
attachment_id,
|
||||||
self.score.0,
|
|
||||||
self.zeta_score.0,
|
|
||||||
self.max_recall,
|
self.max_recall,
|
||||||
self.far_notes
|
self.far_notes
|
||||||
)
|
)
|
||||||
|
@ -85,93 +84,88 @@ impl CreatePlay {
|
||||||
.await?;
|
.await?;
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Update creation ptt data
|
// {{{ Update creation ptt data
|
||||||
let creation_ptt = get_best_plays(
|
let scores = ScoreCollection::from_standard_score(self.score, chart);
|
||||||
&ctx.db,
|
for system in ScoringSystem::SCORING_SYSTEMS {
|
||||||
&ctx.song_cache,
|
let i = system.to_index();
|
||||||
user,
|
let plays = get_best_plays(&ctx.db, &ctx.song_cache, user.id, system, 30, 30, None)
|
||||||
ScoringSystem::Standard,
|
|
||||||
30,
|
|
||||||
30,
|
|
||||||
)
|
|
||||||
.await?
|
.await?
|
||||||
.ok()
|
.ok();
|
||||||
.map(|plays| compute_b30_ptt(ScoringSystem::Standard, &plays));
|
|
||||||
|
|
||||||
let creation_zeta_ptt =
|
let creation_ptt: Option<_> = try { rating_as_fixed(compute_b30_ptt(system, &plays?)) };
|
||||||
get_best_plays(&ctx.db, &ctx.song_cache, user, ScoringSystem::EX, 30, 30)
|
|
||||||
.await?
|
|
||||||
.ok()
|
|
||||||
.map(|plays| compute_b30_ptt(ScoringSystem::EX, &plays));
|
|
||||||
|
|
||||||
query!(
|
query!(
|
||||||
"
|
"
|
||||||
UPDATE plays
|
INSERT INTO scores(play_id, score, creation_ptt, scoring_system)
|
||||||
SET
|
VALUES (?,?,?,?)
|
||||||
creation_ptt=?,
|
|
||||||
creation_zeta_ptt=?
|
|
||||||
WHERE
|
|
||||||
id=?
|
|
||||||
",
|
",
|
||||||
|
play.id,
|
||||||
|
scores.0[i].0,
|
||||||
creation_ptt,
|
creation_ptt,
|
||||||
creation_zeta_ptt,
|
ScoringSystem::SCORING_SYSTEM_DB_STRINGS[i]
|
||||||
play.id
|
|
||||||
)
|
)
|
||||||
.execute(&ctx.db)
|
.execute(&ctx.db)
|
||||||
.await?;
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
// }}}
|
// }}}
|
||||||
|
|
||||||
Ok(Play {
|
Ok(Play {
|
||||||
id: play.id as u32,
|
id: play.id as u32,
|
||||||
created_at: play.created_at,
|
created_at: play.created_at,
|
||||||
chart_id: self.chart_id,
|
chart_id: chart.id,
|
||||||
user_id: user.id,
|
user_id: user.id,
|
||||||
discord_attachment_id: self.discord_attachment_id,
|
scores,
|
||||||
score: self.score,
|
|
||||||
zeta_score: self.zeta_score,
|
|
||||||
max_recall: self.max_recall,
|
max_recall: self.max_recall,
|
||||||
far_notes: self.far_notes,
|
far_notes: self.far_notes,
|
||||||
creation_ptt,
|
|
||||||
creation_zeta_ptt,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ DbPlay
|
// {{{ DbPlay
|
||||||
/// Version of `Play` matching the format sqlx expects
|
/// Construct a `Play` from a sqlite return record.
|
||||||
#[derive(Debug, Clone, sqlx::FromRow)]
|
#[macro_export]
|
||||||
|
macro_rules! play_from_db_record {
|
||||||
|
($chart:expr, $record:expr) => {{
|
||||||
|
use crate::arcaea::play::{Play, ScoreCollection};
|
||||||
|
use crate::arcaea::score::Score;
|
||||||
|
Play {
|
||||||
|
id: $record.id as u32,
|
||||||
|
chart_id: $record.chart_id as u32,
|
||||||
|
user_id: $record.user_id as u32,
|
||||||
|
scores: ScoreCollection::from_standard_score(Score($record.score as u32), $chart),
|
||||||
|
max_recall: $record.max_recall.map(|r| r as u32),
|
||||||
|
far_notes: $record.far_notes.map(|r| r as u32),
|
||||||
|
created_at: $record.created_at,
|
||||||
|
}
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Typed version of the input to the macro above.
|
||||||
|
/// Useful when using the non-macro version of the sqlx functions.
|
||||||
|
#[derive(Debug, sqlx::FromRow)]
|
||||||
pub struct DbPlay {
|
pub struct DbPlay {
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
pub chart_id: i64,
|
pub chart_id: i64,
|
||||||
pub user_id: i64,
|
pub user_id: i64,
|
||||||
pub discord_attachment_id: Option<String>,
|
pub created_at: chrono::NaiveDateTime,
|
||||||
pub score: i64,
|
|
||||||
pub zeta_score: i64,
|
// Score details
|
||||||
pub max_recall: Option<i64>,
|
pub max_recall: Option<i64>,
|
||||||
pub far_notes: Option<i64>,
|
pub far_notes: Option<i64>,
|
||||||
pub created_at: chrono::NaiveDateTime,
|
pub score: i64,
|
||||||
pub creation_ptt: Option<i64>,
|
|
||||||
pub creation_zeta_ptt: Option<i64>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DbPlay {
|
// }}}
|
||||||
#[inline]
|
// {{{ Score data
|
||||||
pub fn into_play(self) -> Play {
|
#[derive(Debug, Clone, Copy)]
|
||||||
Play {
|
pub struct ScoreCollection([Score; ScoringSystem::SCORING_SYSTEMS.len()]);
|
||||||
id: self.id as u32,
|
|
||||||
chart_id: self.chart_id as u32,
|
impl ScoreCollection {
|
||||||
user_id: self.user_id as u32,
|
pub fn from_standard_score(score: Score, chart: &Chart) -> Self {
|
||||||
score: Score(self.score as u32),
|
ScoreCollection(array::from_fn(|i| {
|
||||||
zeta_score: Score(self.zeta_score as u32),
|
score.convert_to(ScoringSystem::SCORING_SYSTEMS[i], chart)
|
||||||
max_recall: self.max_recall.map(|r| r as u32),
|
}))
|
||||||
far_notes: self.far_notes.map(|r| r as u32),
|
|
||||||
created_at: self.created_at,
|
|
||||||
discord_attachment_id: self
|
|
||||||
.discord_attachment_id
|
|
||||||
.and_then(|s| AttachmentId::from_str(&s).ok()),
|
|
||||||
creation_ptt: self.creation_ptt.map(|r| r as i32),
|
|
||||||
creation_zeta_ptt: self.creation_zeta_ptt.map(|r| r as i32),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
|
@ -179,48 +173,38 @@ impl DbPlay {
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Play {
|
pub struct Play {
|
||||||
pub id: u32,
|
pub id: u32,
|
||||||
|
#[allow(unused)]
|
||||||
pub chart_id: u32,
|
pub chart_id: u32,
|
||||||
pub user_id: u32,
|
pub user_id: u32,
|
||||||
|
|
||||||
#[allow(unused)]
|
|
||||||
pub discord_attachment_id: Option<AttachmentId>,
|
|
||||||
|
|
||||||
// Actual score data
|
|
||||||
pub score: Score,
|
|
||||||
pub zeta_score: Score,
|
|
||||||
|
|
||||||
// Optional score details
|
|
||||||
pub max_recall: Option<u32>,
|
|
||||||
pub far_notes: Option<u32>,
|
|
||||||
|
|
||||||
// Creation data
|
|
||||||
pub created_at: chrono::NaiveDateTime,
|
pub created_at: chrono::NaiveDateTime,
|
||||||
|
|
||||||
#[allow(dead_code)]
|
// Score details
|
||||||
pub creation_ptt: Option<i32>,
|
pub max_recall: Option<u32>,
|
||||||
#[allow(dead_code)]
|
pub far_notes: Option<u32>,
|
||||||
pub creation_zeta_ptt: Option<i32>,
|
pub scores: ScoreCollection,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Play {
|
impl Play {
|
||||||
// {{{ Query the underlying score
|
// {{{ Query the underlying score
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn score(&self, system: ScoringSystem) -> Score {
|
pub fn score(&self, system: ScoringSystem) -> Score {
|
||||||
match system {
|
self.scores.0[system.to_index()]
|
||||||
ScoringSystem::Standard => self.score,
|
|
||||||
ScoringSystem::EX => self.zeta_score,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn play_rating(&self, system: ScoringSystem, chart_constant: u32) -> i32 {
|
pub fn play_rating(&self, system: ScoringSystem, chart_constant: u32) -> Rational32 {
|
||||||
self.score(system).play_rating(chart_constant)
|
self.score(system).play_rating(chart_constant)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn play_rating_f32(&self, system: ScoringSystem, chart_constant: u32) -> f32 {
|
||||||
|
rating_as_float(self.score(system).play_rating(chart_constant))
|
||||||
|
}
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Play => distribution
|
// {{{ Play => distribution
|
||||||
pub fn distribution(&self, note_count: u32) -> Option<(u32, u32, u32, u32)> {
|
pub fn distribution(&self, note_count: u32) -> Option<(u32, u32, u32, u32)> {
|
||||||
if let Some(fars) = self.far_notes {
|
if let Some(fars) = self.far_notes {
|
||||||
let (_, shinies, units) = self.score.analyse(note_count);
|
let (_, shinies, units) = self.score(ScoringSystem::Standard).analyse(note_count);
|
||||||
let (pures, rem) = units.checked_sub(fars)?.div_rem_euclid(&2);
|
let (pures, rem) = units.checked_sub(fars)?.div_rem_euclid(&2);
|
||||||
if rem == 1 {
|
if rem == 1 {
|
||||||
println!("The impossible happened: got an invalid amount of far notes!");
|
println!("The impossible happened: got an invalid amount of far notes!");
|
||||||
|
@ -237,8 +221,8 @@ impl Play {
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Play => status
|
// {{{ Play => status
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn status(&self, chart: &Chart) -> Option<String> {
|
pub fn status(&self, scoring_system: ScoringSystem, chart: &Chart) -> Option<String> {
|
||||||
let score = self.score.0;
|
let score = self.score(scoring_system).0;
|
||||||
if score >= 10_000_000 {
|
if score >= 10_000_000 {
|
||||||
if score > chart.note_count + 10_000_000 {
|
if score > chart.note_count + 10_000_000 {
|
||||||
return None;
|
return None;
|
||||||
|
@ -266,8 +250,8 @@ impl Play {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn short_status(&self, chart: &Chart) -> Option<char> {
|
pub fn short_status(&self, scoring_system: ScoringSystem, chart: &Chart) -> Option<char> {
|
||||||
let score = self.score.0;
|
let score = self.score(scoring_system).0;
|
||||||
if score >= 10_000_000 {
|
if score >= 10_000_000 {
|
||||||
let non_max_pures = (chart.note_count + 10_000_000).checked_sub(score)?;
|
let non_max_pures = (chart.note_count + 10_000_000).checked_sub(score)?;
|
||||||
if non_max_pures == 0 {
|
if non_max_pures == 0 {
|
||||||
|
@ -298,14 +282,18 @@ impl Play {
|
||||||
author: Option<&poise::serenity_prelude::User>,
|
author: Option<&poise::serenity_prelude::User>,
|
||||||
) -> Result<(CreateEmbed, Option<CreateAttachment>), Error> {
|
) -> Result<(CreateEmbed, Option<CreateAttachment>), Error> {
|
||||||
// {{{ Get previously best score
|
// {{{ Get previously best score
|
||||||
let prev_play = query_as!(
|
let prev_play = query!(
|
||||||
DbPlay,
|
|
||||||
"
|
"
|
||||||
SELECT * FROM plays
|
SELECT
|
||||||
WHERE user_id=?
|
p.id, p.chart_id, p.user_id, p.created_at,
|
||||||
AND chart_id=?
|
p.max_recall, p.far_notes, s.score
|
||||||
AND created_at<?
|
FROM plays p
|
||||||
ORDER BY score DESC
|
JOIN scores s ON s.play_id = p.id
|
||||||
|
WHERE s.scoring_system='standard'
|
||||||
|
AND p.user_id=?
|
||||||
|
AND p.chart_id=?
|
||||||
|
AND p.created_at<?
|
||||||
|
ORDER BY s.score DESC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
",
|
",
|
||||||
user.id,
|
user.id,
|
||||||
|
@ -320,13 +308,18 @@ impl Play {
|
||||||
song.title, chart.difficulty
|
song.title, chart.difficulty
|
||||||
)
|
)
|
||||||
})?
|
})?
|
||||||
.map(|p| p.into_play());
|
.map(|p| play_from_db_record!(chart, p));
|
||||||
|
|
||||||
let prev_score = prev_play.as_ref().map(|p| p.score);
|
let prev_score = prev_play.as_ref().map(|p| p.score(ScoringSystem::Standard));
|
||||||
let prev_zeta_score = prev_play.as_ref().map(|p| p.zeta_score);
|
let prev_zeta_score = prev_play.as_ref().map(|p| p.score(ScoringSystem::EX));
|
||||||
// }}}
|
// }}}
|
||||||
|
|
||||||
let attachement_name = format!("{:?}-{:?}-{:?}.png", song.id, self.score.0, index);
|
let attachement_name = format!(
|
||||||
|
"{:?}-{:?}-{:?}.png",
|
||||||
|
song.id,
|
||||||
|
self.score(ScoringSystem::Standard).0,
|
||||||
|
index
|
||||||
|
);
|
||||||
let icon_attachement = match chart.cached_jacket.as_ref() {
|
let icon_attachement = match chart.cached_jacket.as_ref() {
|
||||||
Some(jacket) => Some(CreateAttachment::bytes(jacket.raw, &attachement_name)),
|
Some(jacket) => Some(CreateAttachment::bytes(jacket.raw, &attachement_name)),
|
||||||
None => None,
|
None => None,
|
||||||
|
@ -337,30 +330,46 @@ impl Play {
|
||||||
"{} [{:?} {}]",
|
"{} [{:?} {}]",
|
||||||
&song.title, chart.difficulty, chart.level
|
&song.title, chart.difficulty, chart.level
|
||||||
))
|
))
|
||||||
.field("Score", self.score.display_with_diff(prev_score)?, true)
|
|
||||||
.field(
|
.field(
|
||||||
"Rating",
|
"Score",
|
||||||
self.score.display_play_rating(prev_score, chart)?,
|
self.score(ScoringSystem::Standard)
|
||||||
|
.display_with_diff(prev_score)?,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.field(
|
||||||
|
"Rating",
|
||||||
|
self.score(ScoringSystem::Standard)
|
||||||
|
.display_play_rating(prev_score, chart)?,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.field(
|
||||||
|
"Grade",
|
||||||
|
format!("{}", self.score(ScoringSystem::Standard).grade()),
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
.field("Grade", format!("{}", self.score.grade()), true)
|
|
||||||
.field(
|
.field(
|
||||||
"ξ-Score",
|
"ξ-Score",
|
||||||
self.zeta_score.display_with_diff(prev_zeta_score)?,
|
self.score(ScoringSystem::EX)
|
||||||
|
.display_with_diff(prev_zeta_score)?,
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
// {{{ ξ-Rating
|
// {{{ ξ-Rating
|
||||||
.field(
|
.field(
|
||||||
"ξ-Rating",
|
"ξ-Rating",
|
||||||
self.zeta_score
|
self.score(ScoringSystem::EX)
|
||||||
.display_play_rating(prev_zeta_score, chart)?,
|
.display_play_rating(prev_zeta_score, chart)?,
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
// }}}
|
// }}}
|
||||||
.field("ξ-Grade", format!("{}", self.zeta_score.grade()), true)
|
.field(
|
||||||
|
"ξ-Grade",
|
||||||
|
format!("{}", self.score(ScoringSystem::EX).grade()),
|
||||||
|
true,
|
||||||
|
)
|
||||||
.field(
|
.field(
|
||||||
"Status",
|
"Status",
|
||||||
self.status(chart).unwrap_or("-".to_string()),
|
self.status(ScoringSystem::Standard, chart)
|
||||||
|
.unwrap_or("-".to_string()),
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
.field(
|
.field(
|
||||||
|
@ -402,24 +411,33 @@ pub type PlayCollection<'a> = Vec<(Play, &'a Song, &'a Chart)>;
|
||||||
pub async fn get_best_plays<'a>(
|
pub async fn get_best_plays<'a>(
|
||||||
db: &SqlitePool,
|
db: &SqlitePool,
|
||||||
song_cache: &'a SongCache,
|
song_cache: &'a SongCache,
|
||||||
user: &User,
|
user_id: u32,
|
||||||
scoring_system: ScoringSystem,
|
scoring_system: ScoringSystem,
|
||||||
min_amount: usize,
|
min_amount: usize,
|
||||||
max_amount: usize,
|
max_amount: usize,
|
||||||
|
before: Option<NaiveDateTime>,
|
||||||
) -> Result<Result<PlayCollection<'a>, String>, Error> {
|
) -> Result<Result<PlayCollection<'a>, String>, Error> {
|
||||||
// {{{ DB data fetching
|
// {{{ DB data fetching
|
||||||
let plays: Vec<DbPlay> = query_as(
|
let plays: Vec<DbPlay> = query_as(
|
||||||
"
|
"
|
||||||
SELECT id, chart_id, user_id,
|
SELECT
|
||||||
created_at, MAX(score) as score, zeta_score,
|
p.id, p.chart_id, p.user_id, p.created_at,
|
||||||
creation_ptt, creation_zeta_ptt, far_notes, max_recall, discord_attachment_id
|
p.max_recall, p.far_notes, s.score,
|
||||||
|
MAX(s.score) as _cscore
|
||||||
|
-- ^ This is only here to make sqlite pick the correct row for the bare columns
|
||||||
FROM plays p
|
FROM plays p
|
||||||
WHERE user_id = ?
|
JOIN scores s ON s.play_id = p.id
|
||||||
GROUP BY chart_id
|
JOIN scores cs ON cs.play_id = p.id
|
||||||
ORDER BY score DESC
|
WHERE s.scoring_system='standard'
|
||||||
|
AND cs.scoring_system=?
|
||||||
|
AND p.user_id=?
|
||||||
|
AND p.created_at<=?
|
||||||
|
GROUP BY p.chart_id
|
||||||
",
|
",
|
||||||
)
|
)
|
||||||
.bind(user.id)
|
.bind(ScoringSystem::SCORING_SYSTEM_DB_STRINGS[scoring_system.to_index()])
|
||||||
|
.bind(user_id)
|
||||||
|
.bind(before.unwrap_or_else(|| Utc::now().naive_utc()))
|
||||||
.fetch_all(db)
|
.fetch_all(db)
|
||||||
.await?;
|
.await?;
|
||||||
// }}}
|
// }}}
|
||||||
|
@ -437,8 +455,8 @@ pub async fn get_best_plays<'a>(
|
||||||
let mut plays: Vec<(Play, &Song, &Chart)> = plays
|
let mut plays: Vec<(Play, &Song, &Chart)> = plays
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|play| {
|
.map(|play| {
|
||||||
let play = play.into_play();
|
let (song, chart) = song_cache.lookup_chart(play.chart_id as u32)?;
|
||||||
let (song, chart) = song_cache.lookup_chart(play.chart_id)?;
|
let play = play_from_db_record!(chart, play);
|
||||||
Ok((play, song, chart))
|
Ok((play, song, chart))
|
||||||
})
|
})
|
||||||
.collect::<Result<Vec<_>, Error>>()?;
|
.collect::<Result<Vec<_>, Error>>()?;
|
||||||
|
@ -451,12 +469,78 @@ pub async fn get_best_plays<'a>(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn compute_b30_ptt(scoring_system: ScoringSystem, plays: &PlayCollection<'_>) -> i32 {
|
pub fn compute_b30_ptt(scoring_system: ScoringSystem, plays: &PlayCollection<'_>) -> Rational32 {
|
||||||
plays
|
plays
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(play, _, chart)| play.play_rating(scoring_system, chart.chart_constant))
|
.map(|(play, _, chart)| play.play_rating(scoring_system, chart.chart_constant))
|
||||||
.sum::<i32>()
|
.sum::<Rational32>()
|
||||||
.checked_div(plays.len() as i32)
|
.checked_div(&Rational32::from_integer(plays.len() as i32))
|
||||||
.unwrap_or(0)
|
.unwrap_or(Rational32::zero())
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
// {{{ Maintenance functions
|
||||||
|
pub async fn generate_missing_scores(ctx: &UserContext) -> Result<(), Error> {
|
||||||
|
let plays = query!(
|
||||||
|
"
|
||||||
|
SELECT
|
||||||
|
p.id, p.chart_id, p.user_id, p.created_at,
|
||||||
|
p.max_recall, p.far_notes, s.score
|
||||||
|
FROM plays p
|
||||||
|
JOIN scores s ON s.play_id = p.id
|
||||||
|
WHERE s.scoring_system='standard'
|
||||||
|
ORDER BY p.created_at ASC
|
||||||
|
"
|
||||||
|
)
|
||||||
|
// Can't use the stream based version because of db locking...
|
||||||
|
.fetch_all(&ctx.db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut i = 0;
|
||||||
|
|
||||||
|
for play in plays {
|
||||||
|
let (_, chart) = ctx.song_cache.lookup_chart(play.chart_id as u32)?;
|
||||||
|
let play = play_from_db_record!(chart, play);
|
||||||
|
|
||||||
|
for system in ScoringSystem::SCORING_SYSTEMS {
|
||||||
|
let i = system.to_index();
|
||||||
|
let plays = get_best_plays(
|
||||||
|
&ctx.db,
|
||||||
|
&ctx.song_cache,
|
||||||
|
play.user_id,
|
||||||
|
system,
|
||||||
|
30,
|
||||||
|
30,
|
||||||
|
Some(play.created_at),
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
let creation_ptt: Option<_> = try { rating_as_fixed(compute_b30_ptt(system, &plays?)) };
|
||||||
|
let raw_score = play.scores.0[i].0;
|
||||||
|
|
||||||
|
query!(
|
||||||
|
"
|
||||||
|
INSERT INTO scores(play_id, score, creation_ptt, scoring_system)
|
||||||
|
VALUES ($1, $2, $3, $4)
|
||||||
|
ON CONFLICT(play_id, scoring_system)
|
||||||
|
DO UPDATE SET
|
||||||
|
score=$2, creation_ptt=$3
|
||||||
|
WHERE play_id = $1
|
||||||
|
AND scoring_system = $4
|
||||||
|
|
||||||
|
",
|
||||||
|
play.id,
|
||||||
|
raw_score,
|
||||||
|
creation_ptt,
|
||||||
|
ScoringSystem::SCORING_SYSTEM_DB_STRINGS[i],
|
||||||
|
)
|
||||||
|
.execute(&ctx.db)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
i += 1;
|
||||||
|
println!("Processed {i} plays");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
|
|
23
src/arcaea/rating.rs
Normal file
23
src/arcaea/rating.rs
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
use num::Rational32;
|
||||||
|
|
||||||
|
pub type Rating = Rational32;
|
||||||
|
|
||||||
|
/// Saves a rating rational as an integer where it's multiplied by 100.
|
||||||
|
#[inline]
|
||||||
|
pub fn rating_as_fixed(rating: Rating) -> i32 {
|
||||||
|
(rating * Rational32::from_integer(100))
|
||||||
|
.round()
|
||||||
|
.to_integer()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Saves a rating rational as a float with precision 2.
|
||||||
|
#[inline]
|
||||||
|
pub fn rating_as_float(rating: Rating) -> f32 {
|
||||||
|
rating_as_fixed(rating) as f32 / 100.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The pseudo-inverse of `rating_as_fixed`.
|
||||||
|
#[inline]
|
||||||
|
pub fn rating_from_fixed(fixed: i32) -> Rating {
|
||||||
|
Rating::new(fixed, 100)
|
||||||
|
}
|
|
@ -1,10 +1,13 @@
|
||||||
use std::fmt::{Display, Write};
|
use std::fmt::{Display, Write};
|
||||||
|
|
||||||
use num::Rational64;
|
use num::{Rational32, Rational64};
|
||||||
|
|
||||||
use crate::context::Error;
|
use crate::context::Error;
|
||||||
|
|
||||||
use super::chart::Chart;
|
use super::{
|
||||||
|
chart::Chart,
|
||||||
|
rating::{rating_as_float, rating_from_fixed, Rating},
|
||||||
|
};
|
||||||
|
|
||||||
// {{{ Scoring system
|
// {{{ Scoring system
|
||||||
#[derive(Debug, Clone, Copy, poise::ChoiceParameter)]
|
#[derive(Debug, Clone, Copy, poise::ChoiceParameter)]
|
||||||
|
@ -15,6 +18,18 @@ pub enum ScoringSystem {
|
||||||
EX,
|
EX,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ScoringSystem {
|
||||||
|
pub const SCORING_SYSTEMS: [Self; 2] = [Self::Standard, Self::EX];
|
||||||
|
|
||||||
|
/// Values used inside sqlite
|
||||||
|
pub const SCORING_SYSTEM_DB_STRINGS: [&'static str; 2] = ["standard", "ex"];
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn to_index(self) -> usize {
|
||||||
|
self as usize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for ScoringSystem {
|
impl Default for ScoringSystem {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::Standard
|
Self::Standard
|
||||||
|
@ -125,32 +140,39 @@ impl Score {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
|
// {{{ Scoring system conversion
|
||||||
|
/// Convert a standard score to any other scoring system. The output might be
|
||||||
|
/// nonsense if the given score is not using the standard system.
|
||||||
|
#[inline]
|
||||||
|
pub fn convert_to(self, scoring_system: ScoringSystem, chart: &Chart) -> Self {
|
||||||
|
match scoring_system {
|
||||||
|
ScoringSystem::Standard => self,
|
||||||
|
ScoringSystem::EX => self.to_zeta(chart.note_count),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
// {{{ Score => Play rating
|
// {{{ Score => Play rating
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn play_rating(self, chart_constant: u32) -> i32 {
|
pub fn play_rating(self, chart_constant: u32) -> Rating {
|
||||||
chart_constant as i32
|
rating_from_fixed(chart_constant as i32)
|
||||||
+ if self.0 >= 10_000_000 {
|
+ if self.0 >= 10_000_000 {
|
||||||
200
|
Rational32::from_integer(2)
|
||||||
} else if self.0 >= 9_800_000 {
|
} else if self.0 >= 9_800_000 {
|
||||||
100 + (self.0 as i32 - 9_800_000) / 2_000
|
Rational32::from_integer(1)
|
||||||
|
+ Rational32::new(self.0 as i32 - 9_800_000, 200_000).reduced()
|
||||||
} else {
|
} else {
|
||||||
(self.0 as i32 - 9_500_000) / 3_000
|
Rational32::new(self.0 as i32 - 9_500_000, 300_000).reduced()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn play_rating_f32(self, chart_constant: u32) -> f32 {
|
|
||||||
(self.play_rating(chart_constant)) as f32 / 100.0
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn display_play_rating(self, prev: Option<Self>, chart: &Chart) -> Result<String, Error> {
|
pub fn display_play_rating(self, prev: Option<Self>, chart: &Chart) -> Result<String, Error> {
|
||||||
let mut buffer = String::with_capacity(14);
|
let mut buffer = String::with_capacity(14);
|
||||||
|
|
||||||
let play_rating = self.play_rating_f32(chart.chart_constant);
|
let play_rating = rating_as_float(self.play_rating(chart.chart_constant));
|
||||||
write!(buffer, "{:.2}", play_rating)?;
|
write!(buffer, "{:.2}", play_rating)?;
|
||||||
|
|
||||||
if let Some(prev) = prev {
|
if let Some(prev) = prev {
|
||||||
let prev_play_rating = prev.play_rating_f32(chart.chart_constant);
|
let prev_play_rating = rating_as_float(prev.play_rating(chart.chart_constant));
|
||||||
|
|
||||||
if play_rating >= prev_play_rating {
|
if play_rating >= prev_play_rating {
|
||||||
write!(buffer, " (+{:.2})", play_rating - prev_play_rating)?;
|
write!(buffer, " (+{:.2})", play_rating - prev_play_rating)?;
|
||||||
|
|
|
@ -4,7 +4,7 @@ use sqlx::query;
|
||||||
use crate::{
|
use crate::{
|
||||||
arcaea::chart::Side,
|
arcaea::chart::Side,
|
||||||
context::{Context, Error},
|
context::{Context, Error},
|
||||||
get_user,
|
get_user, play_from_db_record,
|
||||||
recognition::fuzzy_song_name::guess_song_and_chart,
|
recognition::fuzzy_song_name::guess_song_and_chart,
|
||||||
};
|
};
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
|
@ -20,13 +20,9 @@ use plotters::{
|
||||||
style::{IntoFont, TextStyle, BLUE, WHITE},
|
style::{IntoFont, TextStyle, BLUE, WHITE},
|
||||||
};
|
};
|
||||||
use poise::CreateReply;
|
use poise::CreateReply;
|
||||||
use sqlx::query_as;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
arcaea::{
|
arcaea::score::{Score, ScoringSystem},
|
||||||
play::DbPlay,
|
|
||||||
score::{Score, ScoringSystem},
|
|
||||||
},
|
|
||||||
user::discord_it_to_discord_user,
|
user::discord_it_to_discord_user,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -121,13 +117,18 @@ async fn best(
|
||||||
let user = get_user!(&ctx);
|
let user = get_user!(&ctx);
|
||||||
|
|
||||||
let (song, chart) = guess_song_and_chart(&ctx.data(), &name)?;
|
let (song, chart) = guess_song_and_chart(&ctx.data(), &name)?;
|
||||||
let play = query_as!(
|
let play = query!(
|
||||||
DbPlay,
|
|
||||||
"
|
"
|
||||||
SELECT * FROM plays
|
SELECT
|
||||||
WHERE user_id=?
|
p.id, p.chart_id, p.user_id, p.created_at,
|
||||||
AND chart_id=?
|
p.max_recall, p.far_notes, s.score
|
||||||
ORDER BY score DESC
|
FROM plays p
|
||||||
|
JOIN scores s ON s.play_id = p.id
|
||||||
|
WHERE s.scoring_system='standard'
|
||||||
|
AND p.user_id=?
|
||||||
|
AND p.chart_id=?
|
||||||
|
ORDER BY s.score DESC
|
||||||
|
LIMIT 1
|
||||||
",
|
",
|
||||||
user.id,
|
user.id,
|
||||||
chart.id
|
chart.id
|
||||||
|
@ -139,8 +140,8 @@ async fn best(
|
||||||
"Could not find any scores for {} [{:?}]",
|
"Could not find any scores for {} [{:?}]",
|
||||||
song.title, chart.difficulty
|
song.title, chart.difficulty
|
||||||
)
|
)
|
||||||
})?
|
})?;
|
||||||
.into_play();
|
let play = play_from_db_record!(chart, play);
|
||||||
|
|
||||||
let (embed, attachment) = play
|
let (embed, attachment) = play
|
||||||
.to_embed(
|
.to_embed(
|
||||||
|
@ -176,13 +177,17 @@ async fn plot(
|
||||||
let (song, chart) = guess_song_and_chart(&ctx.data(), &name)?;
|
let (song, chart) = guess_song_and_chart(&ctx.data(), &name)?;
|
||||||
|
|
||||||
// SAFETY: we limit the amount of plotted plays to 1000.
|
// SAFETY: we limit the amount of plotted plays to 1000.
|
||||||
let plays = query_as!(
|
let plays = query!(
|
||||||
DbPlay,
|
|
||||||
"
|
"
|
||||||
SELECT * FROM plays
|
SELECT
|
||||||
WHERE user_id=?
|
p.id, p.chart_id, p.user_id, p.created_at,
|
||||||
AND chart_id=?
|
p.max_recall, p.far_notes, s.score
|
||||||
ORDER BY created_at ASC
|
FROM plays p
|
||||||
|
JOIN scores s ON s.play_id = p.id
|
||||||
|
WHERE s.scoring_system='standard'
|
||||||
|
AND p.user_id=?
|
||||||
|
AND p.chart_id=?
|
||||||
|
ORDER BY s.score DESC
|
||||||
LIMIT 1000
|
LIMIT 1000
|
||||||
",
|
",
|
||||||
user.id,
|
user.id,
|
||||||
|
@ -204,7 +209,7 @@ async fn plot(
|
||||||
let max_time = plays.iter().map(|p| p.created_at).max().unwrap();
|
let max_time = plays.iter().map(|p| p.created_at).max().unwrap();
|
||||||
let mut min_score = plays
|
let mut min_score = plays
|
||||||
.iter()
|
.iter()
|
||||||
.map(|p| p.clone().into_play().score(scoring_system))
|
.map(|p| play_from_db_record!(chart, p).score(scoring_system))
|
||||||
.min()
|
.min()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.0 as i64;
|
.0 as i64;
|
||||||
|
@ -228,7 +233,7 @@ async fn plot(
|
||||||
{
|
{
|
||||||
let root = BitMapBackend::with_buffer(&mut buffer, (width, height)).into_drawing_area();
|
let root = BitMapBackend::with_buffer(&mut buffer, (width, height)).into_drawing_area();
|
||||||
|
|
||||||
let mut chart = ChartBuilder::on(&root)
|
let mut chart_buider = ChartBuilder::on(&root)
|
||||||
.margin(25)
|
.margin(25)
|
||||||
.caption(
|
.caption(
|
||||||
format!("{} [{:?}]", song.title, chart.difficulty),
|
format!("{} [{:?}]", song.title, chart.difficulty),
|
||||||
|
@ -241,7 +246,7 @@ async fn plot(
|
||||||
min_score..max_score,
|
min_score..max_score,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
chart
|
chart_buider
|
||||||
.configure_mesh()
|
.configure_mesh()
|
||||||
.light_line_style(WHITE)
|
.light_line_style(WHITE)
|
||||||
.y_label_formatter(&|s| format!("{}", Score(*s as u32)))
|
.y_label_formatter(&|s| format!("{}", Score(*s as u32)))
|
||||||
|
@ -261,7 +266,7 @@ async fn plot(
|
||||||
.map(|play| {
|
.map(|play| {
|
||||||
(
|
(
|
||||||
play.created_at.and_utc().timestamp_millis(),
|
play.created_at.and_utc().timestamp_millis(),
|
||||||
play.into_play().score(scoring_system),
|
play_from_db_record!(chart, play).score(scoring_system),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
@ -269,12 +274,12 @@ async fn plot(
|
||||||
points.sort();
|
points.sort();
|
||||||
points.dedup();
|
points.dedup();
|
||||||
|
|
||||||
chart.draw_series(LineSeries::new(
|
chart_buider.draw_series(LineSeries::new(
|
||||||
points.iter().map(|(t, s)| (*t, s.0 as i64)),
|
points.iter().map(|(t, s)| (*t, s.0 as i64)),
|
||||||
&BLUE,
|
&BLUE,
|
||||||
))?;
|
))?;
|
||||||
|
|
||||||
chart.draw_series(points.iter().map(|(t, s)| {
|
chart_buider.draw_series(points.iter().map(|(t, s)| {
|
||||||
Circle::new((*t, s.0 as i64), 3, plotters::style::Color::filled(&BLUE))
|
Circle::new((*t, s.0 as i64), 3, plotters::style::Color::filled(&BLUE))
|
||||||
}))?;
|
}))?;
|
||||||
root.present()?;
|
root.present()?;
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
use crate::arcaea::play::{CreatePlay, Play};
|
use crate::arcaea::play::CreatePlay;
|
||||||
use crate::arcaea::score::Score;
|
use crate::arcaea::score::Score;
|
||||||
use crate::context::{Context, Error};
|
use crate::context::{Context, Error};
|
||||||
use crate::recognition::recognize::{ImageAnalyzer, ScoreKind};
|
use crate::recognition::recognize::{ImageAnalyzer, ScoreKind};
|
||||||
use crate::user::{discord_it_to_discord_user, User};
|
use crate::user::{discord_it_to_discord_user, User};
|
||||||
use crate::{edit_reply, get_user, timed};
|
use crate::{edit_reply, get_user, play_from_db_record, timed};
|
||||||
use image::DynamicImage;
|
use image::DynamicImage;
|
||||||
use poise::serenity_prelude::futures::future::join_all;
|
use poise::serenity_prelude::futures::future::join_all;
|
||||||
use poise::serenity_prelude::CreateMessage;
|
use poise::serenity_prelude::CreateMessage;
|
||||||
|
@ -117,11 +117,11 @@ pub async fn magic(
|
||||||
let maybe_fars =
|
let maybe_fars =
|
||||||
Score::resolve_distibution_ambiguities(score, note_distribution, chart.note_count);
|
Score::resolve_distibution_ambiguities(score, note_distribution, chart.note_count);
|
||||||
|
|
||||||
let play = CreatePlay::new(score, &chart)
|
let play = CreatePlay::new(score)
|
||||||
.with_attachment(file)
|
.with_attachment(file)
|
||||||
.with_fars(maybe_fars)
|
.with_fars(maybe_fars)
|
||||||
.with_max_recall(max_recall)
|
.with_max_recall(max_recall)
|
||||||
.save(&ctx.data(), &user)
|
.save(&ctx.data(), &user, &chart)
|
||||||
.await?;
|
.await?;
|
||||||
// }}}
|
// }}}
|
||||||
// }}}
|
// }}}
|
||||||
|
@ -220,12 +220,16 @@ pub async fn show(
|
||||||
let res = query!(
|
let res = query!(
|
||||||
"
|
"
|
||||||
SELECT
|
SELECT
|
||||||
p.id,p.chart_id,p.user_id,p.score,p.zeta_score,
|
p.id, p.chart_id, p.user_id, p.created_at,
|
||||||
p.max_recall,p.created_at,p.far_notes,
|
p.max_recall, p.far_notes, s.score,
|
||||||
u.discord_id
|
u.discord_id
|
||||||
FROM plays p
|
FROM plays p
|
||||||
|
JOIN scores s ON s.play_id = p.id
|
||||||
JOIN users u ON p.user_id = u.id
|
JOIN users u ON p.user_id = u.id
|
||||||
WHERE p.id=?
|
WHERE s.scoring_system='standard'
|
||||||
|
AND p.id=?
|
||||||
|
ORDER BY s.score DESC
|
||||||
|
LIMIT 1
|
||||||
",
|
",
|
||||||
id
|
id
|
||||||
)
|
)
|
||||||
|
@ -233,24 +237,12 @@ pub async fn show(
|
||||||
.await
|
.await
|
||||||
.map_err(|_| format!("Could not find play with id {}", id))?;
|
.map_err(|_| format!("Could not find play with id {}", id))?;
|
||||||
|
|
||||||
let play = Play {
|
let (song, chart) = ctx.data().song_cache.lookup_chart(res.chart_id as u32)?;
|
||||||
id: res.id as u32,
|
let play = play_from_db_record!(chart, res);
|
||||||
chart_id: res.chart_id as u32,
|
|
||||||
user_id: res.user_id as u32,
|
|
||||||
score: Score(res.score as u32),
|
|
||||||
zeta_score: Score(res.zeta_score as u32),
|
|
||||||
max_recall: res.max_recall.map(|r| r as u32),
|
|
||||||
far_notes: res.far_notes.map(|r| r as u32),
|
|
||||||
created_at: res.created_at,
|
|
||||||
discord_attachment_id: None,
|
|
||||||
creation_ptt: None,
|
|
||||||
creation_zeta_ptt: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let author = discord_it_to_discord_user(&ctx, &res.discord_id).await?;
|
let author = discord_it_to_discord_user(&ctx, &res.discord_id).await?;
|
||||||
let user = User::by_id(&ctx.data().db, play.user_id).await?;
|
let user = User::by_id(&ctx.data().db, play.user_id).await?;
|
||||||
|
|
||||||
let (song, chart) = ctx.data().song_cache.lookup_chart(play.chart_id)?;
|
|
||||||
let (embed, attachment) = play
|
let (embed, attachment) = play
|
||||||
.to_embed(&ctx.data().db, &user, song, chart, i, Some(&author))
|
.to_embed(&ctx.data().db, &user, song, chart, i, Some(&author))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
|
@ -13,6 +13,7 @@ use crate::{
|
||||||
chart::Level,
|
chart::Level,
|
||||||
jacket::BITMAP_IMAGE_SIZE,
|
jacket::BITMAP_IMAGE_SIZE,
|
||||||
play::{compute_b30_ptt, get_best_plays},
|
play::{compute_b30_ptt, get_best_plays},
|
||||||
|
rating::rating_as_float,
|
||||||
score::ScoringSystem,
|
score::ScoringSystem,
|
||||||
},
|
},
|
||||||
assert_is_pookie,
|
assert_is_pookie,
|
||||||
|
@ -55,14 +56,15 @@ async fn best_plays(
|
||||||
get_best_plays(
|
get_best_plays(
|
||||||
&user_ctx.db,
|
&user_ctx.db,
|
||||||
&user_ctx.song_cache,
|
&user_ctx.song_cache,
|
||||||
&user,
|
user.id,
|
||||||
scoring_system,
|
scoring_system,
|
||||||
if require_full {
|
if require_full {
|
||||||
grid_size.0 * grid_size.1
|
grid_size.0 * grid_size.1
|
||||||
} else {
|
} else {
|
||||||
grid_size.0 * (grid_size.1.max(1) - 1) + 1
|
grid_size.0 * (grid_size.1.max(1) - 1) + 1
|
||||||
} as usize,
|
} as usize,
|
||||||
(grid_size.0 * grid_size.1) as usize
|
(grid_size.0 * grid_size.1) as usize,
|
||||||
|
None
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
);
|
);
|
||||||
|
@ -287,7 +289,7 @@ async fn best_plays(
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Display status text
|
// {{{ Display status text
|
||||||
with_font(&EXO_FONT, |faces| {
|
with_font(&EXO_FONT, |faces| {
|
||||||
let status = play.short_status(chart).ok_or_else(|| {
|
let status = play.short_status(scoring_system, chart).ok_or_else(|| {
|
||||||
format!(
|
format!(
|
||||||
"Could not get status for score {}",
|
"Could not get status for score {}",
|
||||||
play.score(scoring_system)
|
play.score(scoring_system)
|
||||||
|
@ -379,7 +381,7 @@ async fn best_plays(
|
||||||
style,
|
style,
|
||||||
&format!(
|
&format!(
|
||||||
"{:.2}",
|
"{:.2}",
|
||||||
play.play_rating(scoring_system, chart.chart_constant) as f32 / 100.0
|
play.play_rating_f32(scoring_system, chart.chart_constant)
|
||||||
),
|
),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
@ -415,7 +417,7 @@ async fn best_plays(
|
||||||
.attachment(CreateAttachment::bytes(out_buffer, "b30.png"))
|
.attachment(CreateAttachment::bytes(out_buffer, "b30.png"))
|
||||||
.content(format!(
|
.content(format!(
|
||||||
"Your ptt is {:.2}",
|
"Your ptt is {:.2}",
|
||||||
compute_b30_ptt(scoring_system, &plays) as f32 / 100.0
|
rating_as_float(compute_b30_ptt(scoring_system, &plays))
|
||||||
));
|
));
|
||||||
ctx.send(reply).await?;
|
ctx.send(reply).await?;
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
#![feature(async_closure)]
|
#![feature(async_closure)]
|
||||||
#![feature(try_blocks)]
|
#![feature(try_blocks)]
|
||||||
#![feature(thread_local)]
|
#![feature(thread_local)]
|
||||||
|
#![feature(generic_arg_infer)]
|
||||||
|
|
||||||
mod arcaea;
|
mod arcaea;
|
||||||
mod assets;
|
mod assets;
|
||||||
|
@ -18,6 +19,7 @@ mod time;
|
||||||
mod transform;
|
mod transform;
|
||||||
mod user;
|
mod user;
|
||||||
|
|
||||||
|
use arcaea::play::generate_missing_scores;
|
||||||
use assets::get_data_dir;
|
use assets::get_data_dir;
|
||||||
use context::{Error, UserContext};
|
use context::{Error, UserContext};
|
||||||
use poise::serenity_prelude::{self as serenity};
|
use poise::serenity_prelude::{self as serenity};
|
||||||
|
@ -89,6 +91,12 @@ async fn main() {
|
||||||
poise::builtins::register_globally(ctx, &framework.options().commands).await?;
|
poise::builtins::register_globally(ctx, &framework.options().commands).await?;
|
||||||
let ctx = UserContext::new(pool).await?;
|
let ctx = UserContext::new(pool).await?;
|
||||||
|
|
||||||
|
if var("SHIMMERING_REGEN_SCORES").unwrap_or_default() == "1" {
|
||||||
|
timed!("generate_missing_scores", {
|
||||||
|
generate_missing_scores(&ctx).await?;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Ok(ctx)
|
Ok(ctx)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue