Add map_age display in ranks and user card

This commit is contained in:
Natsu Kagami 2024-08-04 22:16:19 +02:00 committed by Natsu Kagami
parent 4909f6ea27
commit 8e90006eb9
4 changed files with 151 additions and 55 deletions

View file

@ -19,6 +19,7 @@ use youmubot_prelude::announcer::CacheAndHttp;
use youmubot_prelude::stream::TryStreamExt;
use youmubot_prelude::*;
use crate::discord::calculate_weighted_map_age;
use crate::discord::db::OsuUserMode;
use crate::{
discord::cache::save_beatmap,
@ -114,7 +115,10 @@ impl Announcer {
.await
.pls_ok()
.unwrap_or(0.0),
map_age: 0, // soon
map_age: calculate_weighted_map_age(&top, &env.beatmaps, mode)
.await
.pls_ok()
.unwrap_or(0),
last_update: now,
};
let last = user.modes.insert(mode, stats);

View file

@ -457,8 +457,28 @@ impl<'a> ScoreEmbedBuilder<'a> {
pub(crate) fn user_embed(
u: User,
map_length: f64,
map_age: i64,
best: Option<(Score, BeatmapWithMode, BeatmapInfo)>,
) -> CreateEmbed {
let mut stats = Vec::<(&'static str, String, bool)>::new();
if map_length > 0.0 {
stats.push((
"Weighted Map Length",
{
let secs = map_length.floor() as u64;
let minutes = secs / 60;
let seconds = map_length - (60 * minutes) as f64;
format!(
"**{}**mins **{:05.2}**s (**{:.2}**s)",
minutes, seconds, map_length
)
},
true,
))
}
if map_age > 0 {
stats.push(("Weighted Map Age", format!("<t:{}:F>", map_age), true))
}
CreateEmbed::new()
.title(MessageBuilder::new().push_safe(u.username).build())
.url(format!("https://osu.ppy.sh/users/{}", u.id))
@ -504,19 +524,7 @@ pub(crate) fn user_embed(
),
false,
)
.field(
"Weighted Map Length",
{
let secs = map_length.floor() as u64;
let minutes = secs / 60;
let seconds = map_length - (60 * minutes) as f64;
format!(
"**{}** minutes **{:05.2}** seconds (**{:.2}**s)",
minutes, seconds, map_length
)
},
false,
)
.fields(stats)
.field(
format!("Level {:.0}", u.level),
format!(

View file

@ -1,6 +1,7 @@
use std::{borrow::Borrow, collections::HashMap as Map, str::FromStr, sync::Arc};
use chrono::Utc;
use future::try_join;
use futures_util::join;
use interaction::{beatmap_components, score_components};
use rand::seq::IteratorRandom;
@ -19,6 +20,7 @@ use db::{OsuLastBeatmap, OsuSavedUsers, OsuUser, OsuUserMode};
use embeds::{beatmap_embed, score_embed, user_embed};
pub use hook::{dot_osu_hook, hook, score_hook};
use server_rank::{SERVER_RANK_COMMAND, SHOW_LEADERBOARD_COMMAND};
use stream::FuturesOrdered;
use youmubot_prelude::announcer::AnnouncerHandler;
use youmubot_prelude::{stream::FuturesUnordered, *};
@ -378,7 +380,7 @@ async fn add_user(target: serenity::model::id::UserId, user: User, env: &OsuEnv)
.unwrap_or(None)
.and_then(|u| u.pp)
};
let map_length = async {
let map_length_age = async {
let scores = env
.client
.user_best(UserID::ID(user.id), |f| f.mode(mode).limit(100))
@ -386,22 +388,29 @@ async fn add_user(target: serenity::model::id::UserId, user: User, env: &OsuEnv)
.pls_ok()
.unwrap_or_else(|| vec![]);
calculate_weighted_map_length(&scores, &env.beatmaps, mode)
.await
.pls_ok()
};
let (pp, map_length) = join!(pp, map_length);
pp.zip(map_length).map(|(pp, map_length)| {
(
mode,
OsuUserMode {
pp,
map_length,
map_age: 0, // TODO
last_update: Utc::now(),
},
calculate_weighted_map_length(&scores, &env.beatmaps, mode)
.await
.pls_ok(),
calculate_weighted_map_age(&scores, &env.beatmaps, mode)
.await
.pls_ok(),
)
})
};
let (pp, (map_length, map_age)) = join!(pp, map_length_age);
pp.zip(map_length)
.zip(map_age)
.map(|((pp, map_length), map_age)| {
(
mode,
OsuUserMode {
pp,
map_length,
map_age,
last_update: Utc::now(),
},
)
})
})
.collect::<stream::FuturesOrdered<_>>()
.filter_map(|v| future::ready(v))
@ -837,7 +846,9 @@ async fn get_user(
let bests = osu_client
.user_best(UserID::ID(u.id), |f| f.limit(100).mode(mode))
.await?;
let map_length = calculate_weighted_map_length(&bests, meta_cache, mode).await?;
let map_length = calculate_weighted_map_length(&bests, meta_cache, mode);
let map_age = calculate_weighted_map_age(&bests, meta_cache, mode);
let (map_length, map_age) = try_join(map_length, map_age).await?;
let best = match bests.into_iter().next() {
Some(m) => {
let beatmap = meta_cache.get_beatmap(m.beatmap_id, mode).await?;
@ -858,7 +869,7 @@ async fn get_user(
"{}: here is the user that you requested",
msg.author
))
.embed(user_embed(u, map_length, best)),
.embed(user_embed(u, map_length, map_age, best)),
)
.await?;
}
@ -891,3 +902,36 @@ pub(in crate::discord) async fn calculate_weighted_map_length(
.try_fold(0.0, |a, b| future::ready(Ok(a + b)))
.await
}
pub(in crate::discord) async fn calculate_weighted_map_age(
from_scores: impl IntoIterator<Item = &Score>,
cache: &BeatmapMetaCache,
mode: Mode,
) -> Result<i64> {
const SCALING_FACTOR: f64 = 0.95;
let scales = (0..100)
.scan(1.0, |a, _| Some(*a * SCALING_FACTOR))
.collect::<Vec<_>>();
let scores = from_scores
.into_iter()
.map(|s| async move {
let beatmap = cache.get_beatmap(s.beatmap_id, mode).await?;
Ok(
if let crate::ApprovalStatus::Ranked(at) = beatmap.approval {
at.timestamp() as f64
} else {
0.0
},
) as Result<_>
})
.collect::<FuturesOrdered<_>>()
.try_collect::<Vec<_>>()
.await?;
Ok((scores
.iter()
.zip(scales.iter())
.map(|(a, b)| a * b)
.sum::<f64>()
/ scales.iter().take(scores.len()).sum::<f64>())
.floor() as i64)
}

View file

@ -1,5 +1,6 @@
use std::{borrow::Cow, collections::HashMap, str::FromStr, sync::Arc};
use chrono::DateTime;
use pagination::paginate_with_first_message;
use serenity::{
all::{GuildId, Member},
@ -31,17 +32,20 @@ enum RankQuery {
PP,
TotalPP,
MapLength,
// MapAge,
MapAge {
newest_first: bool,
},
}
impl RankQuery {
// fn col_name(&self) -> &'static str {
// match self {
// RankQuery::PP => "pp",
// RankQuery::TotalPP => "Total pp",
// RankQuery::MapLength => "Map length",
// }
// }
fn col_name(&self) -> &'static str {
match self {
RankQuery::PP => "pp",
RankQuery::TotalPP => "Total pp",
RankQuery::MapLength => "Map length",
RankQuery::MapAge { newest_first: _ } => "Map age",
}
}
fn extract_row(&self, mode: Mode, ou: &OsuUser) -> Cow<'static, str> {
match self {
RankQuery::PP => ou
@ -63,6 +67,12 @@ impl RankQuery {
format!("{}m{:05.2}s", minutes, seconds).into()
})
.unwrap_or_else(|| "-".into()),
RankQuery::MapAge { newest_first: _ } => ou
.modes
.get(&mode)
.and_then(|v| DateTime::from_timestamp(v.map_age, 0))
.map(|time| time.format("%F %T").to_string().into())
.unwrap_or_else(|| "-".into()),
}
}
}
@ -75,6 +85,10 @@ impl FromStr for RankQuery {
"pp" => Ok(RankQuery::PP),
"total" | "total-pp" => Ok(RankQuery::TotalPP),
"map-length" => Ok(RankQuery::MapLength),
"age" | "map-age" => Ok(RankQuery::MapAge { newest_first: true }),
"old" | "age-old" | "map-age-old" => Ok(RankQuery::MapAge {
newest_first: false,
}),
_ => Err(format!("not a query: {}", s)),
}
}
@ -143,6 +157,19 @@ pub async fn server_rank(ctx: &Context, m: &Message, mut args: Args) -> CommandR
.unwrap()
.reverse()
}),
RankQuery::MapAge { newest_first } => Box::new(move |(_, a), (_, b)| {
let r = a
.modes
.get(&mode)
.map(|v| v.map_age)
.partial_cmp(&b.modes.get(&mode).map(|v| v.map_age))
.unwrap();
if newest_first {
r.reverse()
} else {
r
}
}),
};
users.sort_unstable_by(sort_fn);
@ -169,20 +196,8 @@ pub async fn server_rank(ctx: &Context, m: &Message, mut args: Args) -> CommandR
}
let users = &users[start..end];
let table = match query {
RankQuery::PP | RankQuery::MapLength => {
let (headers, first_col, second_col) = if query == RankQuery::PP {
(
["#", "pp", "Map length", "Username", "Member"],
RankQuery::PP,
RankQuery::MapLength,
)
} else {
(
["#", "Map length", "pp", "Username", "Member"],
RankQuery::MapLength,
RankQuery::PP,
)
};
RankQuery::MapAge { newest_first: _ } | RankQuery::MapLength => {
let headers = ["#", query.col_name(), "pp", "Username", "Member"];
const ALIGNS: [Align; 5] = [Right, Right, Right, Left, Left];
let table = users
@ -191,8 +206,8 @@ pub async fn server_rank(ctx: &Context, m: &Message, mut args: Args) -> CommandR
.map(|(i, (mem, ou))| {
[
format!("{}", 1 + i + start),
first_col.extract_row(mode, ou).to_string(),
second_col.extract_row(mode, ou).to_string(),
query.extract_row(mode, ou).to_string(),
RankQuery::PP.extract_row(mode, ou).to_string(),
ou.username.to_string(),
mem.distinct(),
]
@ -200,6 +215,31 @@ pub async fn server_rank(ctx: &Context, m: &Message, mut args: Args) -> CommandR
.collect::<Vec<_>>();
table_formatting(&headers, &ALIGNS, table)
}
RankQuery::PP => {
const HEADERS: [&'static str; 6] =
["#", "pp", "Map length", "Map age", "Username", "Member"];
const ALIGNS: [Align; 6] = [Right, Right, Right, Right, Left, Left];
let table = users
.iter()
.enumerate()
.map(|(i, (mem, ou))| {
[
format!("{}", 1 + i + start),
RankQuery::PP.extract_row(mode, ou).to_string(),
RankQuery::MapLength.extract_row(mode, ou).to_string(),
(RankQuery::MapAge {
newest_first: false,
})
.extract_row(mode, ou)
.to_string(),
ou.username.to_string(),
mem.distinct(),
]
})
.collect::<Vec<_>>();
table_formatting(&HEADERS, &ALIGNS, table)
}
RankQuery::TotalPP => {
const HEADERS: [&'static str; 4] = ["#", "Total pp", "Username", "Member"];
const ALIGNS: [Align; 4] = [Right, Right, Left, Left];