mirror of
https://github.com/natsukagami/youmubot.git
synced 2025-04-19 00:38:54 +00:00
Add map_age display in ranks and user card
This commit is contained in:
parent
4909f6ea27
commit
8e90006eb9
4 changed files with 151 additions and 55 deletions
|
@ -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);
|
||||
|
|
|
@ -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!(
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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];
|
||||
|
|
Loading…
Add table
Reference in a new issue