diff --git a/.gitignore b/.gitignore index 61ca56b..3b2743f 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ data/songs backups dump.sql logs +cache diff --git a/Cargo.lock b/Cargo.lock index 9f593bd..2972394 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -118,6 +118,15 @@ 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" @@ -373,6 +382,12 @@ 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" @@ -470,6 +485,12 @@ 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" @@ -581,7 +602,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", - "hashbrown", + "hashbrown 0.14.5", "lock_api", "once_cell", "parking_lot_core", @@ -701,6 +722,12 @@ 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" @@ -1061,7 +1088,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap", + "indexmap 2.2.6", "slab", "tokio", "tokio-util", @@ -1078,6 +1105,21 @@ 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" @@ -1094,7 +1136,21 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" dependencies = [ - "hashbrown", + "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", ] [[package]] @@ -1326,6 +1382,17 @@ 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" @@ -1333,7 +1400,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.14.5", + "serde", ] [[package]] @@ -1401,6 +1469,7 @@ dependencies = [ "num-traits", "ordered-float", "paste", + "serde", "typenum", ] @@ -1972,6 +2041,18 @@ 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" @@ -2441,9 +2522,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.203" +version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" +checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" dependencies = [ "serde_derive", ] @@ -2459,9 +2540,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.203" +version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" +checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" dependencies = [ "proc-macro2", "quote", @@ -2500,6 +2581,36 @@ 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" @@ -2566,6 +2677,9 @@ dependencies = [ "num", "plotters", "poise", + "postcard", + "serde", + "serde_with", "sqlx", "tesseract", "tokio", @@ -2713,7 +2827,7 @@ dependencies = [ "futures-util", "hashlink", "hex", - "indexmap", + "indexmap 2.2.6", "log", "memchr", "once_cell", @@ -2876,6 +2990,12 @@ 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" @@ -3212,7 +3332,7 @@ version = "0.22.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" dependencies = [ - "indexmap", + "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", @@ -3316,7 +3436,7 @@ checksum = "eb704842c709bc76f63e99e704cb208beeccca2abbabd0d9aec02e48ca1cee0f" dependencies = [ "chrono", "dashmap", - "hashbrown", + "hashbrown 0.14.5", "mini-moka", "parking_lot", "secrecy", diff --git a/Cargo.toml b/Cargo.toml index 7817ea0..c5e8f46 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,10 +7,13 @@ edition = "2021" chrono = "0.4.38" edit-distance = "2.1.0" image = "0.25.1" -kd-tree = "0.6.0" +kd-tree = { version="0.6.0", features=["serde"] } num = "0.4.3" plotters = { git="https://github.com/starlitcanopy/plotters.git", rev="986cd959362a2dbec8d1b25670fd083b904d7b8c" } 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"]} diff --git a/src/chart.rs b/src/chart.rs index 2ea3164..2442121 100644 --- a/src/chart.rs +++ b/src/chart.rs @@ -1,11 +1,14 @@ use std::path::PathBuf; +use serde::{Deserialize, Serialize}; use sqlx::{prelude::FromRow, SqlitePool}; use crate::context::Error; // {{{ Difficuly -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, sqlx::Type)] +#[derive( + Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, sqlx::Type, Serialize, Deserialize, +)] pub enum Difficulty { PST, PRS, @@ -42,21 +45,58 @@ impl TryFrom for Difficulty { } } // }}} +// {{{ Side +#[derive(Debug, Clone, Copy)] +pub enum Side { + Light, + Conflict, + Silent, +} + +impl Side { + pub const SIDES: [Self; 3] = [Self::Light, Self::Conflict, Self::Silent]; + pub const SIDE_STRINGS: [&'static str; 3] = ["light", "conflict", "silent"]; + + #[inline] + pub fn to_index(self) -> usize { + self as usize + } +} + +impl TryFrom for Side { + type Error = String; + + fn try_from(value: String) -> Result { + for (i, s) in Self::SIDE_STRINGS.iter().enumerate() { + if value == **s { + return Ok(Self::SIDES[i]); + } + } + + Err(format!("Cannot convert {} to difficulty", value)) + } +} +// }}} // {{{ Song #[derive(Debug, Clone, FromRow)] pub struct Song { pub id: u32, pub title: String, - #[allow(dead_code)] + pub lowercase_title: String, pub artist: String, + + pub bpm: String, + pub pack: Option, + pub side: Side, } // }}} // {{{ Chart -#[derive(Debug, Clone, FromRow)] +#[derive(Debug, Clone, FromRow, Serialize, Deserialize)] pub struct Chart { pub id: u32, pub song_id: u32, pub shorthand: Option, + pub note_design: Option, pub difficulty: Difficulty, pub level: String, // TODO: this could become an enum @@ -200,8 +240,12 @@ impl SongCache { for song in songs { let song = Song { id: song.id as u32, + lowercase_title: song.title.to_lowercase(), title: song.title, artist: song.artist, + pack: song.pack, + bpm: song.bpm, + side: Side::try_from(song.side)?, }; let song_id = song.id as usize; @@ -225,6 +269,7 @@ impl SongCache { chart_constant: chart.chart_constant as u32, note_count: chart.note_count as u32, cached_jacket: None, + note_design: chart.note_design, }; let index = chart.difficulty.to_index(); diff --git a/src/commands/chart.rs b/src/commands/chart.rs new file mode 100644 index 0000000..989df40 --- /dev/null +++ b/src/commands/chart.rs @@ -0,0 +1,76 @@ +use poise::serenity_prelude::{CreateAttachment, CreateEmbed, CreateMessage}; +use sqlx::query; + +use crate::{ + chart::Side, + context::{Context, Error}, + score::guess_song_and_chart, +}; + +// {{{ Chart +/// Show a chart given it's name +#[poise::command(prefix_command, slash_command)] +pub async fn chart( + ctx: Context<'_>, + #[rest] + #[description = "Name of chart to show (difficulty at the end)"] + name: String, +) -> Result<(), Error> { + let (song, chart) = guess_song_and_chart(&ctx.data(), &name)?; + + let attachement_name = "chart.png"; + let icon_attachement = match chart.cached_jacket { + Some(bytes) => Some(CreateAttachment::bytes(bytes, attachement_name)), + None => None, + }; + + let play_count = query!( + " + SELECT COUNT(*) as count + FROM plays + WHERE chart_id=? + ", + chart.id + ) + .fetch_one(&ctx.data().db) + .await?; + + let mut embed = CreateEmbed::default() + .title(format!( + "{} [{:?} {}]", + &song.title, chart.difficulty, chart.level + )) + .field("Note count", format!("{}", chart.note_count), true) + .field( + "Chart constant", + format!("{:.1}", chart.chart_constant as f32 / 100.0), + true, + ) + .field("Total plays", format!("{}", play_count.count), true) + .field("BPM", &song.bpm, true) + .field("Side", Side::SIDE_STRINGS[song.side.to_index()], true) + .field("Artist", &song.title, true); + + if let Some(note_design) = &chart.note_design { + embed = embed.field("Note design", note_design, true); + } + + if let Some(pack) = &song.pack { + embed = embed.field("Pack", pack, true); + } + + if icon_attachement.is_some() { + embed = embed.thumbnail(format!("attachment://{}", &attachement_name)); + } + + ctx.channel_id() + .send_files( + ctx.http(), + icon_attachement, + CreateMessage::new().embed(embed), + ) + .await?; + + Ok(()) +} +// }}} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 73fd49c..3e75f27 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,2 +1,27 @@ +use crate::context::{Context, Error}; + +pub mod chart; pub mod score; pub mod stats; + +// {{{ Help +/// Show this help menu +#[poise::command(prefix_command, track_edits, slash_command)] +pub async fn help( + ctx: Context<'_>, + #[description = "Specific command to show help about"] + #[autocomplete = "poise::builtins::autocomplete_command"] + command: Option, +) -> Result<(), Error> { + poise::builtins::help( + ctx, + command.as_deref(), + poise::builtins::HelpConfiguration { + extra_text_at_bottom: "For additional support, message @prescientmoon", + ..Default::default() + }, + ) + .await?; + Ok(()) +} +// }}} diff --git a/src/commands/score.rs b/src/commands/score.rs index 2781921..5ce63ba 100644 --- a/src/commands/score.rs +++ b/src/commands/score.rs @@ -10,27 +10,6 @@ use poise::serenity_prelude::{CreateAttachment, CreateEmbed, CreateMessage}; use poise::{serenity_prelude as serenity, CreateReply}; use sqlx::query; -// {{{ Help -/// Show this help menu -#[poise::command(prefix_command, track_edits, slash_command)] -pub async fn help( - ctx: Context<'_>, - #[description = "Specific command to show help about"] - #[autocomplete = "poise::builtins::autocomplete_command"] - command: Option, -) -> Result<(), Error> { - poise::builtins::help( - ctx, - command.as_deref(), - poise::builtins::HelpConfiguration { - extra_text_at_bottom: "For additional support, message @prescientmoon", - ..Default::default() - }, - ) - .await?; - Ok(()) -} -// }}} // {{{ Score /// Score management #[poise::command( diff --git a/src/commands/stats.rs b/src/commands/stats.rs index 7e92e40..1da3d34 100644 --- a/src/commands/stats.rs +++ b/src/commands/stats.rs @@ -17,9 +17,8 @@ use poise::{ use sqlx::query_as; use crate::{ - chart::Difficulty, context::{Context, Error}, - score::{guess_chart_name, DbPlay, Score}, + score::{guess_song_and_chart, DbPlay, Score}, user::{discord_it_to_discord_user, User}, }; @@ -65,22 +64,7 @@ pub async fn best( } }; - let name = name.trim(); - let (name, difficulty) = name - .strip_suffix("PST") - .zip(Some(Difficulty::PST)) - .or_else(|| name.strip_suffix("[PST]").zip(Some(Difficulty::PST))) - .or_else(|| name.strip_suffix("PRS").zip(Some(Difficulty::PRS))) - .or_else(|| name.strip_suffix("[PRS]").zip(Some(Difficulty::PRS))) - .or_else(|| name.strip_suffix("FTR").zip(Some(Difficulty::FTR))) - .or_else(|| name.strip_suffix("[FTR]").zip(Some(Difficulty::FTR))) - .or_else(|| name.strip_suffix("ETR").zip(Some(Difficulty::ETR))) - .or_else(|| name.strip_suffix("[ETR]").zip(Some(Difficulty::ETR))) - .or_else(|| name.strip_suffix("BYD").zip(Some(Difficulty::BYD))) - .or_else(|| name.strip_suffix("[BYD]").zip(Some(Difficulty::BYD))) - .unwrap_or((&name, Difficulty::FTR)); - - let (song, chart) = guess_chart_name(name, &ctx.data().song_cache, Some(difficulty), true)?; + let (song, chart) = guess_song_and_chart(&ctx.data(), &name)?; let play = query_as!( DbPlay, @@ -137,22 +121,7 @@ pub async fn plot( } }; - let name = name.trim(); - let (name, difficulty) = name - .strip_suffix("PST") - .zip(Some(Difficulty::PST)) - .or_else(|| name.strip_suffix("[PST]").zip(Some(Difficulty::PST))) - .or_else(|| name.strip_suffix("PRS").zip(Some(Difficulty::PRS))) - .or_else(|| name.strip_suffix("[PRS]").zip(Some(Difficulty::PRS))) - .or_else(|| name.strip_suffix("FTR").zip(Some(Difficulty::FTR))) - .or_else(|| name.strip_suffix("[FTR]").zip(Some(Difficulty::FTR))) - .or_else(|| name.strip_suffix("ETR").zip(Some(Difficulty::ETR))) - .or_else(|| name.strip_suffix("[ETR]").zip(Some(Difficulty::ETR))) - .or_else(|| name.strip_suffix("BYD").zip(Some(Difficulty::BYD))) - .or_else(|| name.strip_suffix("[BYD]").zip(Some(Difficulty::BYD))) - .unwrap_or((&name, Difficulty::FTR)); - - let (song, chart) = guess_chart_name(name, &ctx.data().song_cache, Some(difficulty), true)?; + let (song, chart) = guess_song_and_chart(&ctx.data(), &name)?; let plays = query_as!( DbPlay, diff --git a/src/context.rs b/src/context.rs index 52a850d..3fb8a8a 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1,4 +1,4 @@ -use std::path::PathBuf; +use std::{fs, path::PathBuf}; use sqlx::SqlitePool; @@ -19,7 +19,10 @@ pub struct UserContext { impl UserContext { #[inline] - pub async fn new(data_dir: PathBuf, db: SqlitePool) -> Result { + 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)?; + let mut song_cache = SongCache::new(&db).await?; let jacket_cache = JacketCache::new(&data_dir, &mut song_cache)?; diff --git a/src/jacket.rs b/src/jacket.rs index cbc0db0..d6c26e7 100644 --- a/src/jacket.rs +++ b/src/jacket.rs @@ -1,8 +1,10 @@ -use std::{collections::HashSet, fs, path::PathBuf, str::FromStr}; +use std::{fs, path::PathBuf, str::FromStr}; use image::{GenericImageView, Rgba}; use kd_tree::{KdMap, KdPoint}; use num::Integer; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; use crate::{ chart::{Difficulty, SongCache}, @@ -14,8 +16,10 @@ use crate::{ pub const SPLIT_FACTOR: u32 = 8; pub const IMAGE_VEC_DIM: usize = (SPLIT_FACTOR * SPLIT_FACTOR * 3) as usize; -#[derive(Debug, Clone)] +#[serde_as] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct ImageVec { + #[serde_as(as = "[_; IMAGE_VEC_DIM]")] pub colors: [f32; IMAGE_VEC_DIM], } @@ -73,9 +77,9 @@ impl KdPoint for ImageVec { } } +#[derive(Serialize, Deserialize)] pub struct JacketCache { - // TODO: make this private - pub tree: KdMap, + tree: KdMap, } impl JacketCache { @@ -90,7 +94,7 @@ impl JacketCache { fs::create_dir_all(&jacket_dir).expect("Could not create jacket dir"); - let mut jackets: HashSet<(PathBuf, u32)> = HashSet::new(); + let mut jackets = Vec::new(); let entries = fs::read_dir(data_dir.join("songs")).expect("Couldn't read songs directory"); for entry in entries { let dir = entry?; @@ -120,7 +124,7 @@ impl JacketCache { let (song, chart) = guess_chart_name(dir_name, &song_cache, difficulty, true)?; - jackets.insert((file.path(), song.id)); + jackets.push((file.path(), song.id)); let contents = fs::read(file.path())?.leak(); diff --git a/src/main.rs b/src/main.rs index 21dcb03..de9d412 100644 --- a/src/main.rs +++ b/src/main.rs @@ -30,6 +30,7 @@ async fn on_error(error: poise::FrameworkError<'_, UserContext, Error>) { #[tokio::main] async fn main() { let data_dir = var("SHIMMERING_DATA_DIR").expect("Missing `SHIMMERING_DATA_DIR` env var"); + 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)) @@ -39,9 +40,10 @@ async fn main() { // {{{ Poise options let options = poise::FrameworkOptions { commands: vec![ - commands::score::help(), + commands::help(), commands::score::score(), commands::stats::stats(), + commands::chart::chart(), ], prefix_options: poise::PrefixFrameworkOptions { stripped_dynamic_prefix: Some(|_ctx, message, _user_ctx| { @@ -76,11 +78,13 @@ 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(PathBuf::from_str(&data_dir)?, pool).await?; + let ctx = UserContext::new( + PathBuf::from_str(&data_dir)?, + PathBuf::from_str(&cache_dir)?, + pool, + ) + .await?; - // for song in ctx.song_cache.lock().unwrap().songs() { - // song.lookup(Difficulty::BYD) - // } Ok(ctx) }) }) diff --git a/src/score.rs b/src/score.rs index 50ebeb6..127d709 100644 --- a/src/score.rs +++ b/src/score.rs @@ -900,7 +900,38 @@ pub fn note_distribution_rects() -> ( // }}} // }}} // }}} -// {{{ Recognise chart name +// {{{ Recognise chart +fn strip_case_insensitive_suffix<'a>(string: &'a str, suffix: &str) -> Option<&'a str> { + let suffix = suffix.to_lowercase(); + if string.to_lowercase().ends_with(&suffix) { + Some(&string[0..string.len() - suffix.len()]) + } else { + None + } +} + +pub fn guess_song_and_chart<'a>( + ctx: &'a UserContext, + name: &'a str, +) -> Result<(&'a Song, &'a Chart), Error> { + let name = name.trim(); + let (name, difficulty) = name + .strip_suffix("PST") + .zip(Some(Difficulty::PST)) + .or_else(|| strip_case_insensitive_suffix(name, "[PST]").zip(Some(Difficulty::PST))) + .or_else(|| strip_case_insensitive_suffix(name, "PRS").zip(Some(Difficulty::PRS))) + .or_else(|| strip_case_insensitive_suffix(name, "[PRS]").zip(Some(Difficulty::PRS))) + .or_else(|| strip_case_insensitive_suffix(name, "FTR").zip(Some(Difficulty::FTR))) + .or_else(|| strip_case_insensitive_suffix(name, "[FTR]").zip(Some(Difficulty::FTR))) + .or_else(|| strip_case_insensitive_suffix(name, "ETR").zip(Some(Difficulty::ETR))) + .or_else(|| strip_case_insensitive_suffix(name, "[ETR]").zip(Some(Difficulty::ETR))) + .or_else(|| strip_case_insensitive_suffix(name, "BYD").zip(Some(Difficulty::BYD))) + .or_else(|| strip_case_insensitive_suffix(name, "[BYD]").zip(Some(Difficulty::BYD))) + .unwrap_or((&name, Difficulty::FTR)); + + guess_chart_name(name, &ctx.song_cache, Some(difficulty), true) +} + /// Runs a specialized fuzzy-search through all charts in the game. /// /// The `unsafe_heuristics` toggle increases the amount of resolvable queries, but might let in @@ -928,7 +959,7 @@ pub fn guess_chart_name<'a>( item.charts().next()? }; - let song_title = song.title.to_lowercase(); + let song_title = &song.lowercase_title; distance_vec.clear(); let base_distance = edit_distance(&text, &song_title);