From 5c062df309ad92650959d987a571d44699f1a154 Mon Sep 17 00:00:00 2001 From: prescientmoon Date: Fri, 16 Aug 2024 23:24:11 +0200 Subject: [PATCH] Introduce missing assets and better folder system --- .gitignore | 14 +- .../assets/count_background.png | Bin shimmering/assets/diff_byd.png | Bin 0 -> 7571 bytes shimmering/assets/diff_etr.png | Bin 0 -> 7608 bytes shimmering/assets/diff_ftr.png | Bin 0 -> 7671 bytes shimmering/assets/diff_prs.png | Bin 0 -> 5856 bytes shimmering/assets/diff_pst.png | Bin 0 -> 6712 bytes .../assets/grade_background.png | Bin .../assets/name_background.png | Bin .../assets/placeholder_jacket.jpg | Bin {data => shimmering}/assets/ptt_emblem.png | Bin .../assets/score_background.png | Bin .../assets/status_background.png | Bin .../assets/top_background.png | Bin {data => shimmering/config}/charts.csv | 0 {data => shimmering/config}/shorthands.csv | 0 {data => shimmering/config}/ui.txt | 0 src/arcaea/jacket.rs | 18 +- src/assets.rs | 197 +++++++----------- src/bitmap.rs | 44 ++-- src/commands/stats.rs | 66 ++---- src/context.rs | 19 +- src/logs.rs | 19 +- src/main.rs | 10 +- src/recognition/ui.rs | 8 +- 25 files changed, 166 insertions(+), 229 deletions(-) rename {data => shimmering}/assets/count_background.png (100%) create mode 100644 shimmering/assets/diff_byd.png create mode 100644 shimmering/assets/diff_etr.png create mode 100644 shimmering/assets/diff_ftr.png create mode 100644 shimmering/assets/diff_prs.png create mode 100644 shimmering/assets/diff_pst.png rename {data => shimmering}/assets/grade_background.png (100%) rename {data => shimmering}/assets/name_background.png (100%) rename {data => shimmering}/assets/placeholder_jacket.jpg (100%) rename {data => shimmering}/assets/ptt_emblem.png (100%) rename {data => shimmering}/assets/score_background.png (100%) rename {data => shimmering}/assets/status_background.png (100%) rename {data => shimmering}/assets/top_background.png (100%) rename {data => shimmering/config}/charts.csv (100%) rename {data => shimmering/config}/shorthands.csv (100%) rename {data => shimmering/config}/ui.txt (100%) diff --git a/.gitignore b/.gitignore index 3b2743f..885013f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,12 @@ -target .direnv .envrc -data/db.sqlite -data/jackets -data/songs + +shimmering/data +shimmering/logs +shimmering/assets/fonts +shimmering/assets/songs +shimmering/assets/b30_background.* + +target backups dump.sql -logs -cache diff --git a/data/assets/count_background.png b/shimmering/assets/count_background.png similarity index 100% rename from data/assets/count_background.png rename to shimmering/assets/count_background.png diff --git a/shimmering/assets/diff_byd.png b/shimmering/assets/diff_byd.png new file mode 100644 index 0000000000000000000000000000000000000000..539955c91c495f5a648145ab43816470c02cf141 GIT binary patch literal 7571 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 literal 0 HcmV?d00001 diff --git a/shimmering/assets/diff_etr.png b/shimmering/assets/diff_etr.png new file mode 100644 index 0000000000000000000000000000000000000000..0ece003284f5807bd11483e389b3cbdb9dad287b GIT binary patch literal 7608 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 a2mk;800000(o>TF00008IT9lia_1)Z(d1n2 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≷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 O0000 Result { - let jacket_dir = data_dir.join("jackets"); - - if jacket_dir.exists() { - fs::remove_dir_all(&jacket_dir).expect("Could not delete jacket dir"); - } - - fs::create_dir_all(&jacket_dir).expect("Could not create jacket dir"); - + pub fn new(song_cache: &mut SongCache) -> Result { let jacket_vectors = if should_skip_jacket_art() { - let path = get_assets_dir().join("placeholder_jacket.jpg"); + let path = get_asset_dir().join("placeholder_jacket.jpg"); let contents: &'static _ = fs::read(path)?.leak(); let image = image::load_from_memory(contents)?; let bitmap: &'static _ = Box::leak(Box::new( @@ -109,7 +101,7 @@ impl JacketCache { Vec::new() } else { let entries = - fs::read_dir(data_dir.join("songs")).expect("Couldn't read songs directory"); + fs::read_dir(get_asset_dir().join("songs")).expect("Couldn't read songs directory"); let mut jacket_vectors = vec![]; for entry in entries { diff --git a/src/assets.rs b/src/assets.rs index ccfadb3..7a6c91c 100644 --- a/src/assets.rs +++ b/src/assets.rs @@ -1,44 +1,56 @@ -use std::{cell::RefCell, env::var, path::PathBuf, str::FromStr, sync::OnceLock, thread::LocalKey}; +use std::{ + cell::RefCell, + env::var, + path::PathBuf, + str::FromStr, + sync::{LazyLock, OnceLock}, + thread::LocalKey, +}; use freetype::{Face, Library}; -use image::{ImageBuffer, Rgb, Rgba}; +use image::{DynamicImage, RgbaImage}; use crate::{arcaea::chart::Difficulty, timed}; +// {{{ Path helpers +#[inline] +pub fn get_var(name: &str) -> String { + var(name).unwrap_or_else(|_| panic!("Missing `{name}` environment variable")) +} + +#[inline] +pub fn get_path(name: &str) -> PathBuf { + PathBuf::from_str(&get_var(name)) + .unwrap_or_else(|_| panic!("`{name}` environment variable is not a valid path")) +} + #[inline] pub fn get_data_dir() -> PathBuf { - PathBuf::from_str(&var("SHIMMERING_DATA_DIR").expect("Missing `SHIMMERING_DATA_DIR` env var")) - .expect("`SHIMMERING_DATA_DIR` is not a valid path") + get_path("SHIMMERING_DATA_DIR") } #[inline] -pub fn get_assets_dir() -> PathBuf { - get_data_dir().join("assets") +pub fn get_config_dir() -> PathBuf { + get_path("SHIMMERING_CONFIG_DIR") } +#[inline] +pub fn get_asset_dir() -> PathBuf { + get_path("SHIMMERING_ASSET_DIR") +} +// }}} +// {{{ Font helpers #[inline] fn get_font(name: &str) -> RefCell { let face = timed!(format!("load font \"{name}\""), { FREETYPE_LIB.with(|lib| { - lib.new_face(get_assets_dir().join(name), 0) + lib.new_face(get_asset_dir().join("fonts").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.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>, @@ -52,7 +64,21 @@ pub fn with_font( // }) }) } - +// }}} +// {{{ Font loading +thread_local! { +pub static FREETYPE_LIB: Library = Library::init().unwrap(); +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"); +} +// }}} +// {{{ Asset art helpers #[inline] pub fn should_skip_jacket_art() -> bool { var("SHIMMERING_NO_JACKETS").unwrap_or_default() == "1" @@ -63,110 +89,49 @@ pub fn should_blur_jacket_art() -> bool { 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(|| { - timed!("load_b30_background", { - let raw_b30_background = image::open(get_assets_dir().join("b30_background.jpg")) - .expect("Could not open b30 background"); - - raw_b30_background.blur(7.0).into_rgb8() - }) - }) +macro_rules! get_asset { + ($name: ident, $path:expr) => { + get_asset!($name, $path, |d: DynamicImage| d); + }; + ($name: ident, $path:expr, $f:expr) => { + pub static $name: LazyLock = LazyLock::new(move || { + timed!($path, { + let image = image::open(get_asset_dir().join($path)) + .unwrap_or_else(|_| panic!("Could no read asset `{}`", $path)); + let f = $f; + f(image).into_rgba8() + }) + }); + }; } +// }}} +// {{{ Asset art loading +get_asset!(COUNT_BACKGROUND, "count_background.png"); +get_asset!(SCORE_BACKGROUND, "score_background.png"); +get_asset!(STATUS_BACKGROUND, "status_background.png"); +get_asset!(GRADE_BACKGROUND, "grade_background.png"); +get_asset!(TOP_BACKGROUND, "top_background.png"); +get_asset!(NAME_BACKGROUND, "name_background.png"); +get_asset!(PTT_EMBLEM, "ptt_emblem.png"); +get_asset!( + B30_BACKGROUND, + "b30_background.jpg", + |image: DynamicImage| image.blur(7.0) +); -pub fn get_count_background() -> &'static ImageBuffer, Vec> { - static CELL: OnceLock, Vec>> = OnceLock::new(); - CELL.get_or_init(|| { - timed!("load_count_backound", { - image::open(get_assets_dir().join("count_background.png")) - .expect("Could not open count background") - .into_rgba8() - }) - }) -} - -pub fn get_score_background() -> &'static ImageBuffer, Vec> { - static CELL: OnceLock, Vec>> = OnceLock::new(); - CELL.get_or_init(|| { - timed!("load_score_background", { - image::open(get_assets_dir().join("score_background.png")) - .expect("Could not open score background") - .into_rgba8() - }) - }) -} - -pub fn get_status_background() -> &'static ImageBuffer, Vec> { - static CELL: OnceLock, Vec>> = OnceLock::new(); - CELL.get_or_init(|| { - timed!("load_status_background", { - image::open(get_assets_dir().join("status_background.png")) - .expect("Could not open status background") - .into_rgba8() - }) - }) -} - -pub fn get_grade_background() -> &'static ImageBuffer, Vec> { - static CELL: OnceLock, Vec>> = OnceLock::new(); - CELL.get_or_init(|| { - timed!("load_grade_background", { - image::open(get_assets_dir().join("grade_background.png")) - .expect("Could not open grade background") - .into_rgba8() - }) - }) -} - -pub fn get_top_backgound() -> &'static ImageBuffer, Vec> { - static CELL: OnceLock, Vec>> = OnceLock::new(); - CELL.get_or_init(|| { - timed!("load_top_background", { - image::open(get_assets_dir().join("top_background.png")) - .expect("Could not open top background") - .into_rgb8() - }) - }) -} - -pub fn get_name_backgound() -> &'static ImageBuffer, Vec> { - static CELL: OnceLock, Vec>> = OnceLock::new(); - CELL.get_or_init(|| { - timed!("load_name_background", { - image::open(get_assets_dir().join("name_background.png")) - .expect("Could not open name background") - .into_rgb8() - }) - }) -} - -pub fn get_ptt_emblem() -> &'static ImageBuffer, Vec> { - static CELL: OnceLock, Vec>> = OnceLock::new(); - CELL.get_or_init(|| { - timed!("load_ptt_emblem", { - image::open(get_assets_dir().join("ptt_emblem.png")) - .expect("Could not open ptt emblem") - .into_rgba8() - }) - }) -} - -pub fn get_difficulty_background( - difficulty: Difficulty, -) -> &'static ImageBuffer, Vec> { - static CELL: OnceLock<[ImageBuffer, Vec>; 5]> = OnceLock::new(); +pub fn get_difficulty_background(difficulty: Difficulty) -> &'static RgbaImage { + static CELL: OnceLock<[RgbaImage; 5]> = OnceLock::new(); &CELL.get_or_init(|| { timed!("load_difficulty_background", { - let assets_dir = get_assets_dir(); + let assets_dir = get_asset_dir(); Difficulty::DIFFICULTY_SHORTHANDS.map(|shorthand| { image::open(assets_dir.join(format!("diff_{}.png", shorthand.to_lowercase()))) - .expect(&format!( - "Could not get background for difficulty {:?}", - shorthand - )) + .unwrap_or_else(|_| { + panic!("Could not get background for difficulty {shorthand:?}") + }) .into_rgba8() }) }) })[difficulty.to_index()] } +// }}} diff --git a/src/bitmap.rs b/src/bitmap.rs index 330026c..b2070cf 100644 --- a/src/bitmap.rs +++ b/src/bitmap.rs @@ -12,7 +12,7 @@ use freetype::{ ffi::{FT_Set_Var_Design_Coordinates, FT_GLYPH_BBOX_PIXELS}, Bitmap, BitmapGlyph, Face, Glyph, StrokerLineCap, StrokerLineJoin, }; -use image::GenericImage; +use image::{GenericImage, RgbImage, RgbaImage}; use num::traits::Euclid; use crate::{assets::FREETYPE_LIB, context::Error}; @@ -184,7 +184,8 @@ impl BitmapCanvas { ((alpha * color.2 as u32 + (255 - alpha) * self.buffer[index + 2] as u32) / 255) as u8; } // }}} - // {{{ Draw RBG image + // {{{ Draw RGB image + /// Draws a bitmap image with no alpha channel. pub fn blit_rbg(&mut self, pos: Position, (iw, ih): (u32, u32), src: &[u8]) { let iw = iw as i32; let ih = ih as i32; @@ -242,8 +243,8 @@ impl BitmapCanvas { } } // }}} - // {{{ Draw scaled up RBG image - pub fn blit_rbg_scaled_up( + // {{{ Draw scaled up RBGA image + pub fn blit_rbga_scaled_up( &mut self, pos: Position, (iw, ih): (u32, u32), @@ -269,11 +270,12 @@ impl BitmapCanvas { // 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 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, 0xff); + let color = Color(r, g, b, a); self.set_pixel((x as u32, y as u32), color); } @@ -712,11 +714,21 @@ impl LayoutDrawer { self.canvas.set_pixel((pos.0 as u32, pos.1 as u32), color); } // }}} - // {{{ Draw RGB image + // {{{ Draw images + /// Draws a bitmap image taking with no alpha channel. #[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, image: &RgbImage) { let pos = self.layout.position_relative_to(id, pos); - self.canvas.blit_rbg(pos, dims, src); + self.canvas + .blit_rbg(pos, image.dimensions(), image.as_raw()); + } + + /// Draws a bitmap image taking care of the alpha channel. + #[inline] + pub fn blit_rbga(&mut self, id: LayoutBoxId, pos: Position, image: &RgbaImage) { + let pos = self.layout.position_relative_to(id, pos); + self.canvas + .blit_rbga(pos, image.dimensions(), image.as_raw()); } #[inline] @@ -729,15 +741,7 @@ impl LayoutDrawer { 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); + self.canvas.blit_rbga_scaled_up(pos, dims, src, scale); } // }}} // {{{ Fill diff --git a/src/commands/stats.rs b/src/commands/stats.rs index 122f55a..09e9c6e 100644 --- a/src/commands/stats.rs +++ b/src/commands/stats.rs @@ -24,9 +24,9 @@ use crate::{ }, 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, with_font, EXO_FONT, + get_difficulty_background, with_font, B30_BACKGROUND, COUNT_BACKGROUND, EXO_FONT, + GRADE_BACKGROUND, NAME_BACKGROUND, PTT_EMBLEM, SCORE_BACKGROUND, STATUS_BACKGROUND, + TOP_BACKGROUND, }, bitmap::{Align, BitmapCanvas, Color, LayoutDrawer, LayoutManager, Rect}, context::{Context, Error}, @@ -300,7 +300,7 @@ async fn best_plays( let mut drawer = LayoutDrawer::new(layout, canvas); // }}} // {{{ Render background - let bg = get_b30_background(); + let bg = &*B30_BACKGROUND; let scale = (drawer.layout.width(root) as f32 / bg.width() as f32) .max(drawer.layout.height(root) as f32 / bg.height() as f32) @@ -325,8 +325,8 @@ async fn best_plays( .layout .edit_to_relative(item_with_margin, item_grid, origin.0, origin.1); - let top_bg = get_top_backgound(); - drawer.blit_rbg(top_area, (0, 0), top_bg.dimensions(), top_bg); + let top_bg = &*TOP_BACKGROUND; + drawer.blit_rbga(top_area, (0, 0), top_bg); let (play, song, chart) = if let Some(item) = plays.get(i) { item @@ -335,11 +335,11 @@ async fn best_plays( }; // {{{ Display index - let bg = get_count_background(); + let bg = &*COUNT_BACKGROUND; let bg_center = Rect::from_image(bg).center(); // 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); with_font(&EXO_FONT, |faces| { drawer.text( item_area, @@ -359,8 +359,8 @@ async fn best_plays( // }}} // {{{ Display chart name // Draw background - let bg = get_name_backgound(); - drawer.blit_rbg(bottom_area, (0, 0), bg.dimensions(), bg.as_raw()); + let bg = &*NAME_BACKGROUND; + drawer.blit_rbga(bottom_area, (0, 0), bg); // Draw text with_font(&EXO_FONT, |faces| { @@ -403,12 +403,7 @@ async fn best_plays( })?; drawer.fill(jacket_with_border, Color::from_rgb_int(0x271E35)); - drawer.blit_rbg( - jacket_area, - (0, 0), - jacket.bitmap.dimensions(), - &jacket.bitmap.as_raw(), - ); + drawer.blit_rbg(jacket_area, (0, 0), jacket.bitmap); // }}} // {{{ Display difficulty background let diff_bg = get_difficulty_background(chart.difficulty); @@ -417,12 +412,7 @@ async fn best_plays( (drawer.layout.width(jacket_with_border) as i32, 0), ); - drawer.blit_rbga( - jacket_with_border, - diff_bg_area.top_left(), - diff_bg.dimensions(), - &diff_bg.as_raw(), - ); + drawer.blit_rbga(jacket_with_border, diff_bg_area.top_left(), diff_bg); // }}} // {{{ Display difficulty text let x_offset = if chart.level.ends_with("+") { @@ -453,7 +443,7 @@ async fn best_plays( })?; // }}} // {{{ Display score background - let score_bg = get_score_background(); + let score_bg = &*SCORE_BACKGROUND; let score_bg_pos = Rect::from_image(score_bg).align( (Align::End, Align::End), ( @@ -462,12 +452,7 @@ async fn best_plays( ), ); - drawer.blit_rbga( - jacket_area, - score_bg_pos, - score_bg.dimensions(), - &score_bg.as_raw(), - ); + drawer.blit_rbga(jacket_area, score_bg_pos, score_bg); // }}} // {{{ Display score text with_font(&EXO_FONT, |faces| { @@ -491,7 +476,7 @@ async fn best_plays( })?; // }}} // {{{ Display status background - let status_bg = get_status_background(); + let status_bg = &*STATUS_BACKGROUND; let status_bg_area = Rect::from_image(status_bg).align_whole( (Align::Center, Align::Center), ( @@ -500,12 +485,7 @@ async fn best_plays( ), ); - drawer.blit_rbga( - jacket_area, - status_bg_area.top_left(), - status_bg.dimensions(), - &status_bg.as_raw(), - ); + drawer.blit_rbga(jacket_area, status_bg_area.top_left(), status_bg); // }}} // {{{ Display status text with_font(&EXO_FONT, |faces| { @@ -543,18 +523,13 @@ async fn best_plays( // }}} // {{{ Display grade background let top_left_center = (drawer.layout.width(top_left_area) as i32 + jacket_margin) / 2; - let grade_bg = get_grade_background(); + let grade_bg = &*GRADE_BACKGROUND; let grade_bg_area = Rect::from_image(grade_bg).align_whole( (Align::Center, Align::Center), (top_left_center, jacket_margin + 140), ); - drawer.blit_rbga( - top_area, - grade_bg_area.top_left(), - grade_bg.dimensions(), - &grade_bg.as_raw(), - ); + drawer.blit_rbga(top_area, grade_bg_area.top_left(), grade_bg); // }}} // {{{ Display grade text with_font(&EXO_FONT, |faces| { @@ -614,13 +589,12 @@ async fn best_plays( })?; // }}} // {{{ Display ptt emblem - let ptt_emblem = get_ptt_emblem(); + let ptt_emblem = &*PTT_EMBLEM; drawer.blit_rbga( top_left_area, Rect::from_image(ptt_emblem) .align((Align::Center, Align::Center), (top_left_center, 115)), - ptt_emblem.dimensions(), - ptt_emblem.as_raw(), + ptt_emblem, ); // }}} } diff --git a/src/context.rs b/src/context.rs index d836d32..6089aab 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1,10 +1,10 @@ -use std::{fs, path::PathBuf}; +use std::fs; use sqlx::SqlitePool; use crate::{ arcaea::{chart::SongCache, jacket::JacketCache}, - assets::{EXO_FONT, GEOSANS_FONT, KAZESAWA_BOLD_FONT, KAZESAWA_FONT}, + assets::{get_data_dir, EXO_FONT, GEOSANS_FONT, KAZESAWA_BOLD_FONT, KAZESAWA_FONT}, recognition::{hyperglass::CharMeasurements, ui::UIMeasurements}, }; @@ -14,9 +14,6 @@ pub type Context<'a> = poise::Context<'a, UserContext, Error>; // Custom user data passed to all command functions pub struct UserContext { - #[allow(dead_code)] - pub data_dir: PathBuf, - pub db: SqlitePool, pub song_cache: SongCache, pub jacket_cache: JacketCache, @@ -31,14 +28,14 @@ pub struct UserContext { impl UserContext { #[inline] - pub async fn new(data_dir: PathBuf, cache_dir: PathBuf, db: SqlitePool) -> Result { - fs::create_dir_all(&cache_dir)?; - fs::create_dir_all(&data_dir)?; + pub async fn new(db: SqlitePool) -> Result { + fs::create_dir_all(get_data_dir())?; let mut song_cache = SongCache::new(&db).await?; - let jacket_cache = JacketCache::new(&data_dir, &mut song_cache)?; - let ui_measurements = UIMeasurements::read(&data_dir)?; + let jacket_cache = JacketCache::new(&mut song_cache)?; + let ui_measurements = UIMeasurements::read()?; + // {{{ Font measurements static WHITELIST: &str = "0123456789'abcdefghklmnopqrstuvwxyzABCDEFGHIJKLMNOPRSTUVWXYZ"; let geosans_measurements = GEOSANS_FONT @@ -49,11 +46,11 @@ impl UserContext { .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"); Ok(Self { - data_dir, db, song_cache, jacket_cache, diff --git a/src/logs.rs b/src/logs.rs index a278ff1..745a6ba 100644 --- a/src/logs.rs +++ b/src/logs.rs @@ -6,11 +6,11 @@ //! allows for a convenient way to throw images into a `logs` directory with //! a simple env var. -use std::{env, ops::Deref, sync::OnceLock, time::Instant}; +use std::{env, ops::Deref, path::PathBuf, sync::OnceLock, time::Instant}; use image::{DynamicImage, EncodableLayout, ImageBuffer, PixelWithColorType}; -use crate::context::Error; +use crate::{assets::get_path, context::Error}; #[inline] fn should_save_debug_images() -> bool { @@ -19,6 +19,11 @@ fn should_save_debug_images() -> bool { .unwrap_or(false) } +#[inline] +fn get_log_dir() -> PathBuf { + get_path("SHIMMERING_LOG_DIR") +} + #[inline] fn get_startup_time() -> Instant { static CELL: OnceLock = OnceLock::new(); @@ -28,10 +33,10 @@ fn get_startup_time() -> Instant { #[inline] pub fn debug_image_log(image: &DynamicImage) -> Result<(), Error> { if should_save_debug_images() { - image.save(format!( - "./logs/{:0>15}.png", + image.save(get_log_dir().join(format!( + "{:0>15}.png", get_startup_time().elapsed().as_nanos() - ))?; + )))?; } Ok(()) @@ -45,10 +50,10 @@ where C: Deref, { if should_save_debug_images() { - image.save(format!( + image.save(get_log_dir().join(format!( "./logs/{:0>15}.png", get_startup_time().elapsed().as_nanos() - ))?; + )))?; } Ok(()) diff --git a/src/main.rs b/src/main.rs index 9e6acaa..445882d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ #![feature(array_try_map)] #![feature(async_closure)] #![feature(try_blocks)] +#![feature(thread_local)] mod arcaea; mod assets; @@ -21,7 +22,7 @@ use assets::get_data_dir; use context::{Error, UserContext}; use poise::serenity_prelude::{self as serenity}; use sqlx::sqlite::SqlitePoolOptions; -use std::{env::var, path::PathBuf, str::FromStr, sync::Arc, time::Duration}; +use std::{env::var, sync::Arc, time::Duration}; // {{{ Error handler async fn on_error(error: poise::FrameworkError<'_, UserContext, Error>) { @@ -37,13 +38,10 @@ async fn on_error(error: poise::FrameworkError<'_, UserContext, Error>) { #[tokio::main] async fn main() { - let data_dir = get_data_dir(); - let cache_dir = var("SHIMMERING_CACHE_DIR").expect("Missing `SHIMMERING_CACHE_DIR` env var"); - let pool = SqlitePoolOptions::new() .connect(&format!( "sqlite://{}/db.sqlite", - data_dir.to_str().unwrap() + get_data_dir().to_str().unwrap() )) .await .unwrap(); @@ -89,7 +87,7 @@ async fn main() { Box::pin(async move { println!("Logged in as {}", _ready.user.name); poise::builtins::register_globally(ctx, &framework.options().commands).await?; - let ctx = UserContext::new(data_dir, PathBuf::from_str(&cache_dir)?, pool).await?; + let ctx = UserContext::new(pool).await?; Ok(ctx) }) diff --git a/src/recognition/ui.rs b/src/recognition/ui.rs index 2c6061a..8886820 100644 --- a/src/recognition/ui.rs +++ b/src/recognition/ui.rs @@ -1,8 +1,8 @@ -use std::{fs, path::PathBuf}; +use std::fs; use image::GenericImage; -use crate::{bitmap::Rect, context::Error}; +use crate::{assets::get_config_dir, bitmap::Rect, context::Error}; // {{{ Rects #[derive(Debug, Clone, Copy)] @@ -94,11 +94,11 @@ pub struct UIMeasurements { impl UIMeasurements { // {{{ Read - pub fn read(data_dir: &PathBuf) -> Result { + pub fn read() -> Result { let mut measurements = Vec::new(); let mut measurement = UIMeasurement::default(); - let path = data_dir.join("ui.txt"); + let path = get_config_dir().join("ui.txt"); let contents = fs::read_to_string(path)?; // {{{ Parse measurement file