From 3f922fcf6ca48bf4ca89eaf1f1542d20160487e9 Mon Sep 17 00:00:00 2001
From: prescientmoon <git@moonythm.dev>
Date: Fri, 16 Aug 2024 15:38:00 +0200
Subject: [PATCH] Back up everything before history rewrite

Signed-off-by: prescientmoon <git@moonythm.dev>
---
 src/arcaea/jacket.rs          | 16 ++++--
 src/arcaea/play.rs            | 40 +++++++++++----
 src/arcaea/score.rs           | 17 +++++-
 src/assets.rs                 | 97 +++++++++++++++++++----------------
 src/commands/stats.rs         | 86 ++++++++++++++++++++++---------
 src/recognition/hyperglass.rs |  2 +-
 6 files changed, 175 insertions(+), 83 deletions(-)

diff --git a/src/arcaea/jacket.rs b/src/arcaea/jacket.rs
index 92d5e83..0897cf7 100644
--- a/src/arcaea/jacket.rs
+++ b/src/arcaea/jacket.rs
@@ -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,
 						});
 					}
diff --git a/src/arcaea/play.rs b/src/arcaea/play.rs
index 848a694..d75b39b 100644
--- a/src/arcaea/play.rs
+++ b/src/arcaea/play.rs
@@ -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)
 }
 // }}}
diff --git a/src/arcaea/score.rs b/src/arcaea/score.rs
index c8fba5d..e9294db 100644
--- a/src/arcaea/score.rs
+++ b/src/arcaea/score.rs
@@ -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)?;
 			}
 		}
 
diff --git a/src/assets.rs b/src/assets.rs
index dddea81..ccfadb3 100644
--- a/src/assets.rs
+++ b/src/assets.rs
@@ -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,93 +55,100 @@ 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(|| {
-		let raw_b30_background = image::open(get_assets_dir().join("b30_background.jpg"))
-			.expect("Could not open b30 background");
+		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(|| {
-		image::open(get_assets_dir().join("count_background.png"))
-			.expect("Could not open count background")
-			.into_rgba8()
+		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(|| {
-		image::open(get_assets_dir().join("score_background.png"))
-			.expect("Could not open score background")
-			.into_rgba8()
+		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(|| {
-		image::open(get_assets_dir().join("status_background.png"))
-			.expect("Could not open status background")
-			.into_rgba8()
+		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(|| {
-		image::open(get_assets_dir().join("grade_background.png"))
-			.expect("Could not open grade background")
-			.into_rgba8()
+		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(|| {
-		image::open(get_assets_dir().join("top_background.png"))
-			.expect("Could not open top background")
-			.into_rgb8()
+		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(|| {
-		image::open(get_assets_dir().join("name_background.png"))
-			.expect("Could not open name background")
-			.into_rgb8()
+		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(|| {
-		image::open(get_assets_dir().join("ptt_emblem.png"))
-			.expect("Could not open ptt emblem")
-			.into_rgba8()
+		timed!("load_ptt_emblem", {
+			image::open(get_assets_dir().join("ptt_emblem.png"))
+				.expect("Could not open ptt emblem")
+				.into_rgba8()
+		})
 	})
 }
 
@@ -150,14 +157,16 @@ 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(|| {
-		let assets_dir = get_assets_dir();
-		Difficulty::DIFFICULTY_SHORTHANDS.map(|shorthand| {
-			image::open(assets_dir.join(format!("diff_{}.png", shorthand.to_lowercase())))
-				.expect(&format!(
-					"Could not get background for difficulty {:?}",
-					shorthand
-				))
-				.into_rgba8()
+		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())))
+					.expect(&format!(
+						"Could not get background for difficulty {:?}",
+						shorthand
+					))
+					.into_rgba8()
+			})
 		})
 	})[difficulty.to_index()]
 }
diff --git a/src/commands/stats.rs b/src/commands/stats.rs
index 7f3f2e0..122f55a 100644
--- a/src/commands/stats.rs
+++ b/src/commands/stats.rs
@@ -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
 }
 // }}}
diff --git a/src/recognition/hyperglass.rs b/src/recognition/hyperglass.rs
index be8a1f8..9dde6c1 100644
--- a/src/recognition/hyperglass.rs
+++ b/src/recognition/hyperglass.rs
@@ -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);
 			}
 		}