1
Fork 0

Way too many changes

Signed-off-by: prescientmoon <git@moonythm.dev>
This commit is contained in:
prescientmoon 2024-08-12 03:13:41 +02:00
parent 48c1f74f93
commit c035ecbb52
Signed by: prescientmoon
SSH key fingerprint: SHA256:UUF9JT2s8Xfyv76b8ZuVL7XrmimH4o49p4b+iexbVH4
13 changed files with 348 additions and 157 deletions

1
Cargo.lock generated
View file

@ -2605,7 +2605,6 @@ dependencies = [
"num", "num",
"plotters", "plotters",
"poise", "poise",
"rand",
"sqlx", "sqlx",
"tokio", "tokio",
] ]

View file

@ -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" } hypertesseract = { features=["image"], git="https://github.com/BlueGhostGH/hypertesseract.git", rev="4e05063" }
tokio = {version="1.38.0", features=["rt-multi-thread"]} tokio = {version="1.38.0", features=["rt-multi-thread"]}
imageproc = "0.25.0" imageproc = "0.25.0"
rand = "0.8.5"
[profile.dev.package."*"] [profile.dev.package."*"]
opt-level = 3 opt-level = 3

View file

@ -1,9 +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
); );
# }}}
# {{{ 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,

View file

@ -5,7 +5,7 @@ use num::Integer;
use crate::{ use crate::{
arcaea::chart::{Difficulty, Jacket, SongCache}, 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, context::Error,
recognition::fuzzy_song_name::guess_chart_name, recognition::fuzzy_song_name::guess_chart_name,
}; };
@ -149,12 +149,14 @@ impl JacketCache {
let image = image::load_from_memory(contents)?; let image = image::load_from_memory(contents)?;
jacket_vectors.push((song_id, ImageVec::from_image(&image))); 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( if should_blur_jacket_art() {
image image = image.blur(20.0);
.resize(BITMAP_IMAGE_SIZE, BITMAP_IMAGE_SIZE, FilterType::Nearest) }
.into_rgb8(),
)); let bitmap: &'static _ = Box::leak(Box::new(image.into_rgb8()));
if name == "base" { if name == "base" {
// Inefficiently iterates over everything, but it's fine for ~1k entries // Inefficiently iterates over everything, but it's fine for ~1k entries

View file

@ -356,10 +356,12 @@ impl Play {
// {{{ General functions // {{{ General functions
pub type PlayCollection<'a> = Vec<(Play, &'a Song, &'a Chart)>; 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, db: &SqlitePool,
song_cache: &'a SongCache, song_cache: &'a SongCache,
user: &User, user: &User,
min_amount: usize,
max_amount: usize,
) -> Result<Result<PlayCollection<'a>, &'static str>, Error> { ) -> Result<Result<PlayCollection<'a>, &'static str>, Error> {
// {{{ DB data fetching // {{{ DB data fetching
let plays: Vec<DbPlay> = query_as( let plays: Vec<DbPlay> = query_as(
@ -378,7 +380,7 @@ pub async fn get_b30_plays<'a>(
.await?; .await?;
// }}} // }}}
if plays.len() < 30 { if plays.len() < min_amount {
return Ok(Err("Not enough plays found")); return Ok(Err("Not enough plays found"));
} }
@ -395,7 +397,7 @@ pub async fn get_b30_plays<'a>(
.collect::<Result<Vec<_>, Error>>()?; .collect::<Result<Vec<_>, Error>>()?;
plays.sort_by_key(|(play, _, chart)| -play.score.play_rating(chart.chart_constant)); plays.sort_by_key(|(play, _, chart)| -play.score.play_rating(chart.chart_constant));
plays.truncate(30); plays.truncate(max_amount);
// }}} // }}}
Ok(Ok(plays)) Ok(Ok(plays))
@ -407,6 +409,6 @@ pub fn compute_b30_ptt(plays: &PlayCollection<'_>) -> i32 {
.iter() .iter()
.map(|(play, _, chart)| play.score.play_rating(chart.chart_constant)) .map(|(play, _, chart)| play.score.play_rating(chart.chart_constant))
.sum::<i32>() .sum::<i32>()
/ 30 / plays.len() as i32
} }
// }}} // }}}

View file

@ -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 freetype::{Face, Library};
use image::{imageops::FilterType, ImageBuffer, Rgb, Rgba}; use image::{imageops::FilterType, ImageBuffer, Rgb, Rgba};
use crate::arcaea::chart::Difficulty; use crate::{arcaea::chart::Difficulty, timed};
#[inline] #[inline]
pub fn get_data_dir() -> PathBuf { pub fn get_data_dir() -> PathBuf {
@ -18,18 +18,39 @@ pub fn get_assets_dir() -> PathBuf {
#[inline] #[inline]
fn get_font(name: &str) -> RefCell<Face> { fn get_font(name: &str) -> RefCell<Face> {
let face = FREETYPE_LIB.with(|lib| { let face = timed!(format!("load font \"{name}\""), {
lib.new_face(get_assets_dir().join(format!("{}.ttf", name)), 0) FREETYPE_LIB.with(|lib| {
.expect(&format!("Could not load {} font", name)) lib.new_face(get_assets_dir().join(name), 0)
.expect(&format!("Could not load {} font", name))
})
}); });
RefCell::new(face) RefCell::new(face)
} }
thread_local! { thread_local! {
pub static FREETYPE_LIB: Library = Library::init().unwrap(); pub static FREETYPE_LIB: Library = Library::init().unwrap();
pub static SAIRA_FONT: RefCell<Face> = get_font("saira-variable"); pub static SAIRA_FONT: RefCell<Face> = get_font("saira-variable.ttf");
pub static EXO_FONT: RefCell<Face> = get_font("exo-variable"); pub static EXO_FONT: RefCell<Face> = get_font("exo-variable.ttf");
pub static GEOSANS_FONT: RefCell<Face> = get_font("geosans-light"); pub static GEOSANS_FONT: RefCell<Face> = get_font("geosans-light.ttf");
pub static KAZESAWA_FONT: RefCell<Face> = get_font("kazesawa-regular.ttf");
pub static KAZESAWA_BOLD_FONT: RefCell<Face> = get_font("kazesawa-bold.ttf");
pub static NOTO_SANS_FONT: RefCell<Face> = get_font("noto-sans.ttf");
pub static ARIAL_FONT: RefCell<Face> = get_font("arial.ttf");
pub static UNI_FONT: RefCell<Face> = get_font("unifont.otf");
}
#[inline]
pub fn with_font<T>(
primary: &'static LocalKey<RefCell<Face>>,
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] #[inline]
@ -38,6 +59,12 @@ pub fn should_skip_jacket_art() -> bool {
*CELL.get_or_init(|| var("SHIMMERING_NO_JACKETS").unwrap_or_default() == "1") *CELL.get_or_init(|| var("SHIMMERING_NO_JACKETS").unwrap_or_default() == "1")
} }
#[inline]
pub fn should_blur_jacket_art() -> bool {
static CELL: OnceLock<bool> = OnceLock::new();
*CELL.get_or_init(|| var("SHIMMERING_BLUR_JACKETS").unwrap_or_default() == "1")
}
pub fn get_b30_background() -> &'static ImageBuffer<Rgb<u8>, Vec<u8>> { pub fn get_b30_background() -> &'static ImageBuffer<Rgb<u8>, Vec<u8>> {
static CELL: OnceLock<ImageBuffer<Rgb<u8>, Vec<u8>>> = OnceLock::new(); static CELL: OnceLock<ImageBuffer<Rgb<u8>, Vec<u8>>> = OnceLock::new();
CELL.get_or_init(|| { CELL.get_or_init(|| {
@ -46,8 +73,8 @@ pub fn get_b30_background() -> &'static ImageBuffer<Rgb<u8>, Vec<u8>> {
raw_b30_background raw_b30_background
.resize( .resize(
3 * raw_b30_background.width(), 8 * raw_b30_background.width(),
3 * raw_b30_background.height(), 8 * raw_b30_background.height(),
FilterType::Lanczos3, FilterType::Lanczos3,
) )
.blur(7.0) .blur(7.0)

View file

@ -9,8 +9,8 @@
use freetype::{ use freetype::{
bitmap::PixelMode, bitmap::PixelMode,
face::{KerningMode, LoadFlag}, face::{KerningMode, LoadFlag},
ffi::{FT_Err_Ok, FT_Set_Var_Design_Coordinates, FT_GLYPH_BBOX_PIXELS}, ffi::{FT_Set_Var_Design_Coordinates, FT_GLYPH_BBOX_PIXELS},
Bitmap, BitmapGlyph, Face, FtResult, Glyph, StrokerLineCap, StrokerLineJoin, Bitmap, BitmapGlyph, Face, Glyph, StrokerLineCap, StrokerLineJoin,
}; };
use image::GenericImage; use image::GenericImage;
use num::traits::Euclid; use num::traits::Euclid;
@ -90,6 +90,11 @@ impl Rect {
Self::new(0, 0, image.width(), image.height()) 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] #[inline]
pub fn align(&self, alignment: (Align, Align), pos: Position) -> Position { pub fn align(&self, alignment: (Align, Align), pos: Position) -> Position {
( (
@ -180,22 +185,29 @@ impl BitmapCanvas {
} }
// }}} // }}}
// {{{ Draw RBG image // {{{ Draw RBG image
/// Draws a bitmap image
pub fn blit_rbg(&mut self, pos: Position, (iw, ih): (u32, u32), src: &[u8]) { pub fn blit_rbg(&mut self, pos: Position, (iw, ih): (u32, u32), src: &[u8]) {
let height = self.height(); let iw = iw as i32;
for dx in 0..iw { let ih = ih as i32;
for dy in 0..ih { let width = self.width as i32;
let x = pos.0 + dx as i32; let height = self.height() 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 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 // {{{ Draw RGBA image
/// Draws a bitmap image taking care of the alpha channel. /// Draws a bitmap image taking care of the alpha channel.
pub fn blit_rbga(&mut self, pos: Position, (iw, ih): (u32, u32), src: &[u8]) { pub fn blit_rbga(&mut self, pos: Position, (iw, ih): (u32, u32), src: &[u8]) {
let height = self.height(); let iw = iw as i32;
for dx in 0..iw { let ih = ih as i32;
for dy in 0..ih { let width = self.width as i32;
let x = pos.0 + dx as i32; let height = self.height() 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 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 // {{{ Draw text
pub fn plan_text_rendering( pub fn plan_text_rendering(
&mut self,
pos: Position, pos: Position,
face: &mut Face, faces: &mut [&mut Face],
style: TextStyle, style: TextStyle,
text: &str, text: &str,
) -> Result<(Position, Rect, Vec<(i64, Glyph)>), Error> { ) -> Result<(Position, Rect, Vec<(i64, Glyph)>), Error> {
// {{{ Control weight // {{{ Control weight
if let Some(weight) = style.weight { if let Some(weight) = style.weight {
unsafe { for face in faces.iter_mut() {
let raw = face.raw_mut() as *mut _; unsafe {
let slice = [(weight as i64) << 16]; let raw = face.raw_mut() as *mut _;
let slice = [(weight as i64) << 16];
// {{{ Debug logging // {{{ Debug logging
// let mut amaster = 0 as *mut FT_MM_Var; // let mut amaster = 0 as *mut FT_MM_Var;
// FT_Get_MM_Var(raw, &mut amaster as *mut _); // FT_Get_MM_Var(raw, &mut amaster as *mut _);
// println!("{:?}", *amaster); // println!("{:?}", *amaster);
// println!("{:?}", *(*amaster).axis); // println!("{:?}", *(*amaster).axis);
// println!("{:?}", *(*amaster).namedstyle); // println!("{:?}", *(*amaster).namedstyle);
// }}} // }}}
// Set variable weight // Set variable weight
let err = FT_Set_Var_Design_Coordinates(raw, 3, slice.as_ptr()); let _err = FT_Set_Var_Design_Coordinates(raw, 3, slice.as_ptr());
if err != FT_Err_Ok { // Some fonts are not variable, so we just ignore errors :/
let err: FtResult<_> = Err(err.into()); // if err != FT_Err_Ok {
err?; // 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 // {{{ Compute layout
let mut pen_x = 0; 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 previous = None;
let mut data = Vec::new(); let mut data = Vec::new();
for c in text.chars() { for c in text.chars() {
let glyph_index = face let c = match c {
.get_char_index(c as usize) '' => '~',
.ok_or_else(|| format!("Could not get glyph index for char {:?}", c))?; c => c,
};
if let Some(previous) = previous let (face_index, glyph_index) = faces
&& kerning .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 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()?)); data.push((pen_x, face.glyph().get_glyph()?));
pen_x += face.glyph().advance().x >> 6; 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( pub fn text(
&mut self, &mut self,
pos: Position, pos: Position,
face: &mut Face, faces: &mut [&mut Face],
style: TextStyle, style: TextStyle,
text: &str, text: &str,
) -> Result<(), Error> { ) -> 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 // {{{ Render glyphs
for (pos_x, glyph) in &data { for (pos_x, glyph) in &data {
@ -620,12 +697,14 @@ impl LayoutManager {
} }
impl LayoutDrawer { impl LayoutDrawer {
#[inline]
pub fn new(layout: LayoutManager, canvas: BitmapCanvas) -> Self { pub fn new(layout: LayoutManager, canvas: BitmapCanvas) -> Self {
Self { layout, canvas } Self { layout, canvas }
} }
// {{{ Drawing // {{{ Drawing
// {{{ Draw pixel // {{{ Draw pixel
#[inline]
pub fn set_pixel(&mut self, id: LayoutBoxId, pos: (u32, u32), color: Color) { pub fn set_pixel(&mut self, id: LayoutBoxId, pos: (u32, u32), color: Color) {
let pos = self let pos = self
.layout .layout
@ -634,14 +713,28 @@ impl LayoutDrawer {
} }
// }}} // }}}
// {{{ Draw RGB image // {{{ Draw RGB image
/// Draws a bitmap image #[inline]
pub fn blit_rbg(&mut self, id: LayoutBoxId, pos: Position, dims: (u32, u32), src: &[u8]) { pub fn blit_rbg(&mut self, id: LayoutBoxId, pos: Position, dims: (u32, u32), src: &[u8]) {
let pos = self.layout.position_relative_to(id, pos); let pos = self.layout.position_relative_to(id, pos);
self.canvas.blit_rbg(pos, dims, src); 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 // {{{ Draw RGBA image
/// Draws a bitmap image taking care of the alpha channel. /// 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]) { pub fn blit_rbga(&mut self, id: LayoutBoxId, pos: Position, dims: (u32, u32), src: &[u8]) {
let pos = self.layout.position_relative_to(id, pos); let pos = self.layout.position_relative_to(id, pos);
self.canvas.blit_rbga(pos, dims, src); self.canvas.blit_rbga(pos, dims, src);
@ -649,6 +742,7 @@ impl LayoutDrawer {
// }}} // }}}
// {{{ Fill // {{{ Fill
/// Fills with solid color /// Fills with solid color
#[inline]
pub fn fill(&mut self, id: LayoutBoxId, color: Color) { pub fn fill(&mut self, id: LayoutBoxId, color: Color) {
let current = self.layout.lookup(id); let current = self.layout.lookup(id);
self.canvas.fill( self.canvas.fill(
@ -660,16 +754,17 @@ impl LayoutDrawer {
// }}} // }}}
// {{{ Draw text // {{{ Draw text
/// Render text /// Render text
#[inline]
pub fn text( pub fn text(
&mut self, &mut self,
id: LayoutBoxId, id: LayoutBoxId,
pos: Position, pos: Position,
face: &mut Face, faces: &mut [&mut Face],
style: TextStyle, style: TextStyle,
text: &str, text: &str,
) -> Result<(), Error> { ) -> Result<(), Error> {
let pos = self.layout.position_relative_to(id, pos); let pos = self.layout.position_relative_to(id, pos);
self.canvas.text(pos, face, style, text) self.canvas.text(pos, faces, style, text)
} }
// }}} // }}}
// }}} // }}}

View file

@ -1,7 +1,7 @@
use std::io::Cursor; use std::io::Cursor;
use chrono::DateTime; use chrono::DateTime;
use image::{ImageBuffer, Rgb}; use image::{DynamicImage, ImageBuffer, Rgb};
use plotters::{ use plotters::{
backend::{BitMapBackend, PixelFormat, RGBPixel}, backend::{BitMapBackend, PixelFormat, RGBPixel},
chart::{ChartBuilder, LabelAreaPosition}, chart::{ChartBuilder, LabelAreaPosition},
@ -19,20 +19,22 @@ use sqlx::query_as;
use crate::{ use crate::{
arcaea::{ arcaea::{
jacket::BITMAP_IMAGE_SIZE, jacket::BITMAP_IMAGE_SIZE,
play::{compute_b30_ptt, get_b30_plays, DbPlay}, play::{compute_b30_ptt, get_best_plays, DbPlay},
score::Score, score::Score,
}, },
assert_is_pookie,
assets::{ assets::{
get_b30_background, get_count_background, get_difficulty_background, get_grade_background, 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_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}, bitmap::{Align, BitmapCanvas, Color, LayoutDrawer, LayoutManager, Rect},
context::{Context, Error}, context::{Context, Error},
get_user, get_user,
logs::debug_image_log,
recognition::fuzzy_song_name::guess_song_and_chart, recognition::fuzzy_song_name::guess_song_and_chart,
reply_errors, reply_errors,
user::discord_it_to_discord_user, user::{discord_it_to_discord_user, User},
}; };
// {{{ Stats // {{{ Stats
@ -40,7 +42,7 @@ use crate::{
#[poise::command( #[poise::command(
prefix_command, prefix_command,
slash_command, slash_command,
subcommands("chart", "b30"), subcommands("chart", "b30", "bany"),
subcommand_required subcommand_required
)] )]
pub async fn stats(_ctx: Context<'_>) -> Result<(), Error> { pub async fn stats(_ctx: Context<'_>) -> Result<(), Error> {
@ -229,15 +231,28 @@ pub async fn plot(
Ok(()) Ok(())
} }
// }}} // }}}
// {{{ B30 // {{{ Render best plays
/// Show the 30 best scores async fn best_plays(
#[poise::command(prefix_command, slash_command)] ctx: &Context<'_>,
pub async fn b30(ctx: Context<'_>) -> Result<(), Error> { user: &User,
let user = get_user!(&ctx); grid_size: (u32, u32),
require_full: bool,
) -> Result<(), Error> {
let user_ctx = ctx.data(); let user_ctx = ctx.data();
let plays = reply_errors!( let plays = reply_errors!(
ctx, 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 // {{{ 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 bottom_in_area = layout.margin_xy(bottom_area, -20, -7);
let item_area = layout.glue_horizontally(top_area, bottom_area); let item_area = layout.glue_horizontally(top_area, bottom_area);
let item_with_margin = layout.margin_xy(item_area, 22, 17); 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); let root = layout.margin_uniform(item_grid, 30);
// }}} // }}}
// {{{ Rendering prep // {{{ Rendering prep
@ -271,15 +287,21 @@ pub async fn b30(ctx: Context<'_>) -> Result<(), Error> {
// {{{ Render background // {{{ Render background
let bg = get_b30_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, root,
// Align the center of the image with the center of the 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), (Align::Center, Align::Center),
drawer.layout.lookup(root).center(), drawer.layout.lookup(root).center(),
), ),
bg.dimensions(), bg.dimensions(),
bg.as_raw(), bg.as_raw(),
scale,
); );
// }}} // }}}
@ -291,7 +313,11 @@ pub async fn b30(ctx: Context<'_>) -> Result<(), Error> {
let top_bg = get_top_backgound(); let top_bg = get_top_backgound();
drawer.blit_rbg(top_area, (0, 0), top_bg.dimensions(), top_bg); 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 // {{{ Display index
let bg = get_count_background(); let bg = get_count_background();
@ -299,12 +325,11 @@ pub async fn b30(ctx: Context<'_>) -> Result<(), Error> {
// Draw background // Draw background
drawer.blit_rbga(item_area, (-8, jacket_margin as i32), bg.dimensions(), bg); drawer.blit_rbga(item_area, (-8, jacket_margin as i32), bg.dimensions(), bg);
with_font(&EXO_FONT, |faces| {
EXO_FONT.with_borrow_mut(|font| {
drawer.text( drawer.text(
item_area, item_area,
(bg_center.0 - 12, bg_center.1 - 3 + jacket_margin), (bg_center.0 - 12, bg_center.1 - 3 + jacket_margin),
font, faces,
crate::bitmap::TextStyle { crate::bitmap::TextStyle {
size: 25, size: 25,
weight: Some(800), 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()); drawer.blit_rbg(bottom_area, (0, 0), bg.dimensions(), bg.as_raw());
// Draw text // Draw text
EXO_FONT.with_borrow_mut(|font| { with_font(&EXO_FONT, |faces| {
let initial_size = 24; let initial_size = 24;
let mut style = crate::bitmap::TextStyle { let mut style = crate::bitmap::TextStyle {
size: initial_size, size: initial_size,
@ -334,9 +359,7 @@ pub async fn b30(ctx: Context<'_>) -> Result<(), Error> {
drop_shadow: None, drop_shadow: None,
}; };
while drawer while BitmapCanvas::plan_text_rendering((0, 0), faces, style, &song.title)?
.canvas
.plan_text_rendering((0, 0), font, style, &song.title)?
.1 .1
.width >= drawer.layout.width(bottom_in_area) .width >= drawer.layout.width(bottom_in_area)
{ {
@ -350,7 +373,7 @@ pub async fn b30(ctx: Context<'_>) -> Result<(), Error> {
drawer.text( drawer.text(
bottom_in_area, bottom_in_area,
(0, drawer.layout.height(bottom_in_area) as i32 / 2), (0, drawer.layout.height(bottom_in_area) as i32 / 2),
font, faces,
style, style,
&song.title, &song.title,
) )
@ -397,11 +420,11 @@ pub async fn b30(ctx: Context<'_>) -> Result<(), Error> {
let diff_area_center = diff_bg_area.center(); let diff_area_center = diff_bg_area.center();
EXO_FONT.with_borrow_mut(|font| { with_font(&EXO_FONT, |faces| {
drawer.text( drawer.text(
jacket_with_border, jacket_with_border,
(diff_area_center.0 + x_offset, diff_area_center.1), (diff_area_center.0 + x_offset, diff_area_center.1),
font, faces,
crate::bitmap::TextStyle { crate::bitmap::TextStyle {
size: 25, size: 25,
weight: Some(600), weight: Some(600),
@ -432,14 +455,14 @@ pub async fn b30(ctx: Context<'_>) -> Result<(), Error> {
); );
// }}} // }}}
// {{{ Display score text // {{{ Display score text
EXO_FONT.with_borrow_mut(|font| { with_font(&EXO_FONT, |faces| {
drawer.text( drawer.text(
jacket_area, jacket_area,
( (
score_bg_pos.0 + 5, score_bg_pos.0 + 5,
score_bg_pos.1 + score_bg.height() as i32 / 2, score_bg_pos.1 + score_bg.height() as i32 / 2,
), ),
font, faces,
crate::bitmap::TextStyle { crate::bitmap::TextStyle {
size: 23, size: 23,
weight: Some(800), weight: Some(800),
@ -470,7 +493,7 @@ pub async fn b30(ctx: Context<'_>) -> Result<(), Error> {
); );
// }}} // }}}
// {{{ Display status text // {{{ Display status text
EXO_FONT.with_borrow_mut(|font| { with_font(&EXO_FONT, |faces| {
let status = play let status = play
.short_status(chart) .short_status(chart)
.ok_or_else(|| format!("Could not get status for score {}", play.score))?; .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( drawer.text(
jacket_area, jacket_area,
(center.0 + x_offset, center.1), (center.0 + x_offset, center.1),
font, faces,
crate::bitmap::TextStyle { crate::bitmap::TextStyle {
size: if status == 'M' { 30 } else { 36 }, size: if status == 'M' { 30 } else { 36 },
weight: Some(if status == 'M' { 800 } else { 500 }), weight: Some(if status == 'M' { 800 } else { 500 }),
@ -516,14 +539,14 @@ pub async fn b30(ctx: Context<'_>) -> Result<(), Error> {
); );
// }}} // }}}
// {{{ Display grade text // {{{ Display grade text
EXO_FONT.with_borrow_mut(|font| { with_font(&EXO_FONT, |faces| {
let grade = play.score.grade(); let grade = play.score.grade();
let center = grade_bg_area.center(); let center = grade_bg_area.center();
drawer.text( drawer.text(
top_left_area, top_left_area,
(center.0, center.1), (center.0, center.1),
font, faces,
crate::bitmap::TextStyle { crate::bitmap::TextStyle {
size: 30, size: 30,
weight: Some(650), weight: Some(650),
@ -537,7 +560,7 @@ pub async fn b30(ctx: Context<'_>) -> Result<(), Error> {
})?; })?;
// }}} // }}}
// {{{ Display rating text // {{{ Display rating text
EXO_FONT.with_borrow_mut(|font| -> Result<(), Error> { with_font(&EXO_FONT, |faces| -> Result<(), Error> {
let mut style = crate::bitmap::TextStyle { let mut style = crate::bitmap::TextStyle {
size: 12, size: 12,
weight: Some(600), weight: Some(600),
@ -550,7 +573,7 @@ pub async fn b30(ctx: Context<'_>) -> Result<(), Error> {
drawer.text( drawer.text(
top_left_area, top_left_area,
(top_left_center, 73), (top_left_center, 73),
font, faces,
style, style,
"POTENTIAL", "POTENTIAL",
)?; )?;
@ -561,7 +584,7 @@ pub async fn b30(ctx: Context<'_>) -> Result<(), Error> {
drawer.text( drawer.text(
top_left_area, top_left_area,
(top_left_center, 94), (top_left_center, 94),
font, faces,
style, style,
&format!("{:.2}", play.score.play_rating_f32(chart.chart_constant)), &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 mut out_buffer = Vec::new();
let image: ImageBuffer<Rgb<u8>, _> = let mut image = DynamicImage::ImageRgb8(
ImageBuffer::from_raw(width, height, drawer.canvas.buffer).unwrap(); 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); 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() let reply = CreateReply::default()
.attachment(CreateAttachment::bytes(out_buffer, "b30.png")) .attachment(CreateAttachment::bytes(out_buffer, "b30.png"))
@ -599,3 +629,18 @@ pub async fn b30(ctx: Context<'_>) -> Result<(), Error> {
Ok(()) 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
}
// }}}

View file

@ -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_export]
macro_rules! reply_errors { macro_rules! reply_errors {
($ctx:expr, $value:expr) => { ($ctx:expr, $value:expr) => {

View file

@ -4,7 +4,7 @@ use sqlx::SqlitePool;
use crate::{ use crate::{
arcaea::{chart::SongCache, jacket::JacketCache}, 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}, recognition::{hyperglass::CharMeasurements, ui::UIMeasurements},
}; };
@ -24,6 +24,9 @@ pub struct UserContext {
pub geosans_measurements: CharMeasurements, pub geosans_measurements: CharMeasurements,
pub exo_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 { impl UserContext {
@ -36,15 +39,16 @@ impl UserContext {
let jacket_cache = JacketCache::new(&data_dir, &mut song_cache)?; let jacket_cache = JacketCache::new(&data_dir, &mut song_cache)?;
let ui_measurements = UIMeasurements::read(&data_dir)?; let ui_measurements = UIMeasurements::read(&data_dir)?;
static WHITELIST: &str = "0123456789'abcdefghklmnopqrstuvwxyzABCDEFGHIJKLMNOPRSTUVWXYZ";
let geosans_measurements = GEOSANS_FONT let geosans_measurements = GEOSANS_FONT
.with_borrow_mut(|font| CharMeasurements::from_text(font, "0123456789'", None))?; .with_borrow_mut(|font| CharMeasurements::from_text(font, WHITELIST, None))?;
let exo_measurements = EXO_FONT.with_borrow_mut(|font| { let kazesawa_measurements = KAZESAWA_FONT
CharMeasurements::from_text( .with_borrow_mut(|font| CharMeasurements::from_text(font, WHITELIST, None))?;
font, let kazesawa_bold_measurements = KAZESAWA_BOLD_FONT
"0123456789'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", .with_borrow_mut(|font| CharMeasurements::from_text(font, WHITELIST, None))?;
Some(700), let exo_measurements = EXO_FONT
) .with_borrow_mut(|font| CharMeasurements::from_text(font, WHITELIST, Some(700)))?;
})?;
println!("Created user context"); println!("Created user context");
@ -56,6 +60,8 @@ impl UserContext {
ui_measurements, ui_measurements,
geosans_measurements, geosans_measurements,
exo_measurements, exo_measurements,
kazesawa_measurements,
kazesawa_bold_measurements,
}) })
} }
} }

View file

@ -234,24 +234,24 @@ impl CharMeasurements {
// {{{ Creation // {{{ Creation
pub fn from_text(face: &mut Face, string: &str, weight: Option<u32>) -> Result<Self, Error> { pub fn from_text(face: &mut Face, string: &str, weight: Option<u32>) -> Result<Self, Error> {
// These are bad estimates lol // These are bad estimates lol
let char_w = 35; let style = TextStyle {
let char_h = 60; 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); let mut canvas = BitmapCanvas::new(
canvas.text( (planned.0 .0) as u32 + planned.1.width + 2 * padding.0 as u32,
(5, 5), (planned.0 .1) as u32 + planned.1.height + 2 * padding.0 as u32,
face, );
TextStyle {
stroke: None, canvas.text(padding, &mut [face], style, &string)?;
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 buffer = ImageBuffer::from_raw(canvas.width, canvas.height(), canvas.buffer.to_vec()) let buffer = ImageBuffer::from_raw(canvas.width, canvas.height(), canvas.buffer.to_vec())
.ok_or_else(|| "Failed to turn buffer into canvas")?; .ok_or_else(|| "Failed to turn buffer into canvas")?;
let image = DynamicImage::ImageRgb8(buffer); let image = DynamicImage::ImageRgb8(buffer);
@ -332,8 +332,8 @@ impl CharMeasurements {
.map(|(i, _, d)| (d.sqrt(), i)) .map(|(i, _, d)| (d.sqrt(), i))
.ok_or_else(|| "No chars in cache")?; .ok_or_else(|| "No chars in cache")?;
// println!("char '{}', distance {}", best_match.1, best_match.0); println!("char '{}', distance {}", best_match.1, best_match.0);
if best_match.0 <= (IMAGE_VEC_DIM * 10) as f32 { if best_match.0 <= 1.0 {
result.push(best_match.1); result.push(best_match.1);
} }
} }

View file

@ -134,14 +134,13 @@ impl ImageAnalyzer {
kind: ScoreKind, kind: ScoreKind,
) -> Result<Score, Error> { ) -> Result<Score, Error> {
let image = timed!("interp_crop_resize", { let image = timed!("interp_crop_resize", {
self.interp_crop_resize( self.interp_crop(
ctx, ctx,
image, image,
match kind { match kind {
ScoreKind::SongSelect => SongSelect(SongSelectRect::Score), ScoreKind::SongSelect => SongSelect(SongSelectRect::Score),
ScoreKind::ScoreScreen => ScoreScreen(ScoreScreenRect::Score), ScoreKind::ScoreScreen => ScoreScreen(ScoreScreenRect::Score),
}, },
(u32::MAX, 100),
)? )?
}); });
@ -219,9 +218,11 @@ impl ImageAnalyzer {
ScoreScreen(ScoreScreenRect::Difficulty), ScoreScreen(ScoreScreenRect::Difficulty),
)?; )?;
let text = let text = ctx.kazesawa_bold_measurements.recognise(
ctx.exo_measurements &image,
.recognise(&image, "PASTPRESENTFUTUREETERNALBEYOND", None)?; "PASTPRESENTFUTUREETERNALBEYOND",
None,
)?;
let difficulty = Difficulty::DIFFICULTIES let difficulty = Difficulty::DIFFICULTIES
.iter() .iter()
@ -241,10 +242,10 @@ impl ImageAnalyzer {
) -> Result<ScoreKind, Error> { ) -> Result<ScoreKind, Error> {
let image = self.interp_crop(ctx, image, PlayKind)?; let image = self.interp_crop(ctx, image, PlayKind)?;
let text = ctx let text = ctx
.exo_measurements .kazesawa_measurements
.recognise(&image, "resultselectasong", None)?; .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 ScoreKind::ScoreScreen
} else { } else {
ScoreKind::SongSelect ScoreKind::SongSelect
@ -344,7 +345,7 @@ impl ImageAnalyzer {
for i in 0..3 { for i in 0..3 {
let image = self.interp_crop(ctx, image, ScoreScreen(KINDS[i]))?; let image = self.interp_crop(ctx, image, ScoreScreen(KINDS[i]))?;
out[i] = ctx out[i] = ctx
.exo_measurements .kazesawa_bold_measurements
.recognise(&image, "0123456789", Some(30))? .recognise(&image, "0123456789", Some(30))?
.parse()?; .parse()?;
} }

View file

@ -9,6 +9,7 @@ use crate::context::{Context, Error};
pub struct User { pub struct User {
pub id: u32, pub id: u32,
pub discord_id: String, pub discord_id: String,
pub is_pookie: bool,
} }
impl User { impl User {
@ -22,6 +23,7 @@ impl User {
Ok(User { Ok(User {
id: user.id as u32, id: user.id as u32,
discord_id: user.discord_id, discord_id: user.discord_id,
is_pookie: user.is_pookie,
}) })
} }
@ -33,6 +35,7 @@ impl User {
Ok(User { Ok(User {
id: user.id as u32, id: user.id as u32,
discord_id: user.discord_id, discord_id: user.discord_id,
is_pookie: user.is_pookie,
}) })
} }
} }