From d260a11263b02648fb504029e65bdf6abae20a9d Mon Sep 17 00:00:00 2001 From: prescientmoon Date: Thu, 8 Aug 2024 15:59:36 +0200 Subject: [PATCH] Commit before deleting lots of code Signed-off-by: prescientmoon --- Cargo.lock | 164 ++------------------------------------- Cargo.toml | 6 -- data/ui.txt | 16 ++++ src/chart.rs | 5 +- src/commands/score.rs | 11 ++- src/commands/stats.rs | 8 +- src/context.rs | 5 +- src/jacket.rs | 47 +++++------ src/levenshtein.rs | 61 +++++++++++++++ src/main.rs | 2 + src/ocr/mod.rs | 1 + src/ocr/ui_interp.rs | 176 ++++++++++++++++++++++++++++++++++++++++++ src/score.rs | 135 ++++++++++++++++++-------------- src/user.rs | 12 +++ 14 files changed, 393 insertions(+), 256 deletions(-) create mode 100644 data/ui.txt create mode 100644 src/levenshtein.rs create mode 100644 src/ocr/mod.rs create mode 100644 src/ocr/ui_interp.rs diff --git a/Cargo.lock b/Cargo.lock index 169b446..f17de62 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -118,15 +118,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "atomic-polyfill" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" -dependencies = [ - "critical-section", -] - [[package]] name = "autocfg" version = "1.3.0" @@ -382,12 +373,6 @@ dependencies = [ "libloading", ] -[[package]] -name = "cobs" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" - [[package]] name = "color_quant" version = "1.1.0" @@ -485,12 +470,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "critical-section" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7059fff8937831a9ae6f0fe4d658ffabf58f2ca96aa9dec1c889f936f705f216" - [[package]] name = "crossbeam-channel" version = "0.5.13" @@ -602,7 +581,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", - "hashbrown 0.14.5", + "hashbrown", "lock_api", "once_cell", "parking_lot_core", @@ -707,12 +686,6 @@ dependencies = [ "wio", ] -[[package]] -name = "edit-distance" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbbaaaf38131deb9ca518a274a45bfdb8771f139517b073b16c2d3d32ae5037b" - [[package]] name = "either" version = "1.12.0" @@ -722,12 +695,6 @@ dependencies = [ "serde", ] -[[package]] -name = "embedded-io" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" - [[package]] name = "encoding_rs" version = "0.8.34" @@ -1099,7 +1066,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.2.6", + "indexmap", "slab", "tokio", "tokio-util", @@ -1116,21 +1083,6 @@ dependencies = [ "crunchy", ] -[[package]] -name = "hash32" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" -dependencies = [ - "byteorder", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - [[package]] name = "hashbrown" version = "0.14.5" @@ -1147,21 +1099,7 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" dependencies = [ - "hashbrown 0.14.5", -] - -[[package]] -name = "heapless" -version = "0.7.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" -dependencies = [ - "atomic-polyfill", - "hash32", - "rustc_version", - "serde", - "spin 0.9.8", - "stable_deref_trait", + "hashbrown", ] [[package]] @@ -1393,17 +1331,6 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44feda355f4159a7c757171a77de25daf6411e217b4cabd03bd6650690468126" -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", - "serde", -] - [[package]] name = "indexmap" version = "2.2.6" @@ -1411,8 +1338,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", - "hashbrown 0.14.5", - "serde", + "hashbrown", ] [[package]] @@ -1471,19 +1397,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "kd-tree" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f89ee4e60e82cf7024e5e94618c646fbf61ce7501dc5898b3d12786442d3682" -dependencies = [ - "num-traits", - "ordered-float", - "paste", - "serde", - "typenum", -] - [[package]] name = "lazy_static" version = "1.4.0" @@ -1851,15 +1764,6 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" -[[package]] -name = "ordered-float" -version = "4.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a76df7075c7d4d01fdcb46c912dd17fba5b60c78ea480b475f2b6ab6f666584e" -dependencies = [ - "num-traits", -] - [[package]] name = "parking_lot" version = "0.12.3" @@ -2052,18 +1956,6 @@ dependencies = [ "syn 2.0.66", ] -[[package]] -name = "postcard" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55c51ee6c0db07e68448e336cf8ea4131a620edefebf9893e759b2d793420f8" -dependencies = [ - "cobs", - "embedded-io", - "heapless", - "serde", -] - [[package]] name = "powerfmt" version = "0.2.0" @@ -2592,36 +2484,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_with" -version = "3.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e73139bc5ec2d45e6c5fd85be5a46949c1c39a4c18e56915f5eb4c12f975e377" -dependencies = [ - "base64 0.22.1", - "chrono", - "hex", - "indexmap 1.9.3", - "indexmap 2.2.6", - "serde", - "serde_derive", - "serde_json", - "serde_with_macros", - "time", -] - -[[package]] -name = "serde_with_macros" -version = "3.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b80d3d6b56b64335c0180e5ffde23b3c5e08c14c585b51a15bd0e95393f46703" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn 2.0.66", -] - [[package]] name = "serenity" version = "0.12.2" @@ -2682,20 +2544,14 @@ name = "shimmeringmoon" version = "0.1.0" dependencies = [ "chrono", - "edit-distance", "freetype-rs", "image 0.25.1", - "kd-tree", "num", "plotters", "poise", - "postcard", - "serde", - "serde_with", "sqlx", "tesseract", "tokio", - "typenum", ] [[package]] @@ -2839,7 +2695,7 @@ dependencies = [ "futures-util", "hashlink", "hex", - "indexmap 2.2.6", + "indexmap", "log", "memchr", "once_cell", @@ -3002,12 +2858,6 @@ dependencies = [ "urlencoding", ] -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - [[package]] name = "stringprep" version = "0.1.5" @@ -3344,7 +3194,7 @@ version = "0.22.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" dependencies = [ - "indexmap 2.2.6", + "indexmap", "serde", "serde_spanned", "toml_datetime", @@ -3448,7 +3298,7 @@ checksum = "eb704842c709bc76f63e99e704cb208beeccca2abbabd0d9aec02e48ca1cee0f" dependencies = [ "chrono", "dashmap", - "hashbrown 0.14.5", + "hashbrown", "mini-moka", "parking_lot", "secrecy", diff --git a/Cargo.toml b/Cargo.toml index 5b23459..b201539 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,20 +5,14 @@ edition = "2021" [dependencies] chrono = "0.4.38" -edit-distance = "2.1.0" freetype-rs = "0.36.0" image = "0.25.1" -kd-tree = { version="0.6.0", features=["serde"] } num = "0.4.3" plotters = { git="https://github.com/starlitcanopy/plotters.git", rev="986cd959362a2dbec8d1b25670fd083b904d7b8c", features=["bitmap_backend"] } poise = "0.6.1" -postcard = { version="1.0.8", features=["use-std"] } -serde = "1.0.204" -serde_with = "3.8.3" sqlx = { version = "0.7.4", features = ["sqlite", "runtime-tokio", "chrono"] } tesseract = "0.15.1" tokio = {version="1.38.0", features=["rt-multi-thread"]} -typenum = "1.17.0" [profile.dev.package."*"] opt-level = 3 diff --git a/data/ui.txt b/data/ui.txt new file mode 100644 index 0000000..65eed86 --- /dev/null +++ b/data/ui.txt @@ -0,0 +1,16 @@ +2160 1620 + 19 15 273 60 Play kind + 841 683 500 92 Score screen — score + 51 655 633 632 Score screen — jacket + 155 546 167 38 Score screen — difficulty +1095 1087 87 34 Score screen — pures +1095 1150 87 34 Score screen — fars +1095 1212 87 34 Score screen — losts + 364 593 87 34 Score screen — max recall + 438 324 1244 104 Score screen — title + 15 264 291 52 Song select — score + 158 411 909 74 Song select — jacket + 12 159 0 0 Song select — PST + 199 159 0 0 Song select — PRS + 389 159 0 0 Song select — FTR + 579 159 0 0 Song select — ETR/BYD diff --git a/src/chart.rs b/src/chart.rs index 3d21f5b..ad49f64 100644 --- a/src/chart.rs +++ b/src/chart.rs @@ -1,15 +1,12 @@ use std::path::PathBuf; use image::{ImageBuffer, Rgb}; -use serde::{Deserialize, Serialize}; use sqlx::SqlitePool; use crate::context::Error; // {{{ Difficuly -#[derive( - Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, sqlx::Type, Serialize, Deserialize, -)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, sqlx::Type)] pub enum Difficulty { PST, PRS, diff --git a/src/commands/score.rs b/src/commands/score.rs index 3254591..1ec6374 100644 --- a/src/commands/score.rs +++ b/src/commands/score.rs @@ -280,7 +280,9 @@ Title error: {:?} // }}} // }}} // {{{ Deliver embed - let (mut embed, attachment) = play.to_embed(&song, &chart, i, None).await?; + let (mut embed, attachment) = play + .to_embed(&ctx.data().db, &user, &song, &chart, i, None) + .await?; if let Some(warning) = score_warning { embed = embed.description(warning); } @@ -401,10 +403,13 @@ pub async fn show( creation_zeta_ptt: None, }; - let user = discord_it_to_discord_user(&ctx, &res.discord_id).await?; + let author = discord_it_to_discord_user(&ctx, &res.discord_id).await?; + let user = User::by_id(&ctx.data().db, play.user_id).await?; let (song, chart) = ctx.data().song_cache.lookup_chart(play.chart_id)?; - let (embed, attachment) = play.to_embed(song, chart, i, Some(&user)).await?; + let (embed, attachment) = play + .to_embed(&ctx.data().db, &user, song, chart, i, Some(&author)) + .await?; embeds.push(embed); attachments.extend(attachment); diff --git a/src/commands/stats.rs b/src/commands/stats.rs index b6e89f5..dfdd389 100644 --- a/src/commands/stats.rs +++ b/src/commands/stats.rs @@ -73,7 +73,6 @@ pub async fn best( }; let (song, chart) = guess_song_and_chart(&ctx.data(), &name)?; - let play = query_as!( DbPlay, " @@ -97,6 +96,8 @@ pub async fn best( let (embed, attachment) = play .to_embed( + &ctx.data().db, + &user, &song, &chart, 0, @@ -602,10 +603,7 @@ pub async fn b30(ctx: Context<'_>) -> Result<(), Error> { (top_left_center, 94), font, style, - &format!( - "{:.2}", - (play.score.play_rating(chart.chart_constant)) as f32 / 100. - ), + &format!("{:.2}", play.score.play_rating_f32(chart.chart_constant)), )?; Ok(()) diff --git a/src/context.rs b/src/context.rs index 3fb8a8a..8be2029 100644 --- a/src/context.rs +++ b/src/context.rs @@ -2,7 +2,7 @@ use std::{fs, path::PathBuf}; use sqlx::SqlitePool; -use crate::{chart::SongCache, jacket::JacketCache}; +use crate::{chart::SongCache, jacket::JacketCache, ocr::ui_interp::UIMeasurements}; // Types used by all command functions pub type Error = Box; @@ -15,6 +15,7 @@ pub struct UserContext { pub db: SqlitePool, pub song_cache: SongCache, pub jacket_cache: JacketCache, + pub ui_measurements: UIMeasurements, } impl UserContext { @@ -25,6 +26,7 @@ impl UserContext { 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)?; println!("Created user context"); @@ -33,6 +35,7 @@ impl UserContext { db, song_cache, jacket_cache, + ui_measurements, }) } } diff --git a/src/jacket.rs b/src/jacket.rs index 63ac7c9..719bc53 100644 --- a/src/jacket.rs +++ b/src/jacket.rs @@ -1,7 +1,6 @@ use std::{fs, path::PathBuf, str::FromStr}; use image::{imageops::FilterType, GenericImageView, Rgba}; -use kd_tree::{KdMap, KdPoint}; use num::Integer; use crate::{ @@ -59,24 +58,23 @@ impl ImageVec { Self { colors } } + + #[inline] + pub fn distance_squared_to(&self, other: &Self) -> f32 { + let mut total = 0.0; + + for i in 0..IMAGE_VEC_DIM { + let d = self.colors[i] - other.colors[i]; + total += d * d; + } + + total + } // }}} } -impl KdPoint for ImageVec { - type Dim = typenum::U75; - type Scalar = f32; - - fn dim() -> usize { - IMAGE_VEC_DIM - } - - fn at(&self, i: usize) -> Self::Scalar { - self.colors[i] - } -} - pub struct JacketCache { - tree: KdMap, + jackets: Vec<(u32, ImageVec)>, } impl JacketCache { @@ -91,7 +89,7 @@ impl JacketCache { fs::create_dir_all(&jacket_dir).expect("Could not create jacket dir"); - let tree_entries = if should_skip_jacket_art() { + let jacket_vectors = if should_skip_jacket_art() { let path = get_assets_dir().join("placeholder_jacket.jpg"); let contents: &'static _ = fs::read(path)?.leak(); let image = image::load_from_memory(contents)?; @@ -114,7 +112,7 @@ impl JacketCache { } else { let entries = fs::read_dir(data_dir.join("songs")).expect("Couldn't read songs directory"); - let mut tree_entries = vec![]; + let mut jacket_vectors = vec![]; for entry in entries { let dir = entry?; @@ -147,7 +145,7 @@ impl JacketCache { let contents: &'static _ = fs::read(file.path())?.leak(); let image = image::load_from_memory(contents)?; - tree_entries.push((ImageVec::from_image(&image), song.id)); + jacket_vectors.push((song.id, ImageVec::from_image(&image))); let bitmap: &'static _ = Box::leak(Box::new( image @@ -201,11 +199,11 @@ impl JacketCache { } } - tree_entries + jacket_vectors }; let result = Self { - tree: KdMap::build_by_ordered_float(tree_entries), + jackets: jacket_vectors, }; Ok(result) @@ -217,9 +215,12 @@ impl JacketCache { &self, image: &impl GenericImageView>, ) -> Option<(f32, &u32)> { - self.tree - .nearest(&ImageVec::from_image(image)) - .map(|p| (p.squared_distance.sqrt(), &p.item.1)) + let vec = ImageVec::from_image(image); + self.jackets + .iter() + .map(|(i, v)| (i, v, v.distance_squared_to(&vec))) + .min_by(|(_, _, d1), (_, _, d2)| d1.partial_cmp(d2).expect("NaN distance encountered")) + .map(|(i, _, d)| (d, i)) } // }}} } diff --git a/src/levenshtein.rs b/src/levenshtein.rs new file mode 100644 index 0000000..4ca4be8 --- /dev/null +++ b/src/levenshtein.rs @@ -0,0 +1,61 @@ +// Modified version of https://docs.rs/edit-distance/latest/src/edit_distance/lib.rs.html#1-76 + +/// Similar to `edit_distance`, but takes in a preallocated vec so consecutive calls are efficient. +pub fn edit_distance_with(a: &str, b: &str, cur: &mut Vec) -> usize { + let len_a = a.chars().count(); + let len_b = b.chars().count(); + if len_a < len_b { + return edit_distance_with(b, a, cur); + } + + // handle special case of 0 length + if len_a == 0 { + return len_b; + } else if len_b == 0 { + return len_a; + } + + let len_b = len_b + 1; + + let mut pre; + let mut tmp; + + cur.clear(); + cur.resize(len_b, 0); + + // initialize string b + for i in 1..len_b { + cur[i] = i; + } + + // calculate edit distance + for (i, ca) in a.chars().enumerate() { + // get first column for this row + pre = cur[0]; + cur[0] = i + 1; + for (j, cb) in b.chars().enumerate() { + tmp = cur[j + 1]; + cur[j + 1] = std::cmp::min( + // deletion + tmp + 1, + std::cmp::min( + // insertion + cur[j] + 1, + // match or substitution + pre + if ca == cb { 0 } else { 1 }, + ), + ); + pre = tmp; + } + } + cur[len_b - 1] +} + +/// Returns the edit distance between strings `a` and `b`. +/// +/// The runtime complexity is `O(m*n)`, where `m` and `n` are the +/// strings' lengths. +#[inline] +pub fn edit_distance(a: &str, b: &str) -> usize { + edit_distance_with(a, b, &mut Vec::new()) +} diff --git a/src/main.rs b/src/main.rs index 39b37a4..0406839 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,6 +11,8 @@ mod commands; mod context; mod image; mod jacket; +mod levenshtein; +mod ocr; mod score; mod user; diff --git a/src/ocr/mod.rs b/src/ocr/mod.rs new file mode 100644 index 0000000..495cbe7 --- /dev/null +++ b/src/ocr/mod.rs @@ -0,0 +1 @@ +pub mod ui_interp; diff --git a/src/ocr/ui_interp.rs b/src/ocr/ui_interp.rs new file mode 100644 index 0000000..1965c2d --- /dev/null +++ b/src/ocr/ui_interp.rs @@ -0,0 +1,176 @@ +#![allow(dead_code)] + +use std::{fs, path::PathBuf}; + +use crate::context::Error; + +// {{{ Rects +#[derive(Debug, Clone, Copy)] +pub enum ScoreScreenRect { + Score, + Jacket, + Difficulty, + Pure, + Far, + Lost, + MaxRecall, + Title, +} + +#[derive(Debug, Clone, Copy)] +pub enum SongSelectRect { + Score, + Jacket, + Past, + Present, + Future, + Beyond, +} + +#[derive(Debug, Clone, Copy)] +pub enum UIMeasurementRect { + PlayKind, + ScoreScreen(ScoreScreenRect), + SongSelect(SongSelectRect), +} + +impl UIMeasurementRect { + #[inline] + pub fn to_index(self) -> usize { + match self { + Self::PlayKind => 0, + Self::ScoreScreen(ScoreScreenRect::Score) => 1, + Self::ScoreScreen(ScoreScreenRect::Jacket) => 2, + Self::ScoreScreen(ScoreScreenRect::Difficulty) => 3, + Self::ScoreScreen(ScoreScreenRect::Pure) => 4, + Self::ScoreScreen(ScoreScreenRect::Far) => 5, + Self::ScoreScreen(ScoreScreenRect::Lost) => 6, + Self::ScoreScreen(ScoreScreenRect::MaxRecall) => 7, + Self::ScoreScreen(ScoreScreenRect::Title) => 8, + Self::SongSelect(SongSelectRect::Score) => 9, + Self::SongSelect(SongSelectRect::Jacket) => 10, + Self::SongSelect(SongSelectRect::Past) => 11, + Self::SongSelect(SongSelectRect::Present) => 12, + Self::SongSelect(SongSelectRect::Future) => 13, + Self::SongSelect(SongSelectRect::Beyond) => 14, + } + } +} + +pub const UI_RECT_COUNT: usize = 15; +// }}} +// {{{ Measurement +pub struct UIMeasurement { + dimensions: [u32; 2], + datapoints: [u32; UI_RECT_COUNT * 4], +} + +impl Default for UIMeasurement { + fn default() -> Self { + Self::new([0; 2], [0; UI_RECT_COUNT * 4]) + } +} + +impl UIMeasurement { + pub fn new(dimensions: [u32; 2], datapoints: [u32; UI_RECT_COUNT * 4]) -> Self { + Self { + dimensions, + datapoints, + } + } + + #[inline] + pub fn aspect_ratio(&self) -> f32 { + self.dimensions[0] as f32 / self.dimensions[1] as f32 + } +} +// }}} +// {{{ Measurements +pub struct UIMeasurements { + pub measurements: Vec, +} + +impl UIMeasurements { + // {{{ Read + pub fn read(data_dir: &PathBuf) -> Result { + let mut measurements = Vec::new(); + let mut measurement = UIMeasurement::default(); + + let path = data_dir.join("ui.txt"); + let contents = fs::read_to_string(path)?; + + // {{{ Parse measurement file + for (i, line) in contents.split('\n').enumerate() { + let i = i % (UI_RECT_COUNT + 2); + if i == 0 { + for (j, str) in line.split_whitespace().enumerate().take(2) { + measurement.dimensions[j] = u32::from_str_radix(str, 10)?; + } + } else if i == UI_RECT_COUNT + 2 { + measurements.push(measurement); + measurement = UIMeasurement::default(); + } else { + for (j, str) in line.split_whitespace().enumerate().take(4) { + measurement.datapoints[(i - 1) * 4 + j] = u32::from_str_radix(str, 10)?; + } + } + } + // }}} + + measurements.push(measurement); + measurements.sort_by_key(|r| (r.aspect_ratio() * 1000.0) as u32); + + // {{{ Filter datapoints that are close together + let mut i = 0; + while i < measurements.len() - 1 { + let low = &measurements[i]; + let high = &measurements[i + 1]; + + if (low.aspect_ratio() - high.aspect_ratio()).abs() < 0.001 { + // TODO: we could interpolate here but oh well + measurements.remove(i + 1); + } + + i += 1; + } + // }}} + + Ok(Self { measurements }) + } + // }}} + // {{{ Interpolate + pub fn interpolate( + &self, + rect: UIMeasurementRect, + dimensions: [u32; 2], + ) -> Result<[u32; 4], Error> { + let aspect_ratio = dimensions[0] as f32 / dimensions[1] as f32; + let r = rect.to_index(); + + for i in 0..(self.measurements.len() - 1) { + let low = &self.measurements[i]; + let high = &self.measurements[i + 1]; + + let low_ratio = low.aspect_ratio(); + let high_ratio = high.aspect_ratio(); + + if (i == 0 || low_ratio <= aspect_ratio) + && (aspect_ratio <= high_ratio || i == self.measurements.len() - 2) + { + let p = (aspect_ratio - low_ratio) / (high_ratio - low_ratio); + let mut out = [0; 4]; + for j in 0..4 { + let l = low.datapoints[4 * r + j] as f32 / low.dimensions[j % 2] as f32; + let h = high.datapoints[4 * r + j] as f32 / high.dimensions[j % 2] as f32; + out[j] = ((l + (h - l) * p) * dimensions[j % 2] as f32) as u32; + } + + return Ok(out); + } + } + + Err(format!("Could no find rect for {rect:?} in image").into()) + } + // }}} +} +// }}} diff --git a/src/score.rs b/src/score.rs index 59dbfd6..a4255df 100644 --- a/src/score.rs +++ b/src/score.rs @@ -5,7 +5,6 @@ use std::io::Cursor; use std::str::FromStr; use std::sync::OnceLock; -use edit_distance::edit_distance; use image::{imageops::FilterType, DynamicImage, GenericImageView}; use num::integer::Roots; use num::{traits::Euclid, Rational64}; @@ -20,8 +19,15 @@ use crate::chart::{Chart, Difficulty, Song, SongCache}; use crate::context::{Error, UserContext}; use crate::image::rotate; use crate::jacket::IMAGE_VEC_DIM; +use crate::levenshtein::{edit_distance, edit_distance_with}; use crate::user::User; +// {{{ Utils +#[inline] +fn lerp(i: f32, a: f32, b: f32) -> f32 { + a + (b - a) * i +} +// }}} // {{{ Grade #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum Grade { @@ -138,6 +144,11 @@ impl Score { (self.0 as i32 - 9_500_000) / 3_000 } } + + #[inline] + pub fn play_rating_f32(self, chart_constant: u32) -> f32 { + (self.play_rating(chart_constant)) as f32 / 100.0 + } // }}} // {{{ Score => grade #[inline] @@ -390,13 +401,13 @@ impl CreatePlay { let attachment_id = self.discord_attachment_id.map(|i| i.get() as i64); let play = sqlx::query!( " - INSERT INTO plays( - user_id,chart_id,discord_attachment_id, - score,zeta_score,max_recall,far_notes - ) - VALUES(?,?,?,?,?,?,?) - RETURNING id, created_at - ", + INSERT INTO plays( + user_id,chart_id,discord_attachment_id, + score,zeta_score,max_recall,far_notes + ) + VALUES(?,?,?,?,?,?,?) + RETURNING id, created_at + ", self.user_id, self.chart_id, attachment_id, @@ -559,23 +570,44 @@ impl Play { /// The `index` variable is only used to create distinct filenames. pub async fn to_embed( &self, + db: &SqlitePool, + user: &User, song: &Song, chart: &Chart, index: usize, author: Option<&poise::serenity_prelude::User>, ) -> Result<(CreateEmbed, Option), Error> { + // {{{ Get previously best score + let previously_best = query_as!( + DbPlay, + " + SELECT * FROM plays + WHERE user_id=? + AND chart_id=? + AND created_at Some(CreateAttachment::bytes(jacket.raw, &attachement_name)), None => None, }; - println!("Rating {:?}", self.score.play_rating(chart.chart_constant)); - println!( - "Rating {:?}", - self.score.play_rating(chart.chart_constant) as f32 / 100.0 - ); - let mut embed = CreateEmbed::default() .title(format!( "{} [{:?} {}]", @@ -586,20 +618,41 @@ impl Play { "Rating", format!( "{:.2} (+?)", - (self.score.play_rating(chart.chart_constant)) as f32 / 100.0 + self.score.play_rating_f32(chart.chart_constant) ), true, ) .field("Grade", format!("{}", self.score.grade()), true) .field("ξ-Score", format!("{} (+?)", self.zeta_score), true) + // {{{ ξ-Rating .field( "ξ-Rating", - format!( - "{:.2} (+?)", - (self.zeta_score.play_rating(chart.chart_constant)) as f32 / 100. - ), + { + let play_rating = self.zeta_score.play_rating_f32(chart.chart_constant); + if let Some(previous) = previously_best { + let previous_play_rating = + previous.zeta_score.play_rating_f32(chart.chart_constant); + + if play_rating >= previous_play_rating { + format!( + "{:.2} (+{})", + play_rating, + play_rating - previous_play_rating + ) + } else { + format!( + "{:.2} (-{})", + play_rating, + play_rating - previous_play_rating + ) + } + } else { + format!("{:.2}", play_rating) + } + }, true, ) + // }}} .field("ξ-Grade", format!("{}", self.zeta_score.grade()), true) .field( "Status", @@ -629,37 +682,6 @@ impl Play { Ok((embed, icon_attachement)) } // }}} - // {{{ Get best play - pub async fn best_play( - db: &SqlitePool, - user: User, - song: Song, - chart: Chart, - ) -> Result { - let play = query_as!( - DbPlay, - " - SELECT * FROM plays - WHERE user_id=? - AND chart_id=? - ORDER BY score DESC - ", - user.id, - chart.id - ) - .fetch_one(db) - .await - .map_err(|_| { - format!( - "Could not find any scores for {} [{:?}]", - song.title, chart.difficulty - ) - })? - .to_play(); - - Ok(play) - } - // }}} } // }}} // {{{ Tests @@ -768,10 +790,6 @@ pub struct RelativeRect { pub dimensions: ImageDimensions, } -fn lerp(i: f32, a: f32, b: f32) -> f32 { - a + (b - a) * i -} - impl RelativeRect { #[inline] pub fn new(x: f32, y: f32, width: f32, height: f32, dimensions: ImageDimensions) -> Self { @@ -1229,8 +1247,11 @@ pub fn guess_chart_name<'a>( let raw_text = raw_text.trim(); // not quite raw 🤔 let mut text: &str = &raw_text.to_lowercase(); + // Cached vec used by the levenshtein distance function + let mut levenshtein_vec = Vec::with_capacity(20); // Cached vec used to store distance calculations let mut distance_vec = Vec::with_capacity(3); + let (song, chart) = loop { let mut close_enough: Vec<_> = cache .songs() @@ -1245,7 +1266,7 @@ pub fn guess_chart_name<'a>( let song_title = &song.lowercase_title; distance_vec.clear(); - let base_distance = edit_distance(&text, &song_title); + let base_distance = edit_distance_with(&text, &song_title, &mut levenshtein_vec); if base_distance < 1.max(song.title.len() / 3) { distance_vec.push(base_distance * 10 + 2); } @@ -1254,7 +1275,7 @@ pub fn guess_chart_name<'a>( if let Some(sliced) = &song_title.get(..shortest_len) && (text.len() >= 6 || unsafe_heuristics) { - let slice_distance = edit_distance(&text, sliced); + let slice_distance = edit_distance_with(&text, sliced, &mut levenshtein_vec); if slice_distance < 1 { distance_vec.push(slice_distance * 10 + 3); } @@ -1263,7 +1284,7 @@ pub fn guess_chart_name<'a>( if let Some(shorthand) = &chart.shorthand && unsafe_heuristics { - let short_distance = edit_distance(&text, shorthand); + let short_distance = edit_distance_with(&text, shorthand, &mut levenshtein_vec); if short_distance < 1.max(shorthand.len() / 3) { distance_vec.push(short_distance * 10 + 1); } diff --git a/src/user.rs b/src/user.rs index 46bb822..a11bd7f 100644 --- a/src/user.rs +++ b/src/user.rs @@ -1,6 +1,7 @@ use std::str::FromStr; use poise::serenity_prelude::UserId; +use sqlx::SqlitePool; use crate::context::{Context, Error}; @@ -22,6 +23,17 @@ impl User { discord_id: user.discord_id, }) } + + pub async fn by_id(db: &SqlitePool, id: u32) -> Result { + let user = sqlx::query!("SELECT * FROM users WHERE id = ?", id) + .fetch_one(db) + .await?; + + Ok(User { + id: user.id as u32, + discord_id: user.discord_id, + }) + } } #[inline]