Make everything build
This commit is contained in:
parent
74f554e058
commit
92dbd181f2
|
@ -41,5 +41,5 @@ anyhow = "1.0.87"
|
||||||
sha2 = "0.10.8"
|
sha2 = "0.10.8"
|
||||||
base16ct = { version = "0.2.0", features = ["alloc"] }
|
base16ct = { version = "0.2.0", features = ["alloc"] }
|
||||||
|
|
||||||
# [profile.dev.package."*"]
|
[profile.dev.package."*"]
|
||||||
# opt-level = 3
|
opt-level = 3
|
||||||
|
|
16
flake.nix
16
flake.nix
|
@ -13,7 +13,12 @@
|
||||||
inputs.flake-utils.lib.eachSystem (with inputs.flake-utils.lib.system; [ x86_64-linux ]) (
|
inputs.flake-utils.lib.eachSystem (with inputs.flake-utils.lib.system; [ x86_64-linux ]) (
|
||||||
system:
|
system:
|
||||||
let
|
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.selectLatestNightlyWith (toolchain: toolchain.default);
|
||||||
# toolchain = pkgs.rust-bin.stable.latest.default;
|
# toolchain = pkgs.rust-bin.stable.latest.default;
|
||||||
toolchain = inputs.fenix.packages.${system}.complete.toolchain;
|
toolchain = inputs.fenix.packages.${system}.complete.toolchain;
|
||||||
|
@ -34,18 +39,17 @@
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
devShell = pkgs.mkShell {
|
devShell = pkgs.mkShell rec {
|
||||||
nativeBuildInputs = with pkgs; [
|
nativeBuildInputs = with pkgs; [
|
||||||
toolchain
|
toolchain
|
||||||
# ruff
|
ruff
|
||||||
# imagemagick
|
imagemagick
|
||||||
pkg-config
|
pkg-config
|
||||||
|
|
||||||
# clang
|
# clang
|
||||||
# llvmPackages.clang
|
# llvmPackages.clang
|
||||||
];
|
];
|
||||||
buildInputs = with pkgs; [
|
buildInputs = with pkgs; [
|
||||||
toolchain
|
|
||||||
freetype
|
freetype
|
||||||
fontconfig
|
fontconfig
|
||||||
leptonica
|
leptonica
|
||||||
|
@ -54,7 +58,7 @@
|
||||||
sqlite
|
sqlite
|
||||||
];
|
];
|
||||||
|
|
||||||
# LD_LIBRARY_PATH = lib.makeLibraryPath buildInputs;
|
LD_LIBRARY_PATH = lib.makeLibraryPath buildInputs;
|
||||||
|
|
||||||
# compilation of -sys packages requires manually setting LIBCLANG_PATH
|
# compilation of -sys packages requires manually setting LIBCLANG_PATH
|
||||||
# LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib";
|
# LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib";
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use std::path::Path;
|
||||||
// {{{ Imports
|
// {{{ Imports
|
||||||
use std::{fmt::Display, num::NonZeroU16, path::PathBuf};
|
use std::{fmt::Display, num::NonZeroU16, path::PathBuf};
|
||||||
|
|
||||||
|
@ -216,7 +217,7 @@ pub struct Chart {
|
||||||
|
|
||||||
impl Chart {
|
impl Chart {
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn jacket_path(&self, data_dir: &PathBuf) -> PathBuf {
|
pub fn jacket_path(&self, data_dir: &Path) -> PathBuf {
|
||||||
data_dir
|
data_dir
|
||||||
.join("jackets")
|
.join("jackets")
|
||||||
.join(format!("{}-{}.jpg", self.song_id, self.id))
|
.join(format!("{}-{}.jpg", self.song_id, self.id))
|
||||||
|
@ -241,10 +242,7 @@ impl CachedSong {
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn charts(&self) -> impl Iterator<Item = u32> {
|
pub fn charts(&self) -> impl Iterator<Item = u32> {
|
||||||
self.chart_ids
|
self.chart_ids.into_iter().flatten().map(|i| i.get() as u32)
|
||||||
.into_iter()
|
|
||||||
.filter_map(|i| i)
|
|
||||||
.map(|i| i.get() as u32)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
|
|
|
@ -55,6 +55,7 @@ impl ImageVec {
|
||||||
let r = (r as f64 / count).sqrt();
|
let r = (r as f64 / count).sqrt();
|
||||||
let g = (g as f64 / count).sqrt();
|
let g = (g as f64 / count).sqrt();
|
||||||
let b = (b 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 + 0] = r as f32;
|
||||||
colors[i as usize * 3 + 1] = g as f32;
|
colors[i as usize * 3 + 1] = g as f32;
|
||||||
colors[i as usize * 3 + 2] = b as f32;
|
colors[i as usize * 3 + 2] = b as f32;
|
||||||
|
|
|
@ -500,7 +500,7 @@ pub fn compute_b30_ptt(scoring_system: ScoringSystem, plays: &PlayCollection<'_>
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Maintenance functions
|
// {{{ 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 conn = ctx.db.get()?;
|
||||||
let mut query = conn.prepare_cached(
|
let mut query = conn.prepare_cached(
|
||||||
"
|
"
|
||||||
|
|
|
@ -226,16 +226,10 @@ impl Score {
|
||||||
|
|
||||||
// {{{ Compute score from note breakdown subpairs
|
// {{{ Compute score from note breakdown subpairs
|
||||||
let pf_score = Score::compute_naive(note_count, pures, fars);
|
let pf_score = Score::compute_naive(note_count, pures, fars);
|
||||||
let fl_score = Score::compute_naive(
|
let fl_score =
|
||||||
note_count,
|
Score::compute_naive(note_count, note_count.saturating_sub(losts + fars), fars);
|
||||||
note_count.checked_sub(losts + fars).unwrap_or(0),
|
let lp_score =
|
||||||
fars,
|
Score::compute_naive(note_count, pures, note_count.saturating_sub(losts + pures));
|
||||||
);
|
|
||||||
let lp_score = Score::compute_naive(
|
|
||||||
note_count,
|
|
||||||
pures,
|
|
||||||
note_count.checked_sub(losts + pures).unwrap_or(0),
|
|
||||||
);
|
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Look for consensus among recomputed scores
|
// {{{ Look for consensus among recomputed scores
|
||||||
// Lemma: if two computed scores agree, then so will the third
|
// Lemma: if two computed scores agree, then so will the third
|
||||||
|
|
|
@ -45,7 +45,7 @@ pub fn get_asset_dir() -> PathBuf {
|
||||||
fn get_font(name: &str) -> RefCell<Face> {
|
fn get_font(name: &str) -> RefCell<Face> {
|
||||||
let face = FREETYPE_LIB.with(|lib| {
|
let face = FREETYPE_LIB.with(|lib| {
|
||||||
lib.new_face(get_asset_dir().join("fonts").join(name), 0)
|
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)
|
RefCell::new(face)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use crate::context::CliContext;
|
use crate::context::CliContext;
|
||||||
|
use shimmeringmoon::commands::discord::MessageContext;
|
||||||
use shimmeringmoon::commands::score::magic_impl;
|
use shimmeringmoon::commands::score::magic_impl;
|
||||||
use shimmeringmoon::context::{Error, UserContext};
|
use shimmeringmoon::context::{Error, UserContext};
|
||||||
// }}}
|
// }}}
|
||||||
|
@ -13,6 +14,7 @@ pub struct Args {
|
||||||
|
|
||||||
pub async fn run(args: Args) -> Result<(), Error> {
|
pub async fn run(args: Args) -> Result<(), Error> {
|
||||||
let mut ctx = CliContext::new(UserContext::new().await?);
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,10 +3,10 @@ use std::num::NonZeroU64;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use poise::serenity_prelude::{CreateAttachment, CreateMessage};
|
|
||||||
|
|
||||||
extern crate shimmeringmoon;
|
extern crate shimmeringmoon;
|
||||||
|
use poise::CreateReply;
|
||||||
use shimmeringmoon::assets::get_var;
|
use shimmeringmoon::assets::get_var;
|
||||||
|
use shimmeringmoon::commands::discord::mock::ReplyEssence;
|
||||||
use shimmeringmoon::context::Error;
|
use shimmeringmoon::context::Error;
|
||||||
use shimmeringmoon::{commands::discord::MessageContext, context::UserContext};
|
use shimmeringmoon::{commands::discord::MessageContext, context::UserContext};
|
||||||
// }}}
|
// }}}
|
||||||
|
@ -53,12 +53,8 @@ impl MessageContext for CliContext {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn send_files(
|
async fn send(&mut self, message: CreateReply) -> Result<(), Error> {
|
||||||
&mut self,
|
let all = toml::to_string(&ReplyEssence::from_reply(message)).unwrap();
|
||||||
_attachments: impl IntoIterator<Item = CreateAttachment>,
|
|
||||||
message: CreateMessage,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
let all = toml::to_string(&message).unwrap();
|
|
||||||
println!("\n========== Message ==========");
|
println!("\n========== Message ==========");
|
||||||
println!("{all}");
|
println!("{all}");
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use command::{Cli, Command};
|
use command::{Cli, Command};
|
||||||
use shimmeringmoon::context::{Error, UserContext};
|
use shimmeringmoon::context::Error;
|
||||||
|
|
||||||
mod command;
|
mod command;
|
||||||
mod commands;
|
mod commands;
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
use poise::serenity_prelude::{self as serenity};
|
use poise::serenity_prelude::{self as serenity};
|
||||||
extern crate shimmeringmoon;
|
|
||||||
use shimmeringmoon::arcaea::play::generate_missing_scores;
|
use shimmeringmoon::arcaea::play::generate_missing_scores;
|
||||||
use shimmeringmoon::context::{Error, UserContext};
|
use shimmeringmoon::context::{Error, UserContext};
|
||||||
use shimmeringmoon::{commands, timed};
|
use shimmeringmoon::{commands, timed};
|
||||||
|
|
|
@ -174,9 +174,11 @@ impl BitmapCanvas {
|
||||||
}
|
}
|
||||||
|
|
||||||
// {{{ Draw pixel
|
// {{{ Draw pixel
|
||||||
|
#[allow(clippy::identity_op)]
|
||||||
pub fn set_pixel(&mut self, pos: (u32, u32), color: Color) {
|
pub fn set_pixel(&mut self, pos: (u32, u32), color: Color) {
|
||||||
let index = 3 * (pos.1 * self.width + pos.0) as usize;
|
let index = 3 * (pos.1 * self.width + pos.0) as usize;
|
||||||
let alpha = color.3 as u32;
|
let alpha = color.3 as u32;
|
||||||
|
|
||||||
self.buffer[index + 0] =
|
self.buffer[index + 0] =
|
||||||
((alpha * color.0 as u32 + (255 - alpha) * self.buffer[index + 0] as u32) / 255) as u8;
|
((alpha * color.0 as u32 + (255 - alpha) * self.buffer[index + 0] as u32) / 255) as u8;
|
||||||
self.buffer[index + 1] =
|
self.buffer[index + 1] =
|
||||||
|
@ -299,6 +301,7 @@ impl BitmapCanvas {
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Draw text
|
// {{{ Draw text
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
pub fn plan_text_rendering(
|
pub fn plan_text_rendering(
|
||||||
pos: Position,
|
pos: Position,
|
||||||
faces: &mut [&mut Face],
|
faces: &mut [&mut Face],
|
||||||
|
@ -490,8 +493,8 @@ impl BitmapCanvas {
|
||||||
|
|
||||||
for dx in 0..iw {
|
for dx in 0..iw {
|
||||||
for dy in 0..ih {
|
for dy in 0..ih {
|
||||||
let x = pos.0 + dx as i32 + b_glyph.left();
|
let x = pos.0 + dx + b_glyph.left();
|
||||||
let y = pos.1 + dy as i32 - b_glyph.top();
|
let y = pos.1 + dy - b_glyph.top();
|
||||||
|
|
||||||
// TODO: gamma correction
|
// TODO: gamma correction
|
||||||
if x >= 0 && (x as u32) < self.width && y >= 0 && (y as u32) < height {
|
if x >= 0 && (x as u32) < self.width && y >= 0 && (y as u32) < height {
|
||||||
|
@ -656,7 +659,7 @@ impl LayoutManager {
|
||||||
|
|
||||||
(
|
(
|
||||||
outer_id,
|
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);
|
let (y, x) = i.div_rem_euclid(&amount.0);
|
||||||
((x * inner.width) as i32, (y * inner.height) as i32)
|
((x * inner.width) as i32, (y * inner.height) as i32)
|
||||||
}),
|
}),
|
||||||
|
@ -689,7 +692,7 @@ impl LayoutManager {
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn position_relative_to(&self, id: LayoutBoxId, pos: Position) -> Position {
|
pub fn position_relative_to(&self, id: LayoutBoxId, pos: Position) -> Position {
|
||||||
let current = self.lookup(id);
|
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]
|
#[inline]
|
||||||
|
|
|
@ -219,7 +219,7 @@ mod best_tests {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn no_scores() -> Result<(), Error> {
|
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?;
|
best_impl(ctx, "Pentiment").await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
|
@ -228,9 +228,9 @@ mod best_tests {
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn pick_correct_score() -> Result<(), Error> {
|
async fn pick_correct_score() -> Result<(), Error> {
|
||||||
with_test_ctx!(
|
with_test_ctx!(
|
||||||
"test/commands/chart/best/last_byd",
|
"test/commands/chart/best/pick_correct_score",
|
||||||
async |ctx: &mut MockContext| {
|
async |ctx: &mut MockContext| {
|
||||||
magic_impl(
|
let plays = magic_impl(
|
||||||
ctx,
|
ctx,
|
||||||
&[
|
&[
|
||||||
PathBuf::from_str("test/screenshots/fracture_ray_ex.jpg")?,
|
PathBuf::from_str("test/screenshots/fracture_ray_ex.jpg")?,
|
||||||
|
@ -243,6 +243,7 @@ mod best_tests {
|
||||||
|
|
||||||
let play = best_impl(ctx, "Fracture ray").await?;
|
let play = best_impl(ctx, "Fracture ray").await?;
|
||||||
assert_eq!(play.score(ScoringSystem::Standard).0, 9_805_651);
|
assert_eq!(play.score(ScoringSystem::Standard).0, 9_805_651);
|
||||||
|
assert_eq!(plays[0], play);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -122,24 +122,17 @@ impl<'a> MessageContext for poise::Context<'a, UserContext, Error> {
|
||||||
pub mod mock {
|
pub mod mock {
|
||||||
use std::{env, fs, path::PathBuf};
|
use std::{env, fs, path::PathBuf};
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
use poise::serenity_prelude::CreateEmbed;
|
use poise::serenity_prelude::CreateEmbed;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
/// A mock context usable for testing. Messages and attachments are
|
// {{{ Message essences
|
||||||
/// 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<ReplyEssence>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Holds test-relevant data about an attachment.
|
/// Holds test-relevant data about an attachment.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
struct AttachmentEssence {
|
pub struct AttachmentEssence {
|
||||||
filename: String,
|
filename: String,
|
||||||
description: Option<String>,
|
description: Option<String>,
|
||||||
/// SHA-256 hash of the file
|
/// SHA-256 hash of the file
|
||||||
|
@ -148,7 +141,7 @@ pub mod mock {
|
||||||
|
|
||||||
/// Holds test-relevant data about a reply.
|
/// Holds test-relevant data about a reply.
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
struct ReplyEssence {
|
pub struct ReplyEssence {
|
||||||
reply: bool,
|
reply: bool,
|
||||||
ephermal: Option<bool>,
|
ephermal: Option<bool>,
|
||||||
content: Option<String>,
|
content: Option<String>,
|
||||||
|
@ -156,6 +149,43 @@ pub mod mock {
|
||||||
attachments: Vec<AttachmentEssence>,
|
attachments: Vec<AttachmentEssence>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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<ReplyEssence>,
|
||||||
|
}
|
||||||
|
|
||||||
impl MockContext {
|
impl MockContext {
|
||||||
pub fn new(data: UserContext) -> Self {
|
pub fn new(data: UserContext) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
@ -223,28 +253,7 @@ pub mod mock {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn send(&mut self, message: CreateReply) -> Result<(), Error> {
|
async fn send(&mut self, message: CreateReply) -> Result<(), Error> {
|
||||||
self.messages.push(ReplyEssence {
|
self.messages.push(ReplyEssence::from_reply(message));
|
||||||
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(),
|
|
||||||
});
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -266,11 +275,14 @@ pub mod mock {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn download(&self, attachment: &Self::Attachment) -> Result<Vec<u8>, Error> {
|
async fn download(&self, attachment: &Self::Attachment) -> Result<Vec<u8>, 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)
|
Ok(res)
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
}
|
}
|
||||||
|
// }}}
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Helpers
|
// {{{ Helpers
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
pub mod two_columns;
|
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! edit_reply {
|
macro_rules! edit_reply {
|
||||||
($ctx:expr, $handle:expr, $($arg:tt)*) => {{
|
($ctx:expr, $handle:expr, $($arg:tt)*) => {{
|
||||||
|
|
|
@ -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<String>)>);
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -9,7 +9,6 @@ use std::sync::LazyLock;
|
||||||
|
|
||||||
use crate::arcaea::{chart::SongCache, jacket::JacketCache};
|
use crate::arcaea::{chart::SongCache, jacket::JacketCache};
|
||||||
use crate::assets::{get_data_dir, EXO_FONT, GEOSANS_FONT, KAZESAWA_BOLD_FONT, KAZESAWA_FONT};
|
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::recognition::{hyperglass::CharMeasurements, ui::UIMeasurements};
|
||||||
use crate::timed;
|
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<V>(
|
|
||||||
ctx: &mut impl MessageContext,
|
|
||||||
res: Result<V, TaggedError>,
|
|
||||||
) -> Result<Option<V>, 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<E: Into<Error>> From<E> for TaggedError {
|
impl<E: Into<Error>> From<E> for TaggedError {
|
||||||
fn from(value: E) -> Self {
|
fn from(value: E) -> Self {
|
||||||
Self::new(ErrorKind::Internal, value.into())
|
Self::new(ErrorKind::Internal, value.into())
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
#![allow(async_fn_in_trait)]
|
#![allow(async_fn_in_trait)]
|
||||||
|
#![allow(clippy::needless_range_loop)]
|
||||||
|
#![allow(clippy::redundant_closure)]
|
||||||
#![feature(iter_map_windows)]
|
#![feature(iter_map_windows)]
|
||||||
#![feature(let_chains)]
|
#![feature(let_chains)]
|
||||||
#![feature(array_try_map)]
|
#![feature(array_try_map)]
|
||||||
|
@ -7,6 +9,7 @@
|
||||||
#![feature(thread_local)]
|
#![feature(thread_local)]
|
||||||
#![feature(generic_arg_infer)]
|
#![feature(generic_arg_infer)]
|
||||||
#![feature(iter_collect_into)]
|
#![feature(iter_collect_into)]
|
||||||
|
#![feature(stmt_expr_attributes)]
|
||||||
|
|
||||||
pub mod arcaea;
|
pub mod arcaea;
|
||||||
pub mod assets;
|
pub mod assets;
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
//! Hyperglass is my own specialized OCR system, created as a result of my
|
//! Hyperglass is my own specialized OCR system.
|
||||||
//! annoyance with how unreliable tesseract is. Assuming we know the font,
|
//!
|
||||||
//! OCR should be almost perfect, even when faced with stange kerning. This is
|
//! Hyperglass was created as a result of my annoyance with how unreliable
|
||||||
//! what this module achieves!
|
//! 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:
|
//! The algorithm is pretty simple:
|
||||||
//! 1. Find the connected components (i.e., "black areas") in the image.
|
//! 1. Find the connected components (i.e., "black areas") in the image.
|
||||||
|
@ -202,10 +203,7 @@ impl ComponentsWithBounds {
|
||||||
for bound in &mut bounds {
|
for bound in &mut bounds {
|
||||||
if bound.map_or(false, |b| {
|
if bound.map_or(false, |b| {
|
||||||
(b.x_max - b.x_min) as f32 >= max_sizes.0 * image.width() as f32
|
(b.x_max - b.x_min) as f32 >= max_sizes.0 * image.width() as f32
|
||||||
}) {
|
|| (b.y_max - b.y_min) as f32 >= max_sizes.1 * image.height() 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
|
|
||||||
}) {
|
}) {
|
||||||
*bound = None;
|
*bound = None;
|
||||||
}
|
}
|
||||||
|
@ -249,14 +247,14 @@ impl CharMeasurements {
|
||||||
weight,
|
weight,
|
||||||
};
|
};
|
||||||
let padding = (5, 5);
|
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(
|
let mut canvas = BitmapCanvas::new(
|
||||||
(planned.0 .0) as u32 + planned.1.width + 2 * padding.0 as u32,
|
(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,
|
(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())
|
let buffer = ImageBuffer::from_raw(canvas.width, canvas.height(), canvas.buffer.to_vec())
|
||||||
.ok_or_else(|| anyhow!("Failed to turn buffer into canvas"))?;
|
.ok_or_else(|| anyhow!("Failed to turn buffer into canvas"))?;
|
||||||
let image = DynamicImage::ImageRgb8(buffer);
|
let image = DynamicImage::ImageRgb8(buffer);
|
||||||
|
|
|
@ -111,14 +111,14 @@ impl UIMeasurements {
|
||||||
let i = i % (UI_RECT_COUNT + 2);
|
let i = i % (UI_RECT_COUNT + 2);
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
for (j, str) in line.split_whitespace().enumerate().take(2) {
|
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 {
|
} else if i == UI_RECT_COUNT + 1 {
|
||||||
measurements.push(measurement);
|
measurements.push(measurement);
|
||||||
measurement = UIMeasurement::default();
|
measurement = UIMeasurement::default();
|
||||||
} else {
|
} else {
|
||||||
for (j, str) in line.split_whitespace().enumerate().take(4) {
|
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()?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
//! This file implements the "rotation as shearing" algorithm,
|
//! 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
|
//! The algorithm can rotate images without making use of any trigonometric
|
||||||
|
//! functions (or working with floats altogether, assuming you don't care
|
||||||
//! about antialiasing).
|
//! 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};
|
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.
|
/// Does not perform anti-aliasing.
|
||||||
pub fn rotate(image: &mut DynamicImage, rect: Rect, center: Position, angle: f32) {
|
pub fn rotate(image: &mut DynamicImage, rect: Rect, center: Position, angle: f32) {
|
||||||
let alpha = -f32::tan(angle / 2.0);
|
let alpha = -f32::tan(angle / 2.0);
|
||||||
|
|
Loading…
Reference in a new issue