Implement a few math commands
This commit is contained in:
parent
f3edaf9e72
commit
2ac13510f2
|
@ -1,6 +1,5 @@
|
||||||
use std::path::Path;
|
|
||||||
// {{{ Imports
|
// {{{ Imports
|
||||||
use std::{fmt::Display, num::NonZeroU16, path::PathBuf};
|
use std::{fmt::Display, num::NonZeroU16};
|
||||||
|
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use image::{ImageBuffer, Rgb};
|
use image::{ImageBuffer, Rgb};
|
||||||
|
@ -26,6 +25,8 @@ impl Difficulty {
|
||||||
[Self::PST, Self::PRS, Self::FTR, Self::ETR, Self::BYD];
|
[Self::PST, Self::PRS, Self::FTR, Self::ETR, Self::BYD];
|
||||||
|
|
||||||
pub const DIFFICULTY_SHORTHANDS: [&'static str; 5] = ["PST", "PRS", "FTR", "ETR", "BYD"];
|
pub const DIFFICULTY_SHORTHANDS: [&'static str; 5] = ["PST", "PRS", "FTR", "ETR", "BYD"];
|
||||||
|
pub const DIFFICULTY_SHORTHANDS_IN_BRACKETS: [&'static str; 5] =
|
||||||
|
["[PST]", "[PRS]", "[FTR]", "[ETR]", "[BYD]"];
|
||||||
pub const DIFFICULTY_STRINGS: [&'static str; 5] =
|
pub const DIFFICULTY_STRINGS: [&'static str; 5] =
|
||||||
["PAST", "PRESENT", "FUTURE", "ETERNAL", "BEYOND"];
|
["PAST", "PRESENT", "FUTURE", "ETERNAL", "BEYOND"];
|
||||||
|
|
||||||
|
@ -53,11 +54,7 @@ impl FromSql for Difficulty {
|
||||||
|
|
||||||
impl Display for Difficulty {
|
impl Display for Difficulty {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
write!(
|
write!(f, "{}", Self::DIFFICULTY_SHORTHANDS[self.to_index()])
|
||||||
f,
|
|
||||||
"{}",
|
|
||||||
Self::DIFFICULTY_SHORTHANDS[self.to_index()].to_lowercase()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -192,6 +189,24 @@ pub struct Song {
|
||||||
pub pack: Option<String>,
|
pub pack: Option<String>,
|
||||||
pub side: Side,
|
pub side: Side,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Song {
|
||||||
|
/// Returns true if multiple songs are known to exist with the given title.
|
||||||
|
#[inline]
|
||||||
|
pub fn ambigous_name(&self) -> bool {
|
||||||
|
self.title == "Genesis" || self.title == "Quon"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Song {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
if self.ambigous_name() {
|
||||||
|
write!(f, "{} ({})", self.title, self.artist)
|
||||||
|
} else {
|
||||||
|
write!(f, "{}", self.title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Chart
|
// {{{ Chart
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
@ -215,6 +230,10 @@ pub struct Chart {
|
||||||
|
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub cached_jacket: Option<Jacket>,
|
pub cached_jacket: Option<Jacket>,
|
||||||
|
|
||||||
|
/// If `None`, the default jacket is used.
|
||||||
|
/// Otherwise, a difficulty-specific jacket exists.
|
||||||
|
pub jacket_source: Option<Difficulty>,
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Cached song
|
// {{{ Cached song
|
||||||
|
@ -374,6 +393,7 @@ impl SongCache {
|
||||||
note_count: row.get("note_count")?,
|
note_count: row.get("note_count")?,
|
||||||
note_design: row.get("note_design")?,
|
note_design: row.get("note_design")?,
|
||||||
cached_jacket: None,
|
cached_jacket: None,
|
||||||
|
jacket_source: None,
|
||||||
})
|
})
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
|
|
@ -106,6 +106,7 @@ impl JacketCache {
|
||||||
|
|
||||||
Vec::new()
|
Vec::new()
|
||||||
} else {
|
} else {
|
||||||
|
let suffix = format!("_{BITMAP_IMAGE_SIZE}.jpg");
|
||||||
let songs_dir = get_asset_dir().join("songs/by_id");
|
let songs_dir = get_asset_dir().join("songs/by_id");
|
||||||
let entries =
|
let entries =
|
||||||
fs::read_dir(songs_dir).with_context(|| "Couldn't read songs directory")?;
|
fs::read_dir(songs_dir).with_context(|| "Couldn't read songs directory")?;
|
||||||
|
@ -127,7 +128,12 @@ impl JacketCache {
|
||||||
for entry in entries {
|
for entry in entries {
|
||||||
let file = entry?;
|
let file = entry?;
|
||||||
let raw_name = file.file_name();
|
let raw_name = file.file_name();
|
||||||
let name = raw_name.to_str().unwrap().strip_suffix(".jpg").unwrap();
|
let name = raw_name.to_str().unwrap();
|
||||||
|
if !name.ends_with(&suffix) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = name.strip_suffix(&suffix).unwrap();
|
||||||
|
|
||||||
let difficulty = Difficulty::DIFFICULTY_SHORTHANDS
|
let difficulty = Difficulty::DIFFICULTY_SHORTHANDS
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -146,6 +152,7 @@ impl JacketCache {
|
||||||
let chart = song_cache
|
let chart = song_cache
|
||||||
.lookup_by_difficulty_mut(song_id, difficulty)
|
.lookup_by_difficulty_mut(song_id, difficulty)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
chart.jacket_source = Some(difficulty);
|
||||||
chart.cached_jacket = Some(Jacket {
|
chart.cached_jacket = Some(Jacket {
|
||||||
raw: contents,
|
raw: contents,
|
||||||
bitmap,
|
bitmap,
|
||||||
|
@ -153,11 +160,12 @@ impl JacketCache {
|
||||||
} else {
|
} else {
|
||||||
for chart_id in song_cache.lookup_song(song_id)?.charts() {
|
for chart_id in song_cache.lookup_song(song_id)?.charts() {
|
||||||
let chart = song_cache.lookup_chart_mut(chart_id)?;
|
let chart = song_cache.lookup_chart_mut(chart_id)?;
|
||||||
if chart.cached_jacket.is_none() {
|
if chart.jacket_source.is_none() {
|
||||||
chart.cached_jacket = Some(Jacket {
|
chart.cached_jacket = Some(Jacket {
|
||||||
raw: contents,
|
raw: contents,
|
||||||
bitmap,
|
bitmap,
|
||||||
});
|
});
|
||||||
|
chart.jacket_source = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use num::Rational32;
|
use num::{Rational32, ToPrimitive};
|
||||||
|
|
||||||
pub type Rating = Rational32;
|
pub type Rating = Rational32;
|
||||||
|
|
||||||
|
@ -13,7 +13,10 @@ pub fn rating_as_fixed(rating: Rating) -> i32 {
|
||||||
/// Saves a rating rational as a float with precision 2.
|
/// Saves a rating rational as a float with precision 2.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn rating_as_float(rating: Rating) -> f32 {
|
pub fn rating_as_float(rating: Rating) -> f32 {
|
||||||
rating_as_fixed(rating) as f32 / 100.0
|
let hundred = Rational32::from_integer(100);
|
||||||
|
let rounded = (rating * hundred).round();
|
||||||
|
|
||||||
|
(rounded / hundred).to_f32().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The pseudo-inverse of `rating_as_fixed`.
|
/// The pseudo-inverse of `rating_as_fixed`.
|
||||||
|
|
|
@ -36,7 +36,7 @@ pub fn run() -> Result<(), Error> {
|
||||||
let entries = fs::read_dir(&raw_songs_dir)
|
let entries = fs::read_dir(&raw_songs_dir)
|
||||||
.with_context(|| "Couldn't read songs directory")?
|
.with_context(|| "Couldn't read songs directory")?
|
||||||
.collect::<Result<Vec<_>, _>>()
|
.collect::<Result<Vec<_>, _>>()
|
||||||
.with_context(|| format!("Could not read member of `songs/raw`"))?;
|
.with_context(|| "Could not read member of `songs/raw`")?;
|
||||||
|
|
||||||
for (i, dir) in entries.iter().enumerate() {
|
for (i, dir) in entries.iter().enumerate() {
|
||||||
let raw_dir_name = dir.file_name();
|
let raw_dir_name = dir.file_name();
|
||||||
|
@ -48,10 +48,7 @@ pub fn run() -> Result<(), Error> {
|
||||||
}
|
}
|
||||||
|
|
||||||
print!("{}/{}: {dir_name}", i, entries.len());
|
print!("{}/{}: {dir_name}", i, entries.len());
|
||||||
|
stdout().flush()?;
|
||||||
if i % 5 == 0 {
|
|
||||||
stdout().flush()?;
|
|
||||||
}
|
|
||||||
// }}}
|
// }}}
|
||||||
|
|
||||||
let entries = fs::read_dir(dir.path())
|
let entries = fs::read_dir(dir.path())
|
||||||
|
@ -126,13 +123,31 @@ pub fn run() -> Result<(), Error> {
|
||||||
|
|
||||||
jacket_vectors.push((song.id, ImageVec::from_image(&image)));
|
jacket_vectors.push((song.id, ImageVec::from_image(&image)));
|
||||||
|
|
||||||
let image = image.resize(BITMAP_IMAGE_SIZE, BITMAP_IMAGE_SIZE, FilterType::Gaussian);
|
let small_image =
|
||||||
let image_out_path =
|
image.resize(BITMAP_IMAGE_SIZE, BITMAP_IMAGE_SIZE, FilterType::Gaussian);
|
||||||
out_dir.join(format!("{difficulty_string}_{BITMAP_IMAGE_SIZE}.jpg"));
|
|
||||||
image
|
{
|
||||||
// .blur(27.5)
|
let image_small_path =
|
||||||
.save(&image_out_path)
|
out_dir.join(format!("{difficulty_string}_{BITMAP_IMAGE_SIZE}.jpg"));
|
||||||
.with_context(|| format!("Could not save image to {image_out_path:?}"))?;
|
small_image
|
||||||
|
.save(&image_small_path)
|
||||||
|
.with_context(|| format!("Could not save image to {image_small_path:?}"))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let image_full_path = out_dir.join(format!("{difficulty_string}_full.jpg"));
|
||||||
|
image
|
||||||
|
.save(&image_full_path)
|
||||||
|
.with_context(|| format!("Could not save image to {image_full_path:?}"))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let blurred_out_path = out_dir.join(format!("{difficulty_string}_blurred.jpg"));
|
||||||
|
small_image
|
||||||
|
.blur(27.5)
|
||||||
|
.save(&blurred_out_path)
|
||||||
|
.with_context(|| format!("Could not save image to {blurred_out_path:?}"))?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -152,9 +167,9 @@ pub fn run() -> Result<(), Error> {
|
||||||
{
|
{
|
||||||
println!("Encoded {} images", jacket_vectors.len());
|
println!("Encoded {} images", jacket_vectors.len());
|
||||||
let bytes = postcard::to_allocvec(&jacket_vectors)
|
let bytes = postcard::to_allocvec(&jacket_vectors)
|
||||||
.with_context(|| format!("Coult not encode jacket matrix"))?;
|
.with_context(|| "Coult not encode jacket matrix")?;
|
||||||
fs::write(songs_dir.join("recognition_matrix"), bytes)
|
fs::write(songs_dir.join("recognition_matrix"), bytes)
|
||||||
.with_context(|| format!("Could not write jacket matrix"))?;
|
.with_context(|| "Could not write jacket matrix")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -21,6 +21,7 @@ async fn main() {
|
||||||
commands::score::score(),
|
commands::score::score(),
|
||||||
commands::stats::stats(),
|
commands::stats::stats(),
|
||||||
commands::chart::chart(),
|
commands::chart::chart(),
|
||||||
|
commands::calc::calc(),
|
||||||
],
|
],
|
||||||
prefix_options: poise::PrefixFrameworkOptions {
|
prefix_options: poise::PrefixFrameworkOptions {
|
||||||
stripped_dynamic_prefix: Some(|_ctx, message, _user_ctx| {
|
stripped_dynamic_prefix: Some(|_ctx, message, _user_ctx| {
|
||||||
|
|
|
@ -21,7 +21,7 @@ async fn main() -> Result<(), Error> {
|
||||||
ipc.connect().map_err(|e| anyhow!("{}", e))?;
|
ipc.connect().map_err(|e| anyhow!("{}", e))?;
|
||||||
|
|
||||||
println!("Starting presence loop...");
|
println!("Starting presence loop...");
|
||||||
for i in 0.. {
|
loop {
|
||||||
println!("Getting most recent score...");
|
println!("Getting most recent score...");
|
||||||
let res = reqwest::get(format!("{}/plays/latest", server_url)).await;
|
let res = reqwest::get(format!("{}/plays/latest", server_url)).await;
|
||||||
|
|
||||||
|
@ -42,7 +42,6 @@ async fn main() -> Result<(), Error> {
|
||||||
"{}/jackets/by_chart_id/{}.png",
|
"{}/jackets/by_chart_id/{}.png",
|
||||||
server_url, &triplet.chart.id
|
server_url, &triplet.chart.id
|
||||||
);
|
);
|
||||||
let jacket_url = "https://static.wikia.nocookie.net/iowiro/images/c/c2/Fracture_Ray.jpg/revision/latest?cb=20230928061927";
|
|
||||||
println!("Jacket url: {}", jacket_url);
|
println!("Jacket url: {}", jacket_url);
|
||||||
|
|
||||||
let jacket_text = format!("{} — {}", &triplet.song.title, &triplet.song.artist);
|
let jacket_text = format!("{} — {}", &triplet.song.title, &triplet.song.artist);
|
||||||
|
@ -68,6 +67,4 @@ async fn main() -> Result<(), Error> {
|
||||||
ipc.set_activity(activity).map_err(|e| anyhow!("{}", e))?;
|
ipc.set_activity(activity).map_err(|e| anyhow!("{}", e))?;
|
||||||
tokio::time::sleep(Duration::from_secs(30)).await;
|
tokio::time::sleep(Duration::from_secs(30)).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
184
src/commands/calc.rs
Normal file
184
src/commands/calc.rs
Normal file
|
@ -0,0 +1,184 @@
|
||||||
|
// {{{ Imports
|
||||||
|
use num::{FromPrimitive, Rational32};
|
||||||
|
|
||||||
|
use crate::arcaea::play::{compute_b30_ptt, get_best_plays};
|
||||||
|
use crate::arcaea::rating::{rating_as_float, rating_from_fixed, Rating};
|
||||||
|
use crate::context::{Context, Error, TaggedError};
|
||||||
|
use crate::recognition::fuzzy_song_name::guess_song_and_chart;
|
||||||
|
use crate::user::User;
|
||||||
|
|
||||||
|
use crate::arcaea::score::{Score, ScoringSystem};
|
||||||
|
|
||||||
|
use super::discord::MessageContext;
|
||||||
|
// }}}
|
||||||
|
|
||||||
|
// {{{ Top command
|
||||||
|
/// Compute different things
|
||||||
|
#[poise::command(
|
||||||
|
prefix_command,
|
||||||
|
slash_command,
|
||||||
|
subcommands("expected", "rating"),
|
||||||
|
subcommand_required
|
||||||
|
)]
|
||||||
|
pub async fn calc(_ctx: Context<'_>) -> Result<(), Error> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
// {{{ Expected
|
||||||
|
// {{{ Implementation
|
||||||
|
async fn expected_impl(
|
||||||
|
ctx: &mut impl MessageContext,
|
||||||
|
ptt: Option<Rational32>,
|
||||||
|
name: &str,
|
||||||
|
) -> Result<Score, TaggedError> {
|
||||||
|
let (song, chart) = guess_song_and_chart(ctx.data(), name)?;
|
||||||
|
|
||||||
|
let ptt = if let Some(ptt) = ptt {
|
||||||
|
ptt
|
||||||
|
} else {
|
||||||
|
let user = User::from_context(ctx)?;
|
||||||
|
compute_b30_ptt(
|
||||||
|
ScoringSystem::Standard,
|
||||||
|
&get_best_plays(ctx.data(), user.id, ScoringSystem::Standard, 30, 30, None)?,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let cc = rating_from_fixed(chart.chart_constant as i32);
|
||||||
|
|
||||||
|
let score = if ptt >= cc + 2 {
|
||||||
|
Rational32::from_integer(chart.note_count as i32 + 10_000_000)
|
||||||
|
} else if ptt >= cc + 1 {
|
||||||
|
Rational32::from_integer(9_800_000)
|
||||||
|
+ (ptt - cc - 1).reduced() * Rational32::from_integer(200_000)
|
||||||
|
} else {
|
||||||
|
Rational32::from_integer(9_500_000)
|
||||||
|
+ (ptt - cc).reduced() * Rational32::from_integer(300_000)
|
||||||
|
};
|
||||||
|
let score = Score(score.to_integer().max(0) as u32);
|
||||||
|
|
||||||
|
ctx.reply(&format!(
|
||||||
|
"The expected score for a player of potential {:.2} on {} [{}] is {}",
|
||||||
|
rating_as_float(ptt),
|
||||||
|
song,
|
||||||
|
chart.difficulty,
|
||||||
|
score
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(score)
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
// {{{ Tests
|
||||||
|
#[cfg(test)]
|
||||||
|
mod expected_tests {
|
||||||
|
use crate::{
|
||||||
|
commands::discord::mock::MockContext, context::testing::get_mock_context, golden_test,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn consistent_with_rating() -> Result<(), Error> {
|
||||||
|
let (mut ctx, _guard) = get_mock_context().await?;
|
||||||
|
ctx.save_messages = false; // We don't want to waste time writing to a vec
|
||||||
|
|
||||||
|
for i in 0..1_000 {
|
||||||
|
let score = Score(i * 10_000);
|
||||||
|
let rating = score.play_rating(1140);
|
||||||
|
let res = expected_impl(&mut ctx, Some(rating), "Pentiment [BYD]")
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.error)?;
|
||||||
|
assert_eq!(
|
||||||
|
score, res,
|
||||||
|
"Wrong expected score for starting score {score} and rating {rating}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
golden_test!(basic_usage, "commands/calc/expected/basic_usage");
|
||||||
|
async fn basic_usage(ctx: &mut MockContext) -> Result<(), TaggedError> {
|
||||||
|
expected_impl(
|
||||||
|
ctx,
|
||||||
|
Some(Rational32::from_f32(12.27).unwrap()),
|
||||||
|
"Vicious anti heorism",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
// {{{ Discord wrapper
|
||||||
|
/// Computes the expected score for a player of some potential on a given chart.
|
||||||
|
#[poise::command(prefix_command, slash_command, user_cooldown = 1)]
|
||||||
|
async fn expected(
|
||||||
|
mut ctx: Context<'_>,
|
||||||
|
#[description = "The potential to compute the expected score for"] ptt: Option<f32>,
|
||||||
|
#[rest]
|
||||||
|
#[description = "Name of chart (difficulty at the end)"]
|
||||||
|
name: String,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let res = expected_impl(&mut ctx, ptt.and_then(Rational32::from_f32), &name).await;
|
||||||
|
ctx.handle_error(res).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
// }}}
|
||||||
|
// {{{ Rating
|
||||||
|
// {{{ Implementation
|
||||||
|
async fn rating_impl(
|
||||||
|
ctx: &mut impl MessageContext,
|
||||||
|
score: Score,
|
||||||
|
name: &str,
|
||||||
|
) -> Result<Rating, TaggedError> {
|
||||||
|
let (song, chart) = guess_song_and_chart(ctx.data(), name)?;
|
||||||
|
|
||||||
|
let rating = score.play_rating(chart.chart_constant);
|
||||||
|
|
||||||
|
ctx.reply(&format!(
|
||||||
|
"The score {} on {} [{}] yields a rating of {:.2}",
|
||||||
|
score,
|
||||||
|
song,
|
||||||
|
chart.difficulty,
|
||||||
|
rating_as_float(rating),
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(rating)
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
// {{{ Tests
|
||||||
|
#[cfg(test)]
|
||||||
|
mod rating_tests {
|
||||||
|
use crate::{commands::discord::mock::MockContext, golden_test};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
golden_test!(basic_usage, "commands/calc/rating/basic_usage");
|
||||||
|
async fn basic_usage(ctx: &mut MockContext) -> Result<(), TaggedError> {
|
||||||
|
rating_impl(ctx, Score(9_349_070), "Arcana Eden [PRS]").await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
// {{{ Discord wrapper
|
||||||
|
/// Computes the rating (potential) of a play on a given chart.
|
||||||
|
#[poise::command(prefix_command, slash_command, user_cooldown = 1)]
|
||||||
|
async fn rating(
|
||||||
|
mut ctx: Context<'_>,
|
||||||
|
score: u32,
|
||||||
|
#[rest]
|
||||||
|
#[description = "Name of chart (difficulty at the end)"]
|
||||||
|
name: String,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let res = rating_impl(&mut ctx, Score(score), &name).await;
|
||||||
|
ctx.handle_error(res).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
// }}}
|
||||||
|
// }}}
|
|
@ -1,5 +1,5 @@
|
||||||
use anyhow::anyhow;
|
|
||||||
// {{{ Imports
|
// {{{ Imports
|
||||||
|
use anyhow::anyhow;
|
||||||
use poise::serenity_prelude::{CreateAttachment, CreateEmbed};
|
use poise::serenity_prelude::{CreateAttachment, CreateEmbed};
|
||||||
|
|
||||||
use crate::arcaea::{chart::Side, play::Play};
|
use crate::arcaea::{chart::Side, play::Play};
|
||||||
|
@ -138,7 +138,7 @@ mod info_tests {
|
||||||
async fn info(
|
async fn info(
|
||||||
mut ctx: Context<'_>,
|
mut ctx: Context<'_>,
|
||||||
#[rest]
|
#[rest]
|
||||||
#[description = "Name of chart to show (difficulty at the end)"]
|
#[description = "Name of chart (difficulty at the end)"]
|
||||||
name: String,
|
name: String,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let res = info_impl(&mut ctx, &name).await;
|
let res = info_impl(&mut ctx, &name).await;
|
||||||
|
@ -251,7 +251,7 @@ mod best_tests {
|
||||||
async fn best(
|
async fn best(
|
||||||
mut ctx: Context<'_>,
|
mut ctx: Context<'_>,
|
||||||
#[rest]
|
#[rest]
|
||||||
#[description = "Name of chart to show (difficulty at the end)"]
|
#[description = "Name of chart (difficulty at the end)"]
|
||||||
name: String,
|
name: String,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let res = best_impl(&mut ctx, &name).await;
|
let res = best_impl(&mut ctx, &name).await;
|
||||||
|
@ -403,7 +403,7 @@ async fn plot(
|
||||||
mut ctx: Context<'_>,
|
mut ctx: Context<'_>,
|
||||||
scoring_system: Option<ScoringSystem>,
|
scoring_system: Option<ScoringSystem>,
|
||||||
#[rest]
|
#[rest]
|
||||||
#[description = "Name of chart to show (difficulty at the end)"]
|
#[description = "Name of chart (difficulty at the end)"]
|
||||||
name: String,
|
name: String,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let res = plot_impl(&mut ctx, scoring_system, name).await;
|
let res = plot_impl(&mut ctx, scoring_system, name).await;
|
||||||
|
|
|
@ -199,6 +199,9 @@ pub mod mock {
|
||||||
pub user_id: u64,
|
pub user_id: u64,
|
||||||
pub data: UserContext,
|
pub data: UserContext,
|
||||||
|
|
||||||
|
/// If true, messages will be saved in a vec.
|
||||||
|
pub save_messages: bool,
|
||||||
|
|
||||||
messages: Vec<ReplyEssence>,
|
messages: Vec<ReplyEssence>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -207,6 +210,7 @@ pub mod mock {
|
||||||
Self {
|
Self {
|
||||||
data,
|
data,
|
||||||
user_id: 666,
|
user_id: 666,
|
||||||
|
save_messages: true,
|
||||||
messages: vec![],
|
messages: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -274,7 +278,10 @@ 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::from_reply(message));
|
if self.save_messages {
|
||||||
|
self.messages.push(ReplyEssence::from_reply(message));
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ pub mod discord;
|
||||||
pub mod score;
|
pub mod score;
|
||||||
pub mod stats;
|
pub mod stats;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
pub mod calc;
|
||||||
|
|
||||||
// {{{ Help
|
// {{{ Help
|
||||||
/// Show this help menu
|
/// Show this help menu
|
||||||
|
|
|
@ -141,6 +141,10 @@ impl UserContext {
|
||||||
// {{{ Testing helpers
|
// {{{ Testing helpers
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub mod testing {
|
pub mod testing {
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
use crate::commands::discord::mock::MockContext;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
pub async fn get_shared_context() -> &'static UserContext {
|
pub async fn get_shared_context() -> &'static UserContext {
|
||||||
|
@ -165,6 +169,16 @@ pub mod testing {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_mock_context() -> Result<(MockContext, TempDir), Error> {
|
||||||
|
let mut data = (*get_shared_context().await).clone();
|
||||||
|
let dir = tempfile::tempdir()?;
|
||||||
|
data.db = connect_db(dir.path());
|
||||||
|
import_songs_and_jackets_from(dir.path());
|
||||||
|
|
||||||
|
let ctx = MockContext::new(data);
|
||||||
|
Ok((ctx, dir))
|
||||||
|
}
|
||||||
|
|
||||||
// rustfmt fucks up the formatting here,
|
// rustfmt fucks up the formatting here,
|
||||||
// but the skip attribute doesn't seem to work well on macros 🤔
|
// but the skip attribute doesn't seem to work well on macros 🤔
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
|
@ -184,12 +198,7 @@ pub mod testing {
|
||||||
($test_path:expr, $f:expr) => {{
|
($test_path:expr, $f:expr) => {{
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
let mut data = (*$crate::context::testing::get_shared_context().await).clone();
|
let (mut ctx, _guard) = $crate::context::testing::get_mock_context().await?;
|
||||||
let dir = tempfile::tempdir()?;
|
|
||||||
data.db = $crate::context::connect_db(dir.path());
|
|
||||||
$crate::context::testing::import_songs_and_jackets_from(dir.path());
|
|
||||||
|
|
||||||
let mut ctx = $crate::commands::discord::mock::MockContext::new(data);
|
|
||||||
let res = $crate::user::User::create_from_context(&ctx);
|
let res = $crate::user::User::create_from_context(&ctx);
|
||||||
ctx.handle_error(res).await?;
|
ctx.handle_error(res).await?;
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
#![allow(async_fn_in_trait)]
|
#![allow(async_fn_in_trait)]
|
||||||
#![allow(clippy::needless_range_loop)]
|
#![allow(clippy::needless_range_loop)]
|
||||||
#![allow(clippy::redundant_closure)]
|
#![allow(clippy::redundant_closure)]
|
||||||
|
// This sometimes triggers for rationals, where it doesn't make sense
|
||||||
|
#![allow(clippy::int_plus_one)]
|
||||||
|
|
||||||
pub mod arcaea;
|
pub mod arcaea;
|
||||||
pub mod assets;
|
pub mod assets;
|
||||||
|
|
|
@ -32,22 +32,23 @@ pub fn guess_song_and_chart<'a>(
|
||||||
ctx: &'a UserContext,
|
ctx: &'a UserContext,
|
||||||
name: &'a str,
|
name: &'a str,
|
||||||
) -> Result<(&'a Song, &'a Chart), Error> {
|
) -> Result<(&'a Song, &'a Chart), Error> {
|
||||||
let name = name.trim();
|
let mut name = name.trim();
|
||||||
let (name, difficulty) = name
|
let mut inferred_difficulty = None;
|
||||||
.strip_suffix("PST")
|
|
||||||
.zip(Some(Difficulty::PST))
|
|
||||||
.or_else(|| strip_case_insensitive_suffix(name, "[PST]").zip(Some(Difficulty::PST)))
|
|
||||||
.or_else(|| strip_case_insensitive_suffix(name, "PRS").zip(Some(Difficulty::PRS)))
|
|
||||||
.or_else(|| strip_case_insensitive_suffix(name, "[PRS]").zip(Some(Difficulty::PRS)))
|
|
||||||
.or_else(|| strip_case_insensitive_suffix(name, "FTR").zip(Some(Difficulty::FTR)))
|
|
||||||
.or_else(|| strip_case_insensitive_suffix(name, "[FTR]").zip(Some(Difficulty::FTR)))
|
|
||||||
.or_else(|| strip_case_insensitive_suffix(name, "ETR").zip(Some(Difficulty::ETR)))
|
|
||||||
.or_else(|| strip_case_insensitive_suffix(name, "[ETR]").zip(Some(Difficulty::ETR)))
|
|
||||||
.or_else(|| strip_case_insensitive_suffix(name, "BYD").zip(Some(Difficulty::BYD)))
|
|
||||||
.or_else(|| strip_case_insensitive_suffix(name, "[BYD]").zip(Some(Difficulty::BYD)))
|
|
||||||
.unwrap_or((name, Difficulty::FTR));
|
|
||||||
|
|
||||||
guess_chart_name(name, &ctx.song_cache, Some(difficulty), true)
|
for difficulty in Difficulty::DIFFICULTIES {
|
||||||
|
for shorthand in [
|
||||||
|
Difficulty::DIFFICULTY_SHORTHANDS[difficulty.to_index()],
|
||||||
|
Difficulty::DIFFICULTY_SHORTHANDS_IN_BRACKETS[difficulty.to_index()],
|
||||||
|
] {
|
||||||
|
if let Some(stripped) = strip_case_insensitive_suffix(name, shorthand) {
|
||||||
|
inferred_difficulty = Some(difficulty);
|
||||||
|
name = stripped;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guess_chart_name(name, &ctx.song_cache, inferred_difficulty, true)
|
||||||
}
|
}
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Guess chart by name
|
// {{{ Guess chart by name
|
||||||
|
@ -74,11 +75,20 @@ pub fn guess_chart_name<'a>(
|
||||||
let mut close_enough: Vec<_> = cache
|
let mut close_enough: Vec<_> = cache
|
||||||
.charts()
|
.charts()
|
||||||
.filter_map(|chart| {
|
.filter_map(|chart| {
|
||||||
if difficulty.map_or(false, |d| d != chart.difficulty) {
|
let cached_song = &cache.lookup_song(chart.song_id).ok()?;
|
||||||
|
let song = &cached_song.song;
|
||||||
|
let plausible_difficulty = match difficulty {
|
||||||
|
Some(difficulty) => difficulty == chart.difficulty,
|
||||||
|
None => {
|
||||||
|
let chart_count = cached_song.charts().count();
|
||||||
|
chart_count == 1 || chart.difficulty == Difficulty::FTR
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if !plausible_difficulty {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let song = &cache.lookup_song(chart.song_id).ok()?.song;
|
|
||||||
let song_title = &song.lowercase_title;
|
let song_title = &song.lowercase_title;
|
||||||
distance_vec.clear();
|
distance_vec.clear();
|
||||||
|
|
||||||
|
|
4
test/commands/calc/expected/basic_usage/0.toml
Normal file
4
test/commands/calc/expected/basic_usage/0.toml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
reply = true
|
||||||
|
content = "The expected score for a player of potential 12.27 on Vicious [ANTi] Heroism [BYD] is 9'834'000"
|
||||||
|
embeds = []
|
||||||
|
attachments = []
|
4
test/commands/calc/rating/basic_usage/0.toml
Normal file
4
test/commands/calc/rating/basic_usage/0.toml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
reply = true
|
||||||
|
content = "The score 9'349'070 on Arcana Eden [PRS] yields a rating of 8.20"
|
||||||
|
embeds = []
|
||||||
|
attachments = []
|
52
test/commands/commands/chart/info/no_suffix/0.toml
Normal file
52
test/commands/commands/chart/info/no_suffix/0.toml
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
reply = true
|
||||||
|
|
||||||
|
[[embeds]]
|
||||||
|
title = "Pentiment [FTR 10]"
|
||||||
|
type = "rich"
|
||||||
|
|
||||||
|
[embeds.thumbnail]
|
||||||
|
url = "attachment://chart.png"
|
||||||
|
|
||||||
|
[[embeds.fields]]
|
||||||
|
name = "Note count"
|
||||||
|
value = "1345"
|
||||||
|
inline = true
|
||||||
|
|
||||||
|
[[embeds.fields]]
|
||||||
|
name = "Chart constant"
|
||||||
|
value = "10.3"
|
||||||
|
inline = true
|
||||||
|
|
||||||
|
[[embeds.fields]]
|
||||||
|
name = "Total plays"
|
||||||
|
value = "0"
|
||||||
|
inline = true
|
||||||
|
|
||||||
|
[[embeds.fields]]
|
||||||
|
name = "BPM"
|
||||||
|
value = "200-222"
|
||||||
|
inline = true
|
||||||
|
|
||||||
|
[[embeds.fields]]
|
||||||
|
name = "Side"
|
||||||
|
value = "conflict"
|
||||||
|
inline = true
|
||||||
|
|
||||||
|
[[embeds.fields]]
|
||||||
|
name = "Artist"
|
||||||
|
value = "Pentiment"
|
||||||
|
inline = true
|
||||||
|
|
||||||
|
[[embeds.fields]]
|
||||||
|
name = "Note design"
|
||||||
|
value = "Paradox Blight"
|
||||||
|
inline = true
|
||||||
|
|
||||||
|
[[embeds.fields]]
|
||||||
|
name = "Pack"
|
||||||
|
value = "Final Verdict"
|
||||||
|
inline = true
|
||||||
|
|
||||||
|
[[attachments]]
|
||||||
|
filename = "chart.png"
|
||||||
|
hash = "sha256_5ffda660ce1c6ddd7c60bbb7f34443a7772e608f930768a147e423cc62b7e25d"
|
52
test/commands/commands/chart/info/specify_difficulty/0.toml
Normal file
52
test/commands/commands/chart/info/specify_difficulty/0.toml
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
reply = true
|
||||||
|
|
||||||
|
[[embeds]]
|
||||||
|
title = "HELLOHELL [ETR 9]"
|
||||||
|
type = "rich"
|
||||||
|
|
||||||
|
[embeds.thumbnail]
|
||||||
|
url = "attachment://chart.png"
|
||||||
|
|
||||||
|
[[embeds.fields]]
|
||||||
|
name = "Note count"
|
||||||
|
value = "770"
|
||||||
|
inline = true
|
||||||
|
|
||||||
|
[[embeds.fields]]
|
||||||
|
name = "Chart constant"
|
||||||
|
value = "9.4"
|
||||||
|
inline = true
|
||||||
|
|
||||||
|
[[embeds.fields]]
|
||||||
|
name = "Total plays"
|
||||||
|
value = "0"
|
||||||
|
inline = true
|
||||||
|
|
||||||
|
[[embeds.fields]]
|
||||||
|
name = "BPM"
|
||||||
|
value = "155"
|
||||||
|
inline = true
|
||||||
|
|
||||||
|
[[embeds.fields]]
|
||||||
|
name = "Side"
|
||||||
|
value = "conflict"
|
||||||
|
inline = true
|
||||||
|
|
||||||
|
[[embeds.fields]]
|
||||||
|
name = "Artist"
|
||||||
|
value = "HELLOHELL"
|
||||||
|
inline = true
|
||||||
|
|
||||||
|
[[embeds.fields]]
|
||||||
|
name = "Note design"
|
||||||
|
value = "eién"
|
||||||
|
inline = true
|
||||||
|
|
||||||
|
[[embeds.fields]]
|
||||||
|
name = "Pack"
|
||||||
|
value = "World Extend 3: Illusions"
|
||||||
|
inline = true
|
||||||
|
|
||||||
|
[[attachments]]
|
||||||
|
filename = "chart.png"
|
||||||
|
hash = "sha256_e00a92ba1abbcf97c7b006447867914b9d95dc4dfb039e05260d71128b60eedb"
|
57
test/score/magic/single_pic/0.toml
Normal file
57
test/score/magic/single_pic/0.toml
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
reply = true
|
||||||
|
|
||||||
|
[[embeds]]
|
||||||
|
title = "ALTER EGO [FTR 10]"
|
||||||
|
type = "rich"
|
||||||
|
|
||||||
|
[embeds.thumbnail]
|
||||||
|
url = "attachment://416-9926250-0.png"
|
||||||
|
|
||||||
|
[[embeds.fields]]
|
||||||
|
name = "Score"
|
||||||
|
value = "9'926'250"
|
||||||
|
inline = true
|
||||||
|
|
||||||
|
[[embeds.fields]]
|
||||||
|
name = "Rating"
|
||||||
|
value = "12.13"
|
||||||
|
inline = true
|
||||||
|
|
||||||
|
[[embeds.fields]]
|
||||||
|
name = "Grade"
|
||||||
|
value = "EX+"
|
||||||
|
inline = true
|
||||||
|
|
||||||
|
[[embeds.fields]]
|
||||||
|
name = "ξ-Score"
|
||||||
|
value = "9'693'042"
|
||||||
|
inline = true
|
||||||
|
|
||||||
|
[[embeds.fields]]
|
||||||
|
name = "ξ-Rating"
|
||||||
|
value = "11.14"
|
||||||
|
inline = true
|
||||||
|
|
||||||
|
[[embeds.fields]]
|
||||||
|
name = "ξ-Grade"
|
||||||
|
value = "AA"
|
||||||
|
inline = true
|
||||||
|
|
||||||
|
[[embeds.fields]]
|
||||||
|
name = "Status"
|
||||||
|
value = "C (-164/-12/-5)"
|
||||||
|
inline = true
|
||||||
|
|
||||||
|
[[embeds.fields]]
|
||||||
|
name = "Max recall"
|
||||||
|
value = "397"
|
||||||
|
inline = true
|
||||||
|
|
||||||
|
[[embeds.fields]]
|
||||||
|
name = "ID"
|
||||||
|
value = "1"
|
||||||
|
inline = true
|
||||||
|
|
||||||
|
[[attachments]]
|
||||||
|
filename = "416-9926250-0.png"
|
||||||
|
hash = "sha256_5f1febcdf44bc22bf7ef5bff0cee197a65a221277ebe9615d469002b4324ada3"
|
113
test/score/magic/weird_kerning/0.toml
Normal file
113
test/score/magic/weird_kerning/0.toml
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
reply = true
|
||||||
|
|
||||||
|
[[embeds]]
|
||||||
|
title = "Antithese [FTR 8+]"
|
||||||
|
type = "rich"
|
||||||
|
|
||||||
|
[embeds.thumbnail]
|
||||||
|
url = "attachment://116-9983744-0.png"
|
||||||
|
|
||||||
|
[[embeds.fields]]
|
||||||
|
name = "Score"
|
||||||
|
value = "9'983'744"
|
||||||
|
inline = true
|
||||||
|
|
||||||
|
[[embeds.fields]]
|
||||||
|
name = "Rating"
|
||||||
|
value = "10.72"
|
||||||
|
inline = true
|
||||||
|
|
||||||
|
[[embeds.fields]]
|
||||||
|
name = "Grade"
|
||||||
|
value = "EX+"
|
||||||
|
inline = true
|
||||||
|
|
||||||
|
[[embeds.fields]]
|
||||||
|
name = "ξ-Score"
|
||||||
|
value = "9'920'182"
|
||||||
|
inline = true
|
||||||
|
|
||||||
|
[[embeds.fields]]
|
||||||
|
name = "ξ-Rating"
|
||||||
|
value = "10.40"
|
||||||
|
inline = true
|
||||||
|
|
||||||
|
[[embeds.fields]]
|
||||||
|
name = "ξ-Grade"
|
||||||
|
value = "EX+"
|
||||||
|
inline = true
|
||||||
|
|
||||||
|
[[embeds.fields]]
|
||||||
|
name = "Status"
|
||||||
|
value = "C (-27/-1/-1)"
|
||||||
|
inline = true
|
||||||
|
|
||||||
|
[[embeds.fields]]
|
||||||
|
name = "Max recall"
|
||||||
|
value = "479"
|
||||||
|
inline = true
|
||||||
|
|
||||||
|
[[embeds.fields]]
|
||||||
|
name = "ID"
|
||||||
|
value = "1"
|
||||||
|
inline = true
|
||||||
|
|
||||||
|
[[embeds]]
|
||||||
|
title = "GENOCIDER [FTR 10+]"
|
||||||
|
type = "rich"
|
||||||
|
|
||||||
|
[embeds.thumbnail]
|
||||||
|
url = "attachment://243-9724775-1.png"
|
||||||
|
|
||||||
|
[[embeds.fields]]
|
||||||
|
name = "Score"
|
||||||
|
value = "9'724'775"
|
||||||
|
inline = true
|
||||||
|
|
||||||
|
[[embeds.fields]]
|
||||||
|
name = "Rating"
|
||||||
|
value = "11.45"
|
||||||
|
inline = true
|
||||||
|
|
||||||
|
[[embeds.fields]]
|
||||||
|
name = "Grade"
|
||||||
|
value = "AA"
|
||||||
|
inline = true
|
||||||
|
|
||||||
|
[[embeds.fields]]
|
||||||
|
name = "ξ-Score"
|
||||||
|
value = "9'453'809"
|
||||||
|
inline = true
|
||||||
|
|
||||||
|
[[embeds.fields]]
|
||||||
|
name = "ξ-Rating"
|
||||||
|
value = "10.55"
|
||||||
|
inline = true
|
||||||
|
|
||||||
|
[[embeds.fields]]
|
||||||
|
name = "ξ-Grade"
|
||||||
|
value = "A"
|
||||||
|
inline = true
|
||||||
|
|
||||||
|
[[embeds.fields]]
|
||||||
|
name = "Status"
|
||||||
|
value = "C (-180/-40/-21)"
|
||||||
|
inline = true
|
||||||
|
|
||||||
|
[[embeds.fields]]
|
||||||
|
name = "Max recall"
|
||||||
|
value = "347"
|
||||||
|
inline = true
|
||||||
|
|
||||||
|
[[embeds.fields]]
|
||||||
|
name = "ID"
|
||||||
|
value = "2"
|
||||||
|
inline = true
|
||||||
|
|
||||||
|
[[attachments]]
|
||||||
|
filename = "116-9983744-0.png"
|
||||||
|
hash = "sha256_75b03ac3392d4bcb9d377396a36708aea1298dd463c08d5d62ca1e1414bfeaef"
|
||||||
|
|
||||||
|
[[attachments]]
|
||||||
|
filename = "243-9724775-1.png"
|
||||||
|
hash = "sha256_4a8ad7af5482104b3ec7c9a50091bb6f01df10a0cc758837cf47dcf7d73fd59d"
|
Loading…
Reference in a new issue