From 92dbd181f2faa83528379aa234b735880d431b86 Mon Sep 17 00:00:00 2001 From: prescientmoon Date: Mon, 23 Sep 2024 21:12:04 +0200 Subject: [PATCH] Make everything build --- Cargo.toml | 4 +- flake.nix | 16 ++++--- src/arcaea/chart.rs | 8 ++-- src/arcaea/jacket.rs | 1 + src/arcaea/play.rs | 2 +- src/arcaea/score.rs | 14 ++---- src/assets.rs | 2 +- src/bin/cli/commands/analyse.rs | 4 +- src/bin/cli/context.rs | 12 ++--- src/bin/cli/main.rs | 2 +- src/bin/discord-bot/main.rs | 1 - src/bitmap.rs | 11 +++-- src/commands/chart.rs | 7 +-- src/commands/discord.rs | 80 ++++++++++++++++++------------- src/commands/utils/mod.rs | 2 - src/commands/utils/two_columns.rs | 54 --------------------- src/context.rs | 19 -------- src/lib.rs | 3 ++ src/recognition/hyperglass.rs | 18 ++++--- src/recognition/ui.rs | 4 +- src/transform.rs | 12 +++-- 21 files changed, 107 insertions(+), 169 deletions(-) delete mode 100644 src/commands/utils/two_columns.rs diff --git a/Cargo.toml b/Cargo.toml index e2da31f..7573afd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,5 +41,5 @@ anyhow = "1.0.87" sha2 = "0.10.8" base16ct = { version = "0.2.0", features = ["alloc"] } -# [profile.dev.package."*"] -# opt-level = 3 +[profile.dev.package."*"] +opt-level = 3 diff --git a/flake.nix b/flake.nix index 698aa8b..58766ed 100644 --- a/flake.nix +++ b/flake.nix @@ -13,7 +13,12 @@ inputs.flake-utils.lib.eachSystem (with inputs.flake-utils.lib.system; [ x86_64-linux ]) ( system: let - pkgs = inputs.nixpkgs.legacyPackages.${system}.extend (import inputs.rust-overlay); + pkgs = inputs.nixpkgs.legacyPackages.${system}; + # pkgs = inputs.nixpkgs.legacyPackages.${system}.extend (import inputs.rust-overlay); + # pkgs = import inputs.nixpkgs { + # inherit system; + # overlays = [ (import inputs.rust-overlay) ]; + # }; # toolchain = pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.default); # toolchain = pkgs.rust-bin.stable.latest.default; toolchain = inputs.fenix.packages.${system}.complete.toolchain; @@ -34,18 +39,17 @@ }; }; }; - devShell = pkgs.mkShell { + devShell = pkgs.mkShell rec { nativeBuildInputs = with pkgs; [ toolchain - # ruff - # imagemagick + ruff + imagemagick pkg-config # clang # llvmPackages.clang ]; buildInputs = with pkgs; [ - toolchain freetype fontconfig leptonica @@ -54,7 +58,7 @@ sqlite ]; - # LD_LIBRARY_PATH = lib.makeLibraryPath buildInputs; + LD_LIBRARY_PATH = lib.makeLibraryPath buildInputs; # compilation of -sys packages requires manually setting LIBCLANG_PATH # LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib"; diff --git a/src/arcaea/chart.rs b/src/arcaea/chart.rs index 21bb03b..8b93689 100644 --- a/src/arcaea/chart.rs +++ b/src/arcaea/chart.rs @@ -1,3 +1,4 @@ +use std::path::Path; // {{{ Imports use std::{fmt::Display, num::NonZeroU16, path::PathBuf}; @@ -216,7 +217,7 @@ pub struct Chart { impl Chart { #[inline] - pub fn jacket_path(&self, data_dir: &PathBuf) -> PathBuf { + pub fn jacket_path(&self, data_dir: &Path) -> PathBuf { data_dir .join("jackets") .join(format!("{}-{}.jpg", self.song_id, self.id)) @@ -241,10 +242,7 @@ impl CachedSong { #[inline] pub fn charts(&self) -> impl Iterator { - self.chart_ids - .into_iter() - .filter_map(|i| i) - .map(|i| i.get() as u32) + self.chart_ids.into_iter().flatten().map(|i| i.get() as u32) } } // }}} diff --git a/src/arcaea/jacket.rs b/src/arcaea/jacket.rs index 3b7a7ee..7ea511b 100644 --- a/src/arcaea/jacket.rs +++ b/src/arcaea/jacket.rs @@ -55,6 +55,7 @@ impl ImageVec { let r = (r as f64 / count).sqrt(); let g = (g as f64 / count).sqrt(); let b = (b as f64 / count).sqrt(); + #[allow(clippy::identity_op)] colors[i as usize * 3 + 0] = r as f32; colors[i as usize * 3 + 1] = g as f32; colors[i as usize * 3 + 2] = b as f32; diff --git a/src/arcaea/play.rs b/src/arcaea/play.rs index b2bafe4..e0459f6 100644 --- a/src/arcaea/play.rs +++ b/src/arcaea/play.rs @@ -500,7 +500,7 @@ pub fn compute_b30_ptt(scoring_system: ScoringSystem, plays: &PlayCollection<'_> } // }}} // {{{ Maintenance functions -pub async fn generate_missing_scores(ctx: &UserContext) -> Result<(), TaggedError> { +pub async fn generate_missing_scores(ctx: &UserContext) -> Result<(), Error> { let conn = ctx.db.get()?; let mut query = conn.prepare_cached( " diff --git a/src/arcaea/score.rs b/src/arcaea/score.rs index 7ba901e..da93b3b 100644 --- a/src/arcaea/score.rs +++ b/src/arcaea/score.rs @@ -226,16 +226,10 @@ impl Score { // {{{ Compute score from note breakdown subpairs let pf_score = Score::compute_naive(note_count, pures, fars); - let fl_score = Score::compute_naive( - note_count, - note_count.checked_sub(losts + fars).unwrap_or(0), - fars, - ); - let lp_score = Score::compute_naive( - note_count, - pures, - note_count.checked_sub(losts + pures).unwrap_or(0), - ); + let fl_score = + Score::compute_naive(note_count, note_count.saturating_sub(losts + fars), fars); + let lp_score = + Score::compute_naive(note_count, pures, note_count.saturating_sub(losts + pures)); // }}} // {{{ Look for consensus among recomputed scores // Lemma: if two computed scores agree, then so will the third diff --git a/src/assets.rs b/src/assets.rs index 7b4ea3a..8c9d5ee 100644 --- a/src/assets.rs +++ b/src/assets.rs @@ -45,7 +45,7 @@ pub fn get_asset_dir() -> PathBuf { fn get_font(name: &str) -> RefCell { let face = FREETYPE_LIB.with(|lib| { lib.new_face(get_asset_dir().join("fonts").join(name), 0) - .expect(&format!("Could not load {} font", name)) + .unwrap_or_else(|_| panic!("Could not load {} font", name)) }); RefCell::new(face) } diff --git a/src/bin/cli/commands/analyse.rs b/src/bin/cli/commands/analyse.rs index 75ba52d..2a82531 100644 --- a/src/bin/cli/commands/analyse.rs +++ b/src/bin/cli/commands/analyse.rs @@ -2,6 +2,7 @@ use std::path::PathBuf; use crate::context::CliContext; +use shimmeringmoon::commands::discord::MessageContext; use shimmeringmoon::commands::score::magic_impl; use shimmeringmoon::context::{Error, UserContext}; // }}} @@ -13,6 +14,7 @@ pub struct Args { pub async fn run(args: Args) -> Result<(), Error> { let mut ctx = CliContext::new(UserContext::new().await?); - magic_impl(&mut ctx, &args.files).await?; + let res = magic_impl(&mut ctx, &args.files).await; + ctx.handle_error(res).await?; Ok(()) } diff --git a/src/bin/cli/context.rs b/src/bin/cli/context.rs index fff301a..5b208c1 100644 --- a/src/bin/cli/context.rs +++ b/src/bin/cli/context.rs @@ -3,10 +3,10 @@ use std::num::NonZeroU64; use std::path::PathBuf; use std::str::FromStr; -use poise::serenity_prelude::{CreateAttachment, CreateMessage}; - extern crate shimmeringmoon; +use poise::CreateReply; use shimmeringmoon::assets::get_var; +use shimmeringmoon::commands::discord::mock::ReplyEssence; use shimmeringmoon::context::Error; use shimmeringmoon::{commands::discord::MessageContext, context::UserContext}; // }}} @@ -53,12 +53,8 @@ impl MessageContext for CliContext { Ok(()) } - async fn send_files( - &mut self, - _attachments: impl IntoIterator, - message: CreateMessage, - ) -> Result<(), Error> { - let all = toml::to_string(&message).unwrap(); + async fn send(&mut self, message: CreateReply) -> Result<(), Error> { + let all = toml::to_string(&ReplyEssence::from_reply(message)).unwrap(); println!("\n========== Message =========="); println!("{all}"); Ok(()) diff --git a/src/bin/cli/main.rs b/src/bin/cli/main.rs index 6be8dd0..c426437 100644 --- a/src/bin/cli/main.rs +++ b/src/bin/cli/main.rs @@ -1,6 +1,6 @@ use clap::Parser; use command::{Cli, Command}; -use shimmeringmoon::context::{Error, UserContext}; +use shimmeringmoon::context::Error; mod command; mod commands; diff --git a/src/bin/discord-bot/main.rs b/src/bin/discord-bot/main.rs index 74a8797..c1663c5 100644 --- a/src/bin/discord-bot/main.rs +++ b/src/bin/discord-bot/main.rs @@ -1,5 +1,4 @@ use poise::serenity_prelude::{self as serenity}; -extern crate shimmeringmoon; use shimmeringmoon::arcaea::play::generate_missing_scores; use shimmeringmoon::context::{Error, UserContext}; use shimmeringmoon::{commands, timed}; diff --git a/src/bitmap.rs b/src/bitmap.rs index a7eee99..7f3f25e 100644 --- a/src/bitmap.rs +++ b/src/bitmap.rs @@ -174,9 +174,11 @@ impl BitmapCanvas { } // {{{ Draw pixel + #[allow(clippy::identity_op)] pub fn set_pixel(&mut self, pos: (u32, u32), color: Color) { let index = 3 * (pos.1 * self.width + pos.0) as usize; let alpha = color.3 as u32; + self.buffer[index + 0] = ((alpha * color.0 as u32 + (255 - alpha) * self.buffer[index + 0] as u32) / 255) as u8; self.buffer[index + 1] = @@ -299,6 +301,7 @@ impl BitmapCanvas { } // }}} // {{{ Draw text + #[allow(clippy::type_complexity)] pub fn plan_text_rendering( pos: Position, faces: &mut [&mut Face], @@ -490,8 +493,8 @@ impl BitmapCanvas { for dx in 0..iw { for dy in 0..ih { - let x = pos.0 + dx as i32 + b_glyph.left(); - let y = pos.1 + dy as i32 - b_glyph.top(); + let x = pos.0 + dx + b_glyph.left(); + let y = pos.1 + dy - b_glyph.top(); // TODO: gamma correction if x >= 0 && (x as u32) < self.width && y >= 0 && (y as u32) < height { @@ -656,7 +659,7 @@ impl LayoutManager { ( outer_id, - (0..amount.0 * amount.1).into_iter().map(move |i| { + (0..amount.0 * amount.1).map(move |i| { let (y, x) = i.div_rem_euclid(&amount.0); ((x * inner.width) as i32, (y * inner.height) as i32) }), @@ -689,7 +692,7 @@ impl LayoutManager { #[inline] pub fn position_relative_to(&self, id: LayoutBoxId, pos: Position) -> Position { let current = self.lookup(id); - ((pos.0 as i32 + current.x), (pos.1 as i32 + current.y)) + ((pos.0 + current.x), (pos.1 + current.y)) } #[inline] diff --git a/src/commands/chart.rs b/src/commands/chart.rs index 7801247..2e1fc03 100644 --- a/src/commands/chart.rs +++ b/src/commands/chart.rs @@ -219,7 +219,7 @@ mod best_tests { #[tokio::test] async fn no_scores() -> Result<(), Error> { - with_test_ctx!("test/commands/chart/best/specify_difficulty", async |ctx| { + with_test_ctx!("test/commands/chart/best/no_scores", async |ctx| { best_impl(ctx, "Pentiment").await?; Ok(()) }) @@ -228,9 +228,9 @@ mod best_tests { #[tokio::test] async fn pick_correct_score() -> Result<(), Error> { with_test_ctx!( - "test/commands/chart/best/last_byd", + "test/commands/chart/best/pick_correct_score", async |ctx: &mut MockContext| { - magic_impl( + let plays = magic_impl( ctx, &[ PathBuf::from_str("test/screenshots/fracture_ray_ex.jpg")?, @@ -243,6 +243,7 @@ mod best_tests { let play = best_impl(ctx, "Fracture ray").await?; assert_eq!(play.score(ScoringSystem::Standard).0, 9_805_651); + assert_eq!(plays[0], play); Ok(()) } diff --git a/src/commands/discord.rs b/src/commands/discord.rs index cfb69cb..05336d3 100644 --- a/src/commands/discord.rs +++ b/src/commands/discord.rs @@ -122,24 +122,17 @@ impl<'a> MessageContext for poise::Context<'a, UserContext, Error> { pub mod mock { use std::{env, fs, path::PathBuf}; + use anyhow::Context; use poise::serenity_prelude::CreateEmbed; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use super::*; - /// A mock context usable for testing. Messages and attachments are - /// accumulated inside a vec, and can be used for golden testing - /// (see [MockContext::golden]) - pub struct MockContext { - pub user_id: u64, - pub data: UserContext, - messages: Vec, - } - + // {{{ Message essences /// Holds test-relevant data about an attachment. #[derive(Debug, Clone, Serialize, Deserialize)] - struct AttachmentEssence { + pub struct AttachmentEssence { filename: String, description: Option, /// SHA-256 hash of the file @@ -148,7 +141,7 @@ pub mod mock { /// Holds test-relevant data about a reply. #[derive(Debug, Clone, Serialize)] - struct ReplyEssence { + pub struct ReplyEssence { reply: bool, ephermal: Option, content: Option, @@ -156,6 +149,43 @@ pub mod mock { attachments: Vec, } + impl ReplyEssence { + pub fn from_reply(message: CreateReply) -> Self { + ReplyEssence { + reply: message.reply, + ephermal: message.ephemeral, + content: message.content, + embeds: message.embeds, + attachments: message + .attachments + .into_iter() + .map(|attachment| AttachmentEssence { + filename: attachment.filename, + description: attachment.description, + hash: { + let hash = Sha256::digest(&attachment.data); + let string = base16ct::lower::encode_string(&hash); + + // We allocate twice, but it's only at the end of tests, + // so it should be fineeeeeeee + format!("sha256_{string}") + }, + }) + .collect(), + } + } + } + // }}} + // {{{ Mock context + /// A mock context usable for testing. Messages and attachments are + /// accumulated inside a vec, and can be used for golden testing + /// (see [MockContext::golden]) + pub struct MockContext { + pub user_id: u64, + pub data: UserContext, + messages: Vec, + } + impl MockContext { pub fn new(data: UserContext) -> Self { Self { @@ -223,28 +253,7 @@ pub mod mock { } async fn send(&mut self, message: CreateReply) -> Result<(), Error> { - self.messages.push(ReplyEssence { - reply: message.reply, - ephermal: message.ephemeral, - content: message.content, - embeds: message.embeds, - attachments: message - .attachments - .into_iter() - .map(|attachment| AttachmentEssence { - filename: attachment.filename, - description: attachment.description, - hash: { - let hash = Sha256::digest(&attachment.data); - let string = base16ct::lower::encode_string(&hash); - - // We allocate twice, but it's only at the end of tests, - // so it should be fineeeeeeee - format!("sha256_{string}") - }, - }) - .collect(), - }); + self.messages.push(ReplyEssence::from_reply(message)); Ok(()) } @@ -266,11 +275,14 @@ pub mod mock { } async fn download(&self, attachment: &Self::Attachment) -> Result, Error> { - let res = tokio::fs::read(attachment).await?; + let res = tokio::fs::read(attachment) + .await + .with_context(|| format!("Could not download attachment {attachment:?}"))?; Ok(res) } // }}} } + // }}} } // }}} // {{{ Helpers diff --git a/src/commands/utils/mod.rs b/src/commands/utils/mod.rs index b4dacad..916390e 100644 --- a/src/commands/utils/mod.rs +++ b/src/commands/utils/mod.rs @@ -1,5 +1,3 @@ -pub mod two_columns; - #[macro_export] macro_rules! edit_reply { ($ctx:expr, $handle:expr, $($arg:tt)*) => {{ diff --git a/src/commands/utils/two_columns.rs b/src/commands/utils/two_columns.rs deleted file mode 100644 index 07184d9..0000000 --- a/src/commands/utils/two_columns.rs +++ /dev/null @@ -1,54 +0,0 @@ -//! These functions have been copy-pasted from internal `poise` code. - -use std::fmt::Write as _; - -/// Convenience function to align descriptions behind commands -pub struct TwoColumnList(Vec<(String, Option)>); - -impl TwoColumnList { - /// Creates a new [`TwoColumnList`] - pub fn new() -> Self { - Self(Vec::new()) - } - - /// Add a line that needs the padding between the columns - pub fn push_two_colums(&mut self, command: String, description: String) { - self.0.push((command, Some(description))); - } - - /// Add a line that doesn't influence the first columns's width - pub fn push_heading(&mut self, category: &str) { - if !self.0.is_empty() { - self.0.push(("".to_string(), None)); - } - let mut category = category.to_string(); - category += ":"; - self.0.push((category, None)); - } - - /// Convert the list into a string with aligned descriptions - pub fn into_string(self) -> String { - let longest_command = self - .0 - .iter() - .filter_map(|(command, description)| { - if description.is_some() { - Some(command.len()) - } else { - None - } - }) - .max() - .unwrap_or(0); - let mut text = String::new(); - for (command, description) in self.0 { - if let Some(description) = description { - let padding = " ".repeat(longest_command - command.len() + 3); - writeln!(text, "{}{}{}", command, padding, description).unwrap(); - } else { - writeln!(text, "{}", command).unwrap(); - } - } - text - } -} diff --git a/src/context.rs b/src/context.rs index 77656eb..e967332 100644 --- a/src/context.rs +++ b/src/context.rs @@ -9,7 +9,6 @@ use std::sync::LazyLock; use crate::arcaea::{chart::SongCache, jacket::JacketCache}; use crate::assets::{get_data_dir, EXO_FONT, GEOSANS_FONT, KAZESAWA_BOLD_FONT, KAZESAWA_FONT}; -use crate::commands::discord::MessageContext; use crate::recognition::{hyperglass::CharMeasurements, ui::UIMeasurements}; use crate::timed; // }}} @@ -48,24 +47,6 @@ macro_rules! get_user_error { }}; } -/// Handles a [TaggedError], showing user errors to the user, -/// and throwing away anything else. -pub async fn discord_error_handler( - ctx: &mut impl MessageContext, - res: Result, -) -> Result, Error> { - match res { - Ok(v) => Ok(Some(v)), - Err(e) => match e.kind { - ErrorKind::Internal => Err(e.error), - ErrorKind::User => { - ctx.reply(&format!("{}", e.error)).await?; - Ok(None) - } - }, - } -} - impl> From for TaggedError { fn from(value: E) -> Self { Self::new(ErrorKind::Internal, value.into()) diff --git a/src/lib.rs b/src/lib.rs index 257ab52..f3abf87 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,6 @@ #![allow(async_fn_in_trait)] +#![allow(clippy::needless_range_loop)] +#![allow(clippy::redundant_closure)] #![feature(iter_map_windows)] #![feature(let_chains)] #![feature(array_try_map)] @@ -7,6 +9,7 @@ #![feature(thread_local)] #![feature(generic_arg_infer)] #![feature(iter_collect_into)] +#![feature(stmt_expr_attributes)] pub mod arcaea; pub mod assets; diff --git a/src/recognition/hyperglass.rs b/src/recognition/hyperglass.rs index f254ebf..594aaa0 100644 --- a/src/recognition/hyperglass.rs +++ b/src/recognition/hyperglass.rs @@ -1,7 +1,8 @@ -//! Hyperglass is my own specialized OCR system, created as a result of my -//! annoyance with how unreliable tesseract is. Assuming we know the font, -//! OCR should be almost perfect, even when faced with stange kerning. This is -//! what this module achieves! +//! Hyperglass is my own specialized OCR system. +//! +//! Hyperglass was created as a result of my annoyance with how unreliable +//! tesseract is. Assuming we know the font, OCR should be almost perfect, +//! even when faced with stange kerning. This is what this module achieves! //! //! The algorithm is pretty simple: //! 1. Find the connected components (i.e., "black areas") in the image. @@ -202,10 +203,7 @@ impl ComponentsWithBounds { for bound in &mut bounds { if bound.map_or(false, |b| { (b.x_max - b.x_min) as f32 >= max_sizes.0 * image.width() as f32 - }) { - *bound = None; - } else if bound.map_or(false, |b| { - (b.y_max - b.y_min) as f32 >= max_sizes.1 * image.height() as f32 + || (b.y_max - b.y_min) as f32 >= max_sizes.1 * image.height() as f32 }) { *bound = None; } @@ -249,14 +247,14 @@ impl CharMeasurements { weight, }; let padding = (5, 5); - let planned = BitmapCanvas::plan_text_rendering(padding, &mut [face], style, &string)?; + let planned = BitmapCanvas::plan_text_rendering(padding, &mut [face], style, string)?; let mut canvas = BitmapCanvas::new( (planned.0 .0) as u32 + planned.1.width + 2 * padding.0 as u32, (planned.0 .1) as u32 + planned.1.height + 2 * padding.0 as u32, ); - canvas.text(padding, &mut [face], style, &string)?; + canvas.text(padding, &mut [face], style, string)?; let buffer = ImageBuffer::from_raw(canvas.width, canvas.height(), canvas.buffer.to_vec()) .ok_or_else(|| anyhow!("Failed to turn buffer into canvas"))?; let image = DynamicImage::ImageRgb8(buffer); diff --git a/src/recognition/ui.rs b/src/recognition/ui.rs index c687f52..d4480d9 100644 --- a/src/recognition/ui.rs +++ b/src/recognition/ui.rs @@ -111,14 +111,14 @@ impl UIMeasurements { 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)?; + measurement.dimensions[j] = str.parse()?; } } else if i == UI_RECT_COUNT + 1 { 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)?; + measurement.datapoints[(i - 1) * 4 + j] = str.parse()?; } } } diff --git a/src/transform.rs b/src/transform.rs index 9272787..4d2a987 100644 --- a/src/transform.rs +++ b/src/transform.rs @@ -1,9 +1,11 @@ -//! This file implements the "rotation as shearing" algorithm, -//! which can rotate images without making use of any trigonometric -//! functions (or working with floats altogether, if you don't care +//! This file implements the "rotation as shearing" algorithm. +//! +//! The algorithm can rotate images without making use of any trigonometric +//! functions (or working with floats altogether, assuming you don't care //! about antialiasing). //! -//! For more information, consult this article: https://www.ocf.berkeley.edu/~fricke/projects/israel/paeth/rotation_by_shearing.html +//! For more information, consult this article: +//! https://www.ocf.berkeley.edu/~fricke/projects/israel/paeth/rotation_by_shearing.html use image::{DynamicImage, GenericImage, GenericImageView}; @@ -57,7 +59,7 @@ pub fn yshear(image: &mut DynamicImage, rect: Rect, center: Position, shear: f32 } } -/// Performs a rotation as a series of three shear operations +/// 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 / 2.0);