Skip to content

Commit

Permalink
feat: add max stars and max clan pp difference parameters to the clan…
Browse files Browse the repository at this point in the history
… wars playlist generator
  • Loading branch information
motzel committed Feb 27, 2024
1 parent 9653ae7 commit 92e62d8
Show file tree
Hide file tree
Showing 6 changed files with 176 additions and 19 deletions.
32 changes: 21 additions & 11 deletions src/discord/bot/beatleader/clan.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#![allow(clippy::too_many_arguments)]

use std::collections::HashMap;
use std::fmt::{Display, Formatter};
use std::hash::{Hash, Hasher};
Expand All @@ -15,6 +17,7 @@ use crate::beatleader::pp::{
calculate_acc_from_pp, calculate_pp_boundary, StarRating, CLAN_WEIGHT_COEFFICIENT,
};
use crate::beatleader::{BlContext, DataWithMeta, SortOrder};
use crate::discord::bot::beatleader::player::Player;
use crate::storage::player_scores::PlayerScoresRepository;
use crate::storage::{StorageKey, StorageValue};
use crate::{beatleader, BL_CLIENT};
Expand Down Expand Up @@ -416,6 +419,8 @@ pub(crate) struct PlaylistCustomData {
pub playlist_type: ClanWarsSort,
pub last_played: ClanWarsPlayDate,
pub count: u32,
pub max_stars: Option<f64>,
pub max_clan_pp_diff: Option<f64>,
}

