From 3f922fcf6ca48bf4ca89eaf1f1542d20160487e9 Mon Sep 17 00:00:00 2001 From: prescientmoon Date: Fri, 16 Aug 2024 15:38:00 +0200 Subject: [PATCH] Back up everything before history rewrite Signed-off-by: prescientmoon --- src/arcaea/jacket.rs | 16 ++++-- src/arcaea/play.rs | 40 +++++++++++---- src/arcaea/score.rs | 17 +++++- src/assets.rs | 97 +++++++++++++++++++---------------- src/commands/stats.rs | 86 ++++++++++++++++++++++--------- src/recognition/hyperglass.rs | 2 +- 6 files changed, 175 insertions(+), 83 deletions(-) diff --git a/src/arcaea/jacket.rs b/src/arcaea/jacket.rs index 92d5e83..0897cf7 100644 --- a/src/arcaea/jacket.rs +++ b/src/arcaea/jacket.rs @@ -1,4 +1,4 @@ -use std::{fs, path::PathBuf}; +use std::{fs, io::Cursor, path::PathBuf}; use image::{imageops::FilterType, GenericImageView, Rgba}; use num::Integer; @@ -153,9 +153,17 @@ impl JacketCache { image.resize(BITMAP_IMAGE_SIZE, BITMAP_IMAGE_SIZE, FilterType::Nearest); if should_blur_jacket_art() { - image = image.blur(20.0); + image = image.blur(40.0); } + let encoded_pic = { + let mut processed_pic = Vec::new(); + image.write_to( + &mut Cursor::new(&mut processed_pic), + image::ImageFormat::Jpeg, + )?; + processed_pic.leak() + }; let bitmap: &'static _ = Box::leak(Box::new(image.into_rgb8())); if name == "base" { @@ -163,7 +171,7 @@ impl JacketCache { for chart in song_cache.charts_mut() { if chart.song_id == song_id && chart.cached_jacket.is_none() { chart.cached_jacket = Some(Jacket { - raw: contents, + raw: encoded_pic, bitmap, }); } @@ -171,7 +179,7 @@ impl JacketCache { } else if difficulty.is_some() { let chart = song_cache.lookup_chart_mut(chart_id).unwrap(); chart.cached_jacket = Some(Jacket { - raw: contents, + raw: encoded_pic, bitmap, }); } diff --git a/src/arcaea/play.rs b/src/arcaea/play.rs index 848a694..d75b39b 100644 --- a/src/arcaea/play.rs +++ b/src/arcaea/play.rs @@ -11,7 +11,7 @@ use crate::context::{Error, UserContext}; use crate::user::User; use super::chart::SongCache; -use super::score::Score; +use super::score::{Score, ScoringSystem}; // {{{ Create play #[derive(Debug, Clone)] @@ -127,7 +127,7 @@ pub struct DbPlay { impl DbPlay { #[inline] - pub fn to_play(self) -> Play { + pub fn into_play(self) -> Play { Play { id: self.id as u32, chart_id: self.chart_id as u32, @@ -175,6 +175,20 @@ pub struct Play { } impl Play { + // {{{ Query the underlying score + #[inline] + pub fn score(&self, system: ScoringSystem) -> Score { + match system { + ScoringSystem::Standard => self.score, + ScoringSystem::EX => self.zeta_score, + } + } + + #[inline] + pub fn play_rating(&self, system: ScoringSystem, chart_constant: u32) -> i32 { + self.score(system).play_rating(chart_constant) + } + // }}} // {{{ Play => distribution pub fn distribution(&self, note_count: u32) -> Option<(u32, u32, u32, u32)> { if let Some(fars) = self.far_notes { @@ -278,7 +292,8 @@ impl Play { song.title, chart.difficulty ) })? - .map(|p| p.to_play()); + .map(|p| p.into_play()); + let prev_score = prev_play.as_ref().map(|p| p.score); let prev_zeta_score = prev_play.as_ref().map(|p| p.zeta_score); // }}} @@ -360,9 +375,10 @@ pub async fn get_best_plays<'a>( db: &SqlitePool, song_cache: &'a SongCache, user: &User, + scoring_system: ScoringSystem, min_amount: usize, max_amount: usize, -) -> Result, &'static str>, Error> { +) -> Result, String>, Error> { // {{{ DB data fetching let plays: Vec = query_as( " @@ -381,7 +397,10 @@ pub async fn get_best_plays<'a>( // }}} if plays.len() < min_amount { - return Ok(Err("Not enough plays found")); + return Ok(Err(format!( + "Not enough plays found ({} out of a minimum of {min_amount})", + plays.len() + ))); } // {{{ B30 computation @@ -390,13 +409,13 @@ pub async fn get_best_plays<'a>( let mut plays: Vec<(Play, &Song, &Chart)> = plays .into_iter() .map(|play| { - let play = play.to_play(); + let play = play.into_play(); let (song, chart) = song_cache.lookup_chart(play.chart_id)?; Ok((play, song, chart)) }) .collect::, Error>>()?; - plays.sort_by_key(|(play, _, chart)| -play.score.play_rating(chart.chart_constant)); + plays.sort_by_key(|(play, _, chart)| -play.play_rating(scoring_system, chart.chart_constant)); plays.truncate(max_amount); // }}} @@ -404,11 +423,12 @@ pub async fn get_best_plays<'a>( } #[inline] -pub fn compute_b30_ptt(plays: &PlayCollection<'_>) -> i32 { +pub fn compute_b30_ptt(scoring_system: ScoringSystem, plays: &PlayCollection<'_>) -> i32 { plays .iter() - .map(|(play, _, chart)| play.score.play_rating(chart.chart_constant)) + .map(|(play, _, chart)| play.play_rating(scoring_system, chart.chart_constant)) .sum::() - / plays.len() as i32 + .checked_div(plays.len() as i32) + .unwrap_or(0) } // }}} diff --git a/src/arcaea/score.rs b/src/arcaea/score.rs index c8fba5d..e9294db 100644 --- a/src/arcaea/score.rs +++ b/src/arcaea/score.rs @@ -6,6 +6,21 @@ use crate::context::Error; use super::chart::Chart; +// {{{ Scoring system +#[derive(Debug, Clone, Copy, poise::ChoiceParameter)] +pub enum ScoringSystem { + Standard, + + // Inspired by sdvx's EX-scoring + EX, +} + +impl Default for ScoringSystem { + fn default() -> Self { + Self::Standard + } +} +// }}} // {{{ Grade #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum Grade { @@ -140,7 +155,7 @@ impl Score { if play_rating >= prev_play_rating { write!(buffer, " (+{:.2})", play_rating - prev_play_rating)?; } else { - write!(buffer, " (-{:.2})", play_rating - prev_play_rating)?; + write!(buffer, " ({:.2})", play_rating - prev_play_rating)?; } } diff --git a/src/assets.rs b/src/assets.rs index dddea81..ccfadb3 100644 --- a/src/assets.rs +++ b/src/assets.rs @@ -1,7 +1,7 @@ use std::{cell::RefCell, env::var, path::PathBuf, str::FromStr, sync::OnceLock, thread::LocalKey}; use freetype::{Face, Library}; -use image::{imageops::FilterType, ImageBuffer, Rgb, Rgba}; +use image::{ImageBuffer, Rgb, Rgba}; use crate::{arcaea::chart::Difficulty, timed}; @@ -55,93 +55,100 @@ pub fn with_font( #[inline] pub fn should_skip_jacket_art() -> bool { - static CELL: OnceLock = OnceLock::new(); - *CELL.get_or_init(|| var("SHIMMERING_NO_JACKETS").unwrap_or_default() == "1") + var("SHIMMERING_NO_JACKETS").unwrap_or_default() == "1" } #[inline] pub fn should_blur_jacket_art() -> bool { - static CELL: OnceLock = OnceLock::new(); - *CELL.get_or_init(|| var("SHIMMERING_BLUR_JACKETS").unwrap_or_default() == "1") + var("SHIMMERING_BLUR_JACKETS").unwrap_or_default() == "1" } pub fn get_b30_background() -> &'static ImageBuffer, Vec> { static CELL: OnceLock, Vec>> = OnceLock::new(); CELL.get_or_init(|| { - let raw_b30_background = image::open(get_assets_dir().join("b30_background.jpg")) - .expect("Could not open b30 background"); + timed!("load_b30_background", { + let raw_b30_background = image::open(get_assets_dir().join("b30_background.jpg")) + .expect("Could not open b30 background"); - raw_b30_background - .resize( - 8 * raw_b30_background.width(), - 8 * raw_b30_background.height(), - FilterType::Lanczos3, - ) - .blur(7.0) - .into_rgb8() + raw_b30_background.blur(7.0).into_rgb8() + }) }) } pub fn get_count_background() -> &'static ImageBuffer, Vec> { static CELL: OnceLock, Vec>> = OnceLock::new(); CELL.get_or_init(|| { - image::open(get_assets_dir().join("count_background.png")) - .expect("Could not open count background") - .into_rgba8() + timed!("load_count_backound", { + image::open(get_assets_dir().join("count_background.png")) + .expect("Could not open count background") + .into_rgba8() + }) }) } pub fn get_score_background() -> &'static ImageBuffer, Vec> { static CELL: OnceLock, Vec>> = OnceLock::new(); CELL.get_or_init(|| { - image::open(get_assets_dir().join("score_background.png")) - .expect("Could not open score background") - .into_rgba8() + timed!("load_score_background", { + image::open(get_assets_dir().join("score_background.png")) + .expect("Could not open score background") + .into_rgba8() + }) }) } pub fn get_status_background() -> &'static ImageBuffer, Vec> { static CELL: OnceLock, Vec>> = OnceLock::new(); CELL.get_or_init(|| { - image::open(get_assets_dir().join("status_background.png")) - .expect("Could not open status background") - .into_rgba8() + timed!("load_status_background", { + image::open(get_assets_dir().join("status_background.png")) + .expect("Could not open status background") + .into_rgba8() + }) }) } pub fn get_grade_background() -> &'static ImageBuffer, Vec> { static CELL: OnceLock, Vec>> = OnceLock::new(); CELL.get_or_init(|| { - image::open(get_assets_dir().join("grade_background.png")) - .expect("Could not open grade background") - .into_rgba8() + timed!("load_grade_background", { + image::open(get_assets_dir().join("grade_background.png")) + .expect("Could not open grade background") + .into_rgba8() + }) }) } pub fn get_top_backgound() -> &'static ImageBuffer, Vec> { static CELL: OnceLock, Vec>> = OnceLock::new(); CELL.get_or_init(|| { - image::open(get_assets_dir().join("top_background.png")) - .expect("Could not open top background") - .into_rgb8() + timed!("load_top_background", { + image::open(get_assets_dir().join("top_background.png")) + .expect("Could not open top background") + .into_rgb8() + }) }) } pub fn get_name_backgound() -> &'static ImageBuffer, Vec> { static CELL: OnceLock, Vec>> = OnceLock::new(); CELL.get_or_init(|| { - image::open(get_assets_dir().join("name_background.png")) - .expect("Could not open name background") - .into_rgb8() + timed!("load_name_background", { + image::open(get_assets_dir().join("name_background.png")) + .expect("Could not open name background") + .into_rgb8() + }) }) } pub fn get_ptt_emblem() -> &'static ImageBuffer, Vec> { static CELL: OnceLock, Vec>> = OnceLock::new(); CELL.get_or_init(|| { - image::open(get_assets_dir().join("ptt_emblem.png")) - .expect("Could not open ptt emblem") - .into_rgba8() + timed!("load_ptt_emblem", { + image::open(get_assets_dir().join("ptt_emblem.png")) + .expect("Could not open ptt emblem") + .into_rgba8() + }) }) } @@ -150,14 +157,16 @@ pub fn get_difficulty_background( ) -> &'static ImageBuffer, Vec> { static CELL: OnceLock<[ImageBuffer, Vec>; 5]> = OnceLock::new(); &CELL.get_or_init(|| { - let assets_dir = get_assets_dir(); - Difficulty::DIFFICULTY_SHORTHANDS.map(|shorthand| { - image::open(assets_dir.join(format!("diff_{}.png", shorthand.to_lowercase()))) - .expect(&format!( - "Could not get background for difficulty {:?}", - shorthand - )) - .into_rgba8() + timed!("load_difficulty_background", { + let assets_dir = get_assets_dir(); + Difficulty::DIFFICULTY_SHORTHANDS.map(|shorthand| { + image::open(assets_dir.join(format!("diff_{}.png", shorthand.to_lowercase()))) + .expect(&format!( + "Could not get background for difficulty {:?}", + shorthand + )) + .into_rgba8() + }) }) })[difficulty.to_index()] } diff --git a/src/commands/stats.rs b/src/commands/stats.rs index 7f3f2e0..122f55a 100644 --- a/src/commands/stats.rs +++ b/src/commands/stats.rs @@ -20,7 +20,7 @@ use crate::{ arcaea::{ jacket::BITMAP_IMAGE_SIZE, play::{compute_b30_ptt, get_best_plays, DbPlay}, - score::Score, + score::{Score, ScoringSystem}, }, assert_is_pookie, assets::{ @@ -92,7 +92,7 @@ pub async fn best( song.title, chart.difficulty ) })? - .to_play(); + .into_play(); let (embed, attachment) = play .to_embed( @@ -117,11 +117,13 @@ pub async fn best( #[poise::command(prefix_command, slash_command)] pub async fn plot( ctx: Context<'_>, + scoring_system: Option, #[rest] #[description = "Name of chart to show (difficulty at the end)"] name: String, ) -> Result<(), Error> { let user = get_user!(&ctx); + let scoring_system = scoring_system.unwrap_or_default(); let (song, chart) = guess_song_and_chart(&ctx.data(), &name)?; @@ -152,7 +154,12 @@ pub async fn plot( let min_time = plays.iter().map(|p| p.created_at).min().unwrap(); let max_time = plays.iter().map(|p| p.created_at).max().unwrap(); - let mut min_score = plays.iter().map(|p| p.score).min().unwrap(); + let mut min_score = plays + .iter() + .map(|p| p.clone().into_play().score(scoring_system)) + .min() + .unwrap() + .0 as i64; if min_score > 9_900_000 { min_score = 9_800_000; @@ -202,20 +209,26 @@ pub async fn plot( .draw()?; let mut points: Vec<_> = plays - .iter() - .map(|play| (play.created_at.and_utc().timestamp_millis(), play.score)) + .into_iter() + .map(|play| { + ( + play.created_at.and_utc().timestamp_millis(), + play.into_play().score(scoring_system), + ) + }) .collect(); points.sort(); points.dedup(); - chart.draw_series(LineSeries::new(points.iter().map(|(t, s)| (*t, *s)), &BLUE))?; + chart.draw_series(LineSeries::new( + points.iter().map(|(t, s)| (*t, s.0 as i64)), + &BLUE, + ))?; - chart.draw_series( - points - .iter() - .map(|(t, s)| Circle::new((*t, *s), 3, plotters::style::Color::filled(&BLUE))), - )?; + chart.draw_series(points.iter().map(|(t, s)| { + Circle::new((*t, s.0 as i64), 3, plotters::style::Color::filled(&BLUE)) + }))?; root.present()?; } @@ -235,6 +248,7 @@ pub async fn plot( async fn best_plays( ctx: &Context<'_>, user: &User, + scoring_system: ScoringSystem, grid_size: (u32, u32), require_full: bool, ) -> Result<(), Error> { @@ -245,10 +259,11 @@ async fn best_plays( &user_ctx.db, &user_ctx.song_cache, &user, + scoring_system, if require_full { grid_size.0 * grid_size.1 } else { - grid_size.0 * (grid_size.1.max(1) - 1) + grid_size.0 * (grid_size.1.max(1) - 1) + 1 } as usize, (grid_size.0 * grid_size.1) as usize ) @@ -471,7 +486,7 @@ async fn best_plays( stroke: Some((Color::BLACK, 1.5)), drop_shadow: None, }, - &format!("{:0>10}", format!("{}", play.score)), + &format!("{:0>10}", format!("{}", play.score(scoring_system))), ) })?; // }}} @@ -494,9 +509,12 @@ async fn best_plays( // }}} // {{{ Display status text with_font(&EXO_FONT, |faces| { - let status = play - .short_status(chart) - .ok_or_else(|| format!("Could not get status for score {}", play.score))?; + let status = play.short_status(chart).ok_or_else(|| { + format!( + "Could not get status for score {}", + play.score(scoring_system) + ) + })?; let x_offset = match status { 'P' => 2, @@ -540,7 +558,7 @@ async fn best_plays( // }}} // {{{ Display grade text with_font(&EXO_FONT, |faces| { - let grade = play.score.grade(); + let grade = play.score(scoring_system).grade(); let center = grade_bg_area.center(); drawer.text( @@ -586,7 +604,10 @@ async fn best_plays( (top_left_center, 94), faces, style, - &format!("{:.2}", play.score.play_rating_f32(chart.chart_constant)), + &format!( + "{:.2}", + play.play_rating(scoring_system, chart.chart_constant) as f32 / 100.0 + ), )?; Ok(()) @@ -622,7 +643,7 @@ async fn best_plays( .attachment(CreateAttachment::bytes(out_buffer, "b30.png")) .content(format!( "Your ptt is {:.2}", - compute_b30_ptt(&plays) as f32 / 100.0 + compute_b30_ptt(scoring_system, &plays) as f32 / 100.0 )); ctx.send(reply).await?; @@ -632,15 +653,34 @@ async fn best_plays( // {{{ B30 /// Show the 30 best scores #[poise::command(prefix_command, slash_command, user_cooldown = 30)] -pub async fn b30(ctx: Context<'_>) -> Result<(), Error> { +pub async fn b30(ctx: Context<'_>, scoring_system: Option) -> Result<(), Error> { let user = get_user!(&ctx); - best_plays(&ctx, &user, (5, 6), true).await + best_plays( + &ctx, + &user, + scoring_system.unwrap_or_default(), + (5, 6), + true, + ) + .await } #[poise::command(prefix_command, slash_command, hide_in_help, global_cooldown = 5)] -pub async fn bany(ctx: Context<'_>, width: u32, height: u32) -> Result<(), Error> { +pub async fn bany( + ctx: Context<'_>, + scoring_system: Option, + width: u32, + height: u32, +) -> Result<(), Error> { let user = get_user!(&ctx); assert_is_pookie!(ctx, user); - best_plays(&ctx, &user, (width, height), false).await + best_plays( + &ctx, + &user, + scoring_system.unwrap_or_default(), + (width, height), + false, + ) + .await } // }}} diff --git a/src/recognition/hyperglass.rs b/src/recognition/hyperglass.rs index be8a1f8..9dde6c1 100644 --- a/src/recognition/hyperglass.rs +++ b/src/recognition/hyperglass.rs @@ -333,7 +333,7 @@ impl CharMeasurements { .ok_or_else(|| "No chars in cache")?; println!("char '{}', distance {}", best_match.1, best_match.0); - if best_match.0 <= 1.0 { + if best_match.0 <= 0.75 { result.push(best_match.1); } }