AAAAAAAAAAAA, squish images until OCR actually works :3
Signed-off-by: prescientmoon <git@moonythm.dev>
This commit is contained in:
parent
51deb8a68a
commit
ef940db80d
34
data/ui.txt
34
data/ui.txt
|
@ -15,6 +15,23 @@
|
||||||
452 153 0 0 Song select — FTR
|
452 153 0 0 Song select — FTR
|
||||||
638 153 0 0 Song select — ETR/BYD
|
638 153 0 0 Song select — ETR/BYD
|
||||||
|
|
||||||
|
2340 1080 KauanHenzon
|
||||||
|
228 10 245 57 Play kind
|
||||||
|
977 418 403 96 Score screen — score
|
||||||
|
255 401 564 564 Score screen — jacket
|
||||||
|
350 305 140 34 Score screen — difficulty
|
||||||
|
1192 783 78 38 Score screen — pures
|
||||||
|
1192 838 78 38 Score screen — fars
|
||||||
|
1192 893 78 38 Score screen — losts
|
||||||
|
549 344 84 36 Score screen — max recall
|
||||||
|
528 112 1284 85 Score screen — title
|
||||||
|
84 235 240 48 Song select — score
|
||||||
|
432 296 676 37 Song select — jacket
|
||||||
|
83 141 0 0 Song select — PST
|
||||||
|
251 141 0 0 Song select — PRS
|
||||||
|
419 141 0 0 Song select — FTR
|
||||||
|
587 141 0 0 Song select — ETR/BYD
|
||||||
|
|
||||||
2160 1620 prescientmoon
|
2160 1620 prescientmoon
|
||||||
19 15 273 60 Play kind
|
19 15 273 60 Play kind
|
||||||
841 682 500 94 Score screen — score
|
841 682 500 94 Score screen — score
|
||||||
|
@ -31,3 +48,20 @@
|
||||||
199 159 0 0 Song select — PRS
|
199 159 0 0 Song select — PRS
|
||||||
389 159 0 0 Song select — FTR
|
389 159 0 0 Song select — FTR
|
||||||
581 159 0 0 Song select — ETR/BYD
|
581 159 0 0 Song select — ETR/BYD
|
||||||
|
|
||||||
|
2220 1080 MathNoob
|
||||||
|
169 16 250 53 Play kind
|
||||||
|
900 419 439 95 Score screen — score
|
||||||
|
193 401 565 565 Score screen — jacket
|
||||||
|
287 304 138 35 Score screen — difficulty
|
||||||
|
1128 782 86 39 Score screen — pures
|
||||||
|
1128 837 86 39 Score screen — fars
|
||||||
|
1128 892 86 39 Score screen — losts
|
||||||
|
486 345 85 35 Score screen — max recall
|
||||||
|
346 112 1467 87 Score screen — title
|
||||||
|
82 233 257 51 Song select — score
|
||||||
|
393 296 674 38 Song select — jacket
|
||||||
|
84 142 0 0 Song select — PST
|
||||||
|
251 142 0 0 Song select — PRS
|
||||||
|
419 142 0 0 Song select — FTR
|
||||||
|
593 142 0 0 Song select — ETR/BYD
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use std::path::PathBuf;
|
use std::{num::NonZeroU16, path::PathBuf};
|
||||||
|
|
||||||
use image::{ImageBuffer, Rgb};
|
use image::{ImageBuffer, Rgb};
|
||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
|
@ -132,41 +132,80 @@ impl Chart {
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct CachedSong {
|
pub struct CachedSong {
|
||||||
pub song: Song,
|
pub song: Song,
|
||||||
charts: [Option<Chart>; 5],
|
chart_ids: [Option<NonZeroU16>; 5],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CachedSong {
|
impl CachedSong {
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn new(song: Song, charts: [Option<Chart>; 5]) -> Self {
|
pub fn new(song: Song) -> Self {
|
||||||
Self { song, charts }
|
Self {
|
||||||
|
song,
|
||||||
|
chart_ids: [None; 5],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
// {{{ Song cache
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct SongCache {
|
||||||
|
pub songs: Vec<Option<CachedSong>>,
|
||||||
|
pub charts: Vec<Option<Chart>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SongCache {
|
||||||
|
#[inline]
|
||||||
|
pub fn lookup_song(&self, id: u32) -> Result<&CachedSong, Error> {
|
||||||
|
self.songs
|
||||||
|
.get(id as usize)
|
||||||
|
.and_then(|i| i.as_ref())
|
||||||
|
.ok_or_else(|| format!("Could not find song with id {}", id).into())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn lookup(&self, difficulty: Difficulty) -> Result<&Chart, Error> {
|
pub fn lookup_chart(&self, chart_id: u32) -> Result<(&Song, &Chart), Error> {
|
||||||
self.charts
|
let chart = self
|
||||||
.get(difficulty.to_index())
|
.charts
|
||||||
.and_then(|c| c.as_ref())
|
.get(chart_id as usize)
|
||||||
.ok_or_else(|| {
|
.and_then(|i| i.as_ref())
|
||||||
format!(
|
.ok_or_else(|| format!("Could not find chart with id {}", chart_id))?;
|
||||||
"Could not find difficulty {:?} for song {}",
|
let song = &self.lookup_song(chart.song_id)?.song;
|
||||||
difficulty, self.song.title
|
|
||||||
)
|
Ok((song, chart))
|
||||||
.into()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn lookup_mut(&mut self, difficulty: Difficulty) -> Result<&mut Chart, Error> {
|
pub fn lookup_song_mut(&mut self, id: u32) -> Result<&mut CachedSong, Error> {
|
||||||
|
self.songs
|
||||||
|
.get_mut(id as usize)
|
||||||
|
.and_then(|i| i.as_mut())
|
||||||
|
.ok_or_else(|| format!("Could not find song with id {}", id).into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn lookup_chart_mut(&mut self, chart_id: u32) -> Result<&mut Chart, Error> {
|
||||||
self.charts
|
self.charts
|
||||||
.get_mut(difficulty.to_index())
|
.get_mut(chart_id as usize)
|
||||||
.and_then(|c| c.as_mut())
|
.and_then(|i| i.as_mut())
|
||||||
|
.ok_or_else(|| format!("Could not find chart with id {}", chart_id).into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn lookup_by_difficulty(
|
||||||
|
&self,
|
||||||
|
id: u32,
|
||||||
|
difficulty: Difficulty,
|
||||||
|
) -> Result<(&Song, &Chart), Error> {
|
||||||
|
let cached_song = self.lookup_song(id)?;
|
||||||
|
let chart_id = cached_song.chart_ids[difficulty.to_index()]
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
format!(
|
format!(
|
||||||
"Could not find difficulty {:?} for song {}",
|
"Cannot find chart {} [{difficulty:?}]",
|
||||||
difficulty, self.song.title
|
cached_song.song.title
|
||||||
)
|
)
|
||||||
.into()
|
})?
|
||||||
})
|
.get() as u32;
|
||||||
|
let chart = self.lookup_chart(chart_id)?.1;
|
||||||
|
Ok((&cached_song.song, chart))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
@ -178,77 +217,13 @@ impl CachedSong {
|
||||||
pub fn charts_mut(&mut self) -> impl Iterator<Item = &mut Chart> {
|
pub fn charts_mut(&mut self) -> impl Iterator<Item = &mut Chart> {
|
||||||
self.charts.iter_mut().filter_map(|i| i.as_mut())
|
self.charts.iter_mut().filter_map(|i| i.as_mut())
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// }}}
|
|
||||||
// {{{ Song cache
|
|
||||||
#[derive(Debug, Clone, Default)]
|
|
||||||
pub struct SongCache {
|
|
||||||
songs: Vec<Option<CachedSong>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SongCache {
|
|
||||||
#[inline]
|
|
||||||
pub fn lookup(&self, id: u32) -> Result<&CachedSong, Error> {
|
|
||||||
self.songs
|
|
||||||
.get(id as usize)
|
|
||||||
.and_then(|i| i.as_ref())
|
|
||||||
.ok_or_else(|| format!("Could not find song with id {}", id).into())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn lookup_chart(&self, chart_id: u32) -> Result<(&Song, &Chart), Error> {
|
|
||||||
self.songs()
|
|
||||||
.find_map(|item| {
|
|
||||||
item.charts().find_map(|chart| {
|
|
||||||
if chart.id == chart_id {
|
|
||||||
Some((&item.song, chart))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.ok_or_else(|| format!("Could not find chart with id {}", chart_id).into())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn lookup_mut(&mut self, id: u32) -> Result<&mut CachedSong, Error> {
|
|
||||||
self.songs
|
|
||||||
.get_mut(id as usize)
|
|
||||||
.and_then(|i| i.as_mut())
|
|
||||||
.ok_or_else(|| format!("Could not find song with id {}", id).into())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn lookup_chart_mut(&mut self, chart_id: u32) -> Result<&mut Chart, Error> {
|
|
||||||
self.songs_mut()
|
|
||||||
.find_map(|item| {
|
|
||||||
item.charts_mut().find_map(|chart| {
|
|
||||||
if chart.id == chart_id {
|
|
||||||
Some(chart)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.ok_or_else(|| format!("Could not find chart with id {}", chart_id).into())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn songs(&self) -> impl Iterator<Item = &CachedSong> {
|
|
||||||
self.songs.iter().filter_map(|i| i.as_ref())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn songs_mut(&mut self) -> impl Iterator<Item = &mut CachedSong> {
|
|
||||||
self.songs.iter_mut().filter_map(|i| i.as_mut())
|
|
||||||
}
|
|
||||||
|
|
||||||
// {{{ Populate cache
|
// {{{ Populate cache
|
||||||
pub async fn new(pool: &SqlitePool) -> Result<Self, Error> {
|
pub async fn new(pool: &SqlitePool) -> Result<Self, Error> {
|
||||||
let mut result = Self::default();
|
let mut result = Self::default();
|
||||||
|
|
||||||
|
// {{{ Songs
|
||||||
let songs = sqlx::query!("SELECT * FROM songs").fetch_all(pool).await?;
|
let songs = sqlx::query!("SELECT * FROM songs").fetch_all(pool).await?;
|
||||||
|
|
||||||
for song in songs {
|
for song in songs {
|
||||||
let song = Song {
|
let song = Song {
|
||||||
id: song.id as u32,
|
id: song.id as u32,
|
||||||
|
@ -265,31 +240,42 @@ impl SongCache {
|
||||||
if song_id >= result.songs.len() {
|
if song_id >= result.songs.len() {
|
||||||
result.songs.resize(song_id + 1, None);
|
result.songs.resize(song_id + 1, None);
|
||||||
}
|
}
|
||||||
|
result.songs[song_id] = Some(CachedSong::new(song));
|
||||||
let charts = sqlx::query!("SELECT * FROM charts WHERE song_id=?", song.id)
|
|
||||||
.fetch_all(pool)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let mut chart_cache: [Option<_>; 5] = Default::default();
|
|
||||||
for chart in charts {
|
|
||||||
let chart = Chart {
|
|
||||||
id: chart.id as u32,
|
|
||||||
song_id: chart.song_id as u32,
|
|
||||||
shorthand: chart.shorthand,
|
|
||||||
difficulty: Difficulty::try_from(chart.difficulty)?,
|
|
||||||
level: chart.level,
|
|
||||||
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();
|
|
||||||
chart_cache[index] = Some(chart);
|
|
||||||
}
|
|
||||||
|
|
||||||
result.songs[song_id] = Some(CachedSong::new(song, chart_cache));
|
|
||||||
}
|
}
|
||||||
|
// }}}
|
||||||
|
// {{{ Charts
|
||||||
|
let charts = sqlx::query!("SELECT * FROM charts").fetch_all(pool).await?;
|
||||||
|
for chart in charts {
|
||||||
|
let chart = Chart {
|
||||||
|
id: chart.id as u32,
|
||||||
|
song_id: chart.song_id as u32,
|
||||||
|
shorthand: chart.shorthand,
|
||||||
|
difficulty: Difficulty::try_from(chart.difficulty)?,
|
||||||
|
level: chart.level,
|
||||||
|
chart_constant: chart.chart_constant as u32,
|
||||||
|
note_count: chart.note_count as u32,
|
||||||
|
cached_jacket: None,
|
||||||
|
note_design: chart.note_design,
|
||||||
|
};
|
||||||
|
|
||||||
|
// {{{ Tie chart to song
|
||||||
|
{
|
||||||
|
let index = chart.difficulty.to_index();
|
||||||
|
result.lookup_song_mut(chart.song_id)?.chart_ids[index] =
|
||||||
|
Some(NonZeroU16::new(chart.id as u16).unwrap());
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
// {{{ Save chart to cache
|
||||||
|
{
|
||||||
|
let index = chart.id as usize;
|
||||||
|
if index >= result.charts.len() {
|
||||||
|
result.charts.resize(index + 1, None);
|
||||||
|
}
|
||||||
|
result.charts[index] = Some(chart);
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use std::{fs, path::PathBuf, str::FromStr};
|
use std::{fs, path::PathBuf};
|
||||||
|
|
||||||
use image::{imageops::FilterType, GenericImageView, Rgba};
|
use image::{imageops::FilterType, GenericImageView, Rgba};
|
||||||
use num::Integer;
|
use num::Integer;
|
||||||
|
@ -99,13 +99,11 @@ impl JacketCache {
|
||||||
.into_rgb8(),
|
.into_rgb8(),
|
||||||
));
|
));
|
||||||
|
|
||||||
for song in song_cache.songs_mut() {
|
for chart in song_cache.charts_mut() {
|
||||||
for chart in song.charts_mut() {
|
chart.cached_jacket = Some(Jacket {
|
||||||
chart.cached_jacket = Some(Jacket {
|
raw: contents,
|
||||||
raw: contents,
|
bitmap,
|
||||||
bitmap,
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Vec::new()
|
Vec::new()
|
||||||
|
@ -126,6 +124,7 @@ impl JacketCache {
|
||||||
if !name.ends_with("_256") {
|
if !name.ends_with("_256") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let name = name.strip_suffix("_256").unwrap();
|
let name = name.strip_suffix("_256").unwrap();
|
||||||
|
|
||||||
let difficulty = match name {
|
let difficulty = match name {
|
||||||
|
@ -140,12 +139,16 @@ impl JacketCache {
|
||||||
_ => Err(format!("Unknown jacket suffix {}", name))?,
|
_ => Err(format!("Unknown jacket suffix {}", name))?,
|
||||||
};
|
};
|
||||||
|
|
||||||
let (song, chart) = guess_chart_name(dir_name, &song_cache, difficulty, true)?;
|
let (song_id, chart_id) = {
|
||||||
|
let (song, chart) =
|
||||||
|
guess_chart_name(dir_name, &song_cache, difficulty, true)?;
|
||||||
|
(song.id, chart.id)
|
||||||
|
};
|
||||||
|
|
||||||
let contents: &'static _ = fs::read(file.path())?.leak();
|
let contents: &'static _ = fs::read(file.path())?.leak();
|
||||||
|
|
||||||
let image = image::load_from_memory(contents)?;
|
let image = image::load_from_memory(contents)?;
|
||||||
jacket_vectors.push((song.id, ImageVec::from_image(&image)));
|
jacket_vectors.push((song_id, ImageVec::from_image(&image)));
|
||||||
|
|
||||||
let bitmap: &'static _ = Box::leak(Box::new(
|
let bitmap: &'static _ = Box::leak(Box::new(
|
||||||
image
|
image
|
||||||
|
@ -154,33 +157,9 @@ impl JacketCache {
|
||||||
));
|
));
|
||||||
|
|
||||||
if name == "base" {
|
if name == "base" {
|
||||||
let item = song_cache.lookup_mut(song.id).unwrap();
|
// Inefficiently iterates over everything, but it's fine for ~1k entries
|
||||||
|
for chart in song_cache.charts_mut() {
|
||||||
for chart in item.charts_mut() {
|
if chart.song_id == song_id && chart.cached_jacket.is_none() {
|
||||||
let difficulty_num = match chart.difficulty {
|
|
||||||
Difficulty::PST => "0",
|
|
||||||
Difficulty::PRS => "1",
|
|
||||||
Difficulty::FTR => "2",
|
|
||||||
Difficulty::BYD => "3",
|
|
||||||
Difficulty::ETR => "4",
|
|
||||||
};
|
|
||||||
|
|
||||||
// We only want to create this path if there's no overwrite for this
|
|
||||||
// jacket.
|
|
||||||
let specialized_path = PathBuf::from_str(
|
|
||||||
&file
|
|
||||||
.path()
|
|
||||||
.to_str()
|
|
||||||
.unwrap()
|
|
||||||
.replace("base_night", difficulty_num)
|
|
||||||
.replace("base", difficulty_num),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let dest = chart.jacket_path(data_dir);
|
|
||||||
if !specialized_path.exists() && !dest.exists() {
|
|
||||||
std::os::unix::fs::symlink(file.path(), dest)
|
|
||||||
.expect("Could not symlink jacket");
|
|
||||||
chart.cached_jacket = Some(Jacket {
|
chart.cached_jacket = Some(Jacket {
|
||||||
raw: contents,
|
raw: contents,
|
||||||
bitmap,
|
bitmap,
|
||||||
|
@ -188,9 +167,7 @@ impl JacketCache {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if difficulty.is_some() {
|
} else if difficulty.is_some() {
|
||||||
std::os::unix::fs::symlink(file.path(), chart.jacket_path(data_dir))
|
let chart = song_cache.lookup_chart_mut(chart_id).unwrap();
|
||||||
.expect("Could not symlink jacket");
|
|
||||||
let chart = song_cache.lookup_chart_mut(chart.id).unwrap();
|
|
||||||
chart.cached_jacket = Some(Jacket {
|
chart.cached_jacket = Some(Jacket {
|
||||||
raw: contents,
|
raw: contents,
|
||||||
bitmap,
|
bitmap,
|
||||||
|
|
|
@ -10,6 +10,7 @@ use crate::arcaea::chart::{Chart, Song};
|
||||||
use crate::context::{Error, UserContext};
|
use crate::context::{Error, UserContext};
|
||||||
use crate::user::User;
|
use crate::user::User;
|
||||||
|
|
||||||
|
use super::chart::SongCache;
|
||||||
use super::score::Score;
|
use super::score::Score;
|
||||||
|
|
||||||
// {{{ Create play
|
// {{{ Create play
|
||||||
|
@ -263,6 +264,7 @@ impl Play {
|
||||||
AND chart_id=?
|
AND chart_id=?
|
||||||
AND created_at<?
|
AND created_at<?
|
||||||
ORDER BY score DESC
|
ORDER BY score DESC
|
||||||
|
LIMIT 1
|
||||||
",
|
",
|
||||||
user.id,
|
user.id,
|
||||||
chart.id,
|
chart.id,
|
||||||
|
@ -351,3 +353,60 @@ impl Play {
|
||||||
// }}}
|
// }}}
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
|
// {{{ General functions
|
||||||
|
pub type PlayCollection<'a> = Vec<(Play, &'a Song, &'a Chart)>;
|
||||||
|
|
||||||
|
pub async fn get_b30_plays<'a>(
|
||||||
|
db: &SqlitePool,
|
||||||
|
song_cache: &'a SongCache,
|
||||||
|
user: &User,
|
||||||
|
) -> Result<Result<PlayCollection<'a>, &'static str>, Error> {
|
||||||
|
// {{{ DB data fetching
|
||||||
|
let plays: Vec<DbPlay> = query_as(
|
||||||
|
"
|
||||||
|
SELECT id, chart_id, user_id,
|
||||||
|
created_at, MAX(score) as score, zeta_score,
|
||||||
|
creation_ptt, creation_zeta_ptt, far_notes, max_recall, discord_attachment_id
|
||||||
|
FROM plays p
|
||||||
|
WHERE user_id = ?
|
||||||
|
GROUP BY chart_id
|
||||||
|
ORDER BY score DESC
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.bind(user.id)
|
||||||
|
.fetch_all(db)
|
||||||
|
.await?;
|
||||||
|
// }}}
|
||||||
|
|
||||||
|
if plays.len() < 30 {
|
||||||
|
return Ok(Err("Not enough plays found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// {{{ B30 computation
|
||||||
|
// NOTE: we reallocate here, although we do not have much of a choice,
|
||||||
|
// unless we want to be lazy about things
|
||||||
|
let mut plays: Vec<(Play, &Song, &Chart)> = plays
|
||||||
|
.into_iter()
|
||||||
|
.map(|play| {
|
||||||
|
let play = play.to_play();
|
||||||
|
let (song, chart) = song_cache.lookup_chart(play.chart_id)?;
|
||||||
|
Ok((play, song, chart))
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, Error>>()?;
|
||||||
|
|
||||||
|
plays.sort_by_key(|(play, _, chart)| -play.score.play_rating(chart.chart_constant));
|
||||||
|
plays.truncate(30);
|
||||||
|
// }}}
|
||||||
|
|
||||||
|
Ok(Ok(plays))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn compute_b30_ptt(plays: &PlayCollection<'_>) -> i32 {
|
||||||
|
plays
|
||||||
|
.iter()
|
||||||
|
.map(|(play, _, chart)| play.score.play_rating(chart.chart_constant))
|
||||||
|
.sum::<i32>()
|
||||||
|
/ 30
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
|
|
@ -3,7 +3,7 @@ use crate::context::{Context, Error};
|
||||||
pub mod chart;
|
pub mod chart;
|
||||||
pub mod score;
|
pub mod score;
|
||||||
pub mod stats;
|
pub mod stats;
|
||||||
mod utils;
|
pub mod utils;
|
||||||
|
|
||||||
// {{{ Help
|
// {{{ Help
|
||||||
/// Show this help menu
|
/// Show this help menu
|
||||||
|
|
|
@ -17,10 +17,11 @@ use poise::{
|
||||||
use sqlx::query_as;
|
use sqlx::query_as;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
arcaea::chart::{Chart, Song},
|
arcaea::{
|
||||||
arcaea::jacket::BITMAP_IMAGE_SIZE,
|
jacket::BITMAP_IMAGE_SIZE,
|
||||||
arcaea::play::{DbPlay, Play},
|
play::{compute_b30_ptt, get_b30_plays, DbPlay},
|
||||||
arcaea::score::Score,
|
score::Score,
|
||||||
|
},
|
||||||
assets::{
|
assets::{
|
||||||
get_b30_background, get_count_background, get_difficulty_background, get_grade_background,
|
get_b30_background, get_count_background, get_difficulty_background, get_grade_background,
|
||||||
get_name_backgound, get_ptt_emblem, get_score_background, get_status_background,
|
get_name_backgound, get_ptt_emblem, get_score_background, get_status_background,
|
||||||
|
@ -30,6 +31,7 @@ use crate::{
|
||||||
context::{Context, Error},
|
context::{Context, Error},
|
||||||
get_user,
|
get_user,
|
||||||
recognition::fuzzy_song_name::guess_song_and_chart,
|
recognition::fuzzy_song_name::guess_song_and_chart,
|
||||||
|
reply_errors,
|
||||||
user::discord_it_to_discord_user,
|
user::discord_it_to_discord_user,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -121,14 +123,16 @@ pub async fn plot(
|
||||||
|
|
||||||
let (song, chart) = guess_song_and_chart(&ctx.data(), &name)?;
|
let (song, chart) = guess_song_and_chart(&ctx.data(), &name)?;
|
||||||
|
|
||||||
|
// SAFETY: we limit the amount of plotted plays to 1000.
|
||||||
let plays = query_as!(
|
let plays = query_as!(
|
||||||
DbPlay,
|
DbPlay,
|
||||||
"
|
"
|
||||||
SELECT * FROM plays
|
SELECT * FROM plays
|
||||||
WHERE user_id=?
|
WHERE user_id=?
|
||||||
AND chart_id=?
|
AND chart_id=?
|
||||||
ORDER BY created_at ASC
|
ORDER BY created_at ASC
|
||||||
",
|
LIMIT 1000
|
||||||
|
",
|
||||||
user.id,
|
user.id,
|
||||||
chart.id
|
chart.id
|
||||||
)
|
)
|
||||||
|
@ -230,41 +234,13 @@ pub async fn plot(
|
||||||
#[poise::command(prefix_command, slash_command)]
|
#[poise::command(prefix_command, slash_command)]
|
||||||
pub async fn b30(ctx: Context<'_>) -> Result<(), Error> {
|
pub async fn b30(ctx: Context<'_>) -> Result<(), Error> {
|
||||||
let user = get_user!(&ctx);
|
let user = get_user!(&ctx);
|
||||||
|
let user_ctx = ctx.data();
|
||||||
|
let plays = reply_errors!(
|
||||||
|
ctx,
|
||||||
|
get_b30_plays(&user_ctx.db, &user_ctx.song_cache, &user).await?
|
||||||
|
);
|
||||||
|
|
||||||
let plays: Vec<DbPlay> = query_as(
|
// {{{ Layout
|
||||||
"
|
|
||||||
SELECT id, chart_id, user_id,
|
|
||||||
created_at, MAX(score) as score, zeta_score,
|
|
||||||
creation_ptt, creation_zeta_ptt, far_notes, max_recall, discord_attachment_id
|
|
||||||
FROM plays p
|
|
||||||
WHERE user_id = ?
|
|
||||||
GROUP BY chart_id
|
|
||||||
ORDER BY score DESC
|
|
||||||
",
|
|
||||||
)
|
|
||||||
.bind(user.id)
|
|
||||||
.fetch_all(&ctx.data().db)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if plays.len() < 30 {
|
|
||||||
ctx.reply("Not enough plays found").await?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: consider not reallocating everything here
|
|
||||||
let mut plays: Vec<(Play, &Song, &Chart)> = plays
|
|
||||||
.into_iter()
|
|
||||||
.map(|play| {
|
|
||||||
let play = play.to_play();
|
|
||||||
// TODO: change the .lookup to perform binary search or something
|
|
||||||
let (song, chart) = ctx.data().song_cache.lookup_chart(play.chart_id)?;
|
|
||||||
Ok((play, song, chart))
|
|
||||||
})
|
|
||||||
.collect::<Result<Vec<_>, Error>>()?;
|
|
||||||
|
|
||||||
plays.sort_by_key(|(play, _, chart)| -play.score.play_rating(chart.chart_constant));
|
|
||||||
plays.truncate(30);
|
|
||||||
|
|
||||||
let mut layout = LayoutManager::default();
|
let mut layout = LayoutManager::default();
|
||||||
let jacket_area = layout.make_box(BITMAP_IMAGE_SIZE, BITMAP_IMAGE_SIZE);
|
let jacket_area = layout.make_box(BITMAP_IMAGE_SIZE, BITMAP_IMAGE_SIZE);
|
||||||
let jacket_with_border = layout.margin_uniform(jacket_area, 3);
|
let jacket_with_border = layout.margin_uniform(jacket_area, 3);
|
||||||
|
@ -284,14 +260,15 @@ pub async fn b30(ctx: Context<'_>) -> Result<(), Error> {
|
||||||
let item_with_margin = layout.margin_xy(item_area, 22, 17);
|
let item_with_margin = layout.margin_xy(item_area, 22, 17);
|
||||||
let (item_grid, item_origins) = layout.repeated_evenly(item_with_margin, (5, 6));
|
let (item_grid, item_origins) = layout.repeated_evenly(item_with_margin, (5, 6));
|
||||||
let root = layout.margin_uniform(item_grid, 30);
|
let root = layout.margin_uniform(item_grid, 30);
|
||||||
|
// }}}
|
||||||
// layout.normalize(root);
|
// {{{ Rendering prep
|
||||||
let width = layout.width(root);
|
let width = layout.width(root);
|
||||||
let height = layout.height(root);
|
let height = layout.height(root);
|
||||||
|
|
||||||
let canvas = BitmapCanvas::new(width, height);
|
let canvas = BitmapCanvas::new(width, height);
|
||||||
let mut drawer = LayoutDrawer::new(layout, canvas);
|
let mut drawer = LayoutDrawer::new(layout, canvas);
|
||||||
|
// }}}
|
||||||
|
// {{{ Render background
|
||||||
let bg = get_b30_background();
|
let bg = get_b30_background();
|
||||||
|
|
||||||
drawer.blit_rbg(
|
drawer.blit_rbg(
|
||||||
|
@ -304,6 +281,7 @@ pub async fn b30(ctx: Context<'_>) -> Result<(), Error> {
|
||||||
bg.dimensions(),
|
bg.dimensions(),
|
||||||
bg.as_raw(),
|
bg.as_raw(),
|
||||||
);
|
);
|
||||||
|
// }}}
|
||||||
|
|
||||||
for (i, origin) in item_origins.enumerate() {
|
for (i, origin) in item_origins.enumerate() {
|
||||||
drawer
|
drawer
|
||||||
|
@ -610,7 +588,12 @@ pub async fn b30(ctx: Context<'_>) -> Result<(), Error> {
|
||||||
let mut cursor = Cursor::new(&mut out_buffer);
|
let mut cursor = Cursor::new(&mut out_buffer);
|
||||||
image.write_to(&mut cursor, image::ImageFormat::Png)?;
|
image.write_to(&mut cursor, image::ImageFormat::Png)?;
|
||||||
|
|
||||||
let reply = CreateReply::default().attachment(CreateAttachment::bytes(out_buffer, "b30.png"));
|
let reply = CreateReply::default()
|
||||||
|
.attachment(CreateAttachment::bytes(out_buffer, "b30.png"))
|
||||||
|
.content(format!(
|
||||||
|
"Your ptt is {:.2}",
|
||||||
|
compute_b30_ptt(&plays) as f32 / 100.0
|
||||||
|
));
|
||||||
ctx.send(reply).await?;
|
ctx.send(reply).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -11,12 +11,18 @@ macro_rules! edit_reply {
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! get_user {
|
macro_rules! get_user {
|
||||||
($ctx:expr) => {
|
($ctx:expr) => {{
|
||||||
match crate::user::User::from_context($ctx).await {
|
crate::reply_errors!($ctx, crate::user::User::from_context($ctx).await)
|
||||||
Ok(user) => user,
|
}};
|
||||||
Err(_) => {
|
}
|
||||||
$ctx.say("You are not an user in my database, sorry!")
|
|
||||||
.await?;
|
#[macro_export]
|
||||||
|
macro_rules! reply_errors {
|
||||||
|
($ctx:expr, $value:expr) => {
|
||||||
|
match $value {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(err) => {
|
||||||
|
$ctx.reply(format!("{err}")).await?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,15 +58,15 @@ pub fn guess_chart_name<'a>(
|
||||||
|
|
||||||
let (song, chart) = loop {
|
let (song, chart) = loop {
|
||||||
let mut close_enough: Vec<_> = cache
|
let mut close_enough: Vec<_> = cache
|
||||||
.songs()
|
.charts()
|
||||||
.filter_map(|item| {
|
.filter_map(|chart| {
|
||||||
let song = &item.song;
|
if let Some(difficulty) = difficulty
|
||||||
let chart = if let Some(difficulty) = difficulty {
|
&& chart.difficulty != difficulty
|
||||||
item.lookup(difficulty).ok()?
|
{
|
||||||
} else {
|
return None;
|
||||||
item.charts().next()?
|
}
|
||||||
};
|
|
||||||
|
|
||||||
|
let song = &cache.lookup_song(chart.song_id).ok()?.song;
|
||||||
let song_title = &song.lowercase_title;
|
let song_title = &song.lowercase_title;
|
||||||
distance_vec.clear();
|
distance_vec.clear();
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,8 @@ use std::str::FromStr;
|
||||||
use std::{env, fs};
|
use std::{env, fs};
|
||||||
|
|
||||||
use hypertesseract::{PageSegMode, Tesseract};
|
use hypertesseract::{PageSegMode, Tesseract};
|
||||||
use image::{DynamicImage, GenericImageView};
|
use image::imageops::{resize, FilterType};
|
||||||
|
use image::{DynamicImage, GenericImageView, RgbaImage};
|
||||||
use image::{ImageBuffer, Rgba};
|
use image::{ImageBuffer, Rgba};
|
||||||
use num::integer::Roots;
|
use num::integer::Roots;
|
||||||
use poise::serenity_prelude::{CreateAttachment, CreateEmbed, CreateMessage, Timestamp};
|
use poise::serenity_prelude::{CreateAttachment, CreateEmbed, CreateMessage, Timestamp};
|
||||||
|
@ -46,26 +47,27 @@ impl ImageAnalyzer {
|
||||||
}
|
}
|
||||||
|
|
||||||
// {{{ Crop
|
// {{{ Crop
|
||||||
pub fn crop_image_to_bytes(&mut self, image: &DynamicImage, rect: Rect) -> Result<(), Error> {
|
#[inline]
|
||||||
|
fn should_save_debug_images() -> bool {
|
||||||
|
env::var("SHIMMERING_DEBUG_IMGS")
|
||||||
|
.map(|s| s == "1")
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_image(&mut self, image: &RgbaImage) -> Result<(), Error> {
|
||||||
self.clear();
|
self.clear();
|
||||||
let image = image.crop_imm(rect.x as u32, rect.y as u32, rect.width, rect.height);
|
|
||||||
let mut cursor = Cursor::new(&mut self.bytes);
|
let mut cursor = Cursor::new(&mut self.bytes);
|
||||||
image.write_to(&mut cursor, image::ImageFormat::Png)?;
|
image.write_to(&mut cursor, image::ImageFormat::Png)?;
|
||||||
|
|
||||||
fs::write(format!("./logs/{}.png", Timestamp::now()), &self.bytes)?;
|
if Self::should_save_debug_images() {
|
||||||
|
fs::write(format!("./logs/{}.png", Timestamp::now()), &self.bytes)?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn crop(&mut self, image: &DynamicImage, rect: Rect) -> ImageBuffer<Rgba<u8>, Vec<u8>> {
|
pub fn crop(&mut self, image: &DynamicImage, rect: Rect) -> ImageBuffer<Rgba<u8>, Vec<u8>> {
|
||||||
if env::var("SHIMMERING_DEBUG_IMGS")
|
|
||||||
.map(|s| s == "1")
|
|
||||||
.unwrap_or(false)
|
|
||||||
{
|
|
||||||
self.crop_image_to_bytes(image, rect).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
image
|
image
|
||||||
.crop_imm(rect.x as u32, rect.y as u32, rect.width, rect.height)
|
.crop_imm(rect.x as u32, rect.y as u32, rect.width, rect.height)
|
||||||
.to_rgba8()
|
.to_rgba8()
|
||||||
|
@ -80,7 +82,35 @@ impl ImageAnalyzer {
|
||||||
) -> Result<ImageBuffer<Rgba<u8>, Vec<u8>>, Error> {
|
) -> Result<ImageBuffer<Rgba<u8>, Vec<u8>>, Error> {
|
||||||
let rect = ctx.ui_measurements.interpolate(ui_rect, image)?;
|
let rect = ctx.ui_measurements.interpolate(ui_rect, image)?;
|
||||||
self.last_rect = Some((ui_rect, rect));
|
self.last_rect = Some((ui_rect, rect));
|
||||||
Ok(self.crop(image, rect))
|
|
||||||
|
let result = self.crop(image, rect);
|
||||||
|
if Self::should_save_debug_images() {
|
||||||
|
self.save_image(&result).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn interp_crop_resize(
|
||||||
|
&mut self,
|
||||||
|
ctx: &UserContext,
|
||||||
|
image: &DynamicImage,
|
||||||
|
ui_rect: UIMeasurementRect,
|
||||||
|
size: impl FnOnce(Rect) -> (u32, u32),
|
||||||
|
) -> Result<ImageBuffer<Rgba<u8>, Vec<u8>>, Error> {
|
||||||
|
let rect = ctx.ui_measurements.interpolate(ui_rect, image)?;
|
||||||
|
let size = size(rect);
|
||||||
|
self.last_rect = Some((ui_rect, rect));
|
||||||
|
|
||||||
|
let result = self.crop(image, rect);
|
||||||
|
let result = resize(&result, size.0, size.1, FilterType::Nearest);
|
||||||
|
|
||||||
|
if Self::should_save_debug_images() {
|
||||||
|
self.save_image(&result).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Error handling
|
// {{{ Error handling
|
||||||
|
@ -100,7 +130,8 @@ impl ImageAnalyzer {
|
||||||
));
|
));
|
||||||
|
|
||||||
if let Some((ui_rect, rect)) = self.last_rect {
|
if let Some((ui_rect, rect)) = self.last_rect {
|
||||||
self.crop_image_to_bytes(image, rect)?;
|
let cropped = self.crop(image, rect);
|
||||||
|
self.save_image(&cropped)?;
|
||||||
|
|
||||||
let bytes = std::mem::take(&mut self.bytes);
|
let bytes = std::mem::take(&mut self.bytes);
|
||||||
let error_attachement = CreateAttachment::bytes(bytes, filename);
|
let error_attachement = CreateAttachment::bytes(bytes, filename);
|
||||||
|
@ -131,13 +162,26 @@ impl ImageAnalyzer {
|
||||||
image: &DynamicImage,
|
image: &DynamicImage,
|
||||||
kind: ScoreKind,
|
kind: ScoreKind,
|
||||||
) -> Result<Vec<Score>, Error> {
|
) -> Result<Vec<Score>, Error> {
|
||||||
let image = self.interp_crop(
|
// yes, this was painfully hand-picked
|
||||||
|
let desired_height = 100;
|
||||||
|
let x_scaling_factor = match kind {
|
||||||
|
ScoreKind::SongSelect => 1.0,
|
||||||
|
ScoreKind::ScoreScreen => 0.66,
|
||||||
|
};
|
||||||
|
|
||||||
|
let image = self.interp_crop_resize(
|
||||||
ctx,
|
ctx,
|
||||||
image,
|
image,
|
||||||
if kind == ScoreKind::ScoreScreen {
|
match kind {
|
||||||
ScoreScreen(ScoreScreenRect::Score)
|
ScoreKind::SongSelect => SongSelect(SongSelectRect::Score),
|
||||||
} else {
|
ScoreKind::ScoreScreen => ScoreScreen(ScoreScreenRect::Score),
|
||||||
SongSelect(SongSelectRect::Score)
|
},
|
||||||
|
|rect| {
|
||||||
|
(
|
||||||
|
(rect.width as f32 * desired_height as f32 / rect.height as f32
|
||||||
|
* x_scaling_factor) as u32,
|
||||||
|
desired_height,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
@ -423,11 +467,9 @@ impl ImageAnalyzer {
|
||||||
Err("No known jacket looks like this")?;
|
Err("No known jacket looks like this")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let item = ctx.song_cache.lookup(*song_id)?;
|
let (song, chart) = ctx.song_cache.lookup_by_difficulty(*song_id, difficulty)?;
|
||||||
let chart = item.lookup(difficulty)?;
|
|
||||||
|
|
||||||
// NOTE: this will reallocate a few strings, but it is what it is
|
Ok((song, chart))
|
||||||
Ok((&item.song, chart))
|
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Read distribution
|
// {{{ Read distribution
|
||||||
|
|
|
@ -16,7 +16,8 @@ impl User {
|
||||||
let id = ctx.author().id.get().to_string();
|
let id = ctx.author().id.get().to_string();
|
||||||
let user = sqlx::query!("SELECT * FROM users WHERE discord_id = ?", id)
|
let user = sqlx::query!("SELECT * FROM users WHERE discord_id = ?", id)
|
||||||
.fetch_one(&ctx.data().db)
|
.fetch_one(&ctx.data().db)
|
||||||
.await?;
|
.await
|
||||||
|
.map_err(|_| "You are not an user in my database, sowwy ^~^")?;
|
||||||
|
|
||||||
Ok(User {
|
Ok(User {
|
||||||
id: user.id as u32,
|
id: user.id as u32,
|
||||||
|
|
Loading…
Reference in a new issue