Way too many changes
Signed-off-by: prescientmoon <git@moonythm.dev>
This commit is contained in:
parent
48c1f74f93
commit
c035ecbb52
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -2605,7 +2605,6 @@ dependencies = [
|
||||||
"num",
|
"num",
|
||||||
"plotters",
|
"plotters",
|
||||||
"poise",
|
"poise",
|
||||||
"rand",
|
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
213
src/bitmap.rs
213
src/bitmap.rs
|
@ -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)
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
// }}}
|
// }}}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()?;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue