Back up everything before history rewrite
Signed-off-by: prescientmoon <git@moonythm.dev>
This commit is contained in:
parent
c035ecbb52
commit
3f922fcf6c
|
@ -1,4 +1,4 @@
|
|||
use std::{fs, path::PathBuf};
|
||||
use std::{fs, io::Cursor, path::PathBuf};
|
||||
|
||||
use image::{imageops::FilterType, GenericImageView, Rgba};
|
||||
use num::Integer;
|
||||
|
@ -153,9 +153,17 @@ impl JacketCache {
|
|||
image.resize(BITMAP_IMAGE_SIZE, BITMAP_IMAGE_SIZE, FilterType::Nearest);
|
||||
|
||||
if should_blur_jacket_art() {
|
||||
image = image.blur(20.0);
|
||||
image = image.blur(40.0);
|
||||
}
|
||||
|
||||
let encoded_pic = {
|
||||
let mut processed_pic = Vec::new();
|
||||
image.write_to(
|
||||
&mut Cursor::new(&mut processed_pic),
|
||||
image::ImageFormat::Jpeg,
|
||||
)?;
|
||||
processed_pic.leak()
|
||||
};
|
||||
let bitmap: &'static _ = Box::leak(Box::new(image.into_rgb8()));
|
||||
|
||||
if name == "base" {
|
||||
|
@ -163,7 +171,7 @@ impl JacketCache {
|
|||
for chart in song_cache.charts_mut() {
|
||||
if chart.song_id == song_id && chart.cached_jacket.is_none() {
|
||||
chart.cached_jacket = Some(Jacket {
|
||||
raw: contents,
|
||||
raw: encoded_pic,
|
||||
bitmap,
|
||||
});
|
||||
}
|
||||
|
@ -171,7 +179,7 @@ impl JacketCache {
|
|||
} else if difficulty.is_some() {
|
||||
let chart = song_cache.lookup_chart_mut(chart_id).unwrap();
|
||||
chart.cached_jacket = Some(Jacket {
|
||||
raw: contents,
|
||||
raw: encoded_pic,
|
||||
bitmap,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ use crate::context::{Error, UserContext};
|
|||
use crate::user::User;
|
||||
|
||||
use super::chart::SongCache;
|
||||
use super::score::Score;
|
||||
use super::score::{Score, ScoringSystem};
|
||||
|
||||
// {{{ Create play
|
||||
#[derive(Debug, Clone)]
|
||||
|
@ -127,7 +127,7 @@ pub struct DbPlay {
|
|||
|
||||
impl DbPlay {
|
||||
#[inline]
|
||||
pub fn to_play(self) -> Play {
|
||||
pub fn into_play(self) -> Play {
|
||||
Play {
|
||||
id: self.id as u32,
|
||||
chart_id: self.chart_id as u32,
|
||||
|
@ -175,6 +175,20 @@ pub struct Play {
|
|||
}
|
||||
|
||||
impl Play {
|
||||
// {{{ Query the underlying score
|
||||
#[inline]
|
||||
pub fn score(&self, system: ScoringSystem) -> Score {
|
||||
match system {
|
||||
ScoringSystem::Standard => self.score,
|
||||
ScoringSystem::EX => self.zeta_score,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn play_rating(&self, system: ScoringSystem, chart_constant: u32) -> i32 {
|
||||
self.score(system).play_rating(chart_constant)
|
||||
}
|
||||
// }}}
|
||||
// {{{ Play => distribution
|
||||
pub fn distribution(&self, note_count: u32) -> Option<(u32, u32, u32, u32)> {
|
||||
if let Some(fars) = self.far_notes {
|
||||
|
@ -278,7 +292,8 @@ impl Play {
|
|||
song.title, chart.difficulty
|
||||
)
|
||||
})?
|
||||
.map(|p| p.to_play());
|
||||
.map(|p| p.into_play());
|
||||
|
||||
let prev_score = prev_play.as_ref().map(|p| p.score);
|
||||
let prev_zeta_score = prev_play.as_ref().map(|p| p.zeta_score);
|
||||
// }}}
|
||||
|
@ -360,9 +375,10 @@ pub async fn get_best_plays<'a>(
|
|||
db: &SqlitePool,
|
||||
song_cache: &'a SongCache,
|
||||
user: &User,
|
||||
scoring_system: ScoringSystem,
|
||||
min_amount: usize,
|
||||
max_amount: usize,
|
||||
) -> Result<Result<PlayCollection<'a>, &'static str>, Error> {
|
||||
) -> Result<Result<PlayCollection<'a>, String>, Error> {
|
||||
// {{{ DB data fetching
|
||||
let plays: Vec<DbPlay> = query_as(
|
||||
"
|
||||
|
@ -381,7 +397,10 @@ pub async fn get_best_plays<'a>(
|
|||
// }}}
|
||||
|
||||
if plays.len() < min_amount {
|
||||
return Ok(Err("Not enough plays found"));
|
||||
return Ok(Err(format!(
|
||||
"Not enough plays found ({} out of a minimum of {min_amount})",
|
||||
plays.len()
|
||||
)));
|
||||
}
|
||||
|
||||
// {{{ B30 computation
|
||||
|
@ -390,13 +409,13 @@ pub async fn get_best_plays<'a>(
|
|||
let mut plays: Vec<(Play, &Song, &Chart)> = plays
|
||||
.into_iter()
|
||||
.map(|play| {
|
||||
let play = play.to_play();
|
||||
let play = play.into_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.sort_by_key(|(play, _, chart)| -play.play_rating(scoring_system, chart.chart_constant));
|
||||
plays.truncate(max_amount);
|
||||
// }}}
|
||||
|
||||
|
@ -404,11 +423,12 @@ pub async fn get_best_plays<'a>(
|
|||
}
|
||||
|
||||
#[inline]
|
||||
pub fn compute_b30_ptt(plays: &PlayCollection<'_>) -> i32 {
|
||||
pub fn compute_b30_ptt(scoring_system: ScoringSystem, plays: &PlayCollection<'_>) -> i32 {
|
||||
plays
|
||||
.iter()
|
||||
.map(|(play, _, chart)| play.score.play_rating(chart.chart_constant))
|
||||
.map(|(play, _, chart)| play.play_rating(scoring_system, chart.chart_constant))
|
||||
.sum::<i32>()
|
||||
/ plays.len() as i32
|
||||
.checked_div(plays.len() as i32)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
// }}}
|
||||
|
|
|
@ -6,6 +6,21 @@ use crate::context::Error;
|
|||
|
||||
use super::chart::Chart;
|
||||
|
||||
// {{{ Scoring system
|
||||
#[derive(Debug, Clone, Copy, poise::ChoiceParameter)]
|
||||
pub enum ScoringSystem {
|
||||
Standard,
|
||||
|
||||
// Inspired by sdvx's EX-scoring
|
||||
EX,
|
||||
}
|
||||
|
||||
impl Default for ScoringSystem {
|
||||
fn default() -> Self {
|
||||
Self::Standard
|
||||
}
|
||||
}
|
||||
// }}}
|
||||
// {{{ Grade
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum Grade {
|
||||
|
@ -140,7 +155,7 @@ impl Score {
|
|||
if play_rating >= prev_play_rating {
|
||||
write!(buffer, " (+{:.2})", play_rating - prev_play_rating)?;
|
||||
} else {
|
||||
write!(buffer, " (-{:.2})", play_rating - prev_play_rating)?;
|
||||
write!(buffer, " ({:.2})", play_rating - prev_play_rating)?;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use std::{cell::RefCell, env::var, path::PathBuf, str::FromStr, sync::OnceLock, thread::LocalKey};
|
||||
|
||||
use freetype::{Face, Library};
|
||||
use image::{imageops::FilterType, ImageBuffer, Rgb, Rgba};
|
||||
use image::{ImageBuffer, Rgb, Rgba};
|
||||
|
||||
use crate::{arcaea::chart::Difficulty, timed};
|
||||
|
||||
|
@ -55,94 +55,101 @@ pub fn with_font<T>(
|
|||
|
||||
#[inline]
|
||||
pub fn should_skip_jacket_art() -> bool {
|
||||
static CELL: OnceLock<bool> = OnceLock::new();
|
||||
*CELL.get_or_init(|| var("SHIMMERING_NO_JACKETS").unwrap_or_default() == "1")
|
||||
var("SHIMMERING_NO_JACKETS").unwrap_or_default() == "1"
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn should_blur_jacket_art() -> bool {
|
||||
static CELL: OnceLock<bool> = OnceLock::new();
|
||||
*CELL.get_or_init(|| var("SHIMMERING_BLUR_JACKETS").unwrap_or_default() == "1")
|
||||
var("SHIMMERING_BLUR_JACKETS").unwrap_or_default() == "1"
|
||||
}
|
||||
|
||||
pub fn get_b30_background() -> &'static ImageBuffer<Rgb<u8>, Vec<u8>> {
|
||||
static CELL: OnceLock<ImageBuffer<Rgb<u8>, Vec<u8>>> = OnceLock::new();
|
||||
CELL.get_or_init(|| {
|
||||
timed!("load_b30_background", {
|
||||
let raw_b30_background = image::open(get_assets_dir().join("b30_background.jpg"))
|
||||
.expect("Could not open b30 background");
|
||||
|
||||
raw_b30_background
|
||||
.resize(
|
||||
8 * raw_b30_background.width(),
|
||||
8 * raw_b30_background.height(),
|
||||
FilterType::Lanczos3,
|
||||
)
|
||||
.blur(7.0)
|
||||
.into_rgb8()
|
||||
raw_b30_background.blur(7.0).into_rgb8()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_count_background() -> &'static ImageBuffer<Rgba<u8>, Vec<u8>> {
|
||||
static CELL: OnceLock<ImageBuffer<Rgba<u8>, Vec<u8>>> = OnceLock::new();
|
||||
CELL.get_or_init(|| {
|
||||
timed!("load_count_backound", {
|
||||
image::open(get_assets_dir().join("count_background.png"))
|
||||
.expect("Could not open count background")
|
||||
.into_rgba8()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_score_background() -> &'static ImageBuffer<Rgba<u8>, Vec<u8>> {
|
||||
static CELL: OnceLock<ImageBuffer<Rgba<u8>, Vec<u8>>> = OnceLock::new();
|
||||
CELL.get_or_init(|| {
|
||||
timed!("load_score_background", {
|
||||
image::open(get_assets_dir().join("score_background.png"))
|
||||
.expect("Could not open score background")
|
||||
.into_rgba8()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_status_background() -> &'static ImageBuffer<Rgba<u8>, Vec<u8>> {
|
||||
static CELL: OnceLock<ImageBuffer<Rgba<u8>, Vec<u8>>> = OnceLock::new();
|
||||
CELL.get_or_init(|| {
|
||||
timed!("load_status_background", {
|
||||
image::open(get_assets_dir().join("status_background.png"))
|
||||
.expect("Could not open status background")
|
||||
.into_rgba8()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_grade_background() -> &'static ImageBuffer<Rgba<u8>, Vec<u8>> {
|
||||
static CELL: OnceLock<ImageBuffer<Rgba<u8>, Vec<u8>>> = OnceLock::new();
|
||||
CELL.get_or_init(|| {
|
||||
timed!("load_grade_background", {
|
||||
image::open(get_assets_dir().join("grade_background.png"))
|
||||
.expect("Could not open grade background")
|
||||
.into_rgba8()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_top_backgound() -> &'static ImageBuffer<Rgb<u8>, Vec<u8>> {
|
||||
static CELL: OnceLock<ImageBuffer<Rgb<u8>, Vec<u8>>> = OnceLock::new();
|
||||
CELL.get_or_init(|| {
|
||||
timed!("load_top_background", {
|
||||
image::open(get_assets_dir().join("top_background.png"))
|
||||
.expect("Could not open top background")
|
||||
.into_rgb8()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_name_backgound() -> &'static ImageBuffer<Rgb<u8>, Vec<u8>> {
|
||||
static CELL: OnceLock<ImageBuffer<Rgb<u8>, Vec<u8>>> = OnceLock::new();
|
||||
CELL.get_or_init(|| {
|
||||
timed!("load_name_background", {
|
||||
image::open(get_assets_dir().join("name_background.png"))
|
||||
.expect("Could not open name background")
|
||||
.into_rgb8()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_ptt_emblem() -> &'static ImageBuffer<Rgba<u8>, Vec<u8>> {
|
||||
static CELL: OnceLock<ImageBuffer<Rgba<u8>, Vec<u8>>> = OnceLock::new();
|
||||
CELL.get_or_init(|| {
|
||||
timed!("load_ptt_emblem", {
|
||||
image::open(get_assets_dir().join("ptt_emblem.png"))
|
||||
.expect("Could not open ptt emblem")
|
||||
.into_rgba8()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_difficulty_background(
|
||||
|
@ -150,6 +157,7 @@ pub fn get_difficulty_background(
|
|||
) -> &'static ImageBuffer<Rgba<u8>, Vec<u8>> {
|
||||
static CELL: OnceLock<[ImageBuffer<Rgba<u8>, Vec<u8>>; 5]> = OnceLock::new();
|
||||
&CELL.get_or_init(|| {
|
||||
timed!("load_difficulty_background", {
|
||||
let assets_dir = get_assets_dir();
|
||||
Difficulty::DIFFICULTY_SHORTHANDS.map(|shorthand| {
|
||||
image::open(assets_dir.join(format!("diff_{}.png", shorthand.to_lowercase())))
|
||||
|
@ -159,5 +167,6 @@ pub fn get_difficulty_background(
|
|||
))
|
||||
.into_rgba8()
|
||||
})
|
||||
})
|
||||
})[difficulty.to_index()]
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ use crate::{
|
|||
arcaea::{
|
||||
jacket::BITMAP_IMAGE_SIZE,
|
||||
play::{compute_b30_ptt, get_best_plays, DbPlay},
|
||||
score::Score,
|
||||
score::{Score, ScoringSystem},
|
||||
},
|
||||
assert_is_pookie,
|
||||
assets::{
|
||||
|
@ -92,7 +92,7 @@ pub async fn best(
|
|||
song.title, chart.difficulty
|
||||
)
|
||||
})?
|
||||
.to_play();
|
||||
.into_play();
|
||||
|
||||
let (embed, attachment) = play
|
||||
.to_embed(
|
||||
|
@ -117,11 +117,13 @@ pub async fn best(
|
|||
#[poise::command(prefix_command, slash_command)]
|
||||
pub async fn plot(
|
||||
ctx: Context<'_>,
|
||||
scoring_system: Option<ScoringSystem>,
|
||||
#[rest]
|
||||
#[description = "Name of chart to show (difficulty at the end)"]
|
||||
name: String,
|
||||
) -> Result<(), Error> {
|
||||
let user = get_user!(&ctx);
|
||||
let scoring_system = scoring_system.unwrap_or_default();
|
||||
|
||||
let (song, chart) = guess_song_and_chart(&ctx.data(), &name)?;
|
||||
|
||||
|
@ -152,7 +154,12 @@ pub async fn plot(
|
|||
|
||||
let min_time = plays.iter().map(|p| p.created_at).min().unwrap();
|
||||
let max_time = plays.iter().map(|p| p.created_at).max().unwrap();
|
||||
let mut min_score = plays.iter().map(|p| p.score).min().unwrap();
|
||||
let mut min_score = plays
|
||||
.iter()
|
||||
.map(|p| p.clone().into_play().score(scoring_system))
|
||||
.min()
|
||||
.unwrap()
|
||||
.0 as i64;
|
||||
|
||||
if min_score > 9_900_000 {
|
||||
min_score = 9_800_000;
|
||||
|
@ -202,20 +209,26 @@ pub async fn plot(
|
|||
.draw()?;
|
||||
|
||||
let mut points: Vec<_> = plays
|
||||
.iter()
|
||||
.map(|play| (play.created_at.and_utc().timestamp_millis(), play.score))
|
||||
.into_iter()
|
||||
.map(|play| {
|
||||
(
|
||||
play.created_at.and_utc().timestamp_millis(),
|
||||
play.into_play().score(scoring_system),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
points.sort();
|
||||
points.dedup();
|
||||
|
||||
chart.draw_series(LineSeries::new(points.iter().map(|(t, s)| (*t, *s)), &BLUE))?;
|
||||
chart.draw_series(LineSeries::new(
|
||||
points.iter().map(|(t, s)| (*t, s.0 as i64)),
|
||||
&BLUE,
|
||||
))?;
|
||||
|
||||
chart.draw_series(
|
||||
points
|
||||
.iter()
|
||||
.map(|(t, s)| Circle::new((*t, *s), 3, plotters::style::Color::filled(&BLUE))),
|
||||
)?;
|
||||
chart.draw_series(points.iter().map(|(t, s)| {
|
||||
Circle::new((*t, s.0 as i64), 3, plotters::style::Color::filled(&BLUE))
|
||||
}))?;
|
||||
root.present()?;
|
||||
}
|
||||
|
||||
|
@ -235,6 +248,7 @@ pub async fn plot(
|
|||
async fn best_plays(
|
||||
ctx: &Context<'_>,
|
||||
user: &User,
|
||||
scoring_system: ScoringSystem,
|
||||
grid_size: (u32, u32),
|
||||
require_full: bool,
|
||||
) -> Result<(), Error> {
|
||||
|
@ -245,10 +259,11 @@ async fn best_plays(
|
|||
&user_ctx.db,
|
||||
&user_ctx.song_cache,
|
||||
&user,
|
||||
scoring_system,
|
||||
if require_full {
|
||||
grid_size.0 * grid_size.1
|
||||
} else {
|
||||
grid_size.0 * (grid_size.1.max(1) - 1)
|
||||
grid_size.0 * (grid_size.1.max(1) - 1) + 1
|
||||
} as usize,
|
||||
(grid_size.0 * grid_size.1) as usize
|
||||
)
|
||||
|
@ -471,7 +486,7 @@ async fn best_plays(
|
|||
stroke: Some((Color::BLACK, 1.5)),
|
||||
drop_shadow: None,
|
||||
},
|
||||
&format!("{:0>10}", format!("{}", play.score)),
|
||||
&format!("{:0>10}", format!("{}", play.score(scoring_system))),
|
||||
)
|
||||
})?;
|
||||
// }}}
|
||||
|
@ -494,9 +509,12 @@ async fn best_plays(
|
|||
// }}}
|
||||
// {{{ Display status text
|
||||
with_font(&EXO_FONT, |faces| {
|
||||
let status = play
|
||||
.short_status(chart)
|
||||
.ok_or_else(|| format!("Could not get status for score {}", play.score))?;
|
||||
let status = play.short_status(chart).ok_or_else(|| {
|
||||
format!(
|
||||
"Could not get status for score {}",
|
||||
play.score(scoring_system)
|
||||
)
|
||||
})?;
|
||||
|
||||
let x_offset = match status {
|
||||
'P' => 2,
|
||||
|
@ -540,7 +558,7 @@ async fn best_plays(
|
|||
// }}}
|
||||
// {{{ Display grade text
|
||||
with_font(&EXO_FONT, |faces| {
|
||||
let grade = play.score.grade();
|
||||
let grade = play.score(scoring_system).grade();
|
||||
let center = grade_bg_area.center();
|
||||
|
||||
drawer.text(
|
||||
|
@ -586,7 +604,10 @@ async fn best_plays(
|
|||
(top_left_center, 94),
|
||||
faces,
|
||||
style,
|
||||
&format!("{:.2}", play.score.play_rating_f32(chart.chart_constant)),
|
||||
&format!(
|
||||
"{:.2}",
|
||||
play.play_rating(scoring_system, chart.chart_constant) as f32 / 100.0
|
||||
),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
|
@ -622,7 +643,7 @@ async fn best_plays(
|
|||
.attachment(CreateAttachment::bytes(out_buffer, "b30.png"))
|
||||
.content(format!(
|
||||
"Your ptt is {:.2}",
|
||||
compute_b30_ptt(&plays) as f32 / 100.0
|
||||
compute_b30_ptt(scoring_system, &plays) as f32 / 100.0
|
||||
));
|
||||
ctx.send(reply).await?;
|
||||
|
||||
|
@ -632,15 +653,34 @@ async fn best_plays(
|
|||
// {{{ B30
|
||||
/// Show the 30 best scores
|
||||
#[poise::command(prefix_command, slash_command, user_cooldown = 30)]
|
||||
pub async fn b30(ctx: Context<'_>) -> Result<(), Error> {
|
||||
pub async fn b30(ctx: Context<'_>, scoring_system: Option<ScoringSystem>) -> Result<(), Error> {
|
||||
let user = get_user!(&ctx);
|
||||
best_plays(&ctx, &user, (5, 6), true).await
|
||||
best_plays(
|
||||
&ctx,
|
||||
&user,
|
||||
scoring_system.unwrap_or_default(),
|
||||
(5, 6),
|
||||
true,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[poise::command(prefix_command, slash_command, hide_in_help, global_cooldown = 5)]
|
||||
pub async fn bany(ctx: Context<'_>, width: u32, height: u32) -> Result<(), Error> {
|
||||
pub async fn bany(
|
||||
ctx: Context<'_>,
|
||||
scoring_system: Option<ScoringSystem>,
|
||||
width: u32,
|
||||
height: u32,
|
||||
) -> Result<(), Error> {
|
||||
let user = get_user!(&ctx);
|
||||
assert_is_pookie!(ctx, user);
|
||||
best_plays(&ctx, &user, (width, height), false).await
|
||||
best_plays(
|
||||
&ctx,
|
||||
&user,
|
||||
scoring_system.unwrap_or_default(),
|
||||
(width, height),
|
||||
false,
|
||||
)
|
||||
.await
|
||||
}
|
||||
// }}}
|
||||
|
|
|
@ -333,7 +333,7 @@ impl CharMeasurements {
|
|||
.ok_or_else(|| "No chars in cache")?;
|
||||
|
||||
println!("char '{}', distance {}", best_match.1, best_match.0);
|
||||
if best_match.0 <= 1.0 {
|
||||
if best_match.0 <= 0.75 {
|
||||
result.push(best_match.1);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue