mirror of
https://github.com/natsukagami/youmubot.git
synced 2025-04-19 00:38:54 +00:00
Update osu to respond to the new sql format and rewrite announcer
This commit is contained in:
parent
e733364d15
commit
a4407df97c
4 changed files with 349 additions and 323 deletions
|
@ -1,4 +1,7 @@
|
||||||
use std::{convert::TryInto, sync::Arc};
|
use chrono::{DateTime, Utc};
|
||||||
|
use futures_util::try_join;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use stream::FuturesUnordered;
|
||||||
|
|
||||||
use serenity::builder::CreateMessage;
|
use serenity::builder::CreateMessage;
|
||||||
use serenity::{
|
use serenity::{
|
||||||
|
@ -14,6 +17,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::db::OsuUserMode;
|
||||||
use crate::{
|
use crate::{
|
||||||
discord::cache::save_beatmap,
|
discord::cache::save_beatmap,
|
||||||
discord::oppai_cache::BeatmapContent,
|
discord::oppai_cache::BeatmapContent,
|
||||||
|
@ -22,22 +26,21 @@ use crate::{
|
||||||
Client as Osu,
|
Client as Osu,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::db::{OsuSavedUsers, OsuUser};
|
use super::db::OsuUser;
|
||||||
use super::interaction::score_components;
|
use super::interaction::score_components;
|
||||||
use super::{calculate_weighted_map_length, OsuEnv};
|
use super::{calculate_weighted_map_length, OsuEnv};
|
||||||
use super::{embeds::score_embed, BeatmapWithMode};
|
use super::{embeds::score_embed, BeatmapWithMode};
|
||||||
|
|
||||||
/// osu! announcer's unique announcer key.
|
/// osu! announcer's unique announcer key.
|
||||||
pub const ANNOUNCER_KEY: &str = "osu";
|
pub const ANNOUNCER_KEY: &str = "osu";
|
||||||
|
const MAX_FAILURES: u8 = 64;
|
||||||
|
|
||||||
/// The announcer struct implementing youmubot_prelude::Announcer
|
/// The announcer struct implementing youmubot_prelude::Announcer
|
||||||
pub struct Announcer {
|
pub struct Announcer {}
|
||||||
client: Arc<Osu>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Announcer {
|
impl Announcer {
|
||||||
pub fn new(client: Arc<Osu>) -> Self {
|
pub fn new() -> Self {
|
||||||
Self { client }
|
Self {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,71 +48,26 @@ impl Announcer {
|
||||||
impl youmubot_prelude::Announcer for Announcer {
|
impl youmubot_prelude::Announcer for Announcer {
|
||||||
async fn updates(
|
async fn updates(
|
||||||
&mut self,
|
&mut self,
|
||||||
c: CacheAndHttp,
|
ctx: CacheAndHttp,
|
||||||
d: AppData,
|
d: AppData,
|
||||||
channels: MemberToChannels,
|
channels: MemberToChannels,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
let env = d.read().await.get::<OsuEnv>().unwrap().clone();
|
||||||
// For each user...
|
// For each user...
|
||||||
let users = {
|
let users = env.saved_users.all().await?;
|
||||||
let env = d.read().await.get::<OsuEnv>().unwrap().clone();
|
|
||||||
env.saved_users.all().await?
|
|
||||||
};
|
|
||||||
let now = chrono::Utc::now();
|
|
||||||
users
|
users
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|mut osu_user| {
|
.map(
|
||||||
let user_id = osu_user.user_id;
|
|osu_user| {
|
||||||
let channels = &channels;
|
channels
|
||||||
let ctx = Context {
|
.channels_of(ctx.clone(), osu_user.user_id)
|
||||||
c: c.clone(),
|
.then(|chs| self.update_user(ctx.clone(), &env, osu_user, chs))
|
||||||
data: d.clone(),
|
.then(|new_user| env.saved_users.save(new_user))
|
||||||
};
|
.map(|r| {
|
||||||
let s = &*self;
|
r.pls_ok();
|
||||||
async move {
|
|
||||||
let channels = channels.channels_of(ctx.c.clone(), user_id).await;
|
|
||||||
if channels.is_empty() {
|
|
||||||
return; // We don't wanna update an user without any active server
|
|
||||||
}
|
|
||||||
match [Mode::Std, Mode::Taiko, Mode::Catch, Mode::Mania]
|
|
||||||
.into_iter()
|
|
||||||
.map(|m| {
|
|
||||||
s.handle_user_mode(&ctx, now, &osu_user, user_id, channels.clone(), m)
|
|
||||||
})
|
})
|
||||||
.collect::<stream::FuturesOrdered<_>>()
|
}, // self.update_user()
|
||||||
.try_collect::<Vec<_>>()
|
)
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(v) => {
|
|
||||||
osu_user.pp = v
|
|
||||||
.iter()
|
|
||||||
.map(|u| u.pp)
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.try_into()
|
|
||||||
.unwrap();
|
|
||||||
osu_user.username = v.into_iter().next().unwrap().username.into();
|
|
||||||
osu_user.last_update = now;
|
|
||||||
osu_user.std_weighted_map_length =
|
|
||||||
Self::std_weighted_map_length(&ctx, &osu_user)
|
|
||||||
.await
|
|
||||||
.pls_ok();
|
|
||||||
let id = osu_user.id;
|
|
||||||
println!("{:?}", osu_user);
|
|
||||||
ctx.data
|
|
||||||
.read()
|
|
||||||
.await
|
|
||||||
.get::<OsuSavedUsers>()
|
|
||||||
.unwrap()
|
|
||||||
.save(osu_user)
|
|
||||||
.await
|
|
||||||
.pls_ok();
|
|
||||||
println!("updating {} done", id);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("osu: Cannot update {}: {}", osu_user.id, e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<stream::FuturesUnordered<_>>()
|
.collect::<stream::FuturesUnordered<_>>()
|
||||||
.collect::<()>()
|
.collect::<()>()
|
||||||
.await;
|
.await;
|
||||||
|
@ -118,98 +76,128 @@ impl youmubot_prelude::Announcer for Announcer {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Announcer {
|
impl Announcer {
|
||||||
/// Handles an user/mode scan, announces all possible new scores, return the new pp value.
|
async fn update_user(
|
||||||
async fn handle_user_mode(
|
|
||||||
&self,
|
&self,
|
||||||
ctx: &Context,
|
ctx: impl CacheHttp + Clone + 'static,
|
||||||
|
env: &OsuEnv,
|
||||||
|
mut user: OsuUser,
|
||||||
|
broadcast_to: Vec<ChannelId>,
|
||||||
|
) -> OsuUser {
|
||||||
|
if user.failures == MAX_FAILURES {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
const MODES: [Mode; 4] = [Mode::Std, Mode::Taiko, Mode::Catch, Mode::Mania];
|
||||||
|
let now = chrono::Utc::now();
|
||||||
|
let broadcast_to = Arc::new(broadcast_to);
|
||||||
|
for mode in MODES {
|
||||||
|
let (u, top, events) = match self.fetch_user_data(env, now, &user, mode).await {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!(
|
||||||
|
"[osu] Updating `{}`[{}] failed with: {}",
|
||||||
|
user.username, user.id, err
|
||||||
|
);
|
||||||
|
user.failures += 1;
|
||||||
|
if user.failures == MAX_FAILURES {
|
||||||
|
eprintln!(
|
||||||
|
"[osu] Too many failures, disabling: `{}`[{}]",
|
||||||
|
user.username, user.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// update stats
|
||||||
|
let stats = OsuUserMode {
|
||||||
|
pp: u.pp.unwrap_or(0.0),
|
||||||
|
map_length: calculate_weighted_map_length(&top, &env.beatmaps, mode)
|
||||||
|
.await
|
||||||
|
.pls_ok()
|
||||||
|
.unwrap_or(0.0),
|
||||||
|
map_age: 0, // soon
|
||||||
|
last_update: now,
|
||||||
|
};
|
||||||
|
let last = user.modes.insert(mode, stats);
|
||||||
|
let last_update = last.as_ref().map(|v| v.last_update);
|
||||||
|
|
||||||
|
// broadcast
|
||||||
|
let mention = user.user_id;
|
||||||
|
let broadcast_to = broadcast_to.clone();
|
||||||
|
let ctx = ctx.clone();
|
||||||
|
let env = env.clone();
|
||||||
|
spawn_future(async move {
|
||||||
|
let top = top
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_, s)| Self::is_announceable_date(s.date, last_update, now))
|
||||||
|
.map(|(rank, score)| {
|
||||||
|
CollectedScore::from_top_score(&u, score, mode, rank as u8)
|
||||||
|
});
|
||||||
|
let recents = events
|
||||||
|
.into_iter()
|
||||||
|
.map(|e| CollectedScore::from_event(&env.client, &u, e))
|
||||||
|
.collect::<FuturesUnordered<_>>()
|
||||||
|
.filter_map(|v| future::ready(v.pls_ok()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.await
|
||||||
|
.into_iter();
|
||||||
|
top.chain(recents)
|
||||||
|
.map(|v| v.send_message(&ctx, &env, mention, &broadcast_to))
|
||||||
|
.collect::<FuturesUnordered<_>>()
|
||||||
|
.filter_map(|v| future::ready(v.pls_ok().map(|_| ())))
|
||||||
|
.collect::<()>()
|
||||||
|
.await
|
||||||
|
});
|
||||||
|
}
|
||||||
|
user.failures = 0;
|
||||||
|
user
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_announceable_date(
|
||||||
|
s: DateTime<Utc>,
|
||||||
|
last_update: Option<DateTime<Utc>>,
|
||||||
|
now: DateTime<Utc>,
|
||||||
|
) -> bool {
|
||||||
|
(match last_update {
|
||||||
|
Some(lu) => s > lu,
|
||||||
|
None => true,
|
||||||
|
}) && s <= now
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles an user/mode scan, announces all possible new scores, return the new pp value.
|
||||||
|
async fn fetch_user_data(
|
||||||
|
&self,
|
||||||
|
env: &OsuEnv,
|
||||||
now: chrono::DateTime<chrono::Utc>,
|
now: chrono::DateTime<chrono::Utc>,
|
||||||
osu_user: &OsuUser,
|
osu_user: &OsuUser,
|
||||||
user_id: UserId,
|
|
||||||
channels: Vec<ChannelId>,
|
|
||||||
mode: Mode,
|
mode: Mode,
|
||||||
) -> Result<User, Error> {
|
) -> Result<(User, Vec<Score>, Vec<UserEventRank>), Error> {
|
||||||
let days_since_last_update = (now - osu_user.last_update).num_days() + 1;
|
let stats = osu_user.modes.get(&mode).cloned();
|
||||||
let last_update = osu_user.last_update;
|
let last_update = stats.as_ref().map(|v| v.last_update);
|
||||||
let (scores, user) = {
|
let user_id = UserID::ID(osu_user.id);
|
||||||
let scores = self.scan_user(osu_user, mode).await?;
|
let user = {
|
||||||
let user = self
|
let days_since_last_update = stats
|
||||||
.client
|
.as_ref()
|
||||||
.user(&UserID::ID(osu_user.id), |f| {
|
.map(|v| (now - v.last_update).num_days() + 1)
|
||||||
f.mode(mode)
|
.unwrap_or(30);
|
||||||
.event_days(days_since_last_update.min(31) as u8)
|
env.client.user(&user_id, move |f| {
|
||||||
})
|
f.mode(mode)
|
||||||
.await?
|
.event_days(days_since_last_update.min(31) as u8)
|
||||||
.ok_or_else(|| Error::msg("user not found"))?;
|
})
|
||||||
(scores, user)
|
|
||||||
};
|
};
|
||||||
let client = self.client.clone();
|
let top_scores = env
|
||||||
let ctx = ctx.clone();
|
|
||||||
let _user = user.clone();
|
|
||||||
spawn_future(async move {
|
|
||||||
let event_scores = user
|
|
||||||
.events
|
|
||||||
.iter()
|
|
||||||
.filter_map(|u| u.to_event_rank())
|
|
||||||
.filter(|u| u.mode == mode && u.date > last_update && u.date <= now)
|
|
||||||
.map(|ev| CollectedScore::from_event(&client, &user, ev, user_id, &channels[..]))
|
|
||||||
.collect::<stream::FuturesUnordered<_>>()
|
|
||||||
.filter_map(|u| future::ready(u.pls_ok()))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.await;
|
|
||||||
let top_scores = scores.into_iter().filter_map(|(rank, score)| {
|
|
||||||
if score.date > last_update && score.date <= now {
|
|
||||||
Some(CollectedScore::from_top_score(
|
|
||||||
&user,
|
|
||||||
score,
|
|
||||||
mode,
|
|
||||||
rank,
|
|
||||||
user_id,
|
|
||||||
&channels[..],
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
});
|
|
||||||
event_scores
|
|
||||||
.into_iter()
|
|
||||||
.chain(top_scores)
|
|
||||||
.map(|v| v.send_message(&ctx))
|
|
||||||
.collect::<stream::FuturesUnordered<_>>()
|
|
||||||
.try_collect::<Vec<_>>()
|
|
||||||
.await
|
|
||||||
.pls_ok();
|
|
||||||
});
|
|
||||||
Ok(_user)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn scan_user(&self, u: &OsuUser, mode: Mode) -> Result<Vec<(u8, Score)>, Error> {
|
|
||||||
let scores = self
|
|
||||||
.client
|
.client
|
||||||
.user_best(UserID::ID(u.id), |f| f.mode(mode).limit(25))
|
.user_best(user_id.clone(), |f| f.mode(mode).limit(100));
|
||||||
.await?;
|
let (user, top_scores) = try_join!(user, top_scores)?;
|
||||||
let scores = scores
|
let mut user = user.unwrap();
|
||||||
|
// if top scores exist, user would too
|
||||||
|
let events = std::mem::replace(&mut user.events, vec![])
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.enumerate()
|
.filter_map(|v| v.to_event_rank())
|
||||||
.filter(|(_, s)| s.date >= u.last_update)
|
.filter(|s| Self::is_announceable_date(s.date, last_update, now))
|
||||||
.map(|(i, v)| ((i + 1) as u8, v))
|
.collect::<Vec<_>>();
|
||||||
.collect();
|
Ok((user, top_scores, events))
|
||||||
Ok(scores)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn std_weighted_map_length(ctx: &Context, u: &OsuUser) -> Result<f64> {
|
|
||||||
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
|
|
||||||
let scores = env
|
|
||||||
.client
|
|
||||||
.user_best(UserID::ID(u.id), |f| f.mode(Mode::Std).limit(100))
|
|
||||||
.await?;
|
|
||||||
calculate_weighted_map_length(&scores, &env.beatmaps, Mode::Std).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct Context {
|
|
||||||
data: AppData,
|
|
||||||
c: CacheAndHttp,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CollectedScore<'a> {
|
struct CollectedScore<'a> {
|
||||||
|
@ -217,27 +205,15 @@ struct CollectedScore<'a> {
|
||||||
pub score: Score,
|
pub score: Score,
|
||||||
pub mode: Mode,
|
pub mode: Mode,
|
||||||
pub kind: ScoreType,
|
pub kind: ScoreType,
|
||||||
|
|
||||||
pub discord_user: UserId,
|
|
||||||
pub channels: &'a [ChannelId],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> CollectedScore<'a> {
|
impl<'a> CollectedScore<'a> {
|
||||||
fn from_top_score(
|
fn from_top_score(user: &'a User, score: Score, mode: Mode, rank: u8) -> Self {
|
||||||
user: &'a User,
|
|
||||||
score: Score,
|
|
||||||
mode: Mode,
|
|
||||||
rank: u8,
|
|
||||||
discord_user: UserId,
|
|
||||||
channels: &'a [ChannelId],
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
user,
|
user,
|
||||||
score,
|
score,
|
||||||
mode,
|
mode,
|
||||||
kind: ScoreType::TopRecord(rank),
|
kind: ScoreType::TopRecord(rank),
|
||||||
discord_user,
|
|
||||||
channels,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -245,8 +221,6 @@ impl<'a> CollectedScore<'a> {
|
||||||
osu: &Osu,
|
osu: &Osu,
|
||||||
user: &'a User,
|
user: &'a User,
|
||||||
event: UserEventRank,
|
event: UserEventRank,
|
||||||
discord_user: UserId,
|
|
||||||
channels: &'a [ChannelId],
|
|
||||||
) -> Result<CollectedScore<'a>> {
|
) -> Result<CollectedScore<'a>> {
|
||||||
let scores = osu
|
let scores = osu
|
||||||
.scores(event.beatmap_id, |f| {
|
.scores(event.beatmap_id, |f| {
|
||||||
|
@ -258,32 +232,40 @@ impl<'a> CollectedScore<'a> {
|
||||||
.find(|s| (s.date - event.date).abs() < chrono::TimeDelta::seconds(5))
|
.find(|s| (s.date - event.date).abs() < chrono::TimeDelta::seconds(5))
|
||||||
{
|
{
|
||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
None => return Err(Error::msg("cannot get score for map...")),
|
None => {
|
||||||
|
return Err(Error::msg(format!(
|
||||||
|
"cannot get score for map..., event = {:?}",
|
||||||
|
event
|
||||||
|
)))
|
||||||
|
}
|
||||||
};
|
};
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
user,
|
user,
|
||||||
score,
|
score,
|
||||||
mode: event.mode,
|
mode: event.mode,
|
||||||
kind: ScoreType::WorldRecord(event.rank),
|
kind: ScoreType::WorldRecord(event.rank),
|
||||||
discord_user,
|
|
||||||
channels,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> CollectedScore<'a> {
|
impl<'a> CollectedScore<'a> {
|
||||||
async fn send_message(self, ctx: &Context) -> Result<Vec<Message>> {
|
async fn send_message(
|
||||||
let (bm, content) = self.get_beatmap(ctx).await?;
|
self,
|
||||||
self.channels
|
ctx: impl CacheHttp,
|
||||||
|
env: &OsuEnv,
|
||||||
|
mention: UserId,
|
||||||
|
channels: &[ChannelId],
|
||||||
|
) -> Result<Vec<Message>> {
|
||||||
|
let (bm, content) = self.get_beatmap(env).await?;
|
||||||
|
channels
|
||||||
.iter()
|
.iter()
|
||||||
.map(|c| self.send_message_to(*c, ctx, &bm, &content))
|
.map(|c| self.send_message_to(mention, *c, &ctx, env, &bm, &content))
|
||||||
.collect::<stream::FuturesUnordered<_>>()
|
.collect::<stream::FuturesUnordered<_>>()
|
||||||
.try_collect()
|
.try_collect()
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_beatmap(&self, ctx: &Context) -> Result<(BeatmapWithMode, BeatmapContent)> {
|
async fn get_beatmap(&self, env: &OsuEnv) -> Result<(BeatmapWithMode, BeatmapContent)> {
|
||||||
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
|
|
||||||
let beatmap = env
|
let beatmap = env
|
||||||
.beatmaps
|
.beatmaps
|
||||||
.get_beatmap_default(self.score.beatmap_id)
|
.get_beatmap_default(self.score.beatmap_id)
|
||||||
|
@ -294,12 +276,14 @@ impl<'a> CollectedScore<'a> {
|
||||||
|
|
||||||
async fn send_message_to(
|
async fn send_message_to(
|
||||||
&self,
|
&self,
|
||||||
|
mention: UserId,
|
||||||
channel: ChannelId,
|
channel: ChannelId,
|
||||||
ctx: &Context,
|
ctx: impl CacheHttp,
|
||||||
|
env: &OsuEnv,
|
||||||
bm: &BeatmapWithMode,
|
bm: &BeatmapWithMode,
|
||||||
content: &BeatmapContent,
|
content: &BeatmapContent,
|
||||||
) -> Result<Message> {
|
) -> Result<Message> {
|
||||||
let guild = match channel.to_channel(&ctx.c).await?.guild() {
|
let guild = match channel.to_channel(&ctx).await?.guild() {
|
||||||
Some(gc) => gc.guild_id,
|
Some(gc) => gc.guild_id,
|
||||||
None => {
|
None => {
|
||||||
eprintln!("Not a guild channel: {}", channel);
|
eprintln!("Not a guild channel: {}", channel);
|
||||||
|
@ -307,27 +291,24 @@ impl<'a> CollectedScore<'a> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let member = match guild.member(&ctx.c, self.discord_user).await {
|
let member = match guild.member(&ctx, mention).await {
|
||||||
Ok(mem) => mem,
|
Ok(mem) => mem,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Cannot get member {}: {}", self.discord_user, e);
|
eprintln!("Cannot get member {}: {}", mention, e);
|
||||||
return Err(e.into());
|
return Err(e.into());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let m = channel
|
let m = channel
|
||||||
.send_message(
|
.send_message(
|
||||||
ctx.c.http(),
|
&ctx,
|
||||||
CreateMessage::new()
|
CreateMessage::new()
|
||||||
.content(match self.kind {
|
.content(match self.kind {
|
||||||
ScoreType::TopRecord(_) => {
|
ScoreType::TopRecord(_) => {
|
||||||
format!("New top record from {}!", self.discord_user.mention())
|
format!("New top record from {}!", mention.mention())
|
||||||
}
|
}
|
||||||
ScoreType::WorldRecord(rank) => {
|
ScoreType::WorldRecord(rank) => {
|
||||||
if rank <= 100 {
|
if rank <= 100 {
|
||||||
format!(
|
format!("New leaderboard record from {}!", mention.mention())
|
||||||
"New leaderboard record from {}!",
|
|
||||||
self.discord_user.mention()
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
format!("New leaderboard record from **{}**!", member.distinct())
|
format!("New leaderboard record from **{}**!", member.distinct())
|
||||||
}
|
}
|
||||||
|
@ -345,8 +326,6 @@ impl<'a> CollectedScore<'a> {
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
|
|
||||||
|
|
||||||
save_beatmap(&env, channel, bm).await.pls_ok();
|
save_beatmap(&env, channel, bm).await.pls_ok();
|
||||||
Ok(m)
|
Ok(m)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
use std::collections::HashMap as Map;
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
@ -29,17 +30,16 @@ impl OsuSavedUsers {
|
||||||
impl OsuSavedUsers {
|
impl OsuSavedUsers {
|
||||||
/// Get all users
|
/// Get all users
|
||||||
pub async fn all(&self) -> Result<Vec<OsuUser>> {
|
pub async fn all(&self) -> Result<Vec<OsuUser>> {
|
||||||
let mut conn = self.pool.acquire().await?;
|
Ok(model::OsuUser::all(&self.pool)
|
||||||
model::OsuUser::all(&mut *conn)
|
.await?
|
||||||
.map(|v| v.map(OsuUser::from).map_err(Error::from))
|
.into_iter()
|
||||||
.try_collect()
|
.map(|v| v.into())
|
||||||
.await
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get an user by their user_id.
|
/// Get an user by their user_id.
|
||||||
pub async fn by_user_id(&self, user_id: UserId) -> Result<Option<OsuUser>> {
|
pub async fn by_user_id(&self, user_id: UserId) -> Result<Option<OsuUser>> {
|
||||||
let mut conn = self.pool.acquire().await?;
|
let u = model::OsuUser::by_user_id(user_id.get() as i64, &self.pool)
|
||||||
let u = model::OsuUser::by_user_id(user_id.get() as i64, &mut *conn)
|
|
||||||
.await?
|
.await?
|
||||||
.map(OsuUser::from);
|
.map(OsuUser::from);
|
||||||
Ok(u)
|
Ok(u)
|
||||||
|
@ -47,15 +47,17 @@ impl OsuSavedUsers {
|
||||||
|
|
||||||
/// Save the given user.
|
/// Save the given user.
|
||||||
pub async fn save(&self, u: OsuUser) -> Result<()> {
|
pub async fn save(&self, u: OsuUser) -> Result<()> {
|
||||||
let mut conn = self.pool.acquire().await?;
|
let mut tx = self.pool.begin().await?;
|
||||||
Ok(model::OsuUser::from(u).store(&mut *conn).await?)
|
model::OsuUser::from(u).store(&mut tx).await?;
|
||||||
|
tx.commit().await?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Save the given user as a completely new user.
|
/// Save the given user as a completely new user.
|
||||||
pub async fn new_user(&self, u: OsuUser) -> Result<()> {
|
pub async fn new_user(&self, u: OsuUser) -> Result<()> {
|
||||||
let mut t = self.pool.begin().await?;
|
let mut t = self.pool.begin().await?;
|
||||||
model::OsuUser::delete(u.user_id.get() as i64, &mut *t).await?;
|
model::OsuUser::delete(u.user_id.get() as i64, &mut *t).await?;
|
||||||
model::OsuUser::from(u).store(&mut *t).await?;
|
model::OsuUser::from(u).store(&mut t).await?;
|
||||||
t.commit().await?;
|
t.commit().await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -107,25 +109,30 @@ pub struct OsuUser {
|
||||||
pub user_id: UserId,
|
pub user_id: UserId,
|
||||||
pub username: Cow<'static, str>,
|
pub username: Cow<'static, str>,
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
pub last_update: DateTime<Utc>,
|
pub modes: Map<Mode, OsuUserMode>,
|
||||||
pub pp: [Option<f64>; 4],
|
|
||||||
pub std_weighted_map_length: Option<f64>,
|
|
||||||
/// More than 5 failures => gone
|
/// More than 5 failures => gone
|
||||||
pub failures: u8,
|
pub failures: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct OsuUserMode {
|
||||||
|
pub pp: f64,
|
||||||
|
pub map_length: f64,
|
||||||
|
pub map_age: i64,
|
||||||
|
pub last_update: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
impl From<OsuUser> for model::OsuUser {
|
impl From<OsuUser> for model::OsuUser {
|
||||||
fn from(u: OsuUser) -> Self {
|
fn from(u: OsuUser) -> Self {
|
||||||
Self {
|
Self {
|
||||||
user_id: u.user_id.get() as i64,
|
user_id: u.user_id.get() as i64,
|
||||||
username: Some(u.username.into_owned()),
|
username: Some(u.username.into_owned()),
|
||||||
id: u.id as i64,
|
id: u.id as i64,
|
||||||
last_update: u.last_update,
|
modes: u
|
||||||
pp_std: u.pp[Mode::Std as usize],
|
.modes
|
||||||
pp_taiko: u.pp[Mode::Taiko as usize],
|
.into_iter()
|
||||||
pp_catch: u.pp[Mode::Catch as usize],
|
.map(|(k, v)| (k as u8, v.into()))
|
||||||
pp_mania: u.pp[Mode::Mania as usize],
|
.collect(),
|
||||||
std_weighted_map_length: u.std_weighted_map_length,
|
|
||||||
failures: u.failures,
|
failures: u.failures,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -137,19 +144,38 @@ impl From<model::OsuUser> for OsuUser {
|
||||||
user_id: UserId::new(u.user_id as u64),
|
user_id: UserId::new(u.user_id as u64),
|
||||||
username: u.username.map(Cow::Owned).unwrap_or("unknown".into()),
|
username: u.username.map(Cow::Owned).unwrap_or("unknown".into()),
|
||||||
id: u.id as u64,
|
id: u.id as u64,
|
||||||
last_update: u.last_update,
|
modes: u
|
||||||
pp: [0, 1, 2, 3].map(|v| match Mode::from(v) {
|
.modes
|
||||||
Mode::Std => u.pp_std,
|
.into_iter()
|
||||||
Mode::Taiko => u.pp_taiko,
|
.map(|(k, v)| (k.into(), v.into()))
|
||||||
Mode::Catch => u.pp_catch,
|
.collect(),
|
||||||
Mode::Mania => u.pp_mania,
|
|
||||||
}),
|
|
||||||
std_weighted_map_length: u.std_weighted_map_length,
|
|
||||||
failures: u.failures,
|
failures: u.failures,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<OsuUserMode> for model::OsuUserMode {
|
||||||
|
fn from(m: OsuUserMode) -> Self {
|
||||||
|
Self {
|
||||||
|
pp: m.pp,
|
||||||
|
map_length: m.map_length,
|
||||||
|
map_age: m.map_age,
|
||||||
|
last_update: m.last_update,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<model::OsuUserMode> for OsuUserMode {
|
||||||
|
fn from(m: model::OsuUserMode) -> Self {
|
||||||
|
Self {
|
||||||
|
pp: m.pp,
|
||||||
|
map_length: m.map_length,
|
||||||
|
map_age: m.map_age,
|
||||||
|
last_update: m.last_update,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
mod legacy {
|
mod legacy {
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use std::{borrow::Borrow, str::FromStr, sync::Arc};
|
use std::{borrow::Borrow, collections::HashMap as Map, str::FromStr, sync::Arc};
|
||||||
|
|
||||||
|
use chrono::Utc;
|
||||||
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;
|
||||||
|
@ -14,7 +15,7 @@ use serenity::{
|
||||||
utils::MessageBuilder,
|
utils::MessageBuilder,
|
||||||
};
|
};
|
||||||
|
|
||||||
use db::{OsuLastBeatmap, OsuSavedUsers, OsuUser};
|
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};
|
||||||
|
@ -108,10 +109,7 @@ pub async fn setup(
|
||||||
let beatmap_cache = BeatmapMetaCache::new(osu_client.clone(), prelude.sql.clone());
|
let beatmap_cache = BeatmapMetaCache::new(osu_client.clone(), prelude.sql.clone());
|
||||||
|
|
||||||
// Announcer
|
// Announcer
|
||||||
announcers.add(
|
announcers.add(announcer::ANNOUNCER_KEY, announcer::Announcer::new());
|
||||||
announcer::ANNOUNCER_KEY,
|
|
||||||
announcer::Announcer::new(osu_client.clone()),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Legacy data
|
// Legacy data
|
||||||
data.insert::<OsuLastBeatmap>(last_beatmaps.clone());
|
data.insert::<OsuLastBeatmap>(last_beatmaps.clone());
|
||||||
|
@ -369,49 +367,53 @@ pub async fn forcesave(ctx: &Context, msg: &Message, mut args: Args) -> CommandR
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn add_user(target: serenity::model::id::UserId, user: User, env: &OsuEnv) -> Result<()> {
|
async fn add_user(target: serenity::model::id::UserId, user: User, env: &OsuEnv) -> Result<()> {
|
||||||
let pp_fut = async {
|
let modes = [Mode::Std, Mode::Taiko, Mode::Catch, Mode::Mania]
|
||||||
[Mode::Std, Mode::Taiko, Mode::Catch, Mode::Mania]
|
.into_iter()
|
||||||
.into_iter()
|
.map(|mode| async move {
|
||||||
.map(|mode| async move {
|
let pp = async {
|
||||||
env.client
|
env.client
|
||||||
.user(&UserID::ID(user.id), |f| f.mode(mode))
|
.user(&UserID::ID(user.id), |f| f.mode(mode))
|
||||||
.await
|
.await
|
||||||
.unwrap_or_else(|err| {
|
.pls_ok()
|
||||||
eprintln!("{}", err);
|
.unwrap_or(None)
|
||||||
None
|
|
||||||
})
|
|
||||||
.and_then(|u| u.pp)
|
.and_then(|u| u.pp)
|
||||||
|
};
|
||||||
|
let map_length = async {
|
||||||
|
let scores = env
|
||||||
|
.client
|
||||||
|
.user_best(UserID::ID(user.id), |f| f.mode(mode).limit(100))
|
||||||
|
.await
|
||||||
|
.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(),
|
||||||
|
},
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.collect::<stream::FuturesOrdered<_>>()
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<stream::FuturesOrdered<_>>()
|
||||||
.await
|
.filter_map(|v| future::ready(v))
|
||||||
};
|
.collect::<Map<_, _>>()
|
||||||
|
.await;
|
||||||
let std_weight_map_length_fut = async {
|
|
||||||
let scores = env
|
|
||||||
.client
|
|
||||||
.user_best(UserID::ID(user.id), |f| f.mode(Mode::Std).limit(100))
|
|
||||||
.await
|
|
||||||
.unwrap_or_else(|err| {
|
|
||||||
eprintln!("{}", err);
|
|
||||||
vec![]
|
|
||||||
});
|
|
||||||
|
|
||||||
calculate_weighted_map_length(&scores, &env.beatmaps, Mode::Std)
|
|
||||||
.await
|
|
||||||
.pls_ok()
|
|
||||||
};
|
|
||||||
|
|
||||||
let (pp, std_weight_map_length) = join!(pp_fut, std_weight_map_length_fut);
|
|
||||||
|
|
||||||
let u = OsuUser {
|
let u = OsuUser {
|
||||||
user_id: target,
|
user_id: target,
|
||||||
username: user.username.into(),
|
username: user.username.into(),
|
||||||
id: user.id,
|
id: user.id,
|
||||||
failures: 0,
|
failures: 0,
|
||||||
last_update: chrono::Utc::now(),
|
modes,
|
||||||
pp: pp.try_into().unwrap(),
|
|
||||||
std_weighted_map_length: std_weight_map_length,
|
|
||||||
};
|
};
|
||||||
env.saved_users.new_user(u).await?;
|
env.saved_users.new_user(u).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
use std::{collections::HashMap, str::FromStr, sync::Arc};
|
use std::{borrow::Cow, collections::HashMap, str::FromStr, sync::Arc};
|
||||||
|
|
||||||
use pagination::paginate_with_first_message;
|
use pagination::paginate_with_first_message;
|
||||||
use serenity::{
|
use serenity::{
|
||||||
all::GuildId,
|
all::{GuildId, Member},
|
||||||
builder::EditMessage,
|
builder::EditMessage,
|
||||||
framework::standard::{macros::command, Args, CommandResult},
|
framework::standard::{macros::command, Args, CommandResult},
|
||||||
model::channel::Message,
|
model::channel::Message,
|
||||||
|
@ -17,7 +17,7 @@ use youmubot_prelude::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
discord::{display::ScoreListStyle, oppai_cache::Accuracy, BeatmapWithMode},
|
discord::{db::OsuUser, display::ScoreListStyle, oppai_cache::Accuracy, BeatmapWithMode},
|
||||||
models::{Mode, Mods},
|
models::{Mode, Mods},
|
||||||
request::UserID,
|
request::UserID,
|
||||||
Score,
|
Score,
|
||||||
|
@ -25,21 +25,57 @@ use crate::{
|
||||||
|
|
||||||
use super::{ModeArg, OsuEnv};
|
use super::{ModeArg, OsuEnv};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||||
enum RankQuery {
|
enum RankQuery {
|
||||||
Total,
|
#[default]
|
||||||
|
PP,
|
||||||
|
TotalPP,
|
||||||
MapLength,
|
MapLength,
|
||||||
Mode(Mode),
|
// MapAge,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RankQuery {
|
||||||
|
// fn col_name(&self) -> &'static str {
|
||||||
|
// match self {
|
||||||
|
// RankQuery::PP => "pp",
|
||||||
|
// RankQuery::TotalPP => "Total pp",
|
||||||
|
// RankQuery::MapLength => "Map length",
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
fn extract_row(&self, mode: Mode, ou: &OsuUser) -> Cow<'static, str> {
|
||||||
|
match self {
|
||||||
|
RankQuery::PP => ou
|
||||||
|
.modes
|
||||||
|
.get(&mode)
|
||||||
|
.map(|v| format!("{:.02}", v.pp).into())
|
||||||
|
.unwrap_or_else(|| "-".into()),
|
||||||
|
RankQuery::TotalPP => {
|
||||||
|
format!("{:.02}", ou.modes.values().map(|v| v.pp).sum::<f64>()).into()
|
||||||
|
}
|
||||||
|
RankQuery::MapLength => ou
|
||||||
|
.modes
|
||||||
|
.get(&mode)
|
||||||
|
.map(|v| {
|
||||||
|
let len = v.map_length;
|
||||||
|
let trunc_secs = len.floor() as u64;
|
||||||
|
let minutes = trunc_secs / 60;
|
||||||
|
let seconds = len - (60 * minutes) as f64;
|
||||||
|
format!("{}m{:05.2}s", minutes, seconds).into()
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| "-".into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for RankQuery {
|
impl FromStr for RankQuery {
|
||||||
type Err = <ModeArg as FromStr>::Err;
|
type Err = String;
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
match s {
|
match s {
|
||||||
"total" => Ok(RankQuery::Total),
|
"pp" => Ok(RankQuery::PP),
|
||||||
|
"total" | "total-pp" => Ok(RankQuery::TotalPP),
|
||||||
"map-length" => Ok(RankQuery::MapLength),
|
"map-length" => Ok(RankQuery::MapLength),
|
||||||
_ => ModeArg::from_str(s).map(|ModeArg(m)| RankQuery::Mode(m)),
|
_ => Err(format!("not a query: {}", s)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -51,52 +87,61 @@ impl FromStr for RankQuery {
|
||||||
#[only_in(guilds)]
|
#[only_in(guilds)]
|
||||||
pub async fn server_rank(ctx: &Context, m: &Message, mut args: Args) -> CommandResult {
|
pub async fn server_rank(ctx: &Context, m: &Message, mut args: Args) -> CommandResult {
|
||||||
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
|
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
|
||||||
let mode = args
|
let mode = args.find::<ModeArg>().map(|v| v.0).unwrap_or(Mode::Std);
|
||||||
.single::<RankQuery>()
|
let query = args.single::<RankQuery>().unwrap_or_default();
|
||||||
.unwrap_or(RankQuery::Mode(Mode::Std));
|
|
||||||
let guild = m.guild_id.expect("Guild-only command");
|
let guild = m.guild_id.expect("Guild-only command");
|
||||||
|
|
||||||
let osu_users = env
|
let mut users = env
|
||||||
.saved_users
|
.saved_users
|
||||||
.all()
|
.all()
|
||||||
.await?
|
.await?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|v| (v.user_id, v))
|
.map(|v| (v.user_id, v))
|
||||||
.collect::<HashMap<_, _>>();
|
.collect::<HashMap<_, _>>();
|
||||||
|
let mut users = env
|
||||||
let users = env
|
|
||||||
.prelude
|
.prelude
|
||||||
.members
|
.members
|
||||||
.query_members(&ctx, guild)
|
.query_members(&ctx, guild)
|
||||||
.await?
|
.await?
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|m| osu_users.get(&m.user.id).map(|ou| (m, ou)))
|
.filter_map(|m| users.remove(&m.user.id).map(|ou| (m.clone(), ou)))
|
||||||
.filter_map(|(member, osu_user)| {
|
.collect::<Vec<_>>();
|
||||||
let pp = match mode {
|
let last_update = users
|
||||||
RankQuery::Total if osu_user.pp.iter().any(|v| v.is_some_and(|v| v > 0.0)) => {
|
.iter()
|
||||||
Some(osu_user.pp.iter().map(|v| v.unwrap_or(0.0)).sum())
|
.filter_map(|(_, u)| {
|
||||||
}
|
if query == RankQuery::TotalPP {
|
||||||
RankQuery::MapLength => osu_user.pp.get(Mode::Std as usize).and_then(|v| *v),
|
u.modes.values().map(|v| v.last_update).min()
|
||||||
RankQuery::Mode(m) => osu_user.pp.get(m as usize).and_then(|v| *v),
|
} else {
|
||||||
_ => None,
|
u.modes.get(&mode).map(|v| v.last_update)
|
||||||
}?;
|
}
|
||||||
Some((pp, member.user.name.clone(), osu_user))
|
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.min();
|
||||||
let last_update = users.iter().map(|(_, _, a)| a.last_update).min();
|
let sort_fn: Box<dyn Fn(&(Member, OsuUser), &(Member, OsuUser)) -> std::cmp::Ordering> =
|
||||||
let mut users = users
|
match query {
|
||||||
.into_iter()
|
RankQuery::PP => Box::new(|(_, a), (_, b)| {
|
||||||
.map(|(a, b, u)| (a, (b, u.clone())))
|
a.modes
|
||||||
.collect::<Vec<_>>();
|
.get(&mode)
|
||||||
if matches!(mode, RankQuery::MapLength) {
|
.map(|v| v.pp)
|
||||||
users.sort_by(|(_, (_, a)), (_, (_, b))| {
|
.partial_cmp(&b.modes.get(&mode).map(|v| v.pp))
|
||||||
(b.std_weighted_map_length)
|
.unwrap()
|
||||||
.partial_cmp(&a.std_weighted_map_length)
|
}),
|
||||||
.unwrap_or(std::cmp::Ordering::Equal)
|
RankQuery::TotalPP => Box::new(|(_, a), (_, b)| {
|
||||||
});
|
a.modes
|
||||||
} else {
|
.values()
|
||||||
users.sort_by(|(a, _), (b, _)| (*b).partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
|
.map(|v| v.pp)
|
||||||
}
|
.sum::<f64>()
|
||||||
|
.partial_cmp(&b.modes.values().map(|v| v.pp).sum())
|
||||||
|
.unwrap()
|
||||||
|
}),
|
||||||
|
RankQuery::MapLength => Box::new(|(_, a), (_, b)| {
|
||||||
|
a.modes
|
||||||
|
.get(&mode)
|
||||||
|
.map(|v| v.map_length)
|
||||||
|
.partial_cmp(&b.modes.get(&mode).map(|v| v.map_length))
|
||||||
|
.unwrap()
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
users.sort_unstable_by(sort_fn);
|
||||||
|
|
||||||
if users.is_empty() {
|
if users.is_empty() {
|
||||||
m.reply(&ctx, "No saved users in the current server...")
|
m.reply(&ctx, "No saved users in the current server...")
|
||||||
|
@ -120,7 +165,7 @@ pub async fn server_rank(ctx: &Context, m: &Message, mut args: Args) -> CommandR
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
let users = &users[start..end];
|
let users = &users[start..end];
|
||||||
let table = if matches!(mode, RankQuery::Mode(Mode::Std) | RankQuery::MapLength) {
|
let table = {
|
||||||
const HEADERS: [&'static str; 5] =
|
const HEADERS: [&'static str; 5] =
|
||||||
["#", "pp", "Map length", "Username", "Member"];
|
["#", "pp", "Map length", "Username", "Member"];
|
||||||
const ALIGNS: [Align; 5] = [Right, Right, Right, Left, Left];
|
const ALIGNS: [Align; 5] = [Right, Right, Right, Left, Left];
|
||||||
|
@ -128,39 +173,13 @@ pub async fn server_rank(ctx: &Context, m: &Message, mut args: Args) -> CommandR
|
||||||
let table = users
|
let table = users
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, (pp, (mem, ou)))| {
|
.map(|(i, (mem, ou))| {
|
||||||
let map_length = match ou.std_weighted_map_length {
|
|
||||||
Some(len) => {
|
|
||||||
let trunc_secs = len.floor() as u64;
|
|
||||||
let minutes = trunc_secs / 60;
|
|
||||||
let seconds = len - (60 * minutes) as f64;
|
|
||||||
format!("{}m{:05.2}s", minutes, seconds)
|
|
||||||
}
|
|
||||||
None => "unknown".to_owned(),
|
|
||||||
};
|
|
||||||
[
|
[
|
||||||
format!("{}", 1 + i + start),
|
format!("{}", 1 + i + start),
|
||||||
format!("{:.2}", pp),
|
RankQuery::PP.extract_row(mode, ou).to_string(),
|
||||||
map_length,
|
RankQuery::MapLength.extract_row(mode, ou).to_string(),
|
||||||
ou.username.clone().into_owned(),
|
ou.username.to_string(),
|
||||||
mem.clone(),
|
mem.distinct(),
|
||||||
]
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
table_formatting(&HEADERS, &ALIGNS, table)
|
|
||||||
} else {
|
|
||||||
const HEADERS: [&'static str; 4] = ["#", "pp", "Username", "Member"];
|
|
||||||
const ALIGNS: [Align; 4] = [Right, Right, Left, Left];
|
|
||||||
|
|
||||||
let table = users
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(i, (pp, (mem, ou)))| {
|
|
||||||
[
|
|
||||||
format!("{}", 1 + i + start),
|
|
||||||
format!("{:.2}", pp),
|
|
||||||
ou.username.clone().into_owned(),
|
|
||||||
mem.clone(),
|
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
Loading…
Add table
Reference in a new issue