diff --git a/Cargo.lock b/Cargo.lock index 7b05670..0c7b8b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2605,7 +2605,6 @@ dependencies = [ "num", "plotters", "poise", - "rand", "sqlx", "tokio", ] diff --git a/Cargo.toml b/Cargo.toml index 329660f..61c9553 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,6 @@ sqlx = { version = "0.8.0", features = ["sqlite", "runtime-tokio", "chrono"] } hypertesseract = { features=["image"], git="https://github.com/BlueGhostGH/hypertesseract.git", rev="4e05063" } tokio = {version="1.38.0", features=["rt-multi-thread"]} imageproc = "0.25.0" -rand = "0.8.5" [profile.dev.package."*"] opt-level = 3 diff --git a/schema.sql b/schema.sql index 07312b0..c4fc7d3 100644 --- a/schema.sql +++ b/schema.sql @@ -1,9 +1,10 @@ # {{{ users +# }}} create table IF NOT EXISTS users ( 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 ); -# }}} # {{{ songs CREATE TABLE IF NOT EXISTS songs ( id INTEGER NOT NULL PRIMARY KEY, diff --git a/src/arcaea/jacket.rs b/src/arcaea/jacket.rs index 2ced254..92d5e83 100644 --- a/src/arcaea/jacket.rs +++ b/src/arcaea/jacket.rs @@ -5,7 +5,7 @@ use num::Integer; use crate::{ arcaea::chart::{Difficulty, Jacket, SongCache}, - assets::{get_assets_dir, should_skip_jacket_art}, + assets::{get_assets_dir, should_blur_jacket_art, should_skip_jacket_art}, context::Error, recognition::fuzzy_song_name::guess_chart_name, }; @@ -149,12 +149,14 @@ impl JacketCache { let image = image::load_from_memory(contents)?; jacket_vectors.push((song_id, ImageVec::from_image(&image))); + let mut image = + image.resize(BITMAP_IMAGE_SIZE, BITMAP_IMAGE_SIZE, FilterType::Nearest); - let bitmap: &'static _ = Box::leak(Box::new( - image - .resize(BITMAP_IMAGE_SIZE, BITMAP_IMAGE_SIZE, FilterType::Nearest) - .into_rgb8(), - )); + if should_blur_jacket_art() { + image = image.blur(20.0); + } + + let bitmap: &'static _ = Box::leak(Box::new(image.into_rgb8())); if name == "base" { // Inefficiently iterates over everything, but it's fine for ~1k entries diff --git a/src/arcaea/play.rs b/src/arcaea/play.rs index d2c1528..848a694 100644 --- a/src/arcaea/play.rs +++ b/src/arcaea/play.rs @@ -356,10 +356,12 @@ impl Play { // {{{ General functions pub type PlayCollection<'a> = Vec<(Play, &'a Song, &'a Chart)>; -pub async fn get_b30_plays<'a>( +pub async fn get_best_plays<'a>( db: &SqlitePool, song_cache: &'a SongCache, user: &User, + min_amount: usize, + max_amount: usize, ) -> Result, &'static str>, Error> { // {{{ DB data fetching let plays: Vec = query_as( @@ -378,7 +380,7 @@ pub async fn get_b30_plays<'a>( .await?; // }}} - if plays.len() < 30 { + if plays.len() < min_amount { return Ok(Err("Not enough plays found")); } @@ -395,7 +397,7 @@ pub async fn get_b30_plays<'a>( .collect::, Error>>()?; plays.sort_by_key(|(play, _, chart)| -play.score.play_rating(chart.chart_constant)); - plays.truncate(30); + plays.truncate(max_amount); // }}} Ok(Ok(plays)) @@ -407,6 +409,6 @@ pub fn compute_b30_ptt(plays: &PlayCollection<'_>) -> i32 { .iter() .map(|(play, _, chart)| play.score.play_rating(chart.chart_constant)) .sum::() - / 30 + / plays.len() as i32 } // }}} diff --git a/src/assets.rs b/src/assets.rs index 84d2e0c..dddea81 100644 --- a/src/assets.rs +++ b/src/assets.rs @@ -1,9 +1,9 @@ -use std::{cell::RefCell, env::var, path::PathBuf, str::FromStr, sync::OnceLock}; +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 crate::arcaea::chart::Difficulty; +use crate::{arcaea::chart::Difficulty, timed}; #[inline] pub fn get_data_dir() -> PathBuf { @@ -18,18 +18,39 @@ pub fn get_assets_dir() -> PathBuf { #[inline] fn get_font(name: &str) -> RefCell { - let face = FREETYPE_LIB.with(|lib| { - lib.new_face(get_assets_dir().join(format!("{}.ttf", name)), 0) - .expect(&format!("Could not load {} font", name)) + let face = timed!(format!("load font \"{name}\""), { + FREETYPE_LIB.with(|lib| { + lib.new_face(get_assets_dir().join(name), 0) + .expect(&format!("Could not load {} font", name)) + }) }); RefCell::new(face) } thread_local! { pub static FREETYPE_LIB: Library = Library::init().unwrap(); -pub static SAIRA_FONT: RefCell = get_font("saira-variable"); -pub static EXO_FONT: RefCell = get_font("exo-variable"); -pub static GEOSANS_FONT: RefCell = get_font("geosans-light"); +pub static SAIRA_FONT: RefCell = get_font("saira-variable.ttf"); +pub static EXO_FONT: RefCell = get_font("exo-variable.ttf"); +pub static GEOSANS_FONT: RefCell = get_font("geosans-light.ttf"); +pub static KAZESAWA_FONT: RefCell = get_font("kazesawa-regular.ttf"); +pub static KAZESAWA_BOLD_FONT: RefCell = get_font("kazesawa-bold.ttf"); +pub static NOTO_SANS_FONT: RefCell = get_font("noto-sans.ttf"); +pub static ARIAL_FONT: RefCell = get_font("arial.ttf"); +pub static UNI_FONT: RefCell = get_font("unifont.otf"); +} + +#[inline] +pub fn with_font( + primary: &'static LocalKey>, + f: impl FnOnce(&mut [&mut Face]) -> T, +) -> T { + UNI_FONT.with_borrow_mut(|uni| { + // NOTO_SANS_FONT.with_borrow_mut(|noto| { + // ARIAL_FONT.with_borrow_mut(|arial| { + primary.with_borrow_mut(|primary| f(&mut [primary, uni])) + // }) + // }) + }) } #[inline] @@ -38,6 +59,12 @@ pub fn should_skip_jacket_art() -> bool { *CELL.get_or_init(|| 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") +} + pub fn get_b30_background() -> &'static ImageBuffer, Vec> { static CELL: OnceLock, Vec>> = OnceLock::new(); CELL.get_or_init(|| { @@ -46,8 +73,8 @@ pub fn get_b30_background() -> &'static ImageBuffer, Vec> { raw_b30_background .resize( - 3 * raw_b30_background.width(), - 3 * raw_b30_background.height(), + 8 * raw_b30_background.width(), + 8 * raw_b30_background.height(), FilterType::Lanczos3, ) .blur(7.0) diff --git a/src/bitmap.rs b/src/bitmap.rs index d0b830b..330026c 100644 --- a/src/bitmap.rs +++ b/src/bitmap.rs @@ -9,8 +9,8 @@ use freetype::{ bitmap::PixelMode, face::{KerningMode, LoadFlag}, - ffi::{FT_Err_Ok, FT_Set_Var_Design_Coordinates, FT_GLYPH_BBOX_PIXELS}, - Bitmap, BitmapGlyph, Face, FtResult, Glyph, StrokerLineCap, StrokerLineJoin, + ffi::{FT_Set_Var_Design_Coordinates, FT_GLYPH_BBOX_PIXELS}, + Bitmap, BitmapGlyph, Face, Glyph, StrokerLineCap, StrokerLineJoin, }; use image::GenericImage; use num::traits::Euclid; @@ -90,6 +90,11 @@ impl Rect { Self::new(0, 0, image.width(), image.height()) } + #[inline] + pub fn scaled(&self, scale: u32) -> Self { + Self::new(self.x, self.y, self.width * scale, self.height * scale) + } + #[inline] pub fn align(&self, alignment: (Align, Align), pos: Position) -> Position { ( @@ -180,22 +185,29 @@ impl BitmapCanvas { } // }}} // {{{ Draw RBG image - /// Draws a bitmap image pub fn blit_rbg(&mut self, pos: Position, (iw, ih): (u32, u32), src: &[u8]) { - let height = self.height(); - for dx in 0..iw { - for dy in 0..ih { - let x = pos.0 + dx as i32; - let y = pos.1 + dy as i32; - if x >= 0 && (x as u32) < self.width && y >= 0 && (y as u32) < height { - let r = src[(dx + dy * iw) as usize * 3]; - let g = src[(dx + dy * iw) as usize * 3 + 1]; - let b = src[(dx + dy * iw) as usize * 3 + 2]; + let iw = iw as i32; + let ih = ih as i32; + let width = self.width as i32; + let height = self.height() as i32; - let color = Color(r, g, b, 0xff); + let x_start = 0.max(-pos.0); + let y_start = 0.max(-pos.1); + let x_end = iw.min(width - pos.0); + let y_end = ih.min(height - pos.1); - self.set_pixel((x as u32, y as u32), color); - } + for dx in x_start..x_end { + for dy in y_start..y_end { + let x = pos.0 + dx; + let y = pos.1 + dy; + + let r = src[(dx + dy * iw) as usize * 3]; + let g = src[(dx + dy * iw) as usize * 3 + 1]; + let b = src[(dx + dy * iw) as usize * 3 + 2]; + + let color = Color(r, g, b, 0xff); + + self.set_pixel((x as u32, y as u32), color); } } } @@ -203,21 +215,67 @@ impl BitmapCanvas { // {{{ Draw RGBA image /// Draws a bitmap image taking care of the alpha channel. pub fn blit_rbga(&mut self, pos: Position, (iw, ih): (u32, u32), src: &[u8]) { - let height = self.height(); - for dx in 0..iw { - for dy in 0..ih { - let x = pos.0 + dx as i32; - let y = pos.1 + dy as i32; - if x >= 0 && (x as u32) < self.width && y >= 0 && (y as u32) < height { - let r = src[(dx + dy * iw) as usize * 4]; - let g = src[(dx + dy * iw) as usize * 4 + 1]; - let b = src[(dx + dy * iw) as usize * 4 + 2]; - let a = src[(dx + dy * iw) as usize * 4 + 3]; + let iw = iw as i32; + let ih = ih as i32; + let width = self.width as i32; + let height = self.height() as i32; - let color = Color(r, g, b, a); + let x_start = 0.max(-pos.0); + let y_start = 0.max(-pos.1); + let x_end = iw.min(width - pos.0); + let y_end = ih.min(height - pos.1); - self.set_pixel((x as u32, y as u32), color); - } + for dx in x_start..x_end { + for dy in y_start..y_end { + let x = pos.0 + dx; + let y = pos.1 + dy; + + let r = src[(dx + dy * iw) as usize * 4]; + let g = src[(dx + dy * iw) as usize * 4 + 1]; + let b = src[(dx + dy * iw) as usize * 4 + 2]; + let a = src[(dx + dy * iw) as usize * 4 + 3]; + + let color = Color(r, g, b, a); + + self.set_pixel((x as u32, y as u32), color); + } + } + } + // }}} + // {{{ Draw scaled up RBG image + pub fn blit_rbg_scaled_up( + &mut self, + pos: Position, + (iw, ih): (u32, u32), + src: &[u8], + scale: u32, + ) { + let scale = scale as i32; + + let iw = iw as i32; + let ih = ih as i32; + let width = self.width as i32; + let height = self.height() as i32; + + let x_start = pos.0.max(0); + let y_start = pos.1.max(0); + let x_end = (pos.0 + iw * scale).min(width); + let y_end = (pos.1 + ih * scale).min(height); + + for x in x_start..x_end { + for y in y_start..y_end { + // NOTE: I could instead keep separate counters. + // It would introduce an additional if statement, + // but would not perform division. + let dx = (x - pos.0) / scale; + let dy = (y - pos.1) / scale; + let r = src[(dx + dy * iw) as usize * 3]; + let g = src[(dx + dy * iw) as usize * 3 + 1]; + let b = src[(dx + dy * iw) as usize * 3 + 2]; + + let color = Color(r, g, b, 0xff); + + self.set_pixel((x as u32, y as u32), color); } } } @@ -239,53 +297,72 @@ impl BitmapCanvas { // }}} // {{{ Draw text pub fn plan_text_rendering( - &mut self, pos: Position, - face: &mut Face, + faces: &mut [&mut Face], style: TextStyle, text: &str, ) -> Result<(Position, Rect, Vec<(i64, Glyph)>), Error> { // {{{ Control weight if let Some(weight) = style.weight { - unsafe { - let raw = face.raw_mut() as *mut _; - let slice = [(weight as i64) << 16]; + for face in faces.iter_mut() { + unsafe { + let raw = face.raw_mut() as *mut _; + let slice = [(weight as i64) << 16]; - // {{{ Debug logging - // let mut amaster = 0 as *mut FT_MM_Var; - // FT_Get_MM_Var(raw, &mut amaster as *mut _); - // println!("{:?}", *amaster); - // println!("{:?}", *(*amaster).axis); - // println!("{:?}", *(*amaster).namedstyle); - // }}} + // {{{ Debug logging + // let mut amaster = 0 as *mut FT_MM_Var; + // FT_Get_MM_Var(raw, &mut amaster as *mut _); + // println!("{:?}", *amaster); + // println!("{:?}", *(*amaster).axis); + // println!("{:?}", *(*amaster).namedstyle); + // }}} - // Set variable weight - let err = FT_Set_Var_Design_Coordinates(raw, 3, slice.as_ptr()); - if err != FT_Err_Ok { - let err: FtResult<_> = Err(err.into()); - err?; + // Set variable weight + let _err = FT_Set_Var_Design_Coordinates(raw, 3, slice.as_ptr()); + // Some fonts are not variable, so we just ignore errors :/ + // if err != FT_Err_Ok { + // let err: FtResult<_> = Err(err.into()); + // err?; + // } } } } // }}} - face.set_char_size((style.size << 6) as isize, 0, 0, 0)?; + for face in faces.iter_mut() { + face.set_char_size((style.size << 6) as isize, 0, 0, 0)?; + } // {{{ Compute layout let mut pen_x = 0; - let kerning = face.has_kerning(); + let kerning: Vec<_> = faces.iter().map(|f| f.has_kerning()).collect(); let mut previous = None; let mut data = Vec::new(); for c in text.chars() { - let glyph_index = face - .get_char_index(c as usize) - .ok_or_else(|| format!("Could not get glyph index for char {:?}", c))?; + let c = match c { + '~' => '~', + c => c, + }; - if let Some(previous) = previous - && kerning + let (face_index, glyph_index) = faces + .iter() + .enumerate() + .find_map(|(i, face)| { + let glyph_index = face.get_char_index(c as usize)?; + Some((i, glyph_index)) + }) + .ok_or_else(|| { + format!("Could not get glyph index for char '{}' in \"{}\"", c, text) + })?; + + let face = &mut faces[face_index]; + if let Some((prev_face_index, prev_glyth_index)) = previous + && prev_face_index == face_index + && kerning[face_index] { - let delta = face.get_kerning(previous, glyph_index, KerningMode::KerningDefault)?; + let delta = + face.get_kerning(prev_glyth_index, glyph_index, KerningMode::KerningDefault)?; pen_x += delta.x >> 6; // we shift to get rid of sub-pixel accuracy } @@ -293,7 +370,7 @@ impl BitmapCanvas { data.push((pen_x, face.glyph().get_glyph()?)); pen_x += face.glyph().advance().x >> 6; - previous = Some(glyph_index); + previous = Some((face_index, glyph_index)); } // }}} @@ -345,11 +422,11 @@ impl BitmapCanvas { pub fn text( &mut self, pos: Position, - face: &mut Face, + faces: &mut [&mut Face], style: TextStyle, text: &str, ) -> Result<(), Error> { - let (pos, bbox, data) = self.plan_text_rendering(pos, face, style, text)?; + let (pos, bbox, data) = Self::plan_text_rendering(pos, faces, style, text)?; // {{{ Render glyphs for (pos_x, glyph) in &data { @@ -620,12 +697,14 @@ impl LayoutManager { } impl LayoutDrawer { + #[inline] pub fn new(layout: LayoutManager, canvas: BitmapCanvas) -> Self { Self { layout, canvas } } // {{{ Drawing // {{{ Draw pixel + #[inline] pub fn set_pixel(&mut self, id: LayoutBoxId, pos: (u32, u32), color: Color) { let pos = self .layout @@ -634,14 +713,28 @@ impl LayoutDrawer { } // }}} // {{{ Draw RGB image - /// Draws a bitmap image + #[inline] pub fn blit_rbg(&mut self, id: LayoutBoxId, pos: Position, dims: (u32, u32), src: &[u8]) { let pos = self.layout.position_relative_to(id, pos); self.canvas.blit_rbg(pos, dims, src); } + + #[inline] + pub fn blit_rbg_scaled_up( + &mut self, + id: LayoutBoxId, + pos: Position, + dims: (u32, u32), + src: &[u8], + scale: u32, + ) { + let pos = self.layout.position_relative_to(id, pos); + self.canvas.blit_rbg_scaled_up(pos, dims, src, scale); + } // }}} // {{{ Draw RGBA image /// Draws a bitmap image taking care of the alpha channel. + #[inline] pub fn blit_rbga(&mut self, id: LayoutBoxId, pos: Position, dims: (u32, u32), src: &[u8]) { let pos = self.layout.position_relative_to(id, pos); self.canvas.blit_rbga(pos, dims, src); @@ -649,6 +742,7 @@ impl LayoutDrawer { // }}} // {{{ Fill /// Fills with solid color + #[inline] pub fn fill(&mut self, id: LayoutBoxId, color: Color) { let current = self.layout.lookup(id); self.canvas.fill( @@ -660,16 +754,17 @@ impl LayoutDrawer { // }}} // {{{ Draw text /// Render text + #[inline] pub fn text( &mut self, id: LayoutBoxId, pos: Position, - face: &mut Face, + faces: &mut [&mut Face], style: TextStyle, text: &str, ) -> Result<(), Error> { let pos = self.layout.position_relative_to(id, pos); - self.canvas.text(pos, face, style, text) + self.canvas.text(pos, faces, style, text) } // }}} // }}} diff --git a/src/commands/stats.rs b/src/commands/stats.rs index 37e8be6..7f3f2e0 100644 --- a/src/commands/stats.rs +++ b/src/commands/stats.rs @@ -1,7 +1,7 @@ use std::io::Cursor; use chrono::DateTime; -use image::{ImageBuffer, Rgb}; +use image::{DynamicImage, ImageBuffer, Rgb}; use plotters::{ backend::{BitMapBackend, PixelFormat, RGBPixel}, chart::{ChartBuilder, LabelAreaPosition}, @@ -19,20 +19,22 @@ use sqlx::query_as; use crate::{ arcaea::{ jacket::BITMAP_IMAGE_SIZE, - play::{compute_b30_ptt, get_b30_plays, DbPlay}, + play::{compute_b30_ptt, get_best_plays, DbPlay}, score::Score, }, + assert_is_pookie, assets::{ get_b30_background, get_count_background, get_difficulty_background, get_grade_background, get_name_backgound, get_ptt_emblem, get_score_background, get_status_background, - get_top_backgound, EXO_FONT, + get_top_backgound, with_font, EXO_FONT, }, bitmap::{Align, BitmapCanvas, Color, LayoutDrawer, LayoutManager, Rect}, context::{Context, Error}, get_user, + logs::debug_image_log, recognition::fuzzy_song_name::guess_song_and_chart, reply_errors, - user::discord_it_to_discord_user, + user::{discord_it_to_discord_user, User}, }; // {{{ Stats @@ -40,7 +42,7 @@ use crate::{ #[poise::command( prefix_command, slash_command, - subcommands("chart", "b30"), + subcommands("chart", "b30", "bany"), subcommand_required )] pub async fn stats(_ctx: Context<'_>) -> Result<(), Error> { @@ -229,15 +231,28 @@ pub async fn plot( Ok(()) } // }}} -// {{{ B30 -/// Show the 30 best scores -#[poise::command(prefix_command, slash_command)] -pub async fn b30(ctx: Context<'_>) -> Result<(), Error> { - let user = get_user!(&ctx); +// {{{ Render best plays +async fn best_plays( + ctx: &Context<'_>, + user: &User, + grid_size: (u32, u32), + require_full: bool, +) -> Result<(), Error> { let user_ctx = ctx.data(); let plays = reply_errors!( ctx, - get_b30_plays(&user_ctx.db, &user_ctx.song_cache, &user).await? + get_best_plays( + &user_ctx.db, + &user_ctx.song_cache, + &user, + if require_full { + grid_size.0 * grid_size.1 + } else { + grid_size.0 * (grid_size.1.max(1) - 1) + } as usize, + (grid_size.0 * grid_size.1) as usize + ) + .await? ); // {{{ Layout @@ -258,7 +273,8 @@ pub async fn b30(ctx: Context<'_>) -> Result<(), Error> { let bottom_in_area = layout.margin_xy(bottom_area, -20, -7); let item_area = layout.glue_horizontally(top_area, bottom_area); let item_with_margin = layout.margin_xy(item_area, 22, 17); - let (item_grid, item_origins) = layout.repeated_evenly(item_with_margin, (5, 6)); + let (item_grid, item_origins) = + layout.repeated_evenly(item_with_margin, (grid_size.0, grid_size.1)); let root = layout.margin_uniform(item_grid, 30); // }}} // {{{ Rendering prep @@ -271,15 +287,21 @@ pub async fn b30(ctx: Context<'_>) -> Result<(), Error> { // {{{ Render background let bg = get_b30_background(); - drawer.blit_rbg( + let scale = (drawer.layout.width(root) as f32 / bg.width() as f32) + .max(drawer.layout.height(root) as f32 / bg.height() as f32) + .max(1.0) + .ceil() as u32; + + drawer.blit_rbg_scaled_up( root, // Align the center of the image with the center of the root - Rect::from_image(bg).align( + Rect::from_image(bg).scaled(scale).align( (Align::Center, Align::Center), drawer.layout.lookup(root).center(), ), bg.dimensions(), bg.as_raw(), + scale, ); // }}} @@ -291,7 +313,11 @@ pub async fn b30(ctx: Context<'_>) -> Result<(), Error> { let top_bg = get_top_backgound(); drawer.blit_rbg(top_area, (0, 0), top_bg.dimensions(), top_bg); - let (play, song, chart) = &plays[i]; + let (play, song, chart) = if let Some(item) = plays.get(i) { + item + } else { + break; + }; // {{{ Display index let bg = get_count_background(); @@ -299,12 +325,11 @@ pub async fn b30(ctx: Context<'_>) -> Result<(), Error> { // Draw background drawer.blit_rbga(item_area, (-8, jacket_margin as i32), bg.dimensions(), bg); - - EXO_FONT.with_borrow_mut(|font| { + with_font(&EXO_FONT, |faces| { drawer.text( item_area, (bg_center.0 - 12, bg_center.1 - 3 + jacket_margin), - font, + faces, crate::bitmap::TextStyle { size: 25, weight: Some(800), @@ -323,7 +348,7 @@ pub async fn b30(ctx: Context<'_>) -> Result<(), Error> { drawer.blit_rbg(bottom_area, (0, 0), bg.dimensions(), bg.as_raw()); // Draw text - EXO_FONT.with_borrow_mut(|font| { + with_font(&EXO_FONT, |faces| { let initial_size = 24; let mut style = crate::bitmap::TextStyle { size: initial_size, @@ -334,9 +359,7 @@ pub async fn b30(ctx: Context<'_>) -> Result<(), Error> { drop_shadow: None, }; - while drawer - .canvas - .plan_text_rendering((0, 0), font, style, &song.title)? + while BitmapCanvas::plan_text_rendering((0, 0), faces, style, &song.title)? .1 .width >= drawer.layout.width(bottom_in_area) { @@ -350,7 +373,7 @@ pub async fn b30(ctx: Context<'_>) -> Result<(), Error> { drawer.text( bottom_in_area, (0, drawer.layout.height(bottom_in_area) as i32 / 2), - font, + faces, style, &song.title, ) @@ -397,11 +420,11 @@ pub async fn b30(ctx: Context<'_>) -> Result<(), Error> { let diff_area_center = diff_bg_area.center(); - EXO_FONT.with_borrow_mut(|font| { + with_font(&EXO_FONT, |faces| { drawer.text( jacket_with_border, (diff_area_center.0 + x_offset, diff_area_center.1), - font, + faces, crate::bitmap::TextStyle { size: 25, weight: Some(600), @@ -432,14 +455,14 @@ pub async fn b30(ctx: Context<'_>) -> Result<(), Error> { ); // }}} // {{{ Display score text - EXO_FONT.with_borrow_mut(|font| { + with_font(&EXO_FONT, |faces| { drawer.text( jacket_area, ( score_bg_pos.0 + 5, score_bg_pos.1 + score_bg.height() as i32 / 2, ), - font, + faces, crate::bitmap::TextStyle { size: 23, weight: Some(800), @@ -470,7 +493,7 @@ pub async fn b30(ctx: Context<'_>) -> Result<(), Error> { ); // }}} // {{{ Display status text - EXO_FONT.with_borrow_mut(|font| { + with_font(&EXO_FONT, |faces| { let status = play .short_status(chart) .ok_or_else(|| format!("Could not get status for score {}", play.score))?; @@ -487,7 +510,7 @@ pub async fn b30(ctx: Context<'_>) -> Result<(), Error> { drawer.text( jacket_area, (center.0 + x_offset, center.1), - font, + faces, crate::bitmap::TextStyle { size: if status == 'M' { 30 } else { 36 }, weight: Some(if status == 'M' { 800 } else { 500 }), @@ -516,14 +539,14 @@ pub async fn b30(ctx: Context<'_>) -> Result<(), Error> { ); // }}} // {{{ Display grade text - EXO_FONT.with_borrow_mut(|font| { + with_font(&EXO_FONT, |faces| { let grade = play.score.grade(); let center = grade_bg_area.center(); drawer.text( top_left_area, (center.0, center.1), - font, + faces, crate::bitmap::TextStyle { size: 30, weight: Some(650), @@ -537,7 +560,7 @@ pub async fn b30(ctx: Context<'_>) -> Result<(), Error> { })?; // }}} // {{{ Display rating text - EXO_FONT.with_borrow_mut(|font| -> Result<(), Error> { + with_font(&EXO_FONT, |faces| -> Result<(), Error> { let mut style = crate::bitmap::TextStyle { size: 12, weight: Some(600), @@ -550,7 +573,7 @@ pub async fn b30(ctx: Context<'_>) -> Result<(), Error> { drawer.text( top_left_area, (top_left_center, 73), - font, + faces, style, "POTENTIAL", )?; @@ -561,7 +584,7 @@ pub async fn b30(ctx: Context<'_>) -> Result<(), Error> { drawer.text( top_left_area, (top_left_center, 94), - font, + faces, style, &format!("{:.2}", play.score.play_rating_f32(chart.chart_constant)), )?; @@ -582,11 +605,18 @@ pub async fn b30(ctx: Context<'_>) -> Result<(), Error> { } let mut out_buffer = Vec::new(); - let image: ImageBuffer, _> = - ImageBuffer::from_raw(width, height, drawer.canvas.buffer).unwrap(); + let mut image = DynamicImage::ImageRgb8( + ImageBuffer::from_raw(width, height, drawer.canvas.buffer.into_vec()).unwrap(), + ); + + debug_image_log(&image)?; + + if image.height() > 4096 { + image = image.resize(4096, 4096, image::imageops::FilterType::Nearest); + } let mut cursor = Cursor::new(&mut out_buffer); - image.write_to(&mut cursor, image::ImageFormat::Png)?; + image.write_to(&mut cursor, image::ImageFormat::WebP)?; let reply = CreateReply::default() .attachment(CreateAttachment::bytes(out_buffer, "b30.png")) @@ -599,3 +629,18 @@ pub async fn b30(ctx: Context<'_>) -> Result<(), Error> { Ok(()) } // }}} +// {{{ B30 +/// Show the 30 best scores +#[poise::command(prefix_command, slash_command, user_cooldown = 30)] +pub async fn b30(ctx: Context<'_>) -> Result<(), Error> { + let user = get_user!(&ctx); + best_plays(&ctx, &user, (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> { + let user = get_user!(&ctx); + assert_is_pookie!(ctx, user); + best_plays(&ctx, &user, (width, height), false).await +} +// }}} diff --git a/src/commands/utils.rs b/src/commands/utils.rs index 1343893..886358a 100644 --- a/src/commands/utils.rs +++ b/src/commands/utils.rs @@ -16,6 +16,17 @@ macro_rules! get_user { }}; } +#[macro_export] +macro_rules! assert_is_pookie { + ($ctx:expr, $user:expr) => {{ + if !$user.is_pookie { + $ctx.reply("This feature is reserved for my pookies. Sowwy :3") + .await?; + return Ok(()); + } + }}; +} + #[macro_export] macro_rules! reply_errors { ($ctx:expr, $value:expr) => { diff --git a/src/context.rs b/src/context.rs index bdbd546..d836d32 100644 --- a/src/context.rs +++ b/src/context.rs @@ -4,7 +4,7 @@ use sqlx::SqlitePool; use crate::{ arcaea::{chart::SongCache, jacket::JacketCache}, - assets::{EXO_FONT, GEOSANS_FONT}, + assets::{EXO_FONT, GEOSANS_FONT, KAZESAWA_BOLD_FONT, KAZESAWA_FONT}, recognition::{hyperglass::CharMeasurements, ui::UIMeasurements}, }; @@ -24,6 +24,9 @@ pub struct UserContext { pub geosans_measurements: CharMeasurements, pub exo_measurements: CharMeasurements, + // TODO: do we really need both after I've fixed the bug in the ocr code? + pub kazesawa_measurements: CharMeasurements, + pub kazesawa_bold_measurements: CharMeasurements, } impl UserContext { @@ -36,15 +39,16 @@ impl UserContext { let jacket_cache = JacketCache::new(&data_dir, &mut song_cache)?; let ui_measurements = UIMeasurements::read(&data_dir)?; + static WHITELIST: &str = "0123456789'abcdefghklmnopqrstuvwxyzABCDEFGHIJKLMNOPRSTUVWXYZ"; + let geosans_measurements = GEOSANS_FONT - .with_borrow_mut(|font| CharMeasurements::from_text(font, "0123456789'", None))?; - let exo_measurements = EXO_FONT.with_borrow_mut(|font| { - CharMeasurements::from_text( - font, - "0123456789'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", - Some(700), - ) - })?; + .with_borrow_mut(|font| CharMeasurements::from_text(font, WHITELIST, None))?; + let kazesawa_measurements = KAZESAWA_FONT + .with_borrow_mut(|font| CharMeasurements::from_text(font, WHITELIST, None))?; + let kazesawa_bold_measurements = KAZESAWA_BOLD_FONT + .with_borrow_mut(|font| CharMeasurements::from_text(font, WHITELIST, None))?; + let exo_measurements = EXO_FONT + .with_borrow_mut(|font| CharMeasurements::from_text(font, WHITELIST, Some(700)))?; println!("Created user context"); @@ -56,6 +60,8 @@ impl UserContext { ui_measurements, geosans_measurements, exo_measurements, + kazesawa_measurements, + kazesawa_bold_measurements, }) } } diff --git a/src/recognition/hyperglass.rs b/src/recognition/hyperglass.rs index e6aefda..be8a1f8 100644 --- a/src/recognition/hyperglass.rs +++ b/src/recognition/hyperglass.rs @@ -234,24 +234,24 @@ impl CharMeasurements { // {{{ Creation pub fn from_text(face: &mut Face, string: &str, weight: Option) -> Result { // These are bad estimates lol - let char_w = 35; - let char_h = 60; + let style = TextStyle { + stroke: None, + drop_shadow: None, + align: (Align::Start, Align::Start), + size: 60, + color: Color::BLACK, + // TODO: do we want to use the weight hint for resilience? + weight, + }; + let padding = (5, 5); + let planned = BitmapCanvas::plan_text_rendering(padding, &mut [face], style, &string)?; - let mut canvas = BitmapCanvas::new(10 + char_w * string.len() as u32, char_h + 10); - canvas.text( - (5, 5), - face, - TextStyle { - stroke: None, - drop_shadow: None, - align: (Align::Start, Align::Start), - size: char_h, - color: Color::BLACK, - // TODO: do we want to use the weight hint for resilience? - weight, - }, - &string, - )?; + let mut canvas = BitmapCanvas::new( + (planned.0 .0) as u32 + planned.1.width + 2 * padding.0 as u32, + (planned.0 .1) as u32 + planned.1.height + 2 * padding.0 as u32, + ); + + canvas.text(padding, &mut [face], style, &string)?; let buffer = ImageBuffer::from_raw(canvas.width, canvas.height(), canvas.buffer.to_vec()) .ok_or_else(|| "Failed to turn buffer into canvas")?; let image = DynamicImage::ImageRgb8(buffer); @@ -332,8 +332,8 @@ impl CharMeasurements { .map(|(i, _, d)| (d.sqrt(), i)) .ok_or_else(|| "No chars in cache")?; - // println!("char '{}', distance {}", best_match.1, best_match.0); - if best_match.0 <= (IMAGE_VEC_DIM * 10) as f32 { + println!("char '{}', distance {}", best_match.1, best_match.0); + if best_match.0 <= 1.0 { result.push(best_match.1); } } diff --git a/src/recognition/recognize.rs b/src/recognition/recognize.rs index ebee196..83020ef 100644 --- a/src/recognition/recognize.rs +++ b/src/recognition/recognize.rs @@ -134,14 +134,13 @@ impl ImageAnalyzer { kind: ScoreKind, ) -> Result { let image = timed!("interp_crop_resize", { - self.interp_crop_resize( + self.interp_crop( ctx, image, match kind { ScoreKind::SongSelect => SongSelect(SongSelectRect::Score), ScoreKind::ScoreScreen => ScoreScreen(ScoreScreenRect::Score), }, - (u32::MAX, 100), )? }); @@ -219,9 +218,11 @@ impl ImageAnalyzer { ScoreScreen(ScoreScreenRect::Difficulty), )?; - let text = - ctx.exo_measurements - .recognise(&image, "PASTPRESENTFUTUREETERNALBEYOND", None)?; + let text = ctx.kazesawa_bold_measurements.recognise( + &image, + "PASTPRESENTFUTUREETERNALBEYOND", + None, + )?; let difficulty = Difficulty::DIFFICULTIES .iter() @@ -241,10 +242,10 @@ impl ImageAnalyzer { ) -> Result { let image = self.interp_crop(ctx, image, PlayKind)?; let text = ctx - .exo_measurements - .recognise(&image, "resultselectasong", None)?; + .kazesawa_measurements + .recognise(&image, "ResultSelectaSong ", None)?; - let result = if edit_distance(&text, "Result") < edit_distance(&text, "Select a song") { + let result = if edit_distance(&text, "Result") < edit_distance(&text, "SelectaSong") { ScoreKind::ScoreScreen } else { ScoreKind::SongSelect @@ -344,7 +345,7 @@ impl ImageAnalyzer { for i in 0..3 { let image = self.interp_crop(ctx, image, ScoreScreen(KINDS[i]))?; out[i] = ctx - .exo_measurements + .kazesawa_bold_measurements .recognise(&image, "0123456789", Some(30))? .parse()?; } diff --git a/src/user.rs b/src/user.rs index a944528..c8d38b2 100644 --- a/src/user.rs +++ b/src/user.rs @@ -9,6 +9,7 @@ use crate::context::{Context, Error}; pub struct User { pub id: u32, pub discord_id: String, + pub is_pookie: bool, } impl User { @@ -22,6 +23,7 @@ impl User { Ok(User { id: user.id as u32, discord_id: user.discord_id, + is_pookie: user.is_pookie, }) } @@ -33,6 +35,7 @@ impl User { Ok(User { id: user.id as u32, discord_id: user.discord_id, + is_pookie: user.is_pookie, }) } }