use std::collections::HashSet; use rand::prelude::SliceRandom; use rand::thread_rng; use serenity::framework::standard::macros::{command, group, help, hook}; use serenity::framework::standard::{ help_commands, Args, CommandGroup, CommandResult, HelpOptions, }; use serenity::framework::StandardFramework; use serenity::model::guild::Member; use serenity::model::id::ChannelId; use serenity::model::prelude::{Message, UserId}; use serenity::prelude::Context; use serenity::utils::MessageBuilder; use crate::discord::helper::{ add_user_to_game, build_system_message, parse_duration_arg, send_msg_to_player_channels, }; use crate::error::{Result, WoxlfError}; use crate::game::global_data::GlobalData; use crate::game::MessageSource; use crate::game::Phase; #[group] #[commands(start, say, end, broadcast, next_phase, terminate, add_time)] struct Host; #[command] #[only_in(guilds)] #[allowed_roles("wolfx host")] async fn start(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { msg.channel_id.say(&ctx.http, "Starting game").await?; let mut data = ctx.data.write().await; let global_data = data.get_mut::().unwrap(); let guild = msg.guild(&ctx.cache).await.unwrap(); let mut global_data = global_data.lock().await; let duration = parse_duration_arg(&mut args).await?; global_data.start_game(Phase::Night, duration.into()); let players: Result> = args .iter::() .flatten() .map(|discord_id| { let discord_id = match discord_id.parse::() { Ok(discord_id) => discord_id, Err(_) => { return Err(WoxlfError::DiscordIdParseError(discord_id)); } }; if let Some(discord_user) = guild.members.get(&UserId::from(discord_id)) { Ok(discord_user) } else { Err(WoxlfError::DiscordIdParseError(discord_id.to_string())) } }) .collect(); let mut players = match players { Ok(players) => players, Err(e) => { let err_msg = match e { WoxlfError::DiscordIdParseError(e) => { format!("Error parsing '{}' as a discord user id.", e) } _ => "Internal bot error".to_string(), }; msg.reply(&ctx.http, err_msg).await?; return Ok(()); } }; players.shuffle(&mut thread_rng()); for player in players { add_user_to_game(ctx, &guild, &mut global_data, player).await?; } global_data.save_game_state().unwrap(); Ok(()) } #[command] #[only_in(guilds)] #[allowed_roles("wolfx host")] async fn end(ctx: &Context, msg: &Message, mut _args: Args) -> CommandResult { let mut data = ctx.data.write().await; let global_data = data.get_mut::().unwrap(); let guild = msg.guild(&ctx.cache).await.unwrap(); let mut global_data = global_data.lock().await; for player_data in &global_data.game_state_mut()?.player_data { let channel = guild .channels .get(&ChannelId::from(player_data.channel)) .unwrap(); channel.delete(&ctx.http).await?; } global_data.clear_game_state()?; msg.reply(&ctx.http, "Game ended!").await.unwrap(); Ok(()) } #[command] #[only_in(guilds)] #[allowed_roles("wolfx host")] async fn say(ctx: &Context, msg: &Message, args: Args) -> CommandResult { let mut data = ctx.data.write().await; let global_data = data.get_mut::().unwrap(); let guild = msg.guild(&ctx.cache).await.unwrap(); let mut global_data = global_data.lock().await; let msg = format!("**wOxlf **> {}", args.rest()); send_msg_to_player_channels( ctx, &guild, &mut global_data, MessageSource::Host, &msg, None, false, ) .await?; Ok(()) } #[command] #[only_in(guilds)] #[allowed_roles("wolfx host")] async fn broadcast(ctx: &Context, msg: &Message, args: Args) -> CommandResult { let mut data = ctx.data.write().await; let global_data = data.get_mut::().unwrap(); let guild = msg.guild(&ctx.cache).await.unwrap(); let mut global_data = global_data.lock().await; let msg = build_system_message(args.rest()); send_msg_to_player_channels( ctx, &guild, &mut global_data, MessageSource::Automated, &msg, None, true, ) .await?; Ok(()) } #[command] #[only_in(guilds)] #[allowed_roles("wolfx host")] async fn next_phase(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { let mut data = ctx.data.write().await; let global_data = data.get_mut::().unwrap(); let guild = msg.guild(&ctx.cache).await.unwrap(); let mut global_data = global_data.lock().await; let duration = parse_duration_arg(&mut args).await?; global_data.game_state_mut()?.next_phase(duration.into()); let broadcast = MessageBuilder::new() .push_line(args.rest()) .push_line("") .push(global_data.print_game_status()) .build(); let broadcast = build_system_message(&broadcast); send_msg_to_player_channels( ctx, &guild, &mut global_data, MessageSource::Automated, &broadcast, None, true, ) .await?; if global_data.game_state_mut()?.current_phase == Phase::Day { let vote_channel = guild .channels .get(&ChannelId::from(global_data.cfg.vote_channel)) .unwrap(); vote_channel .send_message(&ctx.http, |m| { m.content(format!( "**DAY {} VOTES:**", global_data.game_state_mut().unwrap().phase_number )) }) .await?; } msg.reply( &ctx.http, format!( "Phase has been cycled to {}.", &global_data.game_state_mut()?.current_phase ), ) .await?; global_data.save_game_state()?; Ok(()) } #[command] #[only_in(guilds)] #[allowed_roles("wolfx host")] async fn terminate(ctx: &Context, msg: &Message, args: Args) -> CommandResult { let mut data = ctx.data.write().await; let global_data = data.get_mut::().unwrap(); let guild = msg.guild(&ctx.cache).await.unwrap(); let mut global_data = global_data.lock().await; let target = args.rest().to_lowercase(); let index = global_data .game_state_mut()? .player_data .iter() .position(|p| p.codename.to_lowercase() == target); if let Some(index) = index { let player = global_data.game_state_mut()?.player_data.remove(index); let player_channel = guild .channels .get(&ChannelId::from(player.channel)) .unwrap(); player_channel.delete(&ctx.http).await.unwrap(); msg.reply( &ctx.http, format!("{} has been terminated.", player.codename), ) .await .unwrap(); } else { msg.reply(&ctx.http, "No subject found with that codename.") .await .unwrap(); } global_data.save_game_state().unwrap(); Ok(()) } #[command] #[only_in(guilds)] #[allowed_roles("wolfx host")] async fn add_time(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { let mut data = ctx.data.write().await; let global_data = data.get_mut::().unwrap(); let guild = msg.guild(&ctx.cache).await.unwrap(); let mut global_data = global_data.lock().await; let duration = parse_duration_arg(&mut args).await?; global_data .game_state_mut()? .add_time_to_phase(duration.into()); let broadcast = MessageBuilder::new() .push_line("EXPERIMENT PHASE HAS BEEN EXTENDED!!!") .push_line("") .push(global_data.print_game_status()) .build(); let broadcast = build_system_message(&broadcast); send_msg_to_player_channels( ctx, &guild, &mut global_data, MessageSource::Automated, &broadcast, None, true, ) .await?; msg.reply(&ctx.http, "Phase has been updated") .await .unwrap(); global_data.save_game_state().unwrap(); Ok(()) } #[group] #[commands(vote, status, players)] struct Player; #[command] #[only_in(guilds)] #[description = "Vote another subject for termination. $vote "] async fn vote(ctx: &Context, msg: &Message, args: Args) -> CommandResult { let mut data = ctx.data.write().await; let global_data = data.get_mut::().unwrap(); let guild = msg.guild(&ctx.cache).await.unwrap(); let mut global_data = global_data.lock().await; if global_data.game_state_mut()?.current_phase != Phase::Day { msg.reply( &ctx.http, "You can only select subject for termination during the day!", ) .await .unwrap(); return Ok(()); } if global_data .game_state_mut()? .get_player_from_channel(msg.channel_id.0) .is_some() { let target_player = global_data .game_state_mut()? .get_player_by_codename(args.rest()); if let Some(target_player) = target_player { let vote_channel = guild .channels .get(&ChannelId::from(global_data.cfg.vote_channel)) .unwrap(); let player_data = global_data .game_state_mut()? .get_player_from_channel_mut(msg.channel_id.0) .unwrap(); player_data.cast_vote(target_player.discord_id); vote_channel .send_message(&ctx.http, |m| { m.content(format!( "{} has selected {} for termination", &player_data.codename, target_player.codename )) }) .await .unwrap(); } else { msg.reply(&ctx.http, "Subject not found!").await.unwrap(); } } else { msg.reply( &ctx.http, "This command needs to be run in a game channel, goober", ) .await .unwrap(); } global_data.save_game_state().unwrap(); Ok(()) } #[command] #[only_in(guilds)] #[description = "Get the game status. $status"] async fn status(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { let mut data = ctx.data.write().await; let global_data = data.get_mut::().unwrap(); let mut global_data = global_data.lock().await; let mut msg_builder = MessageBuilder::new(); msg_builder.push(global_data.print_game_status()); if global_data.game_state_mut()?.current_phase == Phase::Day { msg_builder.push_line(""); let vote_tallies = global_data.game_state_mut()?.get_vote_tallies(); if vote_tallies.is_empty() { msg_builder.push_line("NO TERMINATION VOTES HAVE BEEN CAST"); } else { msg_builder.push_line("TERMINATION VOTE TALLIES:"); for (player, tally) in global_data.game_state_mut()?.get_vote_tallies() { msg_builder.push_line(format!("{}: {}", player, tally)); } } } msg.reply(&ctx.http, msg_builder.build()).await.unwrap(); Ok(()) } #[command] #[only_in(guilds)] #[description = "Get the other players in the game. $status"] async fn players(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { let data = ctx.data.read().await; let global_data = data.get::().unwrap(); let global_data = global_data.lock().await; let mut msg_builder = MessageBuilder::new(); msg_builder.push_line("Test Subjects:"); for player in &global_data.game_state()?.player_data { msg_builder.push("* ").push(&player.codename); if msg.channel_id.0 == global_data.cfg.host_channel { let guild = msg.guild(&ctx.cache).await.unwrap(); let member = guild.members.get(&UserId::from(player.discord_id)).unwrap(); msg_builder.push_line(format!(" ({})", member.display_name())); } else { msg_builder.push_line(""); } } msg.reply(&ctx.http, msg_builder.build()).await.unwrap(); Ok(()) } #[help] #[individual_command_tip = "If you want more information about a specific command, just pass the command as argument."] #[command_not_found_text = "Could not find: `{}`."] #[max_levenshtein_distance(3)] #[indention_prefix = "+"] #[lacking_role = "Strike"] #[wrong_channel = "Strike"] async fn help( context: &Context, msg: &Message, args: Args, help_options: &'static HelpOptions, groups: &[&'static CommandGroup], owners: HashSet, ) -> CommandResult { let _ = help_commands::with_embeds(context, msg, args, help_options, groups, owners).await; Ok(()) } #[hook] async fn handle_errors( ctx: &Context, msg: &Message, command_name: &str, command_result: CommandResult, ) { match command_result { Ok(()) => println!("Successfully processed command '{}'", command_name), Err(err) => { let reply_msg = format!("Command '{}' returned an error. {}", command_name, err,); println!("{}", reply_msg); msg.reply(&ctx.http, reply_msg).await.unwrap(); } }; } pub fn command_framework() -> StandardFramework { StandardFramework::new() .configure(|c| c.prefix('$')) .group(&HOST_GROUP) .group(&PLAYER_GROUP) .help(&HELP) .after(handle_errors) }