From 0c90628c9d8cdf1b9351a06b431e4e69e6fd17a9 Mon Sep 17 00:00:00 2001
From: prescientmoon <git@moonythm.dev>
Date: Thu, 1 Aug 2024 15:41:20 +0200
Subject: [PATCH] Got score select recognition to work

Signed-off-by: prescientmoon <git@moonythm.dev>
---
 src/bitmap.rs         |   5 +++
 src/commands/score.rs |   7 +--
 src/commands/stats.rs |  10 ++---
 src/image.rs          |  16 +++++--
 src/score.rs          | 102 +++++++++++++++++++++++++++++++-----------
 5 files changed, 101 insertions(+), 39 deletions(-)

diff --git a/src/bitmap.rs b/src/bitmap.rs
index 5783897..7e8158b 100644
--- a/src/bitmap.rs
+++ b/src/bitmap.rs
@@ -32,6 +32,11 @@ impl Color {
 		Self::from_rgba_int((i << 8) + 0xff)
 	}
 
+	#[inline]
+	pub const fn from_bytes(bytes: [u8; 4]) -> Self {
+		Self(bytes[0], bytes[1], bytes[1], bytes[3])
+	}
+
 	#[inline]
 	pub fn alpha(mut self, a: u8) -> Self {
 		self.3 = a;
diff --git a/src/commands/score.rs b/src/commands/score.rs
index 7c09896..3254591 100644
--- a/src/commands/score.rs
+++ b/src/commands/score.rs
@@ -74,11 +74,8 @@ pub async fn magic(
 		for (i, file) in files.iter().enumerate() {
 			if let Some(_) = file.dimensions() {
 				// {{{ Image pre-processing
-				// Download image and guess it's format
 				let bytes = file.download().await?;
-				let format = image::guess_format(&bytes)?;
-
-				let image = image::load_from_memory_with_format(&bytes, format)?;
+				let image = image::load_from_memory(&bytes)?;
 				let mut image = image.resize(1024, 1024, FilterType::Nearest);
 				// }}}
 				// {{{ Detection
@@ -241,7 +238,7 @@ Title error: {:?}
 				handle.edit(ctx, edited).await?;
 
 				let score_possibilities =
-					match cropper.read_score(Some(chart.note_count), &ocr_image) {
+					match cropper.read_score(Some(chart.note_count), &ocr_image, kind) {
 						// {{{ OCR error handling
 						Err(err) => {
 							error_with_image(
diff --git a/src/commands/stats.rs b/src/commands/stats.rs
index cc13418..b6e89f5 100644
--- a/src/commands/stats.rs
+++ b/src/commands/stats.rs
@@ -77,11 +77,11 @@ pub async fn best(
 	let play = query_as!(
 		DbPlay,
 		"
-            SELECT * FROM plays
-            WHERE user_id=?
-            AND chart_id=?
-            ORDER BY score DESC
-        ",
+        SELECT * FROM plays
+        WHERE user_id=?
+        AND chart_id=?
+        ORDER BY score DESC
+    ",
 		user.id,
 		chart.id
 	)
diff --git a/src/image.rs b/src/image.rs
index a054219..d1d4250 100644
--- a/src/image.rs
+++ b/src/image.rs
@@ -12,7 +12,11 @@ pub fn xshear(image: &mut DynamicImage, rect: Rect, center: Position, shear: f32
 	for y in rect.y..rect.y + rect.height as i32 {
 		let skew = (shear * ((y - center.1) as f32)) as i32;
 		for i in rect.x..rect.x + width {
-			let x = if skew < 0 { i } else { rect.x + width - 1 - i };
+			let x = if skew < 0 {
+				i
+			} else {
+				2 * rect.x + width - 1 - i
+			};
 
 			if unsigned_in_bounds(image, x, y) {
 				let pixel = image.get_pixel(x as u32, y as u32);
@@ -27,10 +31,14 @@ pub fn xshear(image: &mut DynamicImage, rect: Rect, center: Position, shear: f32
 /// Performs a horizontal shear operation, without performing anti-aliasing
 pub fn yshear(image: &mut DynamicImage, rect: Rect, center: Position, shear: f32) {
 	let height = rect.height as i32;
-	for x in rect.x..rect.x + rect.height as i32 {
+	for x in rect.x..rect.x + rect.width as i32 {
 		let skew = (shear * ((x - center.0) as f32)) as i32;
 		for i in rect.y..rect.y + height {
-			let y = if skew < 0 { i } else { rect.y + height - 1 - i };
+			let y = if skew < 0 {
+				i
+			} else {
+				2 * rect.y + height - 1 - i
+			};
 
 			if unsigned_in_bounds(image, x, y) {
 				let pixel = image.get_pixel(x as u32, y as u32);
@@ -45,7 +53,7 @@ pub fn yshear(image: &mut DynamicImage, rect: Rect, center: Position, shear: f32
 /// Performs a rotation as a series of three shear operations
 /// Does not perform anti-aliasing.
 pub fn rotate(image: &mut DynamicImage, rect: Rect, center: Position, angle: f32) {
-	let alpha = -f32::tan(angle);
+	let alpha = -f32::tan(angle / 2.0);
 	let beta = f32::sin(angle);
 	xshear(image, rect, center, alpha);
 	yshear(image, rect, center, beta);
diff --git a/src/score.rs b/src/score.rs
index c1bc9b5..59dbfd6 100644
--- a/src/score.rs
+++ b/src/score.rs
@@ -12,6 +12,7 @@ use num::{traits::Euclid, Rational64};
 use poise::serenity_prelude::{
 	Attachment, AttachmentId, CreateAttachment, CreateEmbed, CreateEmbedAuthor, Timestamp,
 };
+use sqlx::{query_as, SqlitePool};
 use tesseract::{PageSegMode, Tesseract};
 
 use crate::bitmap::{Color, Rect};
@@ -602,10 +603,10 @@ impl Play {
 			.field("ξ-Grade", format!("{}", self.zeta_score.grade()), true)
 			.field(
 				"Status",
-				self.status(chart).unwrap_or("?".to_string()),
+				self.status(chart).unwrap_or("-".to_string()),
 				true,
 			)
-			.field("Max recall", "?", true)
+			.field("Max recall", "—", true)
 			.field("ID", format!("{}", self.id), true);
 
 		if icon_attachement.is_some() {
@@ -628,6 +629,37 @@ 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
@@ -918,8 +950,8 @@ fn widen_by(rects: &mut Vec<RelativeRect>, x: f32, y: f32) {
 	}
 }
 // }}}
-// {{{ Score
-fn score_rects() -> &'static [RelativeRect] {
+// {{{ Score (score screen)
+fn score_score_screen_rects() -> &'static [RelativeRect] {
 	static CELL: OnceLock<Vec<RelativeRect>> = OnceLock::new();
 	CELL.get_or_init(|| {
 		let mut rects: Vec<RelativeRect> = vec![
@@ -939,6 +971,19 @@ fn score_rects() -> &'static [RelativeRect] {
 	})
 }
 // }}}
+// {{{ Score (song select)
+pub fn score_song_select_rects() -> &'static [RelativeRect] {
+	static CELL: OnceLock<Vec<RelativeRect>> = OnceLock::new();
+	CELL.get_or_init(|| {
+		let mut rects: Vec<RelativeRect> = vec![
+			AbsoluteRect::new(95, 256, 278, 49, ImageDimensions::new(2532, 1170)).to_relative(),
+			AbsoluteRect::new(15, 264, 291, 52, ImageDimensions::new(2160, 1620)).to_relative(),
+		];
+		process_datapoints(&mut rects);
+		rects
+	})
+}
+// }}}
 // {{{ Difficulty
 fn difficulty_rects() -> &'static [RelativeRect] {
 	static CELL: OnceLock<Vec<RelativeRect>> = OnceLock::new();
@@ -1128,6 +1173,14 @@ fn difficulty_pixel(difficulty: Difficulty) -> &'static [RelativePoint] {
 		Difficulty::BYD => byd_etr_pixel(),
 	}
 }
+
+const DIFFICULTY_MENU_PIXEL_COLORS: [Color; Difficulty::DIFFICULTIES.len()] = [
+	Color::from_rgb_int(0xAAE5F7),
+	Color::from_rgb_int(0xBFDD85),
+	Color::from_rgb_int(0xCB74AB),
+	Color::from_rgb_int(0xC4B7D3),
+	Color::from_rgb_int(0xF89AAC),
+];
 // }}}
 // }}}
 // {{{ Recognise chart
@@ -1276,13 +1329,22 @@ impl ImageCropper {
 		&mut self,
 		note_count: Option<u32>,
 		image: &DynamicImage,
+		kind: ScoreKind,
 	) -> Result<Vec<Score>, Error> {
+		println!("kind {kind:?}");
 		self.crop_image_to_bytes(
 			&image.resize_exact(image.width(), image.height(), FilterType::Nearest),
-			RelativeRect::from_aspect_ratio(ImageDimensions::from_image(image), score_rects())
-				.ok_or_else(|| "Could not find score area in picture")?
-				.to_absolute()
-				.to_rect(),
+			RelativeRect::from_aspect_ratio(
+				ImageDimensions::from_image(image),
+				if kind == ScoreKind::ScoreScreen {
+					score_score_screen_rects()
+				} else {
+					score_song_select_rects()
+				},
+			)
+			.ok_or_else(|| "Could not find score area in picture")?
+			.to_absolute()
+			.to_rect(),
 		)?;
 
 		let mut results = vec![];
@@ -1409,39 +1471,29 @@ impl ImageCropper {
 		kind: ScoreKind,
 	) -> Result<Difficulty, Error> {
 		if kind == ScoreKind::SongSelect {
-			let colors = [
-				Color::BLACK,
-				Color::BLACK,
-				Color::from_rgb_int(0xCB74AB),
-				Color::from_rgb_int(0xC4B7D3),
-				Color::BLACK,
-			];
-
 			let dimensions = ImageDimensions::from_image(image);
 
-			let min = colors
+			let min = DIFFICULTY_MENU_PIXEL_COLORS
 				.iter()
 				.zip(Difficulty::DIFFICULTIES)
 				.min_by_key(|(c, d)| {
 					let points = difficulty_pixel(*d);
 					let point = RelativePoint::from_aspect_ratio(dimensions, points)
 						.ok_or_else(|| "Could not find difficulty pixel in picture")
+						// SAFETY: should I just throwkkk here?
 						.unwrap_or(RelativePoint::new(0.0, 0.0, dimensions))
 						.to_absolute();
 
 					let image_color = image.get_pixel(point.x, point.y);
-					let image_color = Color(
-						image_color[0],
-						image_color[1],
-						image_color[2],
-						image_color[3],
-					);
+					let image_color = Color::from_bytes(image_color.0);
 
 					let distance = c.distance(image_color);
+					println!("distance {distance} image_color {image_color:?} color {c:?} difficulty {d:?}");
 					(distance * 10000.0) as u32
-				});
+				})
+				.unwrap();
 
-			return Ok(min.unwrap().1);
+			return Ok(min.1);
 		}
 
 		self.crop_image_to_bytes(