1
Fork 0

Commit before deleting lots of code

Signed-off-by: prescientmoon <git@moonythm.dev>
This commit is contained in:
prescientmoon 2024-08-08 15:59:36 +02:00
parent 0c90628c9d
commit d260a11263
Signed by: prescientmoon
SSH key fingerprint: SHA256:UUF9JT2s8Xfyv76b8ZuVL7XrmimH4o49p4b+iexbVH4
14 changed files with 393 additions and 256 deletions

164
Cargo.lock generated
View file

@ -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",

View file

@ -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

16
data/ui.txt Normal file
View file

@ -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

View file

@ -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,

View file

@ -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);

View file

@ -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(())

View file

@ -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<dyn std::error::Error + Send + Sync>;
@ -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,
})
}
}

View file

@ -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<ImageVec, u32>,
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<Pixel = Rgba<u8>>,
) -> 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))
}
// }}}
}

61
src/levenshtein.rs Normal file
View file

@ -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>) -> 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())
}

View file

@ -11,6 +11,8 @@ mod commands;
mod context;
mod image;
mod jacket;
mod levenshtein;
mod ocr;
mod score;
mod user;

1
src/ocr/mod.rs Normal file
View file

@ -0,0 +1 @@
pub mod ui_interp;

176
src/ocr/ui_interp.rs Normal file
View file

@ -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<UIMeasurement>,
}
impl UIMeasurements {
// {{{ Read
pub fn read(data_dir: &PathBuf) -> Result<Self, Error> {
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())
}
// }}}
}
// }}}

View file

@ -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]
@ -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<CreateAttachment>), Error> {
// {{{ Get previously best score
let previously_best = query_as!(
DbPlay,
"
SELECT * FROM plays
WHERE user_id=?
AND chart_id=?
AND created_at<?
ORDER BY score DESC
",
user.id,
chart.id,
self.created_at
)
.fetch_optional(db)
.await
.map_err(|_| {
format!(
"Could not find any scores for {} [{:?}]",
song.title, chart.difficulty
)
})?
.map(|p| p.to_play());
// }}}
let attachement_name = format!("{:?}-{:?}-{:?}.png", song.id, self.score.0, index);
let icon_attachement = match chart.cached_jacket.as_ref() {
Some(jacket) => 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",
{
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} (+?)",
(self.zeta_score.play_rating(chart.chart_constant)) as f32 / 100.
),
"{:.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<Self, Error> {
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);
}

View file

@ -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<Self, Error> {
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]