pub(crate) type PlaylistId = String;
Expand All @@ -439,17 +444,19 @@ impl Playlist {
player_scores_repository: &Arc<PlayerScoresRepository>,
server_url: &str,
clan_tag: ClanTag,
player_id: PlayerId,
player: Player,
playlist_type: ClanWarsSort,
last_played: ClanWarsPlayDate,
count: u32,
max_stars: Option<f64>,
max_clan_pp_diff: Option<f64>,
) -> Result<Self, String> {
let maps_list = BL_CLIENT
.clan()
.maps_by_clan_tag(
clan_tag.as_str(),
&[
ClanMapsParam::Count(100),
ClanMapsParam::Count(200),
ClanMapsParam::Page(1),
ClanMapsParam::Order(SortOrder::Descending),
ClanMapsParam::Context(BlContext::General),
Expand All @@ -464,9 +471,7 @@ impl Playlist {

let maps_list = maps_list.unwrap();

if maps_list.list.data.is_empty() {
return Err("No maps of the selected type".to_string());
}
let player_id = player.id.clone();

let player_leaderboard_ids = player_scores_repository
.get(&player_id)
Expand All @@ -479,24 +484,27 @@ impl Playlist {

let played_filter: Option<DateTime<Utc>> = last_played.clone().into();

let max_stars_value = max_stars.unwrap_or(player.top_stars).max(0.0);
let max_clan_pp_diff_value = max_clan_pp_diff.unwrap_or(player.top_pp).max(0.0);

let playlist_maps = maps_list
.list
.data
.into_iter()
.filter(|score| {
let score_timepost = player_leaderboard_ids.get(&score.leaderboard.id);
let map_clan_pp_diff = score.pp.abs();
let map_stars = score.leaderboard.difficulty.stars;

score_timepost.is_none()
(score_timepost.is_none()
|| (played_filter.is_some()
&& played_filter.unwrap() > *score_timepost.unwrap())
&& played_filter.unwrap() > *score_timepost.unwrap()))
&& (max_stars_value == 0.0 || map_stars <= max_stars_value)
&& (max_clan_pp_diff_value == 0.0 || map_clan_pp_diff <= max_clan_pp_diff_value)
})
.take(count as usize)
.collect::<Vec<_>>();

if playlist_maps.is_empty() {
return Err("No maps meeting the criteria".to_string());
}

let playlist_title = format!(
"{}-clan wars-{}",
clan_tag,
Expand All @@ -520,6 +528,8 @@ impl Playlist {
playlist_type,
last_played,
count,
max_stars,
max_clan_pp_diff,
}),
..Playlist::default()
})
Expand Down
8 changes: 7 additions & 1 deletion src/discord/bot/commands/clan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,10 @@ pub(crate) async fn cmd_clan_wars_playlist(
#[description = "Playlist type (default: To Conquer)"] playlist_type: Option<ClanWarsSort>,
#[description = "Last played (default: Never)"] played: Option<ClanWarsPlayDate>,
#[description = "Maps count (max: 100, default: 100)"] count: Option<u8>,
#[description = "Maps map stars (default: player's top stars)"] max_stars: Option<f64>,
#[description = "Maps clan pp difference (default: player's top pp)"] max_clan_pp_diff: Option<
f64,
>,
) -> Result<(), Error> {
ctx.defer().await?;

Expand Down Expand Up @@ -420,10 +424,12 @@ pub(crate) async fn cmd_clan_wars_playlist(
&ctx.data().player_scores_repository.clone(),
&ctx.data().settings.server.url.clone(),
clan_tag,
player.id,
player,
playlist_type_filter,
played_filter,
count as u32,
max_stars,
max_clan_pp_diff,
)
.await
{
Expand Down
6 changes: 6 additions & 0 deletions src/storage/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,12 @@ where
}
}

pub(super) async fn keys(&self) -> Vec<K> {
let read_lock = self.state.read().await;

read_lock.keys().cloned().collect::<Vec<_>>()
}

pub(super) async fn values(&self) -> Vec<V> {
let storage_name = self.storage.get_name();

Expand Down
122 changes: 118 additions & 4 deletions src/storage/player.rs
Original file line number Diff line number Diff line change
@@ -1,28 +1,61 @@
#![allow(clippy::blocks_in_conditions)]

use std::fmt::{Display, Formatter};
use std::sync::Arc;

use poise::serenity_prelude::{GuildId, UserId};
use serde::{Deserialize, Serialize};
use tokio_util::sync::CancellationToken;
use tracing::{debug, trace};

use crate::beatleader::player::{Player as BlPlayer, PlayerId};
use crate::discord::bot::beatleader::player::fetch_player_from_bl;
use crate::discord::bot::beatleader::player::Player as BotPlayer;
use crate::discord::bot::beatleader::player::{fetch_player_from_bl, Player};
use crate::discord::bot::beatleader::score::fetch_ranked_scores_stats;
use crate::storage::persist::PersistInstance;
use crate::storage::player_scores::PlayerScoresRepository;
use crate::storage::{CachedStorage, Storage, StorageError};
use crate::storage::{CachedStorage, Storage, StorageError, StorageKey, StorageValue};

use super::Result;

#[derive(Debug)]
pub(crate) struct PlayerRepository {
storage: CachedStorage<UserId, BotPlayer>,
user_player_idx_repository: PlayerUserIdxRepository,
}

impl<'a> PlayerRepository {
pub(crate) async fn new(persist: Arc<PersistInstance>) -> Result<PlayerRepository> {
let storage = CachedStorage::new(Storage::new("players", persist.clone())).await?;
let user_player_idx_repository = PlayerUserIdxRepository::new(persist).await?;

// refresh index at start if needed
if storage.len().await != user_player_idx_repository.len().await {
let idx_values = user_player_idx_repository.all().await;
let users_to_refresh = storage
.keys()
.await
.into_iter()
.filter(|user_id| !idx_values.iter().any(|idx| idx.user_id == *user_id))
.collect::<Vec<_>>();

tracing::debug!(
"Refreshing player-user index ({} items)...",
users_to_refresh.len()
);

for user_id in users_to_refresh {
if let Some(Player { id, .. }) = storage.get(&user_id).await {
let _ = user_player_idx_repository.set(id, user_id).await?;
}
}

tracing::debug!("player-user index refreshed.");
}

Ok(Self {
storage: CachedStorage::new(Storage::new("players", persist)).await?,
storage,
user_player_idx_repository,
})
}

Expand All @@ -35,7 +68,24 @@ impl<'a> PlayerRepository {
}

pub(crate) async fn get(&self, user_id: &UserId) -> Option<BotPlayer> {
self.storage.get(user_id).await
match self.storage.get(user_id).await {
None => None,
Some(player) => {
let _ = self
.user_player_idx_repository
.set(player.id.clone(), player.user_id)
.await;

Some(player)
}
}
}

pub(crate) async fn get_by_player_id(&self, player_id: &PlayerId) -> Option<BotPlayer> {
match self.user_player_idx_repository.get(player_id).await {
None => None,
Some(user_id) => self.storage.get(&user_id).await,
}
}

pub(crate) async fn link(
Expand Down Expand Up @@ -369,9 +419,73 @@ impl<'a> PlayerRepository {
{
Some(player) => {
debug!("User {} linked with BL player {}.", user_id, player_id);

self.user_player_idx_repository
.set(player.id.clone(), user_id)
.await?;

Ok(player)
}
None => Err(StorageError::Unknown),
}
}
}

#[derive(Debug, Clone, Serialize, Deserialize, Hash, Eq, PartialEq)]
pub(crate) struct PlayerUserIdx {
pub player_id: PlayerId,
pub user_id: UserId,
}

impl StorageKey for PlayerUserIdx {}
impl StorageValue<PlayerId> for PlayerUserIdx {
fn get_key(&self) -> PlayerId {
self.player_id.clone()
}
}

impl Display for PlayerUserIdx {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}-{}", self.player_id.as_str(), self.user_id)
}
}

#[derive(Debug)]
pub(crate) struct PlayerUserIdxRepository {
storage: CachedStorage<PlayerId, PlayerUserIdx>,
}

impl<'a> PlayerUserIdxRepository {
pub(crate) async fn new(persist: Arc<PersistInstance>) -> Result<PlayerUserIdxRepository> {
Ok(Self {
storage: CachedStorage::new(Storage::new("player-user-idx", persist)).await?,
})
}

pub(crate) async fn get(&self, player_id: &PlayerId) -> Option<UserId> {
match self.storage.get(player_id).await {
None => None,
Some(player_user_idx) => Some(player_user_idx.user_id),
}
}

pub(crate) async fn set(&self, player_id: PlayerId, user_id: UserId) -> Result<PlayerUserIdx> {
self.storage
.set(
&player_id,
PlayerUserIdx {
player_id: player_id.clone(),
user_id,
},
)
.await
}

pub(crate) async fn all(&self) -> Vec<PlayerUserIdx> {
self.storage.values().await
}

pub(crate) async fn len(&self) -> usize {
self.storage.len().await
}
}
9 changes: 7 additions & 2 deletions src/webserver/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use tracing::{info, warn};
use crate::config::Settings;
use crate::persist::CommonData;
use crate::storage::guild::GuildSettingsRepository;
use crate::storage::player::PlayerRepository;
use crate::storage::player_oauth_token::PlayerOAuthTokenRepository;
use crate::storage::player_scores::PlayerScoresRepository;
use crate::storage::playlist::PlaylistRepository;
Expand All @@ -21,6 +22,7 @@ mod routes;
pub struct WebServer {
pub guild_settings_repository: Arc<GuildSettingsRepository>,
pub player_oauth_token_repository: Arc<PlayerOAuthTokenRepository>,
pub players_repository: Arc<PlayerRepository>,
pub player_scores_repository: Arc<PlayerScoresRepository>,
pub playlists_repository: Arc<PlaylistRepository>,
pub settings: Settings,
Expand All @@ -30,10 +32,11 @@ pub struct WebServer {

#[derive(Debug, Clone)]
pub(crate) struct AppState {
pub playlists_repository: Arc<PlaylistRepository>,
pub player_scores_repository: Arc<PlayerScoresRepository>,
pub guild_settings_repository: Arc<GuildSettingsRepository>,
pub player_oauth_token_repository: Arc<PlayerOAuthTokenRepository>,
pub players_repository: Arc<PlayerRepository>,
pub player_scores_repository: Arc<PlayerScoresRepository>,
pub playlists_repository: Arc<PlaylistRepository>,
pub settings: Settings,
}

Expand All @@ -42,6 +45,7 @@ impl WebServer {
Self {
guild_settings_repository: data.guild_settings_repository,
player_oauth_token_repository: data.player_oauth_token_repository,
players_repository: data.players_repository,
player_scores_repository: data.player_scores_repository,
playlists_repository: data.playlists_repository,
settings: data.settings,
Expand All @@ -63,6 +67,7 @@ impl WebServer {
let state = AppState {
guild_settings_repository: self.guild_settings_repository,
player_oauth_token_repository: self.player_oauth_token_repository,
players_repository: self.players_repository,
player_scores_repository: self.player_scores_repository,
playlists_repository: self.playlists_repository,
settings: self.settings,
Expand Down
18 changes: 17 additions & 1 deletion src/webserver/routes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,14 +147,30 @@ async fn playlist(
);
}

let player =
match app_state
.players_repository
.get_by_player_id(&player_id)
.await
{
Some(player) => player,
None => return (
StatusCode::NOT_FOUND,
Json(json!({"error": {"code": "player_not_found", "message": "Player not found"}}))
.into_response(),
),
};

match Playlist::for_clan_player(
&app_state.player_scores_repository,
app_state.settings.server.url.as_str(),
custom_data.clan_tag.clone(),
custom_data.player_id.clone(),
player,
custom_data.playlist_type.clone(),
custom_data.last_played.clone(),
custom_data.count,
custom_data.max_stars,
custom_data.max_clan_pp_diff,
)
.await
{
Expand Down

0 comments on commit 92e62d8

Please sign in to comment.