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::stream::TryStreamExt;
|
||||||
use youmubot_prelude::*;
|
use youmubot_prelude::*;
|
||||||
|
|
||||||
|
use crate::discord::calculate_weighted_map_age;
|
||||||
use crate::discord::db::OsuUserMode;
|
use crate::discord::db::OsuUserMode;
|
||||||
use crate::{
|
use crate::{
|
||||||
discord::cache::save_beatmap,
|
discord::cache::save_beatmap,
|
||||||
|
@ -114,7 +115,10 @@ impl Announcer {
|
||||||
.await
|
.await
|
||||||
.pls_ok()
|
.pls_ok()
|
||||||
.unwrap_or(0.0),
|
.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,
|
last_update: now,
|
||||||
};
|
};
|
||||||
let last = user.modes.insert(mode, stats);
|
let last = user.modes.insert(mode, stats);
|
||||||
|
|
|
@ -457,8 +457,28 @@ impl<'a> ScoreEmbedBuilder<'a> {
|
||||||
pub(crate) fn user_embed(
|
pub(crate) fn user_embed(
|
||||||
u: User,
|
u: User,
|
||||||
map_length: f64,
|
map_length: f64,
|
||||||
|
map_age: i64,
|
||||||
best: Option<(Score, BeatmapWithMode, BeatmapInfo)>,
|
best: Option<(Score, BeatmapWithMode, BeatmapInfo)>,
|
||||||
) -> CreateEmbed {
|
) -> 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()
|
CreateEmbed::new()
|
||||||
.title(MessageBuilder::new().push_safe(u.username).build())
|
.title(MessageBuilder::new().push_safe(u.username).build())
|
||||||
.url(format!("https://osu.ppy.sh/users/{}", u.id))
|
.url(format!("https://osu.ppy.sh/users/{}", u.id))
|
||||||
|
@ -504,19 +524,7 @@ pub(crate) fn user_embed(
|
||||||
),
|
),
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
.field(
|
.fields(stats)
|
||||||
"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,
|
|
||||||
)
|
|
||||||
.field(
|
.field(
|
||||||
format!("Level {:.0}", u.level),
|
format!("Level {:.0}", u.level),
|
||||||
format!(
|
format!(
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use std::{borrow::Borrow, collections::HashMap as Map, str::FromStr, sync::Arc};
|
use std::{borrow::Borrow, collections::HashMap as Map, str::FromStr, sync::Arc};
|
||||||
|
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
|
use future::try_join;
|
||||||
use futures_util::join;
|
use futures_util::join;
|
||||||
use interaction::{beatmap_components, score_components};
|
use interaction::{beatmap_components, score_components};
|
||||||
use rand::seq::IteratorRandom;
|
use rand::seq::IteratorRandom;
|
||||||
|
@ -19,6 +20,7 @@ use db::{OsuLastBeatmap, OsuSavedUsers, OsuUser, OsuUserMode};
|
||||||
use embeds::{beatmap_embed, score_embed, user_embed};
|
use embeds::{beatmap_embed, score_embed, user_embed};
|
||||||
pub use hook::{dot_osu_hook, hook, score_hook};
|
pub use hook::{dot_osu_hook, hook, score_hook};
|
||||||
use server_rank::{SERVER_RANK_COMMAND, SHOW_LEADERBOARD_COMMAND};
|
use server_rank::{SERVER_RANK_COMMAND, SHOW_LEADERBOARD_COMMAND};
|
||||||
|
use stream::FuturesOrdered;
|
||||||
use youmubot_prelude::announcer::AnnouncerHandler;
|
use youmubot_prelude::announcer::AnnouncerHandler;
|
||||||
use youmubot_prelude::{stream::FuturesUnordered, *};
|
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)
|
.unwrap_or(None)
|
||||||
.and_then(|u| u.pp)
|
.and_then(|u| u.pp)
|
||||||
};
|
};
|
||||||
let map_length = async {
|
let map_length_age = async {
|
||||||
let scores = env
|
let scores = env
|
||||||
.client
|
.client
|
||||||
.user_best(UserID::ID(user.id), |f| f.mode(mode).limit(100))
|
.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()
|
.pls_ok()
|
||||||
.unwrap_or_else(|| vec![]);
|
.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,
|
calculate_weighted_map_length(&scores, &env.beatmaps, mode)
|
||||||
OsuUserMode {
|
.await
|
||||||
pp,
|
.pls_ok(),
|
||||||
map_length,
|
calculate_weighted_map_age(&scores, &env.beatmaps, mode)
|
||||||
map_age: 0, // TODO
|
.await
|
||||||
last_update: Utc::now(),
|
.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<_>>()
|
.collect::<stream::FuturesOrdered<_>>()
|
||||||
.filter_map(|v| future::ready(v))
|
.filter_map(|v| future::ready(v))
|
||||||
|
@ -837,7 +846,9 @@ async fn get_user(
|
||||||
let bests = osu_client
|
let bests = osu_client
|
||||||
.user_best(UserID::ID(u.id), |f| f.limit(100).mode(mode))
|
.user_best(UserID::ID(u.id), |f| f.limit(100).mode(mode))
|
||||||
.await?;
|
.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() {
|
let best = match bests.into_iter().next() {
|
||||||
Some(m) => {
|
Some(m) => {
|
||||||
let beatmap = meta_cache.get_beatmap(m.beatmap_id, mode).await?;
|
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",
|
"{}: here is the user that you requested",
|
||||||
msg.author
|
msg.author
|
||||||
))
|
))
|
||||||
.embed(user_embed(u, map_length, best)),
|
.embed(user_embed(u, map_length, map_age, best)),
|
||||||
)
|
)
|
||||||
.await?;
|
.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)))
|
.try_fold(0.0, |a, b| future::ready(Ok(a + b)))
|
||||||
.await
|
.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 std::{borrow::Cow, collections::HashMap, str::FromStr, sync::Arc};
|
||||||
|
|
||||||
|
use chrono::DateTime;
|
||||||
use pagination::paginate_with_first_message;
|
use pagination::paginate_with_first_message;
|
||||||
use serenity::{
|
use serenity::{
|
||||||
all::{GuildId, Member},
|
all::{GuildId, Member},
|
||||||
|
@ -31,17 +32,20 @@ enum RankQuery {
|
||||||
PP,
|
PP,
|
||||||
TotalPP,
|
TotalPP,
|
||||||
MapLength,
|
MapLength,
|
||||||
// MapAge,
|
MapAge {
|
||||||
|
newest_first: bool,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RankQuery {
|
impl RankQuery {
|
||||||
// fn col_name(&self) -> &'static str {
|
fn col_name(&self) -> &'static str {
|
||||||
// match self {
|
match self {
|
||||||
// RankQuery::PP => "pp",
|
RankQuery::PP => "pp",
|
||||||
// RankQuery::TotalPP => "Total pp",
|
RankQuery::TotalPP => "Total pp",
|
||||||
// RankQuery::MapLength => "Map length",
|
RankQuery::MapLength => "Map length",
|
||||||
// }
|
RankQuery::MapAge { newest_first: _ } => "Map age",
|
||||||
// }
|
}
|
||||||
|
}
|
||||||
fn extract_row(&self, mode: Mode, ou: &OsuUser) -> Cow<'static, str> {
|
fn extract_row(&self, mode: Mode, ou: &OsuUser) -> Cow<'static, str> {
|
||||||
match self {
|
match self {
|
||||||
RankQuery::PP => ou
|
RankQuery::PP => ou
|
||||||
|
@ -63,6 +67,12 @@ impl RankQuery {
|
||||||
format!("{}m{:05.2}s", minutes, seconds).into()
|
format!("{}m{:05.2}s", minutes, seconds).into()
|
||||||
})
|
})
|
||||||
.unwrap_or_else(|| "-".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),
|
"pp" => Ok(RankQuery::PP),
|
||||||
"total" | "total-pp" => Ok(RankQuery::TotalPP),
|
"total" | "total-pp" => Ok(RankQuery::TotalPP),
|
||||||
"map-length" => Ok(RankQuery::MapLength),
|
"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)),
|
_ => Err(format!("not a query: {}", s)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -143,6 +157,19 @@ pub async fn server_rank(ctx: &Context, m: &Message, mut args: Args) -> CommandR
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.reverse()
|
.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);
|
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 users = &users[start..end];
|
||||||
let table = match query {
|
let table = match query {
|
||||||
RankQuery::PP | RankQuery::MapLength => {
|
RankQuery::MapAge { newest_first: _ } | RankQuery::MapLength => {
|
||||||
let (headers, first_col, second_col) = if query == RankQuery::PP {
|
let headers = ["#", query.col_name(), "pp", "Username", "Member"];
|
||||||
(
|
|
||||||
["#", "pp", "Map length", "Username", "Member"],
|
|
||||||
RankQuery::PP,
|
|
||||||
RankQuery::MapLength,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
(
|
|
||||||
["#", "Map length", "pp", "Username", "Member"],
|
|
||||||
RankQuery::MapLength,
|
|
||||||
RankQuery::PP,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
const ALIGNS: [Align; 5] = [Right, Right, Right, Left, Left];
|
const ALIGNS: [Align; 5] = [Right, Right, Right, Left, Left];
|
||||||
|
|
||||||
let table = users
|
let table = users
|
||||||
|
@ -191,8 +206,8 @@ pub async fn server_rank(ctx: &Context, m: &Message, mut args: Args) -> CommandR
|
||||||
.map(|(i, (mem, ou))| {
|
.map(|(i, (mem, ou))| {
|
||||||
[
|
[
|
||||||
format!("{}", 1 + i + start),
|
format!("{}", 1 + i + start),
|
||||||
first_col.extract_row(mode, ou).to_string(),
|
query.extract_row(mode, ou).to_string(),
|
||||||
second_col.extract_row(mode, ou).to_string(),
|
RankQuery::PP.extract_row(mode, ou).to_string(),
|
||||||
ou.username.to_string(),
|
ou.username.to_string(),
|
||||||
mem.distinct(),
|
mem.distinct(),
|
||||||
]
|
]
|
||||||
|
@ -200,6 +215,31 @@ pub async fn server_rank(ctx: &Context, m: &Message, mut args: Args) -> CommandR
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
table_formatting(&headers, &ALIGNS, table)
|
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 => {
|
RankQuery::TotalPP => {
|
||||||
const HEADERS: [&'static str; 4] = ["#", "Total pp", "Username", "Member"];
|
const HEADERS: [&'static str; 4] = ["#", "Total pp", "Username", "Member"];
|
||||||
const ALIGNS: [Align; 4] = [Right, Right, Left, Left];
|
const ALIGNS: [Align; 4] = [Right, Right, Left, Left];
|
||||||
|
|
Loading…
Add table
Reference in a new issue