From 275f5c9305910b5ac3c8ec2ebb852a3e26098d67 Mon Sep 17 00:00:00 2001 From: Joey Hines Date: Sun, 8 Jan 2023 16:23:50 -0700 Subject: [PATCH] Added basic role handling + Games now have a config for which roles they have + Roles are assigned at game start + Roles don't function within the bot, except the spy + Added a spy listener + Clippy + fmt --- src/config.rs | 2 + src/discord/commands.rs | 30 +++++++-- src/discord/helper.rs | 8 +++ src/error.rs | 4 ++ src/game/global_data.rs | 5 +- src/game/listener/mod.rs | 4 +- src/game/message_router.rs | 26 ++++---- src/game/mod.rs | 1 + src/game/player_data.rs | 2 + src/game/role/mod.rs | 128 +++++++++++++++++++++++++++++++++++++ src/game/role/spy.rs | 64 +++++++++++++++++++ src/main.rs | 6 +- src/messages/mod.rs | 31 +++++++++ 13 files changed, 289 insertions(+), 22 deletions(-) create mode 100644 src/game/role/mod.rs create mode 100644 src/game/role/spy.rs diff --git a/src/config.rs b/src/config.rs index 0b23c5c..e424a08 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,7 @@ use std::path::Path; use std::path::PathBuf; +use crate::game::role::Role; use config::{Config, File}; use serde::{Deserialize, Serialize}; use structopt::StructOpt; @@ -21,6 +22,7 @@ pub struct GameConfig { pub player_group_name: String, pub profile_album_hash: String, pub whispers_allowed: bool, + pub roles: Vec, pub first_name: Vec, pub last_name: Vec, pub messages: MessageConfig, diff --git a/src/discord/commands.rs b/src/discord/commands.rs index a5e5a16..3684a6a 100644 --- a/src/discord/commands.rs +++ b/src/discord/commands.rs @@ -21,6 +21,7 @@ use crate::game::message_router::{ dispatch_message, Median, MessageDest, MessageSource, WoxlfMessage, }; use crate::game::player_data::PlayerData; +use crate::game::role::Role; use crate::game::Phase; use crate::messages::DiscordUser; @@ -93,10 +94,15 @@ async fn start(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { profile_pics.shuffle(&mut thread_rng()); + let mut roles = global_data.game_cfg()?.roles.clone(); + + roles.shuffle(&mut thread_rng()); + for player in players { let first_name = first_names.pop(); let last_name = last_names.pop(); let profile_pic_url = profile_pics.pop(); + let role = roles.pop(); add_user_to_game( ctx, &guild, @@ -105,6 +111,7 @@ async fn start(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { first_name, last_name, profile_pic_url, + role, ) .await?; } @@ -369,6 +376,7 @@ async fn test_theme(ctx: &Context, msg: &Message, args: Args) -> CommandResult { profile_pic_url: "".to_string(), channel_webhook_id: 0, alive: true, + role: Role::Villager, }; let player_1_discord = DiscordUser { @@ -384,6 +392,7 @@ async fn test_theme(ctx: &Context, msg: &Message, args: Args) -> CommandResult { profile_pic_url: "".to_string(), channel_webhook_id: 0, alive: false, + role: Role::Villager, }; let mut players = vec![test_player_1.clone(), test_player_2.clone()]; @@ -623,7 +632,12 @@ async fn players(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { if msg.channel_id.0 == global_data.cfg.discord_config.host_channel { let guild = msg.guild(&ctx.cache).unwrap(); let member = guild.members.get(&UserId::from(player.discord_id)).unwrap(); - msg_builder.push_line(format!(" ({})", member.display_name())); + msg_builder.push_line(format!( + " ({}) [{} {}]", + member.display_name(), + player.role, + player.role.seer_color() + )); } else { msg_builder.push_line(""); } @@ -638,14 +652,20 @@ async fn players(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { #[aliases("pm", "w")] #[description = "Send a private message to another player."] async fn whisper(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { + let data = ctx.data.read().await; + let global_data = data.get::().unwrap(); + let mut global_data = global_data.lock().await; + + if !global_data.game_cfg()?.whispers_allowed { + msg.reply(&ctx.http, "No private messages are allowed in this game") + .await?; + return Ok(()); + } + if args.len() < 2 { msg.reply(&ctx.http, "Need a recipient and message!") .await?; } else { - let data = ctx.data.read().await; - let global_data = data.get::().unwrap(); - let mut global_data = global_data.lock().await; - let target = args.single::()?; let pm = args.rest(); diff --git a/src/discord/helper.rs b/src/discord/helper.rs index fae5e2d..161ae3b 100644 --- a/src/discord/helper.rs +++ b/src/discord/helper.rs @@ -10,8 +10,10 @@ use crate::error::WoxlfError; use crate::game::game_state::PhaseDuration; use crate::game::global_data::GlobalData; use crate::game::player_data::PlayerData; +use crate::game::role::Role; use crate::imgur::Image; +#[allow(clippy::too_many_arguments)] pub async fn add_user_to_game( ctx: &Context, guild: &Guild, @@ -20,6 +22,7 @@ pub async fn add_user_to_game( first_name: Option, last_name: Option, profile_pic: Option, + role: Option, ) -> error::Result { if first_name.is_none() && last_name.is_none() { return Err(WoxlfError::RanOutOfCodenames); @@ -29,6 +32,10 @@ pub async fn add_user_to_game( return Err(WoxlfError::RanOutOfProfilePics); } + if role.is_none() { + return Err(WoxlfError::RanOutOfRoles); + } + let codename = global_data .templates()? .build_name(global_data, first_name, last_name) @@ -67,6 +74,7 @@ pub async fn add_user_to_game( channel_webhook_id: webhook.id.0, profile_pic_url: profile_pic.unwrap().link, alive: true, + role: role.unwrap(), }; global_data diff --git a/src/error.rs b/src/error.rs index 67de7ff..1e37f00 100644 --- a/src/error.rs +++ b/src/error.rs @@ -20,6 +20,8 @@ pub enum WoxlfError { TemplateError(tera::Error), RanOutOfProfilePics, UnsupportedMsgMedium, + RanOutOfRoles, + ConfigNotfound, } impl std::error::Error for WoxlfError {} @@ -45,6 +47,8 @@ impl Display for WoxlfError { WoxlfError::UnsupportedMsgMedium => { "Tried to send a message over an unsupported medium".to_string() } + WoxlfError::RanOutOfRoles => "Ran out of user roles".to_string(), + WoxlfError::ConfigNotfound => "Config not found".to_string(), }; write!(f, "Woxlf Error: {}", msg) diff --git a/src/game/global_data.rs b/src/game/global_data.rs index a8b0149..7adbafc 100644 --- a/src/game/global_data.rs +++ b/src/game/global_data.rs @@ -36,7 +36,10 @@ impl GlobalData { starting_phase: Phase, starting_phase_duration: Duration, ) -> Result<()> { - let game_config = self.cfg.get_game_config(game_name).unwrap(); + let game_config = self + .cfg + .get_game_config(game_name) + .ok_or(WoxlfError::ConfigNotfound)?; self.templates = Some(MessageTemplates::try_from(game_config.messages.clone())?); self.game_cfg = Some(game_config); diff --git a/src/game/listener/mod.rs b/src/game/listener/mod.rs index 7b13338..5a2eb67 100644 --- a/src/game/listener/mod.rs +++ b/src/game/listener/mod.rs @@ -83,8 +83,8 @@ pub enum Priority { } pub struct ListenerContext<'a> { - data: &'a GlobalData, - ctx: &'a Context, + pub data: &'a mut GlobalData, + pub ctx: &'a Context, } #[async_trait] diff --git a/src/game/message_router.rs b/src/game/message_router.rs index 84cec72..ce6fee4 100644 --- a/src/game/message_router.rs +++ b/src/game/message_router.rs @@ -38,7 +38,7 @@ impl Default for MessageDest { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Eq, PartialEq)] #[allow(dead_code)] pub enum Median { DirectMessage, @@ -133,7 +133,7 @@ fn filter_source_channel(player_data: &&PlayerData, msg_source: &MessageSource) true } -async fn send_webhook_msg( +pub async fn send_webhook_msg( http: &Http, webhook_id: WebhookId, username: &str, @@ -162,23 +162,17 @@ async fn send_webhook_msg( Ok(()) } -async fn send_private_message( +pub async fn send_private_message( http: &Http, - src_username: &str, dest: UserId, - msg: &str, + msg_content: &str, attachments: &Option>>, ) -> error::Result<()> { let dest_user = dest.to_user(http).await?; - let mut dm_message = MessageBuilder::new(); - - dm_message.push_bold_line_safe(format!("{} has sent you a DM:", src_username)); - dm_message.push(msg); - dest_user .dm(http, |msg| { - msg.content(dm_message); + msg.content(msg_content); if let Some(attachments) = attachments { msg.add_files(attachments.clone()); @@ -265,11 +259,15 @@ pub async fn send_message( .await?; } Median::DirectMessage => { + let dm_msg = MessageBuilder::new() + .push_bold_line_safe(format!("{} has sent you a private message:", &msg.get_message_username(global_data)?)) + .push(&msg.content) + .build(); + send_private_message( &ctx.http, - &msg.get_message_username(global_data)?, UserId(dest_player.discord_id), - &msg.content, + &dm_msg, &msg.attachments, ) .await?; @@ -283,6 +281,8 @@ pub async fn send_message( Ok(()) } +/// Send a message to the proper channels +/// Note safe to use in an event handler pub async fn dispatch_message( ctx: &Context, global_data: &mut GlobalData, diff --git a/src/game/mod.rs b/src/game/mod.rs index 6107fd7..dbf7a02 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -5,6 +5,7 @@ pub mod global_data; pub mod listener; pub mod message_router; pub mod player_data; +pub mod role; #[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq, Copy)] pub enum Phase { diff --git a/src/game/player_data.rs b/src/game/player_data.rs index 3299d52..8b68f86 100644 --- a/src/game/player_data.rs +++ b/src/game/player_data.rs @@ -1,3 +1,4 @@ +use crate::game::role::Role; use serde::{Deserialize, Serialize}; #[derive(Debug, Deserialize, Serialize, Clone)] @@ -9,6 +10,7 @@ pub struct PlayerData { pub profile_pic_url: String, pub channel_webhook_id: u64, pub alive: bool, + pub role: Role, } impl PlayerData { diff --git a/src/game/role/mod.rs b/src/game/role/mod.rs new file mode 100644 index 0000000..8e7bfa6 --- /dev/null +++ b/src/game/role/mod.rs @@ -0,0 +1,128 @@ +use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter}; +use crate::game::listener::Listeners; +use crate::game::role::spy::SpyListener; + +mod spy; + +#[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)] +pub enum RoleColor { + Green, + Red, + Blue, + Purple, +} + +impl Display for RoleColor { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let s = match self { + RoleColor::Green => "Green", + RoleColor::Red => "Red", + RoleColor::Blue => "Blue", + RoleColor::Purple => "Purple", + }; + + write!(f, "{}", s) + } +} + +#[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)] +pub enum Role { + // Human Roles + Villager, + Seer, + Guardian, + Jailer, + Coroner, + Vigilante, + Hunter, + Psychic, + Miller, + Herring, + Spy, + Jester, + Mason, + Shepard, + // Wolf Roles + MasterWolf, + WolfShaman, + Wolf, + // Custom Role + Custom(String, RoleColor), +} + +impl Display for Role { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let s = match self { + Role::Villager => "Villager", + Role::Seer => "Seer", + Role::Guardian => "Guardian", + Role::Jailer => "Jailer", + Role::Coroner => "Coroner", + Role::Vigilante => "Vigilante", + Role::Hunter => "Hunter", + Role::Psychic => "Psychic", + Role::Miller => "Miller", + Role::Herring => "Herring", + Role::Spy => "Spy", + Role::Jester => "Jester", + Role::Mason => "Mason", + Role::Shepard => "Shepard", + Role::MasterWolf => "Master Wolf", + Role::WolfShaman => "Wolf Shaman", + Role::Wolf => "Wolf", + Role::Custom(role_name, _) => role_name, + }; + + write!(f, "{}", s) + } +} + +impl Role { + /// Color as seen by host or seer + pub fn seer_color(&self) -> RoleColor { + match self { + Role::Villager => RoleColor::Green, + Role::Seer => RoleColor::Blue, + Role::Guardian => RoleColor::Blue, + Role::Jailer => RoleColor::Blue, + Role::Coroner => RoleColor::Blue, + Role::Vigilante => RoleColor::Blue, + Role::Hunter => RoleColor::Blue, + Role::Psychic => RoleColor::Blue, + Role::Miller => RoleColor::Red, + Role::Herring => RoleColor::Blue, + Role::Spy => RoleColor::Blue, + Role::Jester => RoleColor::Purple, + Role::Mason => RoleColor::Green, + Role::Shepard => RoleColor::Green, + Role::MasterWolf => RoleColor::Green, + Role::WolfShaman => RoleColor::Red, + Role::Wolf => RoleColor::Red, + Role::Custom(_, color) => color.clone(), + } + } + + /// Color as seen by player + pub fn player_color(&self) -> RoleColor { + match self { + Role::Miller => RoleColor::Green, + _ => self.seer_color(), + } + } + + /// Role name as seen by the player + pub fn player_role_name(&self) -> String { + match self { + Role::Miller => Role::Villager.to_string(), + _ => self.to_string(), + } + } + + pub fn register_role_listener(&self, listeners: &mut Listeners) { + match self { + Role::Spy => listeners.add_listener(Box::new(SpyListener {})), + _ => {} + } + } +} diff --git a/src/game/role/spy.rs b/src/game/role/spy.rs new file mode 100644 index 0000000..2477b21 --- /dev/null +++ b/src/game/role/spy.rs @@ -0,0 +1,64 @@ +use crate::game::listener::{EventStatus, Listener, ListenerContext, Priority}; +use crate::game::message_router::{Median, MessageDest, MessageSource, send_private_message, WoxlfMessage}; +use crate::game::role::Role; +use serenity::async_trait; +use serenity::model::prelude::UserId; +use serenity::utils::MessageBuilder; + +#[derive(Debug)] +pub struct SpyListener {} + +#[async_trait] +impl Listener for SpyListener { + fn get_priority(&self) -> Priority { + Priority::Logging + } + + async fn on_chat( + &mut self, + ctx: &mut ListenerContext, + msg: &WoxlfMessage, + ) -> crate::error::Result { + if msg.median != Median::DirectMessage { + return Ok(EventStatus::Okay); + } + + let src_player = if let MessageSource::Player(p) = &msg.source { + p + } else { + return Ok(EventStatus::Okay); + }; + + let dest_player = if let MessageDest::Player(p) = &msg.dest { + p + } else { + return Ok(EventStatus::Okay); + }; + + + let spy_player = ctx + .data + .game_state()? + .player_data + .iter() + .find(|p| p.alive && p.role == Role::Spy); + + if let Some(spy_player) = spy_player { + if spy_player.discord_id == dest_player.discord_id || spy_player.discord_id == src_player.discord_id { + return Ok(EventStatus::Okay); + } + + let msg_content = MessageBuilder::default() + .push_bold_line_safe(format!( + "{} Sent {} a private message:", + src_player.codename, dest_player.codename + )) + .push(msg.content.clone()) + .build(); + + send_private_message(&ctx.ctx.http, UserId::from(spy_player.discord_id), &msg_content, &None).await? + } + + return Ok(EventStatus::Okay); + } +} diff --git a/src/main.rs b/src/main.rs index eb13fef..6016339 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,7 @@ use game::global_data::GlobalData; use crate::config::{Args, BotConfig}; use crate::game::listener::Listeners; +use crate::game::role::Role; mod config; mod discord; @@ -32,11 +33,14 @@ async fn main() { .expect("Unable to open saved game state."); } + let mut listeners = Listeners::default(); + Role::Spy.register_role_listener(&mut listeners); + let mut client = Client::builder(&bot_cfg.discord_config.token, GatewayIntents::all()) .event_handler(Handler {}) .framework(command_framework()) .type_map_insert::(Arc::new(Mutex::new(global_data))) - .type_map_insert::(Arc::new(Mutex::new(Listeners::default()))) + .type_map_insert::(Arc::new(Mutex::new(listeners))) .await .expect("Err creating client"); diff --git a/src/messages/mod.rs b/src/messages/mod.rs index b77c45a..f600870 100644 --- a/src/messages/mod.rs +++ b/src/messages/mod.rs @@ -1,5 +1,6 @@ use crate::config::MessageConfig; use crate::game::player_data::PlayerData; +use crate::game::role::Role; use crate::GlobalData; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; @@ -33,6 +34,34 @@ fn time_to_discord_time(time_flag: &str) -> TeraFnRet { ) } +fn role_name() -> TeraFnRet { + Box::new( + move |args: &HashMap| -> tera::Result { + match args.get("role") { + Some(val) => match tera::from_value::(val.clone()) { + Ok(v) => Ok(tera::to_value(v.player_role_name()).unwrap()), + Err(_) => Err("Failed to parse value as role".into()), + }, + None => Err("Missing parameter".into()), + } + }, + ) +} + +fn role_color() -> TeraFnRet { + Box::new( + move |args: &HashMap| -> tera::Result { + match args.get("role") { + Some(val) => match tera::from_value::(val.clone()) { + Ok(v) => Ok(tera::to_value(v.player_color().to_string()).unwrap()), + Err(_) => Err("Failed to parse value as role".into()), + }, + None => Err("Missing parameter".into()), + } + }, + ) +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DiscordUser { pub(crate) display_name: String, @@ -157,6 +186,8 @@ impl TryFrom for MessageTemplates { templates.register_function("to_countdown", time_to_discord_time("R")); templates.register_function("to_local_time", time_to_discord_time("f")); + templates.register_function("player_color", role_color()); + templates.register_function("player_role", role_name()); templates.add_raw_template("welcome_message", &config.welcome_message)?; templates.add_raw_template("status_message", &config.status_message)?